jcl-parser 1.2.1__tar.gz → 1.3.0__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-1.2.1 → jcl_parser-1.3.0}/PKG-INFO +1 -1
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/PKG-INFO +1 -1
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/SOURCES.txt +11 -0
- jcl_parser-1.3.0/jclparser/program_profiles/adrdssu.py +72 -0
- jcl_parser-1.3.0/jclparser/program_profiles/binder.py +73 -0
- jcl_parser-1.3.0/jclparser/program_profiles/compilers.py +67 -0
- jcl_parser-1.3.0/jclparser/program_profiles/ftp.py +59 -0
- jcl_parser-1.3.0/jclparser/program_profiles/icetool.py +94 -0
- jcl_parser-1.3.0/jclparser/program_profiles/iebdg.py +58 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/iebgener.py +21 -11
- jcl_parser-1.3.0/jclparser/program_profiles/iebptpch.py +60 -0
- jcl_parser-1.3.0/jclparser/program_profiles/iebupdte.py +76 -0
- jcl_parser-1.3.0/jclparser/program_profiles/iehlist.py +54 -0
- jcl_parser-1.3.0/jclparser/program_profiles/iehprogm.py +108 -0
- jcl_parser-1.3.0/jclparser/program_profiles/irxjcl.py +58 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/registry.py +34 -11
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/pyproject.toml +1 -1
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/LICENSE +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/README.md +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/entry_points.txt +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/requires.txt +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/__init__.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/__main__.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/_job_builder.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/_model_factories.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/_post_parse.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/_statement_parser.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/cli.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/condition_parser.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/continuation_handler.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/dataset_lifecycle.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/errors.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/explosion.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/include_loader.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/jes_statements.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/lint/__init__.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/lint/base.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/lint/config.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/lint/rules.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/lint/runner.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/metrics.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/model.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/parameter_ast.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/parameter_validators.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/parser.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/plugins/base.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/proc_loader.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/profiling.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/_invoked.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/base.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/dfsrrc00.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/idcams.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/iefbr14.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/ikjeft01.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/program_profiles/sort.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/py.typed +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/rc_analysis.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/sql_detector.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/system_symbols.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/utils.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/jclparser/validators.py +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/setup.cfg +0 -0
- {jcl_parser-1.2.1 → jcl_parser-1.3.0}/setup.py +0 -0
|
@@ -46,12 +46,23 @@ jclparser/plugins/__init__.py
|
|
|
46
46
|
jclparser/plugins/base.py
|
|
47
47
|
jclparser/program_profiles/__init__.py
|
|
48
48
|
jclparser/program_profiles/_invoked.py
|
|
49
|
+
jclparser/program_profiles/adrdssu.py
|
|
49
50
|
jclparser/program_profiles/base.py
|
|
51
|
+
jclparser/program_profiles/binder.py
|
|
52
|
+
jclparser/program_profiles/compilers.py
|
|
50
53
|
jclparser/program_profiles/db2_batch.py
|
|
51
54
|
jclparser/program_profiles/dfsrrc00.py
|
|
55
|
+
jclparser/program_profiles/ftp.py
|
|
56
|
+
jclparser/program_profiles/icetool.py
|
|
52
57
|
jclparser/program_profiles/idcams.py
|
|
58
|
+
jclparser/program_profiles/iebdg.py
|
|
53
59
|
jclparser/program_profiles/iebgener.py
|
|
60
|
+
jclparser/program_profiles/iebptpch.py
|
|
61
|
+
jclparser/program_profiles/iebupdte.py
|
|
54
62
|
jclparser/program_profiles/iefbr14.py
|
|
63
|
+
jclparser/program_profiles/iehlist.py
|
|
64
|
+
jclparser/program_profiles/iehprogm.py
|
|
55
65
|
jclparser/program_profiles/ikjeft01.py
|
|
66
|
+
jclparser/program_profiles/irxjcl.py
|
|
56
67
|
jclparser/program_profiles/registry.py
|
|
57
68
|
jclparser/program_profiles/sort.py
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Profile for ``ADRDSSU`` — DFSMSdss batch (DUMP/RESTORE/COPY).
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN`` and use a keyword-command form,
|
|
4
|
+
e.g. ``DUMP DATASET(INCLUDE(...)) OUTDDNAME(...)``. This is a shallow
|
|
5
|
+
parse: it recognises the leading command keyword and, where present, the
|
|
6
|
+
``DATASET(INCLUDE(...))`` dataset list, without validating the full
|
|
7
|
+
DFSMSdss grammar (which spans many optional sub-parameters).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..model import Step
|
|
19
|
+
|
|
20
|
+
_COMMANDS = (
|
|
21
|
+
"DUMP",
|
|
22
|
+
"RESTORE",
|
|
23
|
+
"COPY",
|
|
24
|
+
"RELEASE",
|
|
25
|
+
"COMPRESS",
|
|
26
|
+
"DEFRAG",
|
|
27
|
+
"CONVERTV",
|
|
28
|
+
"PRINT",
|
|
29
|
+
)
|
|
30
|
+
_RE_COMMAND = re.compile(
|
|
31
|
+
r"^\s*(" + "|".join(_COMMANDS) + r")\b", re.IGNORECASE | re.MULTILINE
|
|
32
|
+
)
|
|
33
|
+
_RE_DATASET_INCLUDE = re.compile(
|
|
34
|
+
r"DATASET\s*\(\s*INCLUDE\s*\(\s*([^)]*)\)", re.IGNORECASE
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ADRDSSUProfile(ProgramProfile):
|
|
39
|
+
program_names = frozenset({"ADRDSSU"})
|
|
40
|
+
|
|
41
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
42
|
+
text = _sysin_text(step) or ""
|
|
43
|
+
notes: list[str] = []
|
|
44
|
+
|
|
45
|
+
matches = list(_RE_COMMAND.finditer(text))
|
|
46
|
+
for i, m in enumerate(matches):
|
|
47
|
+
command = m.group(1).upper()
|
|
48
|
+
# Scope the INCLUDE search to this command's own statement —
|
|
49
|
+
# up to the next command keyword (or end of text) — so a later
|
|
50
|
+
# command's DATASET(INCLUDE(...)) can't be misattributed here.
|
|
51
|
+
segment_end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
52
|
+
segment = text[m.end() : segment_end]
|
|
53
|
+
ds_m = _RE_DATASET_INCLUDE.search(segment)
|
|
54
|
+
if ds_m:
|
|
55
|
+
names = [n.strip() for n in ds_m.group(1).split() if n.strip()]
|
|
56
|
+
notes.append(f"{command} DATASET(INCLUDE({' '.join(names)}))")
|
|
57
|
+
else:
|
|
58
|
+
notes.append(command)
|
|
59
|
+
|
|
60
|
+
summary = (
|
|
61
|
+
f"ADRDSSU: {len(notes)} command(s)"
|
|
62
|
+
if notes
|
|
63
|
+
else "ADRDSSU with no recognised commands"
|
|
64
|
+
)
|
|
65
|
+
return ProgramAnalysis(
|
|
66
|
+
program="ADRDSSU",
|
|
67
|
+
step_name=step.name or "",
|
|
68
|
+
summary=summary,
|
|
69
|
+
dataset_events=[],
|
|
70
|
+
notes=notes,
|
|
71
|
+
raw_control=text or None,
|
|
72
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Profile for ``IEWL`` / ``HEWL`` — the Binder / Linkage Editor.
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN`` (or, when absent, ``SYSLIN``):
|
|
4
|
+
``INCLUDE`` pulls in an object/load module, ``ENTRY`` sets the entry point,
|
|
5
|
+
``NAME`` names the output load module member, and ``LIBRARY``/``ALIAS``
|
|
6
|
+
add search libraries and alternate names. Output lands on ``SYSLMOD``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .base import ProgramAnalysis, ProgramProfile
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..model import Step
|
|
18
|
+
|
|
19
|
+
_RE_INCLUDE = re.compile(
|
|
20
|
+
r"^\s*INCLUDE\s+([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
21
|
+
)
|
|
22
|
+
_RE_ENTRY = re.compile(r"^\s*ENTRY\s+([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE)
|
|
23
|
+
_RE_NAME = re.compile(r"^\s*NAME\s+([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE)
|
|
24
|
+
_RE_LIBRARY = re.compile(
|
|
25
|
+
r"^\s*LIBRARY\s+([A-Za-z0-9$#@,()]+)", re.IGNORECASE | re.MULTILINE
|
|
26
|
+
)
|
|
27
|
+
_RE_ALIAS = re.compile(r"^\s*ALIAS\s+([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _control_text(step: Step) -> str:
|
|
31
|
+
for dd_name in ("SYSIN", "SYSLIN"):
|
|
32
|
+
dd = step.get_dd(dd_name)
|
|
33
|
+
if dd and dd.inline_data:
|
|
34
|
+
return dd.inline_data
|
|
35
|
+
return ""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BinderProfile(ProgramProfile):
|
|
39
|
+
program_names = frozenset({"IEWL", "HEWL"})
|
|
40
|
+
|
|
41
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
42
|
+
text = _control_text(step)
|
|
43
|
+
notes: list[str] = []
|
|
44
|
+
|
|
45
|
+
includes = [m.group(1) for m in _RE_INCLUDE.finditer(text)]
|
|
46
|
+
if includes:
|
|
47
|
+
notes.append(f"INCLUDE {', '.join(includes)}")
|
|
48
|
+
for m in _RE_ENTRY.finditer(text):
|
|
49
|
+
notes.append(f"ENTRY {m.group(1)}")
|
|
50
|
+
for m in _RE_NAME.finditer(text):
|
|
51
|
+
notes.append(f"NAME {m.group(1)}")
|
|
52
|
+
for m in _RE_LIBRARY.finditer(text):
|
|
53
|
+
notes.append(f"LIBRARY {m.group(1)}")
|
|
54
|
+
for m in _RE_ALIAS.finditer(text):
|
|
55
|
+
notes.append(f"ALIAS {m.group(1)}")
|
|
56
|
+
|
|
57
|
+
syslmod = step.get_dd("SYSLMOD")
|
|
58
|
+
if syslmod and syslmod.dataset_name:
|
|
59
|
+
notes.append(f"output module: {syslmod.dataset_name}")
|
|
60
|
+
|
|
61
|
+
summary = (
|
|
62
|
+
f"{step.program}: link-edits {len(includes)} object module(s)"
|
|
63
|
+
if notes
|
|
64
|
+
else f"{step.program} with no recognised control statements"
|
|
65
|
+
)
|
|
66
|
+
return ProgramAnalysis(
|
|
67
|
+
program=step.program or "IEWL",
|
|
68
|
+
step_name=step.name or "",
|
|
69
|
+
summary=summary,
|
|
70
|
+
dataset_events=[],
|
|
71
|
+
notes=notes,
|
|
72
|
+
raw_control=text or None,
|
|
73
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Profile for language-translator (compiler) steps.
|
|
2
|
+
|
|
3
|
+
Covers the common z/OS compilers invoked directly via ``EXEC PGM=``:
|
|
4
|
+
COBOL (``IGYCRCTL``), PL/I (``IEL1CL``, ``IBMZPLI``), and High Level
|
|
5
|
+
Assembler (``ASMA90``). These programs do not read a command-language
|
|
6
|
+
control stream the way utilities do — the "control" is the source itself
|
|
7
|
+
on ``SYSIN`` plus copybook/include libraries on ``SYSLIB``. The profile
|
|
8
|
+
reports which of those DDs are present and where the compiled object
|
|
9
|
+
lands (``SYSLIN``/``SYSPUNCH``), which is enough to model a compile step's
|
|
10
|
+
dataset dependencies without parsing the source language.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from .base import ProgramAnalysis, ProgramProfile
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..model import Step
|
|
21
|
+
|
|
22
|
+
_COMPILER_LABELS = {
|
|
23
|
+
"IGYCRCTL": "COBOL",
|
|
24
|
+
"IEL1CL": "PL/I",
|
|
25
|
+
"IBMZPLI": "PL/I",
|
|
26
|
+
"ASMA90": "Assembler",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CompilerProfile(ProgramProfile):
|
|
31
|
+
program_names = frozenset(_COMPILER_LABELS)
|
|
32
|
+
|
|
33
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
34
|
+
program = step.program or ""
|
|
35
|
+
language = _COMPILER_LABELS.get(program.upper(), "")
|
|
36
|
+
notes: list[str] = []
|
|
37
|
+
|
|
38
|
+
sysin = step.get_dd("SYSIN")
|
|
39
|
+
if sysin and sysin.dataset_name:
|
|
40
|
+
notes.append(f"source: {sysin.dataset_name}")
|
|
41
|
+
elif sysin and sysin.inline_data:
|
|
42
|
+
notes.append("source: in-stream SYSIN")
|
|
43
|
+
|
|
44
|
+
syslib = step.get_dd("SYSLIB")
|
|
45
|
+
if syslib and syslib.dataset_name:
|
|
46
|
+
notes.append(f"copy/include library: {syslib.dataset_name}")
|
|
47
|
+
|
|
48
|
+
for out_dd in ("SYSLIN", "SYSPUNCH"):
|
|
49
|
+
dd = step.get_dd(out_dd)
|
|
50
|
+
if dd and dd.dataset_name:
|
|
51
|
+
notes.append(f"object output ({out_dd}): {dd.dataset_name}")
|
|
52
|
+
|
|
53
|
+
summary = (
|
|
54
|
+
f"{language} compile ({program})" if language else f"compile ({program})"
|
|
55
|
+
)
|
|
56
|
+
if notes:
|
|
57
|
+
summary += f": {len(notes)} relevant DD(s)"
|
|
58
|
+
else:
|
|
59
|
+
summary += " with no recognised SYSIN/SYSLIB/SYSLIN DDs"
|
|
60
|
+
|
|
61
|
+
return ProgramAnalysis(
|
|
62
|
+
program=program or "COMPILER",
|
|
63
|
+
step_name=step.name or "",
|
|
64
|
+
summary=summary,
|
|
65
|
+
dataset_events=[],
|
|
66
|
+
notes=notes,
|
|
67
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Profile for batch ``FTP`` (``PGM=FTP``).
|
|
2
|
+
|
|
3
|
+
z/OS batch FTP jobs drive the client from ``SYSIN`` subcommands: ``OPEN``
|
|
4
|
+
names the remote host, ``USER`` authenticates, ``PUT``/``GET``/``MPUT``/
|
|
5
|
+
``MGET`` transfer files, and ``ASCII``/``BINARY`` set transfer mode. The
|
|
6
|
+
profile surfaces the host and the file operations without attempting to
|
|
7
|
+
resolve remote paths.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..model import Step
|
|
19
|
+
|
|
20
|
+
_RE_OPEN = re.compile(r"^\s*OPEN\s+(\S+)", re.IGNORECASE | re.MULTILINE)
|
|
21
|
+
_RE_USER = re.compile(r"^\s*USER\s+(\S+)", re.IGNORECASE | re.MULTILINE)
|
|
22
|
+
_RE_TRANSFER = re.compile(
|
|
23
|
+
r"^\s*(PUT|GET|MPUT|MGET)\s+(\S+)", re.IGNORECASE | re.MULTILINE
|
|
24
|
+
)
|
|
25
|
+
_RE_MODE = re.compile(r"^\s*(ASCII|BINARY)\b", re.IGNORECASE | re.MULTILINE)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FTPProfile(ProgramProfile):
|
|
29
|
+
program_names = frozenset({"FTP"})
|
|
30
|
+
|
|
31
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
32
|
+
text = _sysin_text(step) or ""
|
|
33
|
+
notes: list[str] = []
|
|
34
|
+
|
|
35
|
+
host_m = _RE_OPEN.search(text)
|
|
36
|
+
if host_m:
|
|
37
|
+
notes.append(f"OPEN {host_m.group(1)}")
|
|
38
|
+
user_m = _RE_USER.search(text)
|
|
39
|
+
if user_m:
|
|
40
|
+
notes.append(f"USER {user_m.group(1)}")
|
|
41
|
+
for m in _RE_TRANSFER.finditer(text):
|
|
42
|
+
notes.append(f"{m.group(1).upper()} {m.group(2)}")
|
|
43
|
+
mode_m = _RE_MODE.search(text)
|
|
44
|
+
if mode_m:
|
|
45
|
+
notes.append(mode_m.group(1).upper())
|
|
46
|
+
|
|
47
|
+
summary = (
|
|
48
|
+
f"FTP: {len(notes)} subcommand(s)"
|
|
49
|
+
if notes
|
|
50
|
+
else "FTP with no recognised SYSIN subcommands"
|
|
51
|
+
)
|
|
52
|
+
return ProgramAnalysis(
|
|
53
|
+
program="FTP",
|
|
54
|
+
step_name=step.name or "",
|
|
55
|
+
summary=summary,
|
|
56
|
+
dataset_events=[],
|
|
57
|
+
notes=notes,
|
|
58
|
+
raw_control=text or None,
|
|
59
|
+
)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Profile for ``ICETOOL`` — the DFSORT batch toolkit.
|
|
2
|
+
|
|
3
|
+
ICETOOL reads a sequence of operator statements from ``TOOLIN`` (one
|
|
4
|
+
operator per logical statement), each naming its input/output DDs via
|
|
5
|
+
``FROM(ddname)`` / ``TO(ddname)`` / ``USING(member)``. Common operators:
|
|
6
|
+
``COPY``, ``SORT``, ``MERGE``, ``COUNT``, ``STATS``, ``SELECT``, ``SPLICE``,
|
|
7
|
+
``UNIQUE``, ``OCCUR``, ``DISPLAY``, ``DEFAULTS``, ``MODE``, ``RESIZE``,
|
|
8
|
+
``VERIFY``, ``WHENSAME``, ``WHENDIFF``, ``DEDUP``, ``RANGE``, ``CORR``.
|
|
9
|
+
|
|
10
|
+
Like :mod:`jclparser.program_profiles.sort`, this is a shallow parse: it
|
|
11
|
+
surfaces which operators ran and which DDs they touched without validating
|
|
12
|
+
field specifications.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ..model import Step
|
|
24
|
+
|
|
25
|
+
_OPERATORS = (
|
|
26
|
+
"COPY",
|
|
27
|
+
"SORT",
|
|
28
|
+
"MERGE",
|
|
29
|
+
"COUNT",
|
|
30
|
+
"STATS",
|
|
31
|
+
"SELECT",
|
|
32
|
+
"SPLICE",
|
|
33
|
+
"UNIQUE",
|
|
34
|
+
"OCCUR",
|
|
35
|
+
"DISPLAY",
|
|
36
|
+
"DEFAULTS",
|
|
37
|
+
"MODE",
|
|
38
|
+
"RESIZE",
|
|
39
|
+
"VERIFY",
|
|
40
|
+
"WHENSAME",
|
|
41
|
+
"WHENDIFF",
|
|
42
|
+
"DEDUP",
|
|
43
|
+
"RANGE",
|
|
44
|
+
"CORR",
|
|
45
|
+
)
|
|
46
|
+
_RE_OPERATOR_LINE = re.compile(
|
|
47
|
+
r"^\s*(" + "|".join(_OPERATORS) + r")\b(.*)$",
|
|
48
|
+
re.IGNORECASE | re.MULTILINE,
|
|
49
|
+
)
|
|
50
|
+
_RE_FROM = re.compile(r"\bFROM\s*\(\s*([A-Za-z0-9$#@]+)\s*\)", re.IGNORECASE)
|
|
51
|
+
_RE_TO = re.compile(r"\bTO\s*\(\s*([A-Za-z0-9$#@]+)\s*\)", re.IGNORECASE)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _toolin_text(step: Step) -> str:
|
|
55
|
+
dd = step.get_dd("TOOLIN")
|
|
56
|
+
if dd and dd.inline_data:
|
|
57
|
+
return dd.inline_data
|
|
58
|
+
# Some jobs route TOOLIN through SYSIN instead.
|
|
59
|
+
return _sysin_text(step) or ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ICETOOLProfile(ProgramProfile):
|
|
63
|
+
program_names = frozenset({"ICETOOL"})
|
|
64
|
+
|
|
65
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
66
|
+
text = _toolin_text(step)
|
|
67
|
+
notes: list[str] = []
|
|
68
|
+
|
|
69
|
+
for m in _RE_OPERATOR_LINE.finditer(text):
|
|
70
|
+
operator = m.group(1).upper()
|
|
71
|
+
tail = m.group(2)
|
|
72
|
+
froms = _RE_FROM.findall(tail)
|
|
73
|
+
tos = _RE_TO.findall(tail)
|
|
74
|
+
parts = []
|
|
75
|
+
if froms:
|
|
76
|
+
parts.append(f"FROM({','.join(froms)})")
|
|
77
|
+
if tos:
|
|
78
|
+
parts.append(f"TO({','.join(tos)})")
|
|
79
|
+
detail = f" {' '.join(parts)}" if parts else ""
|
|
80
|
+
notes.append(f"{operator}{detail}")
|
|
81
|
+
|
|
82
|
+
summary = (
|
|
83
|
+
f"ICETOOL: {len(notes)} operator(s)"
|
|
84
|
+
if notes
|
|
85
|
+
else "ICETOOL with no recognised TOOLIN operators"
|
|
86
|
+
)
|
|
87
|
+
return ProgramAnalysis(
|
|
88
|
+
program="ICETOOL",
|
|
89
|
+
step_name=step.name or "",
|
|
90
|
+
summary=summary,
|
|
91
|
+
dataset_events=[],
|
|
92
|
+
notes=notes,
|
|
93
|
+
raw_control=text or None,
|
|
94
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Profile for ``IEBDG`` — test data generator.
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN``: ``DSD`` names the output
|
|
4
|
+
dataset description, ``FD`` describes a field, and ``CREATE`` requests
|
|
5
|
+
record generation (optionally with a ``REPEAT`` count sub-parameter). The
|
|
6
|
+
generated output lands on ``SYSUT2``, referenced by DDNAME= on the DSD
|
|
7
|
+
statement or the step's SYSUT2 DD.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..model import Step
|
|
19
|
+
|
|
20
|
+
_RE_DSD = re.compile(
|
|
21
|
+
r"^\s*DSD\b[^\n]*DDNAME=([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
22
|
+
)
|
|
23
|
+
_RE_FD = re.compile(
|
|
24
|
+
r"^\s*FD\b[^\n]*NAME=([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
25
|
+
)
|
|
26
|
+
_RE_CREATE = re.compile(
|
|
27
|
+
r"^\s*CREATE\b(?:[^\n]*\bREPEAT=(\d+))?", re.IGNORECASE | re.MULTILINE
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IEBDGProfile(ProgramProfile):
|
|
32
|
+
program_names = frozenset({"IEBDG"})
|
|
33
|
+
|
|
34
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
35
|
+
text = _sysin_text(step) or ""
|
|
36
|
+
notes: list[str] = []
|
|
37
|
+
|
|
38
|
+
for m in _RE_DSD.finditer(text):
|
|
39
|
+
notes.append(f"DSD DDNAME={m.group(1)}")
|
|
40
|
+
field_names = [m.group(1) for m in _RE_FD.finditer(text)]
|
|
41
|
+
if field_names:
|
|
42
|
+
notes.append(f"FD fields: {', '.join(field_names)}")
|
|
43
|
+
for m in _RE_CREATE.finditer(text):
|
|
44
|
+
notes.append(f"CREATE REPEAT={m.group(1)}" if m.group(1) else "CREATE")
|
|
45
|
+
|
|
46
|
+
summary = (
|
|
47
|
+
f"IEBDG: generates test data ({len(field_names)} field(s))"
|
|
48
|
+
if notes
|
|
49
|
+
else "IEBDG with no recognised control statements"
|
|
50
|
+
)
|
|
51
|
+
return ProgramAnalysis(
|
|
52
|
+
program="IEBDG",
|
|
53
|
+
step_name=step.name or "",
|
|
54
|
+
summary=summary,
|
|
55
|
+
dataset_events=[],
|
|
56
|
+
notes=notes,
|
|
57
|
+
raw_control=text or None,
|
|
58
|
+
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
"""Profiles for simple IBM copy utilities: ``IEBGENER`` and
|
|
1
|
+
"""Profiles for simple IBM copy utilities: ``IEBGENER``, ``IEBCOPY``, and
|
|
2
|
+
``ICEGENER`` (the DFSORT-based drop-in replacement for IEBGENER).
|
|
2
3
|
|
|
3
4
|
These programs copy data from one dataset to another. The profile reports
|
|
4
5
|
SYSUT1 (input) and SYSUT2 (output) as the canonical source/destination pair
|
|
@@ -8,27 +9,29 @@ dispositions.
|
|
|
8
9
|
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
|
-
from
|
|
12
|
-
from ..dataset_lifecycle import DatasetEvent
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
13
|
|
|
14
|
+
from ..dataset_lifecycle import DatasetEvent, _parse_disp_tokens
|
|
15
|
+
from .base import ProgramAnalysis, ProgramProfile
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from ..model import Step
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _copy_utility_analysis(step: Step, program: str) -> ProgramAnalysis:
|
|
16
22
|
sysut1 = step.get_dd("SYSUT1")
|
|
17
23
|
sysut2 = step.get_dd("SYSUT2")
|
|
18
24
|
events: list[DatasetEvent] = []
|
|
19
25
|
notes: list[str] = []
|
|
20
26
|
|
|
21
27
|
if sysut1 and sysut1.dataset_name:
|
|
28
|
+
status, _, _ = _parse_disp_tokens(sysut1.disposition)
|
|
22
29
|
events.append(
|
|
23
30
|
DatasetEvent(
|
|
24
31
|
step_name=step.name or "",
|
|
25
32
|
dd_name="SYSUT1",
|
|
26
33
|
action="OPEN_SHARED",
|
|
27
|
-
disp_status=
|
|
28
|
-
.split(",")[0]
|
|
29
|
-
.strip("()")
|
|
30
|
-
.upper()
|
|
31
|
-
or "SHR",
|
|
34
|
+
disp_status=status or "SHR",
|
|
32
35
|
line_number=sysut1.line_number,
|
|
33
36
|
)
|
|
34
37
|
)
|
|
@@ -63,12 +66,19 @@ def _copy_utility_analysis(step, program: str) -> ProgramAnalysis:
|
|
|
63
66
|
class IEBGENERProfile(ProgramProfile):
|
|
64
67
|
program_names = frozenset({"IEBGENER"})
|
|
65
68
|
|
|
66
|
-
def analyze(self, step) -> ProgramAnalysis:
|
|
69
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
67
70
|
return _copy_utility_analysis(step, "IEBGENER")
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
class IEBCOPYProfile(ProgramProfile):
|
|
71
74
|
program_names = frozenset({"IEBCOPY"})
|
|
72
75
|
|
|
73
|
-
def analyze(self, step) -> ProgramAnalysis:
|
|
76
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
74
77
|
return _copy_utility_analysis(step, "IEBCOPY")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ICEGENERProfile(ProgramProfile):
|
|
81
|
+
program_names = frozenset({"ICEGENER"})
|
|
82
|
+
|
|
83
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
84
|
+
return _copy_utility_analysis(step, "ICEGENER")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Profile for ``IEBPTPCH`` — sequential/PDS print & punch utility.
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN``: ``PRINT`` or ``PUNCH`` selects
|
|
4
|
+
the mode, ``TITLE`` supplies a report heading, ``MEMBER`` selects a PDS
|
|
5
|
+
member, and ``RECORD`` narrows the field range printed/punched. Input
|
|
6
|
+
comes from ``SYSUT1``; output goes to ``SYSUT2``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..model import Step
|
|
18
|
+
|
|
19
|
+
_RE_MODE = re.compile(r"^\s*(PRINT|PUNCH)\b", re.IGNORECASE | re.MULTILINE)
|
|
20
|
+
_RE_MEMBER = re.compile(
|
|
21
|
+
r"^\s*MEMBER\s+NAME=([A-Za-z0-9$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
22
|
+
)
|
|
23
|
+
_RE_TITLE = re.compile(r"^\s*TITLE\b", re.IGNORECASE | re.MULTILINE)
|
|
24
|
+
_RE_RECORD = re.compile(r"^\s*RECORD\b", re.IGNORECASE | re.MULTILINE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IEBPTPCHProfile(ProgramProfile):
|
|
28
|
+
program_names = frozenset({"IEBPTPCH"})
|
|
29
|
+
|
|
30
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
31
|
+
text = _sysin_text(step) or ""
|
|
32
|
+
notes: list[str] = []
|
|
33
|
+
|
|
34
|
+
mode_m = _RE_MODE.search(text)
|
|
35
|
+
if mode_m:
|
|
36
|
+
notes.append(mode_m.group(1).upper())
|
|
37
|
+
for m in _RE_MEMBER.finditer(text):
|
|
38
|
+
notes.append(f"MEMBER {m.group(1)}")
|
|
39
|
+
if _RE_TITLE.search(text):
|
|
40
|
+
notes.append("TITLE")
|
|
41
|
+
if _RE_RECORD.search(text):
|
|
42
|
+
notes.append("RECORD (field selection)")
|
|
43
|
+
|
|
44
|
+
sysut1 = step.get_dd("SYSUT1")
|
|
45
|
+
if sysut1 and sysut1.dataset_name:
|
|
46
|
+
notes.append(f"reads {sysut1.dataset_name}")
|
|
47
|
+
|
|
48
|
+
summary = (
|
|
49
|
+
f"IEBPTPCH: {len(notes)} control clause(s)"
|
|
50
|
+
if notes
|
|
51
|
+
else "IEBPTPCH with no recognised control statements"
|
|
52
|
+
)
|
|
53
|
+
return ProgramAnalysis(
|
|
54
|
+
program="IEBPTPCH",
|
|
55
|
+
step_name=step.name or "",
|
|
56
|
+
summary=summary,
|
|
57
|
+
dataset_events=[],
|
|
58
|
+
notes=notes,
|
|
59
|
+
raw_control=text or None,
|
|
60
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Profile for ``IEBUPDTE`` — sequential/PDS member update utility.
|
|
2
|
+
|
|
3
|
+
Control statements in ``SYSIN`` start with ``./`` (a change-control
|
|
4
|
+
statement, not a JCL statement) and use ``./ ADD``, ``./ CHANGE``,
|
|
5
|
+
``./ REPL``, and ``./ NUMBER``, typically with a ``NAME=member`` operand.
|
|
6
|
+
The profile reports the base input (SYSUT1, when present), the output
|
|
7
|
+
(SYSUT2), and which member operations the control stream requests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from ..dataset_lifecycle import DatasetEvent, _parse_disp_tokens
|
|
16
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ..model import Step
|
|
20
|
+
|
|
21
|
+
_RE_CTL = re.compile(
|
|
22
|
+
r"^\./\s*(ADD|CHANGE|REPL|NUMBER)\b(?:[^\n]*\bNAME=([A-Za-z0-9$#@]+))?",
|
|
23
|
+
re.IGNORECASE | re.MULTILINE,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IEBUPDTEProfile(ProgramProfile):
|
|
28
|
+
program_names = frozenset({"IEBUPDTE"})
|
|
29
|
+
|
|
30
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
31
|
+
text = _sysin_text(step) or ""
|
|
32
|
+
notes: list[str] = []
|
|
33
|
+
|
|
34
|
+
for m in _RE_CTL.finditer(text):
|
|
35
|
+
action = m.group(1).upper()
|
|
36
|
+
member = m.group(2)
|
|
37
|
+
notes.append(f"{action} NAME={member}" if member else action)
|
|
38
|
+
|
|
39
|
+
events: list[DatasetEvent] = []
|
|
40
|
+
sysut1 = step.get_dd("SYSUT1")
|
|
41
|
+
sysut2 = step.get_dd("SYSUT2")
|
|
42
|
+
if sysut1 and sysut1.dataset_name:
|
|
43
|
+
status, _, _ = _parse_disp_tokens(sysut1.disposition)
|
|
44
|
+
events.append(
|
|
45
|
+
DatasetEvent(
|
|
46
|
+
step_name=step.name or "",
|
|
47
|
+
dd_name="SYSUT1",
|
|
48
|
+
action="OPEN_SHARED",
|
|
49
|
+
disp_status=status or "SHR",
|
|
50
|
+
line_number=sysut1.line_number,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
if sysut2 and sysut2.dataset_name:
|
|
54
|
+
events.append(
|
|
55
|
+
DatasetEvent(
|
|
56
|
+
step_name=step.name or "",
|
|
57
|
+
dd_name="SYSUT2",
|
|
58
|
+
action="ALLOCATE",
|
|
59
|
+
disp_status="NEW",
|
|
60
|
+
line_number=sysut2.line_number,
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
summary = (
|
|
65
|
+
f"IEBUPDTE: {len(notes)} member operation(s)"
|
|
66
|
+
if notes
|
|
67
|
+
else "IEBUPDTE with no recognised control statements"
|
|
68
|
+
)
|
|
69
|
+
return ProgramAnalysis(
|
|
70
|
+
program="IEBUPDTE",
|
|
71
|
+
step_name=step.name or "",
|
|
72
|
+
summary=summary,
|
|
73
|
+
dataset_events=events,
|
|
74
|
+
notes=notes,
|
|
75
|
+
raw_control=text or None,
|
|
76
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Profile for ``IEHLIST`` — VTOC / PDS directory listing utility.
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN``: ``LISTVTOC``, ``LISTPDS``, and
|
|
4
|
+
``LISTDG`` (the latter also usable as ``LISTIDG``). IEHLIST is read-only —
|
|
5
|
+
it never mutates a dataset — so the profile only reports which listing
|
|
6
|
+
commands ran and, where present, the volume/dataset they target.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..model import Step
|
|
18
|
+
|
|
19
|
+
_RE_LISTVTOC = re.compile(
|
|
20
|
+
r"^\s*LISTVTOC\b[^\n]*VOL=([A-Za-z0-9,=]+)", re.IGNORECASE | re.MULTILINE
|
|
21
|
+
)
|
|
22
|
+
_RE_LISTPDS = re.compile(
|
|
23
|
+
r"^\s*LISTPDS\b[^\n]*DSNAME=([A-Za-z0-9.$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
24
|
+
)
|
|
25
|
+
_RE_LISTDG = re.compile(r"^\s*LIST(?:I)?DG\b", re.IGNORECASE | re.MULTILINE)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IEHLISTProfile(ProgramProfile):
|
|
29
|
+
program_names = frozenset({"IEHLIST"})
|
|
30
|
+
|
|
31
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
32
|
+
text = _sysin_text(step) or ""
|
|
33
|
+
notes: list[str] = []
|
|
34
|
+
|
|
35
|
+
for m in _RE_LISTVTOC.finditer(text):
|
|
36
|
+
notes.append(f"LISTVTOC VOL={m.group(1)}")
|
|
37
|
+
for m in _RE_LISTPDS.finditer(text):
|
|
38
|
+
notes.append(f"LISTPDS DSNAME={m.group(1)}")
|
|
39
|
+
for _ in _RE_LISTDG.finditer(text):
|
|
40
|
+
notes.append("LISTDG")
|
|
41
|
+
|
|
42
|
+
summary = (
|
|
43
|
+
f"IEHLIST: {len(notes)} listing command(s)"
|
|
44
|
+
if notes
|
|
45
|
+
else "IEHLIST with no recognised commands"
|
|
46
|
+
)
|
|
47
|
+
return ProgramAnalysis(
|
|
48
|
+
program="IEHLIST",
|
|
49
|
+
step_name=step.name or "",
|
|
50
|
+
summary=summary,
|
|
51
|
+
dataset_events=[],
|
|
52
|
+
notes=notes,
|
|
53
|
+
raw_control=text or None,
|
|
54
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Profile for ``IEHPROGM`` — catalog/VTOC maintenance utility.
|
|
2
|
+
|
|
3
|
+
Control statements are read from ``SYSIN``: ``SCRATCH`` (delete),
|
|
4
|
+
``RENAME``, ``CATLG``, ``UNCATLG``, ``BLDG``, and ``CONNECT``. Unlike the
|
|
5
|
+
read-only listing in IEHLIST, several of these commands mutate the catalog,
|
|
6
|
+
so — like :mod:`jclparser.program_profiles.idcams` — the profile emits
|
|
7
|
+
:class:`~jclparser.dataset_lifecycle.DatasetEvent` entries for
|
|
8
|
+
``SCRATCH``/``CATLG``/``UNCATLG``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from ..dataset_lifecycle import DatasetEvent
|
|
17
|
+
from .base import ProgramAnalysis, ProgramProfile, _sysin_text
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ..model import Step
|
|
21
|
+
|
|
22
|
+
_RE_SCRATCH = re.compile(
|
|
23
|
+
r"^\s*SCRATCH\b[^\n]*DSNAME=([A-Za-z0-9.$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
24
|
+
)
|
|
25
|
+
_RE_RENAME = re.compile(
|
|
26
|
+
r"^\s*RENAME\b[^\n]*DSNAME=([A-Za-z0-9.$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
27
|
+
)
|
|
28
|
+
_RE_CATLG = re.compile(
|
|
29
|
+
r"^\s*CATLG\b[^\n]*DSNAME=([A-Za-z0-9.$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
30
|
+
)
|
|
31
|
+
_RE_UNCATLG = re.compile(
|
|
32
|
+
r"^\s*UNCATLG\b[^\n]*DSNAME=([A-Za-z0-9.$#@]+)", re.IGNORECASE | re.MULTILINE
|
|
33
|
+
)
|
|
34
|
+
_RE_BLDG = re.compile(r"^\s*BLDG\b", re.IGNORECASE | re.MULTILINE)
|
|
35
|
+
_RE_CONNECT = re.compile(r"^\s*CONNECT\b", re.IGNORECASE | re.MULTILINE)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class IEHPROGMProfile(ProgramProfile):
|
|
39
|
+
program_names = frozenset({"IEHPROGM"})
|
|
40
|
+
|
|
41
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
42
|
+
text = _sysin_text(step) or ""
|
|
43
|
+
events: list[DatasetEvent] = []
|
|
44
|
+
notes: list[str] = []
|
|
45
|
+
|
|
46
|
+
for m in _RE_SCRATCH.finditer(text):
|
|
47
|
+
dsn = m.group(1)
|
|
48
|
+
events.append(
|
|
49
|
+
DatasetEvent(
|
|
50
|
+
step_name=step.name or "",
|
|
51
|
+
dd_name="SYSIN",
|
|
52
|
+
action="DELETE",
|
|
53
|
+
disp_status="OLD",
|
|
54
|
+
disp_normal="DELETE",
|
|
55
|
+
line_number=step.line_number,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
notes.append(f"SCRATCH {dsn}")
|
|
59
|
+
|
|
60
|
+
for m in _RE_RENAME.finditer(text):
|
|
61
|
+
notes.append(f"RENAME {m.group(1)}")
|
|
62
|
+
|
|
63
|
+
for m in _RE_CATLG.finditer(text):
|
|
64
|
+
dsn = m.group(1)
|
|
65
|
+
events.append(
|
|
66
|
+
DatasetEvent(
|
|
67
|
+
step_name=step.name or "",
|
|
68
|
+
dd_name="SYSIN",
|
|
69
|
+
action="ALLOCATE",
|
|
70
|
+
disp_status="NEW",
|
|
71
|
+
disp_normal="CATLG",
|
|
72
|
+
line_number=step.line_number,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
notes.append(f"CATLG {dsn}")
|
|
76
|
+
|
|
77
|
+
for m in _RE_UNCATLG.finditer(text):
|
|
78
|
+
dsn = m.group(1)
|
|
79
|
+
events.append(
|
|
80
|
+
DatasetEvent(
|
|
81
|
+
step_name=step.name or "",
|
|
82
|
+
dd_name="SYSIN",
|
|
83
|
+
action="UNCATALOG",
|
|
84
|
+
disp_status="OLD",
|
|
85
|
+
disp_normal="UNCATLG",
|
|
86
|
+
line_number=step.line_number,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
notes.append(f"UNCATLG {dsn}")
|
|
90
|
+
|
|
91
|
+
if _RE_BLDG.search(text):
|
|
92
|
+
notes.append("BLDG (build generation data group index)")
|
|
93
|
+
if _RE_CONNECT.search(text):
|
|
94
|
+
notes.append("CONNECT (add user catalog connection)")
|
|
95
|
+
|
|
96
|
+
summary = (
|
|
97
|
+
f"IEHPROGM: {len(events)} dataset mutations, {len(notes)} commands"
|
|
98
|
+
if events or notes
|
|
99
|
+
else "IEHPROGM with no recognised commands"
|
|
100
|
+
)
|
|
101
|
+
return ProgramAnalysis(
|
|
102
|
+
program="IEHPROGM",
|
|
103
|
+
step_name=step.name or "",
|
|
104
|
+
summary=summary,
|
|
105
|
+
dataset_events=events,
|
|
106
|
+
notes=notes,
|
|
107
|
+
raw_control=text or None,
|
|
108
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Profile for ``IRXJCL`` — batch REXX exec runner.
|
|
2
|
+
|
|
3
|
+
IRXJCL runs a single REXX exec named as the first token of ``PARM``; any
|
|
4
|
+
remaining PARM text is passed as the exec's argument string. The exec
|
|
5
|
+
source itself is read from ``SYSEXEC`` (a concatenation of PDS
|
|
6
|
+
exec libraries) rather than SYSIN.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from .base import ProgramAnalysis, ProgramProfile
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..model import Step
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class IRXJCLProfile(ProgramProfile):
|
|
20
|
+
program_names = frozenset({"IRXJCL"})
|
|
21
|
+
|
|
22
|
+
def analyze(self, step: Step) -> ProgramAnalysis:
|
|
23
|
+
notes: list[str] = []
|
|
24
|
+
raw_parm = step.parameters.get("PARM")
|
|
25
|
+
# PARM=(subparm,...) is parsed into a list of strings; PARM=value or
|
|
26
|
+
# PARM='value text' stays a plain string. Normalise both to a string
|
|
27
|
+
# before splitting so a parenthesised PARM can't crash this profile.
|
|
28
|
+
if isinstance(raw_parm, list):
|
|
29
|
+
parm = " ".join(str(p) for p in raw_parm)
|
|
30
|
+
else:
|
|
31
|
+
parm = str(raw_parm) if raw_parm else ""
|
|
32
|
+
parts = parm.split(None, 1)
|
|
33
|
+
exec_name = parts[0].upper() if parts else ""
|
|
34
|
+
exec_args = parts[1] if len(parts) > 1 else ""
|
|
35
|
+
|
|
36
|
+
if exec_name:
|
|
37
|
+
notes.append(f"exec: {exec_name}")
|
|
38
|
+
if exec_args:
|
|
39
|
+
notes.append(f"args: {exec_args}")
|
|
40
|
+
|
|
41
|
+
sysexec = step.get_dd("SYSEXEC")
|
|
42
|
+
if sysexec and sysexec.dataset_name:
|
|
43
|
+
notes.append(f"SYSEXEC library: {sysexec.dataset_name}")
|
|
44
|
+
|
|
45
|
+
summary = (
|
|
46
|
+
f"IRXJCL: runs REXX exec {exec_name}"
|
|
47
|
+
if exec_name
|
|
48
|
+
else "IRXJCL with no exec name in PARM"
|
|
49
|
+
)
|
|
50
|
+
return ProgramAnalysis(
|
|
51
|
+
program="IRXJCL",
|
|
52
|
+
step_name=step.name or "",
|
|
53
|
+
summary=summary,
|
|
54
|
+
dataset_events=[],
|
|
55
|
+
notes=notes,
|
|
56
|
+
raw_control=parm or None,
|
|
57
|
+
invoked_programs=[exec_name] if exec_name else [],
|
|
58
|
+
)
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from .base import
|
|
13
|
+
from .base import ProgramAnalysis, ProgramProfile
|
|
14
14
|
|
|
15
15
|
if TYPE_CHECKING:
|
|
16
16
|
from ..model import Job, Step
|
|
@@ -45,7 +45,7 @@ def get_profile(program_name: str | None) -> ProgramProfile | None:
|
|
|
45
45
|
return None
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
def analyze_step(step:
|
|
48
|
+
def analyze_step(step: Step) -> ProgramAnalysis | None:
|
|
49
49
|
"""Run the matching profile (if any) against *step*."""
|
|
50
50
|
profile = get_profile(step.program)
|
|
51
51
|
if profile is None:
|
|
@@ -53,7 +53,7 @@ def analyze_step(step: "Step") -> ProgramAnalysis | None:
|
|
|
53
53
|
return profile.analyze(step)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def analyze_job(job:
|
|
56
|
+
def analyze_job(job: Job) -> list[ProgramAnalysis]:
|
|
57
57
|
"""Run all applicable profiles across every step in *job*."""
|
|
58
58
|
from ..model import iter_execution_steps
|
|
59
59
|
|
|
@@ -71,30 +71,53 @@ def analyze_job(job: "Job") -> list[ProgramAnalysis]:
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def _register_builtins() -> None:
|
|
74
|
-
from .
|
|
75
|
-
from .
|
|
76
|
-
from .
|
|
77
|
-
from .sort import SortProfile
|
|
78
|
-
from .ikjeft01 import IKJEFT01Profile
|
|
74
|
+
from .adrdssu import ADRDSSUProfile
|
|
75
|
+
from .binder import BinderProfile
|
|
76
|
+
from .compilers import CompilerProfile
|
|
79
77
|
from .db2_batch import DB2BatchProfile
|
|
80
78
|
from .dfsrrc00 import DFSRRC00Profile
|
|
79
|
+
from .ftp import FTPProfile
|
|
80
|
+
from .icetool import ICETOOLProfile
|
|
81
|
+
from .idcams import IDCAMSProfile
|
|
82
|
+
from .iebdg import IEBDGProfile
|
|
83
|
+
from .iebgener import ICEGENERProfile, IEBCOPYProfile, IEBGENERProfile
|
|
84
|
+
from .iebptpch import IEBPTPCHProfile
|
|
85
|
+
from .iebupdte import IEBUPDTEProfile
|
|
86
|
+
from .iefbr14 import IEFBR14Profile
|
|
87
|
+
from .iehlist import IEHLISTProfile
|
|
88
|
+
from .iehprogm import IEHPROGMProfile
|
|
89
|
+
from .ikjeft01 import IKJEFT01Profile
|
|
90
|
+
from .irxjcl import IRXJCLProfile
|
|
91
|
+
from .sort import SortProfile
|
|
81
92
|
|
|
82
93
|
register_profile(IEFBR14Profile())
|
|
83
94
|
register_profile(IDCAMSProfile())
|
|
84
95
|
register_profile(IEBGENERProfile())
|
|
85
96
|
register_profile(IEBCOPYProfile())
|
|
97
|
+
register_profile(ICEGENERProfile())
|
|
86
98
|
register_profile(SortProfile())
|
|
87
99
|
register_profile(IKJEFT01Profile())
|
|
88
100
|
register_profile(DB2BatchProfile())
|
|
89
101
|
register_profile(DFSRRC00Profile())
|
|
102
|
+
register_profile(ICETOOLProfile())
|
|
103
|
+
register_profile(ADRDSSUProfile())
|
|
104
|
+
register_profile(IEHLISTProfile())
|
|
105
|
+
register_profile(IEHPROGMProfile())
|
|
106
|
+
register_profile(IEBUPDTEProfile())
|
|
107
|
+
register_profile(IEBPTPCHProfile())
|
|
108
|
+
register_profile(IEBDGProfile())
|
|
109
|
+
register_profile(BinderProfile())
|
|
110
|
+
register_profile(CompilerProfile())
|
|
111
|
+
register_profile(IRXJCLProfile())
|
|
112
|
+
register_profile(FTPProfile())
|
|
90
113
|
|
|
91
114
|
|
|
92
115
|
_register_builtins()
|
|
93
116
|
|
|
94
117
|
|
|
95
118
|
__all__ = [
|
|
96
|
-
"register_profile",
|
|
97
|
-
"get_profile",
|
|
98
|
-
"analyze_step",
|
|
99
119
|
"analyze_job",
|
|
120
|
+
"analyze_step",
|
|
121
|
+
"get_profile",
|
|
122
|
+
"register_profile",
|
|
100
123
|
]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jcl-parser"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Parse IBM JCL (Job Control Language) scripts into Python objects"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "JCL Parser Team", email = "team@jclparser.dev"}
|
|
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
|