jcl-parser 2.2.3__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/PKG-INFO +1 -1
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/PKG-INFO +1 -1
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/__init__.py +1 -1
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_job_builder.py +24 -4
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_statement_parser.py +1 -1
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/cli.py +125 -7
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/explosion.py +109 -2
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/include_loader.py +39 -7
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lineage.py +38 -2
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/dfsrrc00.py +48 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/validators.py +20 -2
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/pyproject.toml +1 -1
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/LICENSE +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/README.md +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/SOURCES.txt +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/entry_points.txt +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/requires.txt +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/__main__.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_include_origins.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_model_factories.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_post_parse.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/_source_scanner.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/checks.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/condition_parser.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/continuation_handler.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/dataset_lifecycle.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/errors.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/jes_statements.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/__init__.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/base.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/config.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/rules.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/runner.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/lint/suppress.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/metrics.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/model.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/parameter_ast.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/parameter_validators.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/parser.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/plugins/base.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/proc_loader.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/profiling.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/_control.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/_invoked.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/adrdssu.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/base.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/binder.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/bpxbatch.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/compilers.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/dsnutilb.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/ftp.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/icetool.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/idcams.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iebcompr.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iebdg.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iebgener.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iebptpch.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iebupdte.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iefbr14.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iehinitt.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iehlist.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/iehprogm.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/ikjeft01.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/irxjcl.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/registry.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/program_profiles/sort.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/py.typed +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/rc_analysis.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/serialization.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/sql_detector.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/system_symbols.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/jclparser/utils.py +0 -0
- {jcl_parser-2.2.3 → jcl_parser-2.3.1}/setup.cfg +0 -0
|
@@ -20,7 +20,7 @@ application, for example::
|
|
|
20
20
|
import logging as _logging
|
|
21
21
|
|
|
22
22
|
# Package metadata
|
|
23
|
-
__version__ = "2.
|
|
23
|
+
__version__ = "2.3.1" # x-release-please-version
|
|
24
24
|
__author__ = "JCL Parser Team"
|
|
25
25
|
__description__ = "Parse IBM JCL scripts into Python objects"
|
|
26
26
|
|
|
@@ -621,11 +621,31 @@ class JobBuilder:
|
|
|
621
621
|
sink.append(member)
|
|
622
622
|
# Found members are expanded before the parse loop runs, so an
|
|
623
623
|
# INCLUDE that reaches here could not be loaded.
|
|
624
|
-
self._warnings_sink().append(
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
624
|
+
self._warnings_sink().append(self._include_warning(stmt, str(member)))
|
|
625
|
+
|
|
626
|
+
def _include_warning(self, stmt: Statement, member: str) -> str:
|
|
627
|
+
"""The warning for an INCLUDE whose member could not be loaded."""
|
|
628
|
+
line = f"Line {stmt.line_number}: INCLUDE member"
|
|
629
|
+
missing = " its statements are missing from the job"
|
|
630
|
+
if "&" in member:
|
|
631
|
+
from .include_loader import _substitute_member_symbols
|
|
632
|
+
|
|
633
|
+
symbols = (
|
|
634
|
+
{**self.pending_sets, **self.job.set_statements}
|
|
635
|
+
if self.job
|
|
636
|
+
else self.pending_sets
|
|
628
637
|
)
|
|
638
|
+
resolved = _substitute_member_symbols(member, symbols)
|
|
639
|
+
if "&" in resolved:
|
|
640
|
+
# Not a missing member: its name could not be built (#180).
|
|
641
|
+
return (
|
|
642
|
+
f"{line} {member} uses a symbol with no SET value before "
|
|
643
|
+
f"it;{missing}"
|
|
644
|
+
)
|
|
645
|
+
member = f"{resolved} (from {member})"
|
|
646
|
+
return (
|
|
647
|
+
f"{line} {member} not found in the JCLLIB / procedure libraries;{missing}"
|
|
648
|
+
)
|
|
629
649
|
|
|
630
650
|
def _on_conditional(self, stmt: Statement, line_num: int) -> None:
|
|
631
651
|
# Conditional logic - track conditionals properly
|
|
@@ -803,7 +803,7 @@ def _extract_jcllib_order(content: str) -> list[str]:
|
|
|
803
803
|
symbols: dict[str, str] = {}
|
|
804
804
|
lines = split_source_lines(content)
|
|
805
805
|
for index, line in enumerate(lines):
|
|
806
|
-
_record_set_symbols(line.lstrip(), symbols)
|
|
806
|
+
_record_set_symbols(line.lstrip(), symbols, lines, index + 1)
|
|
807
807
|
params_part = _jcllib_operands(line)
|
|
808
808
|
if params_part is None:
|
|
809
809
|
continue
|
|
@@ -119,9 +119,15 @@ def _main(argv: list[str] | None = None) -> int:
|
|
|
119
119
|
# ("FILE --explode steps"); plain parse_args() hands the whole positional
|
|
120
120
|
# run to FILE/query before the flag and then rejects "steps".
|
|
121
121
|
arg_parser = _build_arg_parser()
|
|
122
|
+
if not _argv:
|
|
123
|
+
# A bare `jcl-parser` used to print only "the following arguments
|
|
124
|
+
# are required: FILE"; show the queries and examples instead.
|
|
125
|
+
arg_parser.print_help()
|
|
126
|
+
return 2
|
|
122
127
|
args = arg_parser.parse_intermixed_args(argv)
|
|
123
128
|
if args.profile_output and not args.profile:
|
|
124
129
|
arg_parser.error("--profile-output requires --profile") # exits 2
|
|
130
|
+
_apply_format(args)
|
|
125
131
|
|
|
126
132
|
source: Path | str
|
|
127
133
|
if args.file == "-":
|
|
@@ -160,6 +166,14 @@ def _main(argv: list[str] | None = None) -> int:
|
|
|
160
166
|
return _run_query_with_profile(job, args, prof, source)
|
|
161
167
|
|
|
162
168
|
|
|
169
|
+
def _apply_format(args: argparse.Namespace) -> None:
|
|
170
|
+
"""``--format json|sarif`` sets the older ``--json`` / ``--sarif`` flags."""
|
|
171
|
+
if args.format == "json":
|
|
172
|
+
args.json_output = True
|
|
173
|
+
elif args.format == "sarif":
|
|
174
|
+
args.sarif = True
|
|
175
|
+
|
|
176
|
+
|
|
163
177
|
def _read_stdin() -> str:
|
|
164
178
|
"""The JCL piped on stdin, decoded like a file (``FILE`` is ``-``)."""
|
|
165
179
|
return read_source(getattr(sys.stdin, "buffer", sys.stdin))
|
|
@@ -196,10 +210,70 @@ def _is_step_or_dd_name(job: Job, token: str) -> bool:
|
|
|
196
210
|
)
|
|
197
211
|
|
|
198
212
|
|
|
213
|
+
#: The query words ``jcl-parser FILE <word>`` understands, for typo hints.
|
|
214
|
+
_QUERY_WORDS = (
|
|
215
|
+
"steps",
|
|
216
|
+
"procs",
|
|
217
|
+
"detect",
|
|
218
|
+
"metrics",
|
|
219
|
+
"lint",
|
|
220
|
+
"datasets",
|
|
221
|
+
"sql",
|
|
222
|
+
"analyze",
|
|
223
|
+
"paths",
|
|
224
|
+
"lineage",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
_HELP_EPILOG = """\
|
|
228
|
+
queries (the words after FILE):
|
|
229
|
+
(none) job summary; with --json the whole parse as JSON
|
|
230
|
+
steps every step, the program or PROC it runs, its DDs
|
|
231
|
+
procs [PROCNAME] inline/external PROCs; --explode shows the expansion
|
|
232
|
+
STEP one step in detail e.g. STEP010
|
|
233
|
+
STEP.DDNAME one DD statement e.g. STEP010.SYSIN
|
|
234
|
+
DDNAME that DD in every step e.g. SYSOUT
|
|
235
|
+
detect is the file a JOB, a PROC, or neither
|
|
236
|
+
metrics step, DD, PROC and dataset counts, complexity
|
|
237
|
+
lint lint rules (--lint-list lists them; --sarif for CI)
|
|
238
|
+
datasets dataset lifecycle: created, read, deleted, passed
|
|
239
|
+
sql DB2 / SQL usage by step
|
|
240
|
+
analyze [STEP] what each utility step (SORT, IDCAMS, ...) does
|
|
241
|
+
paths which steps can run, from the COND / IF logic
|
|
242
|
+
|
|
243
|
+
FILE may be a directory: 'lineage [dot|mermaid]' (the default) maps
|
|
244
|
+
datasets across its jobs, 'lint' and --validate check every file
|
|
245
|
+
(--glob picks them). FILE '-' reads the JCL from stdin.
|
|
246
|
+
|
|
247
|
+
examples:
|
|
248
|
+
jcl-parser PAYROLL.jcl
|
|
249
|
+
jcl-parser PAYROLL.jcl steps --explode --proc-path ./proclib
|
|
250
|
+
jcl-parser PAYROLL.jcl STEP010.SYSIN
|
|
251
|
+
jcl-parser PAYROLL.jcl lint --fail-on warning
|
|
252
|
+
jcl-parser PAYROLL.jcl --validate --format json
|
|
253
|
+
jcl-parser jobs/ lineage mermaid > lineage.mmd
|
|
254
|
+
jcl-lint jobs/*.jcl --format sarif -o jcl.sarif (several files, for CI)
|
|
255
|
+
|
|
256
|
+
exit codes:
|
|
257
|
+
0 success
|
|
258
|
+
1 the file could not be read or parsed, a name was not found, or a check
|
|
259
|
+
reported something (--validate errors, lint findings at --fail-on or
|
|
260
|
+
above, dataset / SQL findings)
|
|
261
|
+
2 the command line was rejected
|
|
262
|
+
|
|
263
|
+
defaults:
|
|
264
|
+
lint reads [tool.jcl-parser.lint] from the nearest pyproject.toml (or
|
|
265
|
+
--config). External PROCs are loaded only with --proc-path, except by
|
|
266
|
+
--validate, which falls back to the JCL_PROCLIB_PATH variable.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
|
|
199
270
|
def _build_arg_parser() -> argparse.ArgumentParser:
|
|
200
271
|
parser = argparse.ArgumentParser(
|
|
201
272
|
prog="jcl-parser",
|
|
202
|
-
|
|
273
|
+
usage="jcl-parser FILE [QUERY ...] [options]",
|
|
274
|
+
description="Parse and inspect IBM z/OS JCL: jobs, steps, DDs and PROCs.",
|
|
275
|
+
epilog=_HELP_EPILOG,
|
|
276
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
203
277
|
)
|
|
204
278
|
parser.add_argument(
|
|
205
279
|
"file",
|
|
@@ -210,15 +284,22 @@ def _build_arg_parser() -> argparse.ArgumentParser:
|
|
|
210
284
|
"query",
|
|
211
285
|
nargs="*",
|
|
212
286
|
default=[],
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
"or a DD name to find across all steps; for a directory, "
|
|
216
|
-
"'lineage [dot|mermaid]'"
|
|
217
|
-
),
|
|
287
|
+
metavar="QUERY",
|
|
288
|
+
help="What to show (see 'queries' below); none prints the job summary",
|
|
218
289
|
)
|
|
219
290
|
parser.add_argument(
|
|
220
291
|
"--json", action="store_true", dest="json_output", help="Output as JSON"
|
|
221
292
|
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--format",
|
|
295
|
+
choices=("text", "json", "sarif"),
|
|
296
|
+
default=None,
|
|
297
|
+
help=(
|
|
298
|
+
"Output format: text (default), json (same as --json) or sarif "
|
|
299
|
+
"(same as --sarif; lint and --validate only). The spelling "
|
|
300
|
+
"cobol-parser and jcl-lint use"
|
|
301
|
+
),
|
|
302
|
+
)
|
|
222
303
|
parser.add_argument(
|
|
223
304
|
"--validate", action="store_true", help="Validate the JCL and report errors"
|
|
224
305
|
)
|
|
@@ -469,7 +550,44 @@ def _cmd_step_or_dd(job: Job, query: str, json_out: bool) -> int:
|
|
|
469
550
|
return 0
|
|
470
551
|
|
|
471
552
|
# Fall back to DD name across all steps
|
|
472
|
-
|
|
553
|
+
status = _print_dd_across_steps(job, query.upper(), json_out)
|
|
554
|
+
if status != 0:
|
|
555
|
+
hint = _not_found_hint(job, query)
|
|
556
|
+
if hint:
|
|
557
|
+
print(hint, file=sys.stderr)
|
|
558
|
+
return status
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
def _not_found_hint(job: Job, query: str) -> str | None:
|
|
562
|
+
"""A "did you mean" line for a query that named no step or DD.
|
|
563
|
+
|
|
564
|
+
A mistyped query word (``stpes``) used to read only as "No step or DD
|
|
565
|
+
named 'STPES' found", which sends a new user looking for a step.
|
|
566
|
+
"""
|
|
567
|
+
import difflib
|
|
568
|
+
|
|
569
|
+
words = difflib.get_close_matches(query.lower(), _QUERY_WORDS, n=1, cutoff=0.6)
|
|
570
|
+
names = sorted(
|
|
571
|
+
{s.name for s in iter_execution_steps(job.steps) if s.name}
|
|
572
|
+
| {
|
|
573
|
+
dd.name
|
|
574
|
+
for s in iter_execution_steps(job.steps)
|
|
575
|
+
for dd in s.dd_statements
|
|
576
|
+
if dd.name
|
|
577
|
+
}
|
|
578
|
+
)
|
|
579
|
+
close = difflib.get_close_matches(query.upper(), names, n=3, cutoff=0.6)
|
|
580
|
+
# JCL names are uppercase: an uppercase query is a misspelt name first,
|
|
581
|
+
# a lowercase one a misspelt query word first.
|
|
582
|
+
name_hint = "Did you mean: " + ", ".join(close) + "?" if close else None
|
|
583
|
+
word_hint = (
|
|
584
|
+
f"Did you mean the query '{words[0]}'? (see 'jcl-parser --help')"
|
|
585
|
+
if words
|
|
586
|
+
else None
|
|
587
|
+
)
|
|
588
|
+
if query == query.upper():
|
|
589
|
+
return name_hint or word_hint
|
|
590
|
+
return word_hint or name_hint
|
|
473
591
|
|
|
474
592
|
|
|
475
593
|
def _cmd_directory(path: Path, args: argparse.Namespace) -> int:
|
|
@@ -1219,6 +1219,104 @@ def is_exec_override_keyword(key: str) -> bool:
|
|
|
1219
1219
|
return key.upper().partition(".")[0] in _EXEC_OVERRIDE_KEYWORDS
|
|
1220
1220
|
|
|
1221
1221
|
|
|
1222
|
+
_SYMBOL_REFERENCE = re.compile(r"&&|&([A-Z@#$][A-Z0-9@#$]{0,7})", re.IGNORECASE)
|
|
1223
|
+
_INCLUDE_STATEMENT = re.compile(r"^//\S*\s+INCLUDE\s", re.IGNORECASE | re.MULTILINE)
|
|
1224
|
+
|
|
1225
|
+
|
|
1226
|
+
def _referenced_symbols(value: Any, found: set[str], seen: set[int]) -> None:
|
|
1227
|
+
"""Add every ``&NAME`` written anywhere in *value* to *found*.
|
|
1228
|
+
|
|
1229
|
+
Walks strings, dicts, lists and the model dataclasses (steps, DDs,
|
|
1230
|
+
conditional blocks) a procedure body is made of. ``&&`` is an escaped
|
|
1231
|
+
ampersand, not a reference.
|
|
1232
|
+
"""
|
|
1233
|
+
if isinstance(value, str):
|
|
1234
|
+
for match in _SYMBOL_REFERENCE.finditer(value):
|
|
1235
|
+
if match.group(1):
|
|
1236
|
+
found.add(match.group(1).upper())
|
|
1237
|
+
return
|
|
1238
|
+
if id(value) in seen:
|
|
1239
|
+
return
|
|
1240
|
+
seen.add(id(value))
|
|
1241
|
+
if isinstance(value, dict):
|
|
1242
|
+
for item in value.values():
|
|
1243
|
+
_referenced_symbols(item, found, seen)
|
|
1244
|
+
elif isinstance(value, (list, tuple)):
|
|
1245
|
+
for item in value:
|
|
1246
|
+
_referenced_symbols(item, found, seen)
|
|
1247
|
+
elif isinstance(value, (Step, DDStatement, ConditionalBlock)):
|
|
1248
|
+
for name, item in vars(value).items():
|
|
1249
|
+
if name not in ("comments", "inline_data", "original_proc_call"):
|
|
1250
|
+
_referenced_symbols(item, found, seen)
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def call_override_symbols(call: Step) -> set[str]:
|
|
1254
|
+
"""Every ``&NAME`` in the overrides coded with PROC call *call*.
|
|
1255
|
+
|
|
1256
|
+
The overriding and added DDs after the call (and their concatenations)
|
|
1257
|
+
and the values of its keyword overrides (``PARM.PS1='&X'``) are merged
|
|
1258
|
+
into the procedure, so the call's symbolics are substituted there too
|
|
1259
|
+
(issue #176).
|
|
1260
|
+
"""
|
|
1261
|
+
found: set[str] = set()
|
|
1262
|
+
_referenced_symbols(
|
|
1263
|
+
[
|
|
1264
|
+
call.dd_statements,
|
|
1265
|
+
list(call.dd_overrides.values()),
|
|
1266
|
+
list(call.dd_override_concatenations.values()),
|
|
1267
|
+
[
|
|
1268
|
+
value
|
|
1269
|
+
for key, value in call.parameters.items()
|
|
1270
|
+
if is_exec_override_keyword(key)
|
|
1271
|
+
],
|
|
1272
|
+
],
|
|
1273
|
+
found,
|
|
1274
|
+
set(),
|
|
1275
|
+
)
|
|
1276
|
+
return found
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def unused_proc_symbols(
|
|
1280
|
+
call: Step, procedure: Procedure, call_references: set[str] | None = None
|
|
1281
|
+
) -> list[str]:
|
|
1282
|
+
"""The symbolics PROC call *call* assigns that *procedure* never uses.
|
|
1283
|
+
|
|
1284
|
+
A symbolic coded on ``EXEC procname`` that is neither declared on the
|
|
1285
|
+
PROC statement nor referenced as ``&NAME`` in the procedure body is a
|
|
1286
|
+
JCL error on z/OS (``IEFC657I THE SYMBOL x WAS NOT USED``), and the
|
|
1287
|
+
expansion silently runs with the PROC's defaults (issue #172). EXEC
|
|
1288
|
+
keyword overrides (``PARM``, ``COND.PS1``, ...) are not symbolics. A
|
|
1289
|
+
reference in the call's overriding or added DDs, or in a keyword
|
|
1290
|
+
override's value, counts as a use (issue #176); *call_references* gives
|
|
1291
|
+
those references when the call's overrides were already substituted
|
|
1292
|
+
(default: :func:`call_override_symbols` of *call*).
|
|
1293
|
+
|
|
1294
|
+
Returns an empty list when the body cannot be fully checked: a procedure
|
|
1295
|
+
with no steps (unloaded), or one whose text has an ``INCLUDE`` (its
|
|
1296
|
+
members' references are not known before explosion).
|
|
1297
|
+
"""
|
|
1298
|
+
if not procedure.steps:
|
|
1299
|
+
return []
|
|
1300
|
+
if procedure.source_text and _INCLUDE_STATEMENT.search(procedure.source_text):
|
|
1301
|
+
return []
|
|
1302
|
+
known = {name.upper() for name in procedure.parameters}
|
|
1303
|
+
_referenced_symbols(
|
|
1304
|
+
[procedure.steps, procedure.set_statements, procedure.parameters],
|
|
1305
|
+
known,
|
|
1306
|
+
set(),
|
|
1307
|
+
)
|
|
1308
|
+
if procedure.source_text:
|
|
1309
|
+
_referenced_symbols(procedure.source_text, known, set())
|
|
1310
|
+
known |= call_override_symbols(call) if call_references is None else call_references
|
|
1311
|
+
return [
|
|
1312
|
+
key
|
|
1313
|
+
for key in call.parameters
|
|
1314
|
+
if not is_exec_override_keyword(key)
|
|
1315
|
+
and key.upper() != "PROC"
|
|
1316
|
+
and key.upper() not in known
|
|
1317
|
+
]
|
|
1318
|
+
|
|
1319
|
+
|
|
1222
1320
|
def _substitute_proc_call_parameters(
|
|
1223
1321
|
parameters: dict[str, Any],
|
|
1224
1322
|
scope: SymbolScope,
|
|
@@ -1800,8 +1898,9 @@ class _IncludeExpandingLoader(ProcedureLoader):
|
|
|
1800
1898
|
from .utils import split_source_lines
|
|
1801
1899
|
|
|
1802
1900
|
seen = dict(symbols) if symbols is not None else {}
|
|
1803
|
-
|
|
1804
|
-
|
|
1901
|
+
lines = split_source_lines(expanded_text)
|
|
1902
|
+
for index, line in enumerate(lines):
|
|
1903
|
+
_record_set_symbols(line.rstrip(), seen, lines, index + 1)
|
|
1805
1904
|
missing = _extract_include_member(line.rstrip())
|
|
1806
1905
|
if not missing:
|
|
1807
1906
|
continue
|
|
@@ -2099,6 +2198,8 @@ def _explode_step(
|
|
|
2099
2198
|
# against the job's library order. Pass an immutable tuple: loaders key their
|
|
2100
2199
|
# caches on the library list, so a hashable value keeps that contract safe.
|
|
2101
2200
|
_libs = tuple(jcllib_order) if jcllib_order else None
|
|
2201
|
+
# Read before loading resolves the call's overrides (issue #176).
|
|
2202
|
+
call_references = call_override_symbols(step)
|
|
2102
2203
|
procedure = _load_called_procedure(
|
|
2103
2204
|
step, proc_loader, _libs, base_scope, call_scope, strict_symbols, warnings
|
|
2104
2205
|
)
|
|
@@ -2121,6 +2222,12 @@ def _explode_step(
|
|
|
2121
2222
|
)
|
|
2122
2223
|
|
|
2123
2224
|
_apply_call_overrides(expanded, step, procedure.name or proc_name, warnings)
|
|
2225
|
+
unused = unused_proc_symbols(step, procedure, call_references)
|
|
2226
|
+
if unused and warnings is not None:
|
|
2227
|
+
warnings.append(
|
|
2228
|
+
f"EXEC {step.name or 'PROC'}: symbol(s) {', '.join(unused)} not used "
|
|
2229
|
+
f"by PROC {procedure.name or proc_name} (IEFC657I); its defaults apply"
|
|
2230
|
+
)
|
|
2124
2231
|
_record_expansion(
|
|
2125
2232
|
step, procedure, overrides, expanded, depth, audit_records, expansion_map
|
|
2126
2233
|
)
|
|
@@ -26,6 +26,7 @@ import errno
|
|
|
26
26
|
import logging
|
|
27
27
|
import os
|
|
28
28
|
from abc import ABC, abstractmethod
|
|
29
|
+
from collections.abc import Sequence
|
|
29
30
|
from pathlib import Path
|
|
30
31
|
from typing import NamedTuple
|
|
31
32
|
|
|
@@ -335,21 +336,22 @@ class _IncludeExpander:
|
|
|
335
336
|
for index, line in enumerate(lines):
|
|
336
337
|
expanded = None
|
|
337
338
|
if index not in data_indices:
|
|
338
|
-
expanded = self._expand_statement(
|
|
339
|
+
expanded = self._expand_statement(lines, index, depth, active)
|
|
339
340
|
if expanded is None:
|
|
340
341
|
expanded = [(line, LineOrigin(None, index + 1))]
|
|
341
342
|
result.extend(expanded)
|
|
342
343
|
return result
|
|
343
344
|
|
|
344
345
|
def _expand_statement(
|
|
345
|
-
self,
|
|
346
|
+
self, lines: list[str], index: int, depth: int, active: frozenset[str]
|
|
346
347
|
) -> list[_Entry] | None:
|
|
347
|
-
"""The expansion of
|
|
348
|
+
"""The expansion of ``lines[index]`` if it is a resolvable INCLUDE."""
|
|
349
|
+
line_number = index + 1
|
|
348
350
|
# Preserve leading whitespace: only strip the line terminator so that
|
|
349
351
|
# in-stream data lines (which may contain '//' after leading spaces)
|
|
350
352
|
# are never mistaken for JCL statements.
|
|
351
|
-
statement_line =
|
|
352
|
-
_record_set_symbols(statement_line, self.symbols)
|
|
353
|
+
statement_line = lines[index].rstrip("\r\n")
|
|
354
|
+
_record_set_symbols(statement_line, self.symbols, lines, index + 1)
|
|
353
355
|
member = _extract_include_member(statement_line)
|
|
354
356
|
if member is None:
|
|
355
357
|
return None
|
|
@@ -413,12 +415,19 @@ def _member_origin(origin: LineOrigin, member: str, include_line: int) -> LineOr
|
|
|
413
415
|
return LineOrigin(origin.member, include_line, origin.member_line)
|
|
414
416
|
|
|
415
417
|
|
|
416
|
-
def _record_set_symbols(
|
|
418
|
+
def _record_set_symbols(
|
|
419
|
+
line: str,
|
|
420
|
+
symbols: dict[str, str],
|
|
421
|
+
lines: Sequence[str] = (),
|
|
422
|
+
follow: int = 0,
|
|
423
|
+
) -> None:
|
|
417
424
|
"""Record the assignments of a ``// SET`` statement *line* into *symbols*.
|
|
418
425
|
|
|
419
426
|
Values are resolved against the symbols already recorded, mirroring the
|
|
420
427
|
parser's left-to-right SET evaluation. Lines that are not a SET statement
|
|
421
|
-
are ignored.
|
|
428
|
+
are ignored. A SET whose operand field ends in a comma is continued on the
|
|
429
|
+
following ``//`` records: *lines* from index *follow* on (the record after
|
|
430
|
+
*line*) are read for its remaining assignments (#180).
|
|
422
431
|
"""
|
|
423
432
|
if not line.startswith("//") or line.startswith("//*"):
|
|
424
433
|
return
|
|
@@ -436,6 +445,7 @@ def _record_set_symbols(line: str, symbols: dict[str, str]) -> None:
|
|
|
436
445
|
from .utils import is_positional_key, jcl_value_text
|
|
437
446
|
|
|
438
447
|
statement, _ = split_comment_field(line)
|
|
448
|
+
statement = _continue_operands(statement, lines, follow)
|
|
439
449
|
stmt = parse_statement(statement)
|
|
440
450
|
if stmt is None or stmt.type != "SET":
|
|
441
451
|
return
|
|
@@ -445,6 +455,28 @@ def _record_set_symbols(line: str, symbols: dict[str, str]) -> None:
|
|
|
445
455
|
symbols[key.upper()] = _resolve_set_value(jcl_value_text(value), symbols)
|
|
446
456
|
|
|
447
457
|
|
|
458
|
+
def _continue_operands(statement: str, lines: Sequence[str], follow: int) -> str:
|
|
459
|
+
"""Append the operands of the continuation records of *statement*.
|
|
460
|
+
|
|
461
|
+
A statement whose operand field ends in a comma continues on the next
|
|
462
|
+
``//`` record that has a blank in column 3; its operand field (up to the
|
|
463
|
+
first blank) is appended. ``//*`` comment records in between are skipped.
|
|
464
|
+
"""
|
|
465
|
+
from ._statement_parser import split_comment_field
|
|
466
|
+
|
|
467
|
+
statement = statement.rstrip()
|
|
468
|
+
while statement.endswith(",") and follow < len(lines):
|
|
469
|
+
cont = lines[follow].rstrip("\r\n")
|
|
470
|
+
follow += 1
|
|
471
|
+
if cont.startswith("//*"):
|
|
472
|
+
continue
|
|
473
|
+
if not cont.startswith("// ") or not cont[2:].strip():
|
|
474
|
+
break
|
|
475
|
+
operands, _ = split_comment_field("// " + cont[2:].lstrip())
|
|
476
|
+
statement += operands[2:].strip()
|
|
477
|
+
return statement
|
|
478
|
+
|
|
479
|
+
|
|
448
480
|
def _substitute_member_symbols(member: str, symbols: dict[str, str]) -> str:
|
|
449
481
|
"""Resolve ``&SYMBOL`` references in an INCLUDE member name.
|
|
450
482
|
|
|
@@ -22,7 +22,8 @@ share a load library or DBRMLIB do not depend on one another (issue #145). The
|
|
|
22
22
|
A DD's role comes from its ``DISP`` status: ``NEW`` / ``MOD`` write the
|
|
23
23
|
dataset, ``SHR`` / ``OLD`` read it (``OLD`` may also update it in place, which
|
|
24
24
|
DISP alone can't tell). A normal disposition of ``DELETE`` makes any status
|
|
25
|
-
but ``NEW`` a read (a delete runs after the dataset's producer
|
|
25
|
+
but ``NEW`` a read (a delete runs after the dataset's producer and after
|
|
26
|
+
every other job that reads or writes it), and
|
|
26
27
|
``(NEW,DELETE)`` -- a scratch dataset -- neither. Temporary datasets (``&&TEMP``), ``NULLFILE``,
|
|
27
28
|
``SYSOUT`` and DDs without a ``DISP`` are left out; a ``DSN=*.STEP.DD``
|
|
28
29
|
backward reference counts as the dataset it points at.
|
|
@@ -93,6 +94,9 @@ class DatasetUse:
|
|
|
93
94
|
dataset: str
|
|
94
95
|
disp: str
|
|
95
96
|
line_number: int = 0
|
|
97
|
+
#: The DD deletes the dataset (normal disposition ``DELETE``, any status
|
|
98
|
+
#: but ``NEW``); it is a read that must also follow every other use.
|
|
99
|
+
deletes: bool = False
|
|
96
100
|
|
|
97
101
|
def to_dict(self) -> dict[str, Any]:
|
|
98
102
|
return {
|
|
@@ -102,6 +106,7 @@ class DatasetUse:
|
|
|
102
106
|
"dataset": self.dataset,
|
|
103
107
|
"disp": self.disp,
|
|
104
108
|
"line_number": self.line_number,
|
|
109
|
+
"deletes": self.deletes,
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
|
|
@@ -131,7 +136,8 @@ class DatasetLineage:
|
|
|
131
136
|
|
|
132
137
|
@dataclass(frozen=True)
|
|
133
138
|
class JobDependency:
|
|
134
|
-
"""*consumer* reads datasets that *producer* writes
|
|
139
|
+
"""*consumer* reads datasets that *producer* writes, or deletes datasets
|
|
140
|
+
that *producer* also reads or writes (it must run after that job)."""
|
|
135
141
|
|
|
136
142
|
producer: str
|
|
137
143
|
consumer: str
|
|
@@ -257,6 +263,7 @@ def _record_uses(job_id: str, job: Job, datasets: dict[str, DatasetLineage]) ->
|
|
|
257
263
|
dataset=dsn,
|
|
258
264
|
disp=evt.disp_status,
|
|
259
265
|
line_number=evt.line_number,
|
|
266
|
+
deletes=evt.disp_normal == "DELETE" and evt.disp_status != "NEW",
|
|
260
267
|
)
|
|
261
268
|
role = _role(evt.disp_status, evt.disp_normal)
|
|
262
269
|
if role == "write":
|
|
@@ -313,12 +320,41 @@ def _dependencies(datasets: Mapping[str, DatasetLineage]) -> list[JobDependency]
|
|
|
313
320
|
labels = edges.setdefault((producer, consumer), [])
|
|
314
321
|
if label not in labels:
|
|
315
322
|
labels.append(label)
|
|
323
|
+
_add_delete_edges(datasets, edges)
|
|
316
324
|
return [
|
|
317
325
|
JobDependency(producer, consumer, tuple(sorted(keys)))
|
|
318
326
|
for (producer, consumer), keys in sorted(edges.items())
|
|
319
327
|
]
|
|
320
328
|
|
|
321
329
|
|
|
330
|
+
def _add_delete_edges(
|
|
331
|
+
datasets: Mapping[str, DatasetLineage], edges: dict[tuple[str, str], list[str]]
|
|
332
|
+
) -> None:
|
|
333
|
+
"""A job that deletes a dataset runs after every other job that uses it.
|
|
334
|
+
|
|
335
|
+
The producer edge is already there (a delete is a read); this adds one
|
|
336
|
+
from each other reader of the dataset -- or of its library or a member
|
|
337
|
+
of it -- so the deleter cannot be ordered before a job that still needs
|
|
338
|
+
the dataset (#179). Two jobs that each delete a dataset the other reads
|
|
339
|
+
depend on each other, a cycle.
|
|
340
|
+
"""
|
|
341
|
+
for deleted, lineage in datasets.items():
|
|
342
|
+
deleters = _unique(u.job for u in lineage.readers if u.deletes)
|
|
343
|
+
if not deleters:
|
|
344
|
+
continue
|
|
345
|
+
for used, other in datasets.items():
|
|
346
|
+
if used != deleted and not _feeds(used, deleted):
|
|
347
|
+
continue
|
|
348
|
+
# Label the edge with the more specific name (the member).
|
|
349
|
+
label = deleted if deleted != library_of(deleted) else used
|
|
350
|
+
for user in _unique([*other.reader_jobs, *other.writer_jobs]):
|
|
351
|
+
for deleter in deleters:
|
|
352
|
+
if user != deleter:
|
|
353
|
+
labels = edges.setdefault((user, deleter), [])
|
|
354
|
+
if label not in labels:
|
|
355
|
+
labels.append(label)
|
|
356
|
+
|
|
357
|
+
|
|
322
358
|
def _run_order(
|
|
323
359
|
jobs: list[str], dependencies: list[JobDependency]
|
|
324
360
|
) -> tuple[list[str], list[str]]:
|
|
@@ -13,12 +13,29 @@ comma-delimited sub-parameter of PARM:
|
|
|
13
13
|
The profile extracts the application program and PSB name from PARM so that
|
|
14
14
|
``analyze_step``/``analyze_job`` can report what application is actually
|
|
15
15
|
running, not just the region controller.
|
|
16
|
+
|
|
17
|
+
That layout holds only for the batch regions (BMP, DLI, DBB). A message
|
|
18
|
+
processing region codes ``PARM=(MSG,&CL1&CL2&CL3&CL4,...)``: PARM[1] is the
|
|
19
|
+
four three-digit transaction classes it serves and it names no program. For
|
|
20
|
+
MSG, IFP and any other region type, only the region type (and, for MSG, the
|
|
21
|
+
classes) is reported (issue #173).
|
|
16
22
|
"""
|
|
17
23
|
|
|
18
24
|
from __future__ import annotations
|
|
19
25
|
|
|
26
|
+
from ..model import Step
|
|
20
27
|
from .base import ProgramAnalysis, ProgramProfile
|
|
21
28
|
|
|
29
|
+
#: Region types whose PARM[1] / PARM[2] are the application program and PSB.
|
|
30
|
+
_BATCH_REGION_TYPES = frozenset({"BMP", "DLI", "DBB"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _transaction_classes(field: str) -> list[str]:
|
|
34
|
+
"""Split an MPP's ``&CL1&CL2&CL3&CL4`` field into its three-digit classes."""
|
|
35
|
+
if not field.isdigit() or len(field) % 3:
|
|
36
|
+
return []
|
|
37
|
+
return [field[i : i + 3] for i in range(0, len(field), 3)]
|
|
38
|
+
|
|
22
39
|
|
|
23
40
|
class DFSRRC00Profile(ProgramProfile):
|
|
24
41
|
program_names = frozenset({"DFSRRC00", "DFSRRC02"})
|
|
@@ -42,6 +59,9 @@ class DFSRRC00Profile(ProgramProfile):
|
|
|
42
59
|
parts = [p.strip() for p in parm.split(",")]
|
|
43
60
|
|
|
44
61
|
region_type = parts[0].upper() if parts and parts[0] else ""
|
|
62
|
+
if region_type and region_type not in _BATCH_REGION_TYPES:
|
|
63
|
+
return _online_region_analysis(step, region_type, parts, parm)
|
|
64
|
+
|
|
45
65
|
app_program = parts[1].upper() if len(parts) > 1 and parts[1] else ""
|
|
46
66
|
psb_name = parts[2].upper() if len(parts) > 2 and parts[2] else ""
|
|
47
67
|
|
|
@@ -67,3 +87,31 @@ class DFSRRC00Profile(ProgramProfile):
|
|
|
67
87
|
notes=notes,
|
|
68
88
|
raw_control=parm or None,
|
|
69
89
|
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _online_region_analysis(
|
|
93
|
+
step: Step, region_type: str, parts: list[str], parm: str
|
|
94
|
+
) -> ProgramAnalysis:
|
|
95
|
+
"""A region whose PARM names no program (MSG, IFP, ...): issue #173."""
|
|
96
|
+
notes = [f"IMS region type: {region_type}"]
|
|
97
|
+
classes = (
|
|
98
|
+
_transaction_classes(parts[1])
|
|
99
|
+
if region_type == "MSG" and len(parts) > 1
|
|
100
|
+
else []
|
|
101
|
+
)
|
|
102
|
+
summary = f"IMS {region_type} region ({step.program})"
|
|
103
|
+
if classes:
|
|
104
|
+
summary += f", classes {' '.join(classes)}"
|
|
105
|
+
notes.append(f"transaction classes: {' '.join(classes)}")
|
|
106
|
+
notes.append(
|
|
107
|
+
f"an IMS {region_type} region runs the transactions scheduled to it; "
|
|
108
|
+
"its PARM names no application program"
|
|
109
|
+
)
|
|
110
|
+
return ProgramAnalysis(
|
|
111
|
+
program=step.program or "DFSRRC00",
|
|
112
|
+
step_name=step.name or "",
|
|
113
|
+
summary=summary,
|
|
114
|
+
dataset_events=[],
|
|
115
|
+
notes=notes,
|
|
116
|
+
raw_control=parm or None,
|
|
117
|
+
)
|
|
@@ -321,7 +321,10 @@ def validate_backward_references(job: Job) -> list[ValidationError]:
|
|
|
321
321
|
|
|
322
322
|
|
|
323
323
|
def validate_procedure_references(job: Job) -> list[ValidationError]:
|
|
324
|
-
"""
|
|
324
|
+
"""Check ``EXEC procname`` calls against the PROCs they call.
|
|
325
|
+
|
|
326
|
+
A symbolic the call assigns that the PROC never declares or references
|
|
327
|
+
is an ERROR (``IEFC657I``, issue #172).
|
|
325
328
|
|
|
326
329
|
:func:`jclparser.parse_jcl` adds every PROC it can load (in-stream,
|
|
327
330
|
JCLLIB / system PROCLIB under the base path, built-in) to
|
|
@@ -330,9 +333,24 @@ def validate_procedure_references(job: Job) -> list[ValidationError]:
|
|
|
330
333
|
a WARNING, not an ERROR (issue #116). A strict parse of a job with a
|
|
331
334
|
``JCLLIB ORDER`` still fails on it before this runs.
|
|
332
335
|
"""
|
|
336
|
+
from .explosion import unused_proc_symbols
|
|
337
|
+
|
|
333
338
|
errors: list[ValidationError] = []
|
|
334
339
|
for step in iter_execution_steps(job.steps):
|
|
335
|
-
|
|
340
|
+
procedure = job.get_procedure(step.procedure) if step.procedure else None
|
|
341
|
+
if procedure is not None:
|
|
342
|
+
# A symbolic the PROC never declares or references (IEFC657I).
|
|
343
|
+
for name in unused_proc_symbols(step, procedure):
|
|
344
|
+
errors.append(
|
|
345
|
+
ValidationError(
|
|
346
|
+
f"Step '{step.name or ''}': symbol {name} is not used by "
|
|
347
|
+
f"procedure '{step.procedure}' (IEFC657I); the "
|
|
348
|
+
"procedure's defaults apply",
|
|
349
|
+
ErrorType.SEMANTIC_ERROR,
|
|
350
|
+
line_number=step.line_number,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
if step.procedure and procedure is None:
|
|
336
354
|
errors.append(
|
|
337
355
|
ValidationError(
|
|
338
356
|
f"Procedure '{step.procedure}' not found in-stream or in "
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jcl-parser"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.1"
|
|
8
8
|
description = "Parse IBM JCL (Job Control Language) scripts into Python objects"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Samuel Dion", email = "sam94dion@gmail.com"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|