-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshell.py
More file actions
69 lines (55 loc) · 2.26 KB
/
Copy pathshell.py
File metadata and controls
69 lines (55 loc) · 2.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""Encode Compose string values as POSIX-shell fragments the runtime shell expands."""
import re
from compose2pod.exceptions import UnsupportedComposeError
_PATTERN = re.compile(
r"""
\$(?:
(?P<escaped>\$)
|(?P<named>[a-zA-Z_][a-zA-Z0-9_]*)
|\{(?P<braced>[a-zA-Z_][a-zA-Z0-9_]*)(?P<op>:-|-|:\?|\?|:\+|\+)?(?P<arg>[^}]*)\}
)
""",
re.VERBOSE,
)
_DQUOTE_ESCAPES = {"\\": "\\\\", '"': '\\"', "$": "\\$", "`": "\\`"}
def _escape_literal(text: str) -> str:
"""Escape `text` so a POSIX shell treats it literally inside double quotes."""
return "".join(_DQUOTE_ESCAPES.get(char, char) for char in text)
def _encode_match(match: re.Match[str]) -> str:
if match.group("escaped"):
return "\\$" # Compose `$$` -> a literal `$`
if match.group("named") is not None:
return "${" + match.group("named") + "-}" # unset -> empty, survives `set -u`
name = match.group("braced")
op = match.group("op")
arg = match.group("arg")
if op is None:
if arg:
# `${NAME<garbage>}` -- text after the name is not a valid operator.
msg = f"malformed variable reference: ${{{name}{arg}}}"
raise UnsupportedComposeError(msg)
return "${" + name + "-}"
return "${" + name + op + _escape_literal(arg) + "}"
def to_shell(value: str) -> str:
"""Return `value` as a double-quoted shell fragment.
Compose variable references (`$VAR`, `${VAR:-d}`, ...) stay live so the
shell running the generated script resolves them against its own
environment; every other character is inert (no command substitution, no
accidental expansion). `$$` becomes a literal `$`.
"""
out: list[str] = []
pos = 0
for match in _PATTERN.finditer(value):
out.append(_escape_literal(value[pos : match.start()]))
out.append(_encode_match(match))
pos = match.end()
out.append(_escape_literal(value[pos:]))
return '"' + "".join(out) + '"'
def variable_names(value: str) -> set[str]:
"""Variable names `value` references (excluding `$$`)."""
names: set[str] = set()
for match in _PATTERN.finditer(value):
if match.group("escaped"):
continue
names.add(match.group("named") or match.group("braced"))
return names