jcl-parser 2.0.3__tar.gz → 2.1.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-2.0.3 → jcl_parser-2.1.0}/PKG-INFO +2 -2
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/PKG-INFO +2 -2
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/SOURCES.txt +1 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/__init__.py +17 -2
- jcl_parser-2.1.0/jclparser/_include_origins.py +126 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/_job_builder.py +164 -52
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/_model_factories.py +28 -1
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/_post_parse.py +163 -89
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/_source_scanner.py +109 -38
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/_statement_parser.py +72 -7
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/cli.py +32 -4
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/condition_parser.py +18 -12
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/continuation_handler.py +46 -21
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/dataset_lifecycle.py +68 -31
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/errors.py +13 -7
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/explosion.py +390 -169
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/include_loader.py +173 -92
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/lint/rules.py +6 -6
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/metrics.py +4 -1
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/model.py +11 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/parser.py +80 -24
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/plugins/base.py +7 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/proc_loader.py +18 -6
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/_control.py +6 -3
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/_invoked.py +2 -1
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/base.py +8 -3
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/binder.py +2 -1
- jcl_parser-2.1.0/jclparser/program_profiles/ftp.py +187 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/icetool.py +2 -1
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/sort.py +65 -9
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/rc_analysis.py +60 -7
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/sql_detector.py +56 -17
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/utils.py +69 -3
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/validators.py +135 -76
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/pyproject.toml +2 -2
- jcl_parser-2.0.3/jclparser/program_profiles/ftp.py +0 -59
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/LICENSE +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/README.md +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/dependency_links.txt +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/entry_points.txt +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/requires.txt +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jcl_parser.egg-info/top_level.txt +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/__main__.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/builtin_procs.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/jcl_file_type.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/jes_statements.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/lint/__init__.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/lint/base.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/lint/config.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/lint/runner.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/parameter_ast.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/parameter_validators.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/plugins/__init__.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/profiling.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/__init__.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/adrdssu.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/bpxbatch.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/compilers.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/db2_batch.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/dfsrrc00.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/dsnutilb.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/idcams.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iebcompr.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iebdg.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iebgener.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iebptpch.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iebupdte.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iefbr14.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iehinitt.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iehlist.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/iehprogm.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/ikjeft01.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/irxjcl.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/program_profiles/registry.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/py.typed +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/jclparser/system_symbols.py +0 -0
- {jcl_parser-2.0.3 → jcl_parser-2.1.0}/setup.cfg +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jcl-parser
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
|
|
5
5
|
Author-email: Samuel Dion <sam94dion@gmail.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/landparty/jcl-parser
|
|
8
8
|
Project-URL: Repository, https://github.com/landparty/jcl-parser
|
|
9
|
-
Project-URL: Documentation, https://github.com/landparty/jcl-parser/tree/main
|
|
9
|
+
Project-URL: Documentation, https://github.com/landparty/jcl-parser/tree/main/.okf
|
|
10
10
|
Project-URL: Bug Reports, https://github.com/landparty/jcl-parser/issues
|
|
11
11
|
Keywords: jcl,mainframe,parser,ibm,job-control-language
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jcl-parser
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Parse IBM JCL (Job Control Language) scripts into Python objects
|
|
5
5
|
Author-email: Samuel Dion <sam94dion@gmail.com>
|
|
6
6
|
License: AGPL-3.0-or-later
|
|
7
7
|
Project-URL: Homepage, https://github.com/landparty/jcl-parser
|
|
8
8
|
Project-URL: Repository, https://github.com/landparty/jcl-parser
|
|
9
|
-
Project-URL: Documentation, https://github.com/landparty/jcl-parser/tree/main
|
|
9
|
+
Project-URL: Documentation, https://github.com/landparty/jcl-parser/tree/main/.okf
|
|
10
10
|
Project-URL: Bug Reports, https://github.com/landparty/jcl-parser/issues
|
|
11
11
|
Keywords: jcl,mainframe,parser,ibm,job-control-language
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
@@ -20,7 +20,7 @@ application, for example::
|
|
|
20
20
|
import logging as _logging
|
|
21
21
|
|
|
22
22
|
# Package metadata
|
|
23
|
-
__version__ = "2.0
|
|
23
|
+
__version__ = "2.1.0" # x-release-please-version
|
|
24
24
|
__author__ = "JCL Parser Team"
|
|
25
25
|
__description__ = "Parse IBM JCL scripts into Python objects"
|
|
26
26
|
|
|
@@ -66,7 +66,9 @@ from .include_loader import (
|
|
|
66
66
|
DictMemberLoader,
|
|
67
67
|
IncludeMemberLoader,
|
|
68
68
|
JCLLIBMemberLoader,
|
|
69
|
+
LineOrigin,
|
|
69
70
|
resolve_includes,
|
|
71
|
+
resolve_includes_with_origins,
|
|
70
72
|
)
|
|
71
73
|
from .jcl_file_type import (
|
|
72
74
|
FileType,
|
|
@@ -100,7 +102,16 @@ from .lint import (
|
|
|
100
102
|
to_sarif as lint_to_sarif,
|
|
101
103
|
)
|
|
102
104
|
from .metrics import JobMetrics, compute_metrics
|
|
103
|
-
from .model import
|
|
105
|
+
from .model import (
|
|
106
|
+
ConditionalBlock,
|
|
107
|
+
DDStatement,
|
|
108
|
+
Job,
|
|
109
|
+
OutputStatement,
|
|
110
|
+
Parameter,
|
|
111
|
+
Statement,
|
|
112
|
+
Step,
|
|
113
|
+
iter_execution_steps,
|
|
114
|
+
)
|
|
104
115
|
from .parser import parse_jcl, parse_statement, validate_jcl
|
|
105
116
|
from .plugins import (
|
|
106
117
|
ParserConfig,
|
|
@@ -172,6 +183,8 @@ __all__ = [ # noqa: RUF022 — grouped by API tier, not alphabetically
|
|
|
172
183
|
"Parameter",
|
|
173
184
|
"Statement",
|
|
174
185
|
"OutputStatement",
|
|
186
|
+
"ConditionalBlock",
|
|
187
|
+
"iter_execution_steps",
|
|
175
188
|
"JCLParseError",
|
|
176
189
|
"JCLSyntaxError",
|
|
177
190
|
"JCLValidationError",
|
|
@@ -230,7 +243,9 @@ __all__ = [ # noqa: RUF022 — grouped by API tier, not alphabetically
|
|
|
230
243
|
"IncludeMemberLoader",
|
|
231
244
|
"DictMemberLoader",
|
|
232
245
|
"JCLLIBMemberLoader",
|
|
246
|
+
"LineOrigin",
|
|
233
247
|
"resolve_includes",
|
|
248
|
+
"resolve_includes_with_origins",
|
|
234
249
|
# ── Tier 2: Dataset lifecycle analysis ───────────────────────────────────
|
|
235
250
|
"DatasetEvent",
|
|
236
251
|
"analyze_dataset_lifecycle",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Report INCLUDE-expanded line numbers against the main source (issue #118).
|
|
2
|
+
|
|
3
|
+
``// INCLUDE MEMBER=`` statements are expanded before the source is scanned,
|
|
4
|
+
so every line number the parser computes counts the inlined member lines.
|
|
5
|
+
:class:`IncludeLineMap` translates such a number back: a main-source line to
|
|
6
|
+
its own line, a member line to the line of the ``// INCLUDE`` statement in
|
|
7
|
+
the main source (the outermost one, for a nested INCLUDE). Messages about a
|
|
8
|
+
member line also name the member and the line within it::
|
|
9
|
+
|
|
10
|
+
Line 2 (INCLUDE member COMMON, line 3): EXEC statement missing ...
|
|
11
|
+
|
|
12
|
+
The parse runs on the expanded text unchanged; :func:`_parse_jcl_core
|
|
13
|
+
<jclparser.parser._parse_jcl_core>` applies the translation once, at the end
|
|
14
|
+
(:meth:`IncludeLineMap.relabel_job`) or to the exception that ends the parse
|
|
15
|
+
(:meth:`IncludeLineMap.relabel_error`).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import dataclasses
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from .errors import JCLParseError
|
|
26
|
+
from .include_loader import LineOrigin
|
|
27
|
+
from .model import Job, Procedure
|
|
28
|
+
|
|
29
|
+
_LEADING_LINE = re.compile(r"^Line (\d+)")
|
|
30
|
+
_INNER_LINE = re.compile(r"\bon line (\d+)\b")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IncludeLineMap:
|
|
34
|
+
"""Line number translation for one INCLUDE-expanded source."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, origins: list[LineOrigin]) -> None:
|
|
37
|
+
self._origins = origins
|
|
38
|
+
|
|
39
|
+
def origin(self, line_number: int) -> LineOrigin | None:
|
|
40
|
+
"""Where expanded line *line_number* came from (None if unknown)."""
|
|
41
|
+
if 1 <= line_number <= len(self._origins):
|
|
42
|
+
return self._origins[line_number - 1]
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def translate(self, line_number: int) -> int:
|
|
46
|
+
"""The main-source line of expanded line *line_number*."""
|
|
47
|
+
origin = self.origin(line_number)
|
|
48
|
+
return origin.line if origin is not None else line_number
|
|
49
|
+
|
|
50
|
+
def note(self, line_number: int) -> str:
|
|
51
|
+
"""``"INCLUDE member M, line k"`` for a member line, else ``""``."""
|
|
52
|
+
origin = self.origin(line_number)
|
|
53
|
+
if origin is None or origin.member is None:
|
|
54
|
+
return ""
|
|
55
|
+
return f"INCLUDE member {origin.member}, line {origin.member_line}"
|
|
56
|
+
|
|
57
|
+
def label(self, line_number: int) -> str:
|
|
58
|
+
"""``"Line N"``, plus the member note for a member line."""
|
|
59
|
+
note = self.note(line_number)
|
|
60
|
+
main_line = self.translate(line_number)
|
|
61
|
+
return f"Line {main_line} ({note})" if note else f"Line {main_line}"
|
|
62
|
+
|
|
63
|
+
def relabel_text(self, text: str) -> str:
|
|
64
|
+
"""Translate the ``Line N`` prefix (and ``on line N``) of a message."""
|
|
65
|
+
text = _LEADING_LINE.sub(lambda m: self.label(int(m.group(1))), text, 1)
|
|
66
|
+
return _INNER_LINE.sub(
|
|
67
|
+
lambda m: f"on line {self.translate(int(m.group(1)))}", text
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def relabel_error(self, exc: JCLParseError) -> None:
|
|
71
|
+
"""Translate the line number of the exception that ended the parse."""
|
|
72
|
+
if exc.line_number <= 0:
|
|
73
|
+
return
|
|
74
|
+
exc.include_note = self.note(exc.line_number)
|
|
75
|
+
exc.line_number = self.translate(exc.line_number)
|
|
76
|
+
exc.args = (exc._format_message(),)
|
|
77
|
+
|
|
78
|
+
def relabel_job(self, job: Job, own_procedures: Iterable[Procedure]) -> None:
|
|
79
|
+
"""Translate every line number on *job* and in its warnings.
|
|
80
|
+
|
|
81
|
+
Only the procedures in *own_procedures* -- the in-stream ones, read
|
|
82
|
+
from this source -- are relabelled: an external or built-in PROC
|
|
83
|
+
carries line numbers of its own library member.
|
|
84
|
+
"""
|
|
85
|
+
own = {id(proc) for proc in own_procedures}
|
|
86
|
+
skip = {id(p) for p in job.procedures if id(p) not in own}
|
|
87
|
+
self._relabel_object(job, skip)
|
|
88
|
+
job.warnings[:] = [self.relabel_text(w) for w in job.warnings]
|
|
89
|
+
job.unresolved_output_refs[:] = [
|
|
90
|
+
(step_name, ref, self.translate(line))
|
|
91
|
+
for step_name, ref, line in job.unresolved_output_refs
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
def _relabel_object(self, obj: Any, seen: set[int]) -> None:
|
|
95
|
+
"""Translate ``line_number`` on *obj* and every model object under it."""
|
|
96
|
+
if id(obj) in seen:
|
|
97
|
+
return
|
|
98
|
+
seen.add(id(obj))
|
|
99
|
+
if isinstance(obj, (list, tuple)):
|
|
100
|
+
for item in obj:
|
|
101
|
+
self._relabel_object(item, seen)
|
|
102
|
+
elif isinstance(obj, dict):
|
|
103
|
+
for item in obj.values():
|
|
104
|
+
self._relabel_object(item, seen)
|
|
105
|
+
elif _is_model_object(obj):
|
|
106
|
+
self._relabel_fields(obj, seen)
|
|
107
|
+
|
|
108
|
+
def _relabel_fields(self, obj: Any, seen: set[int]) -> None:
|
|
109
|
+
line_number = getattr(obj, "line_number", None)
|
|
110
|
+
if isinstance(line_number, int) and line_number > 0:
|
|
111
|
+
try:
|
|
112
|
+
obj.line_number = self.translate(line_number)
|
|
113
|
+
except dataclasses.FrozenInstanceError:
|
|
114
|
+
pass
|
|
115
|
+
for field in dataclasses.fields(obj):
|
|
116
|
+
if not field.name.startswith("_") and field.name != "line_number":
|
|
117
|
+
self._relabel_object(getattr(obj, field.name), seen)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_model_object(obj: Any) -> bool:
|
|
121
|
+
"""A jclparser dataclass instance (not a class, not a foreign type)."""
|
|
122
|
+
return (
|
|
123
|
+
dataclasses.is_dataclass(obj)
|
|
124
|
+
and not isinstance(obj, type)
|
|
125
|
+
and type(obj).__module__.startswith("jclparser")
|
|
126
|
+
)
|
|
@@ -34,6 +34,7 @@ from ._model_factories import (
|
|
|
34
34
|
_create_step_from_statement,
|
|
35
35
|
_exec_has_program_spec,
|
|
36
36
|
_has_unresolved_symbol_ref,
|
|
37
|
+
_resolve_jcllib_library,
|
|
37
38
|
_resolve_set_value,
|
|
38
39
|
)
|
|
39
40
|
from ._post_parse import (
|
|
@@ -59,6 +60,8 @@ from .utils import (
|
|
|
59
60
|
instream_delimiter,
|
|
60
61
|
is_positional_key,
|
|
61
62
|
jcl_value_text,
|
|
63
|
+
jes_xmit_delimiter,
|
|
64
|
+
xmit_delimiter,
|
|
62
65
|
)
|
|
63
66
|
|
|
64
67
|
logger = logging.getLogger(__name__)
|
|
@@ -103,8 +106,8 @@ class JobBuilder:
|
|
|
103
106
|
|
|
104
107
|
# Statement type -> name of its ``_on_<type>`` handler. Every type in
|
|
105
108
|
# ``_STATEMENT_TYPES`` must appear here, even when its handler is a
|
|
106
|
-
# deliberate no-op (NULL, ENDCNTL)
|
|
107
|
-
#
|
|
109
|
+
# deliberate no-op (NULL, ENDCNTL). Any other type -- a custom type
|
|
110
|
+
# returned by a StatementPlugin -- is kept in ``Job.custom_statements``.
|
|
108
111
|
_HANDLERS: ClassVar[dict[str, str]] = {
|
|
109
112
|
"JOB": "_on_job",
|
|
110
113
|
"EXEC": "_on_exec",
|
|
@@ -122,10 +125,12 @@ class JobBuilder:
|
|
|
122
125
|
"JES": "_on_jes",
|
|
123
126
|
"OUTPUT": "_on_output",
|
|
124
127
|
"NULL": "_on_null",
|
|
125
|
-
# No richer model representation for COMMAND / SCHEDULE / CNTL
|
|
128
|
+
# No richer model representation for COMMAND / SCHEDULE / CNTL /
|
|
129
|
+
# NOTIFY (z/OS 2.3).
|
|
126
130
|
"COMMAND": "_on_control",
|
|
127
131
|
"SCHEDULE": "_on_control",
|
|
128
132
|
"CNTL": "_on_control",
|
|
133
|
+
"NOTIFY": "_on_control",
|
|
129
134
|
"ENDCNTL": "_on_endcntl",
|
|
130
135
|
"UNKNOWN": "_on_unknown",
|
|
131
136
|
"XMIT": "_on_xmit",
|
|
@@ -151,8 +156,15 @@ class JobBuilder:
|
|
|
151
156
|
self.pending_procedures: list[Procedure] = []
|
|
152
157
|
self.current_delimiter = "/*"
|
|
153
158
|
self.current_terminates_on_slash = False
|
|
154
|
-
#:
|
|
159
|
+
#: Job-level DDs that appear before the first job-level EXEC (e.g.
|
|
160
|
+
#: ``SYSCHK``/``JOBCAT``); attached to that step, never to a step of an
|
|
161
|
+
#: in-stream PROC defined in between (#103).
|
|
155
162
|
self.orphaned_dds: list[DDStatement] = []
|
|
163
|
+
#: DDs inside a PROC/PEND body before the procedure's first EXEC.
|
|
164
|
+
self.proc_orphaned_dds: list[DDStatement] = []
|
|
165
|
+
#: ``len(conditional_stack)`` when the open PROC began: IF blocks at
|
|
166
|
+
#: or below it belong to the enclosing job, not to the procedure.
|
|
167
|
+
self.proc_cond_depth = 0
|
|
156
168
|
#: True while accumulating JOBLIB concatenation lines
|
|
157
169
|
self.in_joblib_concat = False
|
|
158
170
|
#: Qualified name (``PROCSTEP.DDNAME``) of the override DD the current
|
|
@@ -185,6 +197,9 @@ class JobBuilder:
|
|
|
185
197
|
self.pending_conditionals: list[ConditionalBlock] = []
|
|
186
198
|
self.pending_steps: list[Step | ConditionalBlock] = []
|
|
187
199
|
self.pending_control: list[Statement] = []
|
|
200
|
+
#: Statements of a type the builder has no handler for (e.g. a custom
|
|
201
|
+
#: type returned by a StatementPlugin) seen before the JOB statement.
|
|
202
|
+
self.pending_custom: list[Statement] = []
|
|
188
203
|
#: The open ``CNTL`` block, whose program control records are collected
|
|
189
204
|
#: verbatim until its ``ENDCNTL``.
|
|
190
205
|
self.current_cntl: Statement | None = None
|
|
@@ -284,13 +299,17 @@ class JobBuilder:
|
|
|
284
299
|
# ------------------------------------------------------------------ #
|
|
285
300
|
|
|
286
301
|
def handle_statement(self, stmt: Statement, line_num: int) -> None:
|
|
287
|
-
"""Record statement warnings, then dispatch to the type handler.
|
|
302
|
+
"""Record statement warnings, then dispatch to the type handler.
|
|
303
|
+
|
|
304
|
+
A statement whose type has no handler in :attr:`_HANDLERS` (e.g. a
|
|
305
|
+
custom type returned by a :class:`~jclparser.plugins.StatementPlugin`)
|
|
306
|
+
is kept verbatim in :attr:`Job.custom_statements` (#130).
|
|
307
|
+
"""
|
|
288
308
|
if stmt.warnings:
|
|
289
309
|
self._warnings_sink().extend(stmt.warnings)
|
|
290
310
|
|
|
291
|
-
handler_name = self._HANDLERS.get(stmt.type)
|
|
292
|
-
|
|
293
|
-
getattr(self, handler_name)(stmt, line_num)
|
|
311
|
+
handler_name = self._HANDLERS.get(stmt.type, "_on_custom")
|
|
312
|
+
getattr(self, handler_name)(stmt, line_num)
|
|
294
313
|
|
|
295
314
|
# ------------------------------------------------------------------ #
|
|
296
315
|
# Per-statement-type handlers
|
|
@@ -327,24 +346,19 @@ class JobBuilder:
|
|
|
327
346
|
self.in_joblib_concat = False # JOBLIB concatenation ends at first EXEC
|
|
328
347
|
self.concat_override_key = None
|
|
329
348
|
|
|
330
|
-
# Attach
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
# Add step to procedure if we're inside PROC/PEND, otherwise to job
|
|
337
|
-
if self.conditional_stack:
|
|
338
|
-
active_cond, active_branch = self.conditional_stack[-1]
|
|
339
|
-
if active_branch == "THEN":
|
|
340
|
-
active_cond.add_then_step(self.current_step)
|
|
341
|
-
else:
|
|
342
|
-
active_cond.add_else_step(self.current_step)
|
|
343
|
-
elif self.current_procedure is not None:
|
|
344
|
-
self.current_procedure.steps.append(self.current_step)
|
|
349
|
+
# Attach DDs seen before the first EXEC of this scope to this step.
|
|
350
|
+
# Job-level DDs (SYSCHK, JOBCAT, ...) wait for the first job-level
|
|
351
|
+
# EXEC: a step inside an in-stream PROC definition must never pick
|
|
352
|
+
# them up, or every expansion of the PROC would copy them (#103).
|
|
353
|
+
if self.current_procedure is not None:
|
|
354
|
+
pending_dds, self.proc_orphaned_dds = self.proc_orphaned_dds, []
|
|
345
355
|
else:
|
|
346
|
-
|
|
347
|
-
|
|
356
|
+
pending_dds, self.orphaned_dds = self.orphaned_dds, []
|
|
357
|
+
for dd in pending_dds:
|
|
358
|
+
self.current_step.add_dd(dd)
|
|
359
|
+
|
|
360
|
+
# Add step to the open IF branch, the PROC being defined, or the job
|
|
361
|
+
self._attach(self.current_step)
|
|
348
362
|
|
|
349
363
|
def _on_dd(self, stmt: Statement, line_num: int) -> None:
|
|
350
364
|
# Create the DD statement
|
|
@@ -360,9 +374,12 @@ class JobBuilder:
|
|
|
360
374
|
elif not stmt.name and self.in_joblib_concat and self.job is not None:
|
|
361
375
|
# Empty-name DD concatenating onto the JOBLIB group
|
|
362
376
|
self.job.joblib_dds.append(current_dd)
|
|
377
|
+
elif self.current_procedure is not None:
|
|
378
|
+
# DD in a PROC body before its first EXEC
|
|
379
|
+
self.proc_orphaned_dds.append(current_dd)
|
|
363
380
|
else:
|
|
364
381
|
self.in_joblib_concat = False
|
|
365
|
-
#
|
|
382
|
+
# Job-level DD: attached to the next job-level step
|
|
366
383
|
self.orphaned_dds.append(current_dd)
|
|
367
384
|
elif stmt.name and "." in stmt.name:
|
|
368
385
|
# This is a DD override for a procedure step
|
|
@@ -428,14 +445,43 @@ class JobBuilder:
|
|
|
428
445
|
self.job.add_procedure(self.current_procedure)
|
|
429
446
|
else:
|
|
430
447
|
self.pending_procedures.append(self.current_procedure)
|
|
448
|
+
# IF blocks already open belong to the job, not to this procedure.
|
|
449
|
+
self.proc_cond_depth = len(self.conditional_stack)
|
|
431
450
|
# Reset current_step since we're now inside procedure
|
|
432
451
|
self.current_step = None
|
|
452
|
+
self.in_joblib_concat = False
|
|
453
|
+
self.proc_orphaned_dds = []
|
|
433
454
|
|
|
434
455
|
def _on_pend(self, stmt: Statement, line_num: int) -> None:
|
|
435
|
-
# End of procedure definition - stop tracking
|
|
456
|
+
# End of procedure definition - stop tracking. PEND closes the PROC
|
|
457
|
+
# scope: an IF opened in the body and never closed by ENDIF ends here
|
|
458
|
+
# (it is already attached to the procedure), so later job steps are
|
|
459
|
+
# not read into it (#102).
|
|
460
|
+
procedure = self.current_procedure
|
|
461
|
+
if procedure is not None:
|
|
462
|
+
while len(self.conditional_stack) > self.proc_cond_depth:
|
|
463
|
+
cond, _branch = self.conditional_stack.pop()
|
|
464
|
+
self._warnings_sink().append(
|
|
465
|
+
f"Line {cond.line_number}: IF block in PROC {procedure.name} "
|
|
466
|
+
f"has no matching ENDIF; closed at PEND (line {line_num})"
|
|
467
|
+
)
|
|
468
|
+
self._warn_proc_orphaned_dds(procedure, line_num)
|
|
469
|
+
self.proc_orphaned_dds = []
|
|
470
|
+
self.proc_cond_depth = 0
|
|
436
471
|
self.current_procedure = None
|
|
437
472
|
self.current_step = None
|
|
438
473
|
|
|
474
|
+
def _warn_proc_orphaned_dds(self, procedure: Procedure, line_num: int) -> None:
|
|
475
|
+
"""Report DDs of a PROC body that had no EXEC step to attach to."""
|
|
476
|
+
if self.proc_orphaned_dds:
|
|
477
|
+
dd_names = ", ".join(
|
|
478
|
+
dd.name or "(unnamed)" for dd in self.proc_orphaned_dds
|
|
479
|
+
)
|
|
480
|
+
self._warnings_sink().append(
|
|
481
|
+
f"Line {line_num}: PROC {procedure.name} has DD statement(s) "
|
|
482
|
+
f"but no EXEC step to attach them to; discarded: {dd_names}"
|
|
483
|
+
)
|
|
484
|
+
|
|
439
485
|
def _on_set(self, stmt: Statement, line_num: int) -> None:
|
|
440
486
|
# SET symbol=value statement. A SET before the JOB statement (or in a
|
|
441
487
|
# JOB-less member) is buffered rather than dropped; finish() flushes it.
|
|
@@ -507,11 +553,26 @@ class JobBuilder:
|
|
|
507
553
|
order_value = stmt.parameters["ORDER"]
|
|
508
554
|
sink = self.job.jcllib_order if self.job else self.pending_jcllib
|
|
509
555
|
# ORDER parameter might already be parsed as a list
|
|
556
|
+
libs: list[str] = []
|
|
510
557
|
if isinstance(order_value, list):
|
|
511
|
-
|
|
558
|
+
libs = [str(lib) for lib in order_value]
|
|
512
559
|
elif isinstance(order_value, str):
|
|
513
|
-
libs = order_value.strip("()").split(",")
|
|
514
|
-
|
|
560
|
+
libs = [lib.strip() for lib in order_value.strip("()").split(",")]
|
|
561
|
+
# Only the SETs coded before this statement are in effect here.
|
|
562
|
+
symbols = (
|
|
563
|
+
{**self.pending_sets, **self.job.set_statements}
|
|
564
|
+
if self.job
|
|
565
|
+
else self.pending_sets
|
|
566
|
+
)
|
|
567
|
+
for lib in libs:
|
|
568
|
+
resolved = _resolve_jcllib_library(lib, symbols)
|
|
569
|
+
if resolved is None:
|
|
570
|
+
self._warnings_sink().append(
|
|
571
|
+
f"Line {stmt.line_number}: JCLLIB ORDER library {lib} "
|
|
572
|
+
"contains an unresolved symbol; kept as written"
|
|
573
|
+
)
|
|
574
|
+
resolved = lib
|
|
575
|
+
sink.append(resolved)
|
|
515
576
|
|
|
516
577
|
def _on_include(self, stmt: Statement, line_num: int) -> None:
|
|
517
578
|
# INCLUDE MEMBER=name
|
|
@@ -554,20 +615,32 @@ class JobBuilder:
|
|
|
554
615
|
condition = condition[:-5]
|
|
555
616
|
return condition
|
|
556
617
|
|
|
557
|
-
def
|
|
558
|
-
"""
|
|
559
|
-
|
|
560
|
-
|
|
618
|
+
def _innermost_if(self) -> tuple[ConditionalBlock, str] | None:
|
|
619
|
+
"""The innermost IF block open in the current scope, or None.
|
|
620
|
+
|
|
621
|
+
Inside a PROC/PEND body only IF blocks opened in that body count; an
|
|
622
|
+
IF the job opened before the PROC statement stays the job's (#102).
|
|
623
|
+
"""
|
|
624
|
+
floor = self.proc_cond_depth if self.current_procedure is not None else 0
|
|
625
|
+
if len(self.conditional_stack) > floor:
|
|
626
|
+
return self.conditional_stack[-1]
|
|
627
|
+
return None
|
|
628
|
+
|
|
629
|
+
def _attach(self, item: Step | ConditionalBlock) -> None:
|
|
630
|
+
"""Place a step or IF block in the open branch, PROC, job or buffer."""
|
|
631
|
+
open_if = self._innermost_if()
|
|
632
|
+
if open_if is not None:
|
|
633
|
+
active_cond, active_branch = open_if
|
|
561
634
|
if active_branch == "THEN":
|
|
562
|
-
active_cond.add_then_step(
|
|
635
|
+
active_cond.add_then_step(item)
|
|
563
636
|
else:
|
|
564
|
-
active_cond.add_else_step(
|
|
637
|
+
active_cond.add_else_step(item)
|
|
565
638
|
elif self.current_procedure is not None:
|
|
566
|
-
self.current_procedure.steps.append(
|
|
639
|
+
self.current_procedure.steps.append(item)
|
|
567
640
|
elif self.job is not None:
|
|
568
|
-
self.job.steps.append(
|
|
641
|
+
self.job.steps.append(item)
|
|
569
642
|
else:
|
|
570
|
-
self.pending_steps.append(
|
|
643
|
+
self.pending_steps.append(item)
|
|
571
644
|
|
|
572
645
|
def _open_if(self, stmt: Statement) -> None:
|
|
573
646
|
"""Handle ``IF``: record a new :class:`ConditionalBlock` and enter it."""
|
|
@@ -581,14 +654,14 @@ class JobBuilder:
|
|
|
581
654
|
self.job.add_conditional(conditional)
|
|
582
655
|
else:
|
|
583
656
|
self.pending_conditionals.append(conditional)
|
|
584
|
-
self.
|
|
657
|
+
self._attach(conditional)
|
|
585
658
|
self.conditional_stack.append((conditional, "THEN"))
|
|
586
659
|
|
|
587
660
|
def _switch_branch(self, branch: str, line_num: int) -> None:
|
|
588
661
|
"""Handle ``THEN`` / ``ELSE``: make *branch* the open IF's active one."""
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
self.conditional_stack
|
|
662
|
+
open_if = self._innermost_if()
|
|
663
|
+
if open_if is not None:
|
|
664
|
+
self.conditional_stack[-1] = (open_if[0], branch)
|
|
592
665
|
else:
|
|
593
666
|
self._warnings_sink().append(
|
|
594
667
|
f"Line {line_num}: {branch} with no matching IF; ignored"
|
|
@@ -596,7 +669,7 @@ class JobBuilder:
|
|
|
596
669
|
|
|
597
670
|
def _close_if(self, line_num: int) -> None:
|
|
598
671
|
"""Handle ``ENDIF``: leave the innermost open IF block."""
|
|
599
|
-
if self.
|
|
672
|
+
if self._innermost_if() is not None:
|
|
600
673
|
self.conditional_stack.pop()
|
|
601
674
|
else:
|
|
602
675
|
self._warnings_sink().append(
|
|
@@ -610,6 +683,13 @@ class JobBuilder:
|
|
|
610
683
|
from .jes_statements import parse_jes_control
|
|
611
684
|
|
|
612
685
|
typed = parse_jes_control(stmt.name, raw_params, stmt.line_number)
|
|
686
|
+
if stmt.name.upper() == "XMIT":
|
|
687
|
+
# The records that follow are a job stream for another node,
|
|
688
|
+
# not JCL of this job; they end at /* or DLM= (issue #117).
|
|
689
|
+
self.current_dd = None
|
|
690
|
+
self._start_transmitted_data(
|
|
691
|
+
jes_xmit_delimiter(raw_params), stmt.line_number
|
|
692
|
+
)
|
|
613
693
|
if self.dialect == "jes3" and stmt.name.upper() not in _JES3_SLASH_STAR:
|
|
614
694
|
# JES3 does not process JES2 control statements: keep the text,
|
|
615
695
|
# but do not interpret it, and say so.
|
|
@@ -649,7 +729,7 @@ class JobBuilder:
|
|
|
649
729
|
pass
|
|
650
730
|
|
|
651
731
|
def _on_control(self, stmt: Statement, line_num: int) -> None:
|
|
652
|
-
"""Record a COMMAND / SCHEDULE / CNTL statement verbatim."""
|
|
732
|
+
"""Record a COMMAND / SCHEDULE / CNTL / NOTIFY statement verbatim."""
|
|
653
733
|
# No richer model representation, but they are real statements —
|
|
654
734
|
# record them verbatim rather than discarding them.
|
|
655
735
|
if stmt.type == "CNTL":
|
|
@@ -668,6 +748,22 @@ class JobBuilder:
|
|
|
668
748
|
if self.current_cntl is not None:
|
|
669
749
|
self.current_cntl.parameters["_records"].append(line)
|
|
670
750
|
|
|
751
|
+
def _on_custom(self, stmt: Statement, line_num: int) -> None:
|
|
752
|
+
"""Keep a statement of a type the builder has no handler for.
|
|
753
|
+
|
|
754
|
+
Such statements come from a :class:`~jclparser.plugins.StatementPlugin`
|
|
755
|
+
returning a custom ``Statement.type``; they used to be dropped
|
|
756
|
+
silently (#130).
|
|
757
|
+
"""
|
|
758
|
+
logger.debug(
|
|
759
|
+
"no handler for statement type %r at line %d; kept in "
|
|
760
|
+
"Job.custom_statements",
|
|
761
|
+
stmt.type,
|
|
762
|
+
line_num,
|
|
763
|
+
)
|
|
764
|
+
sink = self.job.custom_statements if self.job else self.pending_custom
|
|
765
|
+
sink.append(stmt)
|
|
766
|
+
|
|
671
767
|
def _on_unknown(self, stmt: Statement, line_num: int) -> None:
|
|
672
768
|
# Unknown statement type - log but continue processing
|
|
673
769
|
statement_type_name = stmt.parameters.get("_statement_type", "")
|
|
@@ -691,8 +787,6 @@ class JobBuilder:
|
|
|
691
787
|
line_number=stmt.line_number,
|
|
692
788
|
)
|
|
693
789
|
self.current_step.add_dd(self.current_dd)
|
|
694
|
-
self.in_inline_data = True
|
|
695
|
-
self.inline_data_lines = []
|
|
696
790
|
else:
|
|
697
791
|
# Job-level XMIT has no step to attach a DD to, but the
|
|
698
792
|
# transmitted data block that follows must still be
|
|
@@ -702,12 +796,29 @@ class JobBuilder:
|
|
|
702
796
|
# it; clear current_dd so the inline-data-end handler
|
|
703
797
|
# doesn't misattach it to an unrelated earlier DD.
|
|
704
798
|
self.current_dd = None
|
|
705
|
-
self.in_inline_data = True
|
|
706
|
-
self.inline_data_lines = []
|
|
707
799
|
self._warnings_sink().append(
|
|
708
800
|
f"Line {stmt.line_number}: job-level XMIT has no step to attach "
|
|
709
801
|
f"its data to; the transmitted records were discarded"
|
|
710
802
|
)
|
|
803
|
+
self._start_transmitted_data(xmit_delimiter(stmt.parameters), stmt.line_number)
|
|
804
|
+
|
|
805
|
+
def _start_transmitted_data(self, delimiter: Any, line_num: int) -> None:
|
|
806
|
+
"""Open the data block of an XMIT / ``/*XMIT`` statement.
|
|
807
|
+
|
|
808
|
+
The transmitted records are a job stream for another node, so a
|
|
809
|
+
``//`` record never ends them: only ``/*`` or the ``DLM=`` delimiter
|
|
810
|
+
does (the pre-scan in ``_merge_source_lines`` uses the same rule).
|
|
811
|
+
"""
|
|
812
|
+
if not isinstance(delimiter, str):
|
|
813
|
+
raise JCLSyntaxError(
|
|
814
|
+
f"DLM= must be a one- or two-character delimiter, got "
|
|
815
|
+
f"{jcl_value_text(delimiter)}",
|
|
816
|
+
line_num,
|
|
817
|
+
)
|
|
818
|
+
self.in_inline_data = True
|
|
819
|
+
self.inline_data_lines = []
|
|
820
|
+
self.current_delimiter = delimiter
|
|
821
|
+
self.current_terminates_on_slash = False
|
|
711
822
|
|
|
712
823
|
# ------------------------------------------------------------------ #
|
|
713
824
|
# Finalisation
|
|
@@ -776,7 +887,7 @@ class JobBuilder:
|
|
|
776
887
|
return self.job
|
|
777
888
|
if not self.pending_procedures:
|
|
778
889
|
raise JCLSyntaxError("No JOB statement found", 0)
|
|
779
|
-
job = Job(name="", line_number=0)
|
|
890
|
+
job = Job(name="", line_number=0, is_procedure_member=True)
|
|
780
891
|
for proc in self.pending_procedures:
|
|
781
892
|
job.add_procedure(proc)
|
|
782
893
|
# A JOB-less member (a cataloged procedure) has no job
|
|
@@ -814,6 +925,7 @@ class JobBuilder:
|
|
|
814
925
|
job.conditionals[:0] = self.pending_conditionals
|
|
815
926
|
job.steps[:0] = self.pending_steps
|
|
816
927
|
job.control_statements[:0] = self.pending_control
|
|
928
|
+
job.custom_statements[:0] = self.pending_custom
|
|
817
929
|
|
|
818
930
|
def _flush_pending_sets(self, job: Job) -> None:
|
|
819
931
|
# Statements seen before the JOB statement (or in a JOB-less member)
|
|
@@ -838,8 +950,8 @@ class JobBuilder:
|
|
|
838
950
|
|
|
839
951
|
def _warn_orphaned_dds(self, job: Job) -> None:
|
|
840
952
|
# orphaned_dds is only ever non-empty here when the whole job had no
|
|
841
|
-
# EXEC statement at all (the first
|
|
842
|
-
# list) -- those DD statements have nowhere to attach and are
|
|
953
|
+
# job-level EXEC statement at all (the first one always flushes and
|
|
954
|
+
# clears the list) -- those DD statements have nowhere to attach and are
|
|
843
955
|
# discarded, so record what was lost instead of vanishing them silently.
|
|
844
956
|
if self.orphaned_dds:
|
|
845
957
|
dd_names = ", ".join(dd.name or "(unnamed)" for dd in self.orphaned_dds)
|
|
@@ -23,7 +23,12 @@ from typing import Any
|
|
|
23
23
|
|
|
24
24
|
from .errors import JCLSyntaxError
|
|
25
25
|
from .model import DDStatement, Job, Statement, Step
|
|
26
|
-
from .utils import
|
|
26
|
+
from .utils import (
|
|
27
|
+
MAX_SYMBOL_VALUE_LENGTH,
|
|
28
|
+
POSITIONAL_PARAM_PREFIX,
|
|
29
|
+
is_positional_key,
|
|
30
|
+
jcl_value_text,
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
_SET_SYMBOL_PATTERN = re.compile(r"&&|&([A-Z0-9@#$]+)\.?")
|
|
29
34
|
|
|
@@ -97,6 +102,23 @@ def _report_set_overflow(
|
|
|
97
102
|
warnings.append(f"{prefix}{message}; value left unresolved")
|
|
98
103
|
|
|
99
104
|
|
|
105
|
+
def _resolve_jcllib_library(library: str, known_symbols: dict[str, str]) -> str | None:
|
|
106
|
+
"""Substitute SET symbols into one ``JCLLIB ORDER`` library name.
|
|
107
|
+
|
|
108
|
+
z/OS resolves the SET symbols defined before the JCLLIB statement, so
|
|
109
|
+
*known_symbols* must be the SET table in effect at that statement. Returns
|
|
110
|
+
``None`` when a ``&symbol`` is still unresolved afterwards (a system symbol
|
|
111
|
+
such as ``&SYSUID``, or a SET coded later); the caller keeps the library as
|
|
112
|
+
written then.
|
|
113
|
+
"""
|
|
114
|
+
if "&" not in library:
|
|
115
|
+
return library
|
|
116
|
+
resolved = _resolve_set_value(library, known_symbols)
|
|
117
|
+
if _has_unresolved_symbol_ref(resolved):
|
|
118
|
+
return None
|
|
119
|
+
return resolved
|
|
120
|
+
|
|
121
|
+
|
|
100
122
|
def _has_unresolved_symbol_ref(
|
|
101
123
|
value: str, ignore: Callable[[str], bool] | None = None
|
|
102
124
|
) -> bool:
|
|
@@ -215,6 +237,11 @@ def _create_dd_from_statement(stmt: Statement) -> DDStatement:
|
|
|
215
237
|
dataset_name = stmt.parameters.get("DSN")
|
|
216
238
|
if dataset_name is None:
|
|
217
239
|
dataset_name = stmt.parameters.get("DSNAME")
|
|
240
|
+
if isinstance(dataset_name, list):
|
|
241
|
+
# ``DSN=(A.B)`` tokenizes to a list; dataset_name is always text, so
|
|
242
|
+
# keep it as coded ("(A.B)") for validate_jcl() to report as an
|
|
243
|
+
# invalid dataset name instead of crashing on it (#121).
|
|
244
|
+
dataset_name = jcl_value_text(dataset_name)
|
|
218
245
|
disposition = stmt.parameters.get("DISP")
|
|
219
246
|
|
|
220
247
|
return DDStatement(
|