shellscriptor 0.1.0__py3-none-any.whl
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.
- shellscriptor/__init__.py +23 -0
- shellscriptor/__main__.py +142 -0
- shellscriptor/_argparse.py +213 -0
- shellscriptor/_code.py +76 -0
- shellscriptor/_log.py +125 -0
- shellscriptor/_output.py +72 -0
- shellscriptor/_script.py +268 -0
- shellscriptor/_types.py +271 -0
- shellscriptor/_validate.py +413 -0
- shellscriptor/py.typed +0 -0
- shellscriptor-0.1.0.dist-info/METADATA +761 -0
- shellscriptor-0.1.0.dist-info/RECORD +15 -0
- shellscriptor-0.1.0.dist-info/WHEEL +5 -0
- shellscriptor-0.1.0.dist-info/entry_points.txt +2 -0
- shellscriptor-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
from shellscriptor._argparse import param_name_to_var_name
|
|
6
|
+
from shellscriptor._code import indent
|
|
7
|
+
from shellscriptor._log import log
|
|
8
|
+
from shellscriptor._types import (
|
|
9
|
+
ParameterDef,
|
|
10
|
+
PathType,
|
|
11
|
+
ScriptType,
|
|
12
|
+
ValidationDef,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_validation_block(
|
|
17
|
+
parameters: dict[str, ParameterDef],
|
|
18
|
+
*,
|
|
19
|
+
local: bool,
|
|
20
|
+
script_type: ScriptType,
|
|
21
|
+
) -> list[str]:
|
|
22
|
+
"""Generate validation lines for all parameters.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
parameters:
|
|
27
|
+
Mapping of parameter name → :class:`~shellscriptor._types.ParameterDef`.
|
|
28
|
+
local:
|
|
29
|
+
Whether variables are function-local.
|
|
30
|
+
script_type:
|
|
31
|
+
Controls whether informational log calls are emitted.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
List of shell lines covering all parameter validations.
|
|
36
|
+
"""
|
|
37
|
+
lines: list[str] = []
|
|
38
|
+
for param_name, param_data in sorted((parameters or {}).items()):
|
|
39
|
+
lines.extend(
|
|
40
|
+
validate_variable(
|
|
41
|
+
var_name=param_name_to_var_name(param_name, local=local),
|
|
42
|
+
var_type=param_data.type,
|
|
43
|
+
default=param_data.default,
|
|
44
|
+
required=param_data.required,
|
|
45
|
+
validations=param_data.validation,
|
|
46
|
+
script_type=script_type,
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
return lines
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_variable(
|
|
53
|
+
var_name: str,
|
|
54
|
+
var_type: Literal["string", "boolean", "array", "integer"],
|
|
55
|
+
default: str | list[str] | None,
|
|
56
|
+
required: bool | None,
|
|
57
|
+
validations: ValidationDef | None,
|
|
58
|
+
script_type: ScriptType,
|
|
59
|
+
) -> list[str]:
|
|
60
|
+
"""Generate all validation checks for a single variable.
|
|
61
|
+
|
|
62
|
+
Applies, in order:
|
|
63
|
+
|
|
64
|
+
1. Missing-argument / default assignment check — **unless** the
|
|
65
|
+
parameter is explicitly optional (``required=False``) and has no
|
|
66
|
+
default, in which case the variable is allowed to be empty after
|
|
67
|
+
argument parsing and the check is skipped entirely.
|
|
68
|
+
Boolean variables are always normalised to ``"false"`` when unset
|
|
69
|
+
regardless of ``required``.
|
|
70
|
+
2. For scalars/integers: enum, path-existence, integer-range, and regex
|
|
71
|
+
checks directly on the variable.
|
|
72
|
+
3. For arrays with structural validators (enum / path-existence /
|
|
73
|
+
integer-range / regex): wraps checks in a ``for elem in …`` loop.
|
|
74
|
+
4. Custom shell lines verbatim.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
var_name:
|
|
79
|
+
Shell variable name.
|
|
80
|
+
var_type:
|
|
81
|
+
One of ``"string"``, ``"boolean"``, ``"array"``, ``"integer"``.
|
|
82
|
+
default:
|
|
83
|
+
Default value(s). ``None`` with ``required`` unset or ``True``
|
|
84
|
+
makes the parameter required.
|
|
85
|
+
required:
|
|
86
|
+
Explicit required flag.
|
|
87
|
+
|
|
88
|
+
* ``True`` — exit if missing (``default`` is ignored).
|
|
89
|
+
* ``False`` — optional; if ``default`` is also ``None`` the check
|
|
90
|
+
is skipped entirely and the variable may remain empty.
|
|
91
|
+
* ``None`` — inferred: required when ``default is None``, optional
|
|
92
|
+
otherwise.
|
|
93
|
+
validations:
|
|
94
|
+
Optional validation rules applied after the missing-arg check.
|
|
95
|
+
script_type:
|
|
96
|
+
Controls whether informational log calls are emitted.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
List of shell validation lines.
|
|
101
|
+
"""
|
|
102
|
+
# Determine effective required flag
|
|
103
|
+
is_required = required if required is not None else (default is None)
|
|
104
|
+
|
|
105
|
+
# Boolean: always normalise "" → "false"; never treated as "skip".
|
|
106
|
+
# Other types: only emit check when required OR a default is provided.
|
|
107
|
+
# When required=False and default=None the variable is genuinely optional
|
|
108
|
+
# (empty after parsing is acceptable) — skip the check.
|
|
109
|
+
emit_missing_check = var_type == "boolean" or is_required or default is not None
|
|
110
|
+
|
|
111
|
+
out: list[str] = []
|
|
112
|
+
if emit_missing_check:
|
|
113
|
+
out.append(
|
|
114
|
+
validate_missing_arg(
|
|
115
|
+
var_name=var_name,
|
|
116
|
+
var_type=var_type,
|
|
117
|
+
default=default if not is_required else None,
|
|
118
|
+
script_type=script_type,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if var_type == "boolean":
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
validations = validations or ValidationDef()
|
|
126
|
+
has_structural = bool(
|
|
127
|
+
validations.enum
|
|
128
|
+
or validations.path_existence
|
|
129
|
+
or validations.integer_range
|
|
130
|
+
or validations.regex
|
|
131
|
+
)
|
|
132
|
+
in_for_loop = has_structural and var_type == "array"
|
|
133
|
+
|
|
134
|
+
if in_for_loop:
|
|
135
|
+
out.append(f'for elem in "${{{var_name}[@]}}"; do')
|
|
136
|
+
|
|
137
|
+
check_var = "elem" if in_for_loop else var_name
|
|
138
|
+
level = 1 if in_for_loop else 0
|
|
139
|
+
|
|
140
|
+
if validations.enum:
|
|
141
|
+
out.extend(indent(validate_enum(var_name=check_var, enum=validations.enum), level))
|
|
142
|
+
if validations.path_existence:
|
|
143
|
+
pe = validations.path_existence
|
|
144
|
+
out.extend(indent(validate_path_existence(var_name=check_var, must_exist=pe.must_exist, path_type=pe.type), level))
|
|
145
|
+
if validations.integer_range:
|
|
146
|
+
ir = validations.integer_range
|
|
147
|
+
out.extend(indent(validate_integer_range(var_name=check_var, min_val=ir.min, max_val=ir.max), level))
|
|
148
|
+
if validations.regex:
|
|
149
|
+
out.extend(indent(validate_regex(var_name=check_var, pattern=validations.regex), level))
|
|
150
|
+
|
|
151
|
+
if in_for_loop:
|
|
152
|
+
out.append("done")
|
|
153
|
+
|
|
154
|
+
if validations.custom:
|
|
155
|
+
out.extend(validations.custom)
|
|
156
|
+
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def validate_missing_arg(
|
|
161
|
+
var_name: str,
|
|
162
|
+
var_type: Literal["string", "boolean", "array", "integer"],
|
|
163
|
+
default: str | list[str] | None,
|
|
164
|
+
script_type: ScriptType,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Generate a one-liner that enforces presence or applies a default.
|
|
167
|
+
|
|
168
|
+
* When *default* is ``None`` the variable is treated as **required** and
|
|
169
|
+
the generated code calls ``exit 1`` if the variable is unset or empty.
|
|
170
|
+
* When *default* is provided, the generated code assigns that value and
|
|
171
|
+
logs the action (in ``shell_exec`` mode only).
|
|
172
|
+
* For ``boolean`` variables the default is always ``"false"``; the
|
|
173
|
+
*default* argument is therefore ignored and this function should be
|
|
174
|
+
called unconditionally for booleans to normalise ``""`` → ``"false"``.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
var_name:
|
|
179
|
+
Shell variable name.
|
|
180
|
+
var_type:
|
|
181
|
+
One of ``"string"``, ``"boolean"``, ``"array"``, ``"integer"``.
|
|
182
|
+
default:
|
|
183
|
+
Default value, or ``None`` to require the argument.
|
|
184
|
+
script_type:
|
|
185
|
+
Controls whether informational log calls are emitted.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
A single shell line (one-liner).
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def _info_default(default_repr: str) -> str:
|
|
193
|
+
if script_type == "shell_src":
|
|
194
|
+
return ""
|
|
195
|
+
msg = f"Argument '{var_name}' set to default value '{default_repr}'."
|
|
196
|
+
return f"{log(msg, 'info')}; "
|
|
197
|
+
|
|
198
|
+
err = log(f"Missing required argument '{var_name}'.", "critical")
|
|
199
|
+
|
|
200
|
+
if var_type in ("string", "integer"):
|
|
201
|
+
check = f'[ -z "${{{var_name}-}}" ]'
|
|
202
|
+
if default is None:
|
|
203
|
+
action = err
|
|
204
|
+
else:
|
|
205
|
+
action = f'{_info_default(str(default))}{var_name}="{default}"'
|
|
206
|
+
elif var_type == "boolean":
|
|
207
|
+
check = f'[ -z "${{{var_name}-}}" ]'
|
|
208
|
+
action = f"{_info_default('false')}{var_name}=false"
|
|
209
|
+
elif var_type == "array":
|
|
210
|
+
check = f'{{ [ "${{{var_name}+isset}}" != "isset" ] || [ ${{#{var_name}[@]}} -eq 0 ]; }}'
|
|
211
|
+
if default is None:
|
|
212
|
+
action = err
|
|
213
|
+
else:
|
|
214
|
+
default_list: list[str] = (
|
|
215
|
+
default if isinstance(default, list) else [default]
|
|
216
|
+
)
|
|
217
|
+
quoted = " ".join('"' + e + '"' for e in default_list)
|
|
218
|
+
default_str = f"({quoted})"
|
|
219
|
+
action = f"{_info_default(default_str)}{var_name}={default_str}"
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(f"Unsupported var_type: {var_type!r}")
|
|
222
|
+
|
|
223
|
+
return f"{check} && {{ {action}; }}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def validate_enum(var_name: str, enum: list[str]) -> list[str]:
|
|
227
|
+
"""Generate a ``case`` statement that rejects values not in *enum*.
|
|
228
|
+
|
|
229
|
+
The empty string is intentionally excluded from the allowed set; the
|
|
230
|
+
missing-arg check (which must run first) already handles unset/empty
|
|
231
|
+
variables.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
var_name:
|
|
236
|
+
Shell variable name to check.
|
|
237
|
+
enum:
|
|
238
|
+
Allowed values. Must contain at least one entry.
|
|
239
|
+
|
|
240
|
+
Returns
|
|
241
|
+
-------
|
|
242
|
+
List of shell lines.
|
|
243
|
+
|
|
244
|
+
Raises
|
|
245
|
+
------
|
|
246
|
+
ValueError
|
|
247
|
+
If *enum* is empty.
|
|
248
|
+
"""
|
|
249
|
+
if not enum:
|
|
250
|
+
raise ValueError("validate_enum: 'enum' must contain at least one value")
|
|
251
|
+
enum_str = "|".join('"' + v + '"' for v in enum)
|
|
252
|
+
_invalid_msg = f"Invalid value for argument '--{var_name}': '${{{var_name}}}'"
|
|
253
|
+
return [
|
|
254
|
+
f'case "${{{var_name}}}" in',
|
|
255
|
+
indent(f"{enum_str});;", 1),
|
|
256
|
+
indent(
|
|
257
|
+
f'*) {log(_invalid_msg, "critical")};;',
|
|
258
|
+
1,
|
|
259
|
+
),
|
|
260
|
+
"esac",
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def validate_path_existence(
|
|
265
|
+
var_name: str,
|
|
266
|
+
must_exist: bool,
|
|
267
|
+
path_type: PathType | None = None,
|
|
268
|
+
) -> list[str]:
|
|
269
|
+
"""Generate a check that a path exists (or does not exist).
|
|
270
|
+
|
|
271
|
+
The check is guarded by ``[ -n "${VAR-}" ]`` so that it is silently
|
|
272
|
+
skipped when the variable is empty. This means the missing-arg check
|
|
273
|
+
(``validate_missing_arg``) must run before this one if a non-empty value
|
|
274
|
+
is required.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
var_name:
|
|
279
|
+
Shell variable holding the path.
|
|
280
|
+
must_exist:
|
|
281
|
+
``True`` → path must exist; ``False`` → path must *not* exist.
|
|
282
|
+
path_type:
|
|
283
|
+
Kind of filesystem entry to test, or ``None`` for any entry
|
|
284
|
+
(uses ``-e``).
|
|
285
|
+
|
|
286
|
+
Returns
|
|
287
|
+
-------
|
|
288
|
+
A single-element list containing the shell check line.
|
|
289
|
+
"""
|
|
290
|
+
_op: dict[PathType | None, str] = {
|
|
291
|
+
None: "e",
|
|
292
|
+
"dir": "d",
|
|
293
|
+
"exec": "x",
|
|
294
|
+
"file": "f",
|
|
295
|
+
"symlink": "L",
|
|
296
|
+
}
|
|
297
|
+
_name: dict[PathType | None, str] = {
|
|
298
|
+
None: "Path",
|
|
299
|
+
"dir": "Directory",
|
|
300
|
+
"exec": "Executable",
|
|
301
|
+
"file": "File",
|
|
302
|
+
"symlink": "Symbolic link",
|
|
303
|
+
}
|
|
304
|
+
condition = "not found" if must_exist else "already exists"
|
|
305
|
+
err_msg = (
|
|
306
|
+
f"{_name[path_type]} argument to parameter '{var_name}' "
|
|
307
|
+
f"{condition}: '${{{var_name}}}'"
|
|
308
|
+
)
|
|
309
|
+
negation = "! " if must_exist else ""
|
|
310
|
+
cmd = (
|
|
311
|
+
f'[ -n "${{{var_name}-}}" ] && '
|
|
312
|
+
f'[ {negation}-{_op[path_type]} "${{{var_name}}}" ] && '
|
|
313
|
+
f'{{ {log(err_msg, "critical")}; }}'
|
|
314
|
+
)
|
|
315
|
+
return [cmd]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def validate_integer_range(
|
|
319
|
+
var_name: str,
|
|
320
|
+
min_val: int | None,
|
|
321
|
+
max_val: int | None,
|
|
322
|
+
) -> list[str]:
|
|
323
|
+
"""Generate a check that an integer variable is within [*min_val*, *max_val*].
|
|
324
|
+
|
|
325
|
+
Always emits a regex guard to ensure the value is a valid integer (matches
|
|
326
|
+
``^-?[0-9]+$``) before comparing bounds. Either bound may be ``None`` to
|
|
327
|
+
indicate no bound in that direction.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
var_name:
|
|
332
|
+
Shell variable holding the integer string.
|
|
333
|
+
min_val:
|
|
334
|
+
Inclusive lower bound, or ``None``.
|
|
335
|
+
max_val:
|
|
336
|
+
Inclusive upper bound, or ``None``.
|
|
337
|
+
|
|
338
|
+
Returns
|
|
339
|
+
-------
|
|
340
|
+
List of shell lines.
|
|
341
|
+
"""
|
|
342
|
+
lines: list[str] = [
|
|
343
|
+
f'if ! [[ "${{{var_name}}}" =~ ^-?[0-9]+$ ]]; then',
|
|
344
|
+
indent(
|
|
345
|
+
log(
|
|
346
|
+
f"Argument '{var_name}' is not an integer: '${{{var_name}}}'",
|
|
347
|
+
"critical",
|
|
348
|
+
),
|
|
349
|
+
1,
|
|
350
|
+
),
|
|
351
|
+
"fi",
|
|
352
|
+
]
|
|
353
|
+
if min_val is not None:
|
|
354
|
+
lines.extend(
|
|
355
|
+
[
|
|
356
|
+
f'if [[ "${{{var_name}}}" -lt {min_val} ]]; then',
|
|
357
|
+
indent(
|
|
358
|
+
log(
|
|
359
|
+
f"Argument '{var_name}' must be >= {min_val}, got '${{{var_name}}}'",
|
|
360
|
+
"critical",
|
|
361
|
+
),
|
|
362
|
+
1,
|
|
363
|
+
),
|
|
364
|
+
"fi",
|
|
365
|
+
]
|
|
366
|
+
)
|
|
367
|
+
if max_val is not None:
|
|
368
|
+
lines.extend(
|
|
369
|
+
[
|
|
370
|
+
f'if [[ "${{{var_name}}}" -gt {max_val} ]]; then',
|
|
371
|
+
indent(
|
|
372
|
+
log(
|
|
373
|
+
f"Argument '{var_name}' must be <= {max_val}, got '${{{var_name}}}'",
|
|
374
|
+
"critical",
|
|
375
|
+
),
|
|
376
|
+
1,
|
|
377
|
+
),
|
|
378
|
+
"fi",
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
return lines
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def validate_regex(var_name: str, pattern: str) -> list[str]:
|
|
385
|
+
"""Generate a check that a variable's value matches *pattern*.
|
|
386
|
+
|
|
387
|
+
Uses bash's ``[[ … =~ … ]]`` extended regex operator (ERE syntax).
|
|
388
|
+
The check exits the script with code 1 if the value does not match.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
var_name:
|
|
393
|
+
Shell variable to check.
|
|
394
|
+
pattern:
|
|
395
|
+
ERE pattern passed directly into ``[[ "${VAR}" =~ … ]]``.
|
|
396
|
+
Must not contain unescaped shell-special characters that would
|
|
397
|
+
break the surrounding ``if`` construct.
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
List of shell lines.
|
|
402
|
+
"""
|
|
403
|
+
return [
|
|
404
|
+
f'if ! [[ "${{{var_name}}}" =~ {pattern} ]]; then',
|
|
405
|
+
indent(
|
|
406
|
+
log(
|
|
407
|
+
f"Argument '{var_name}' does not match pattern '{pattern}': '${{{var_name}}}'",
|
|
408
|
+
"critical",
|
|
409
|
+
),
|
|
410
|
+
1,
|
|
411
|
+
),
|
|
412
|
+
"fi",
|
|
413
|
+
]
|
shellscriptor/py.typed
ADDED
|
File without changes
|