envlockup 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: envlockup
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Read envlockup files from Python: encrypted .env files you can commit, decrypted with your SSH or age key.
5
5
  Project-URL: Homepage, https://github.com/denisbilli/envlockup
6
6
  Author: denisbilli
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "envlockup"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Read envlockup files from Python: encrypted .env files you can commit, decrypted with your SSH or age key."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -34,7 +34,7 @@ from .errors import (
34
34
  PassphraseRequiredError,
35
35
  )
36
36
 
37
- __version__ = "0.2.0"
37
+ __version__ = "0.2.1"
38
38
  __all__ = [
39
39
  "config",
40
40
  "load",
@@ -10,6 +10,7 @@ import getpass
10
10
  import json
11
11
  import os
12
12
  import shutil
13
+ import re
13
14
  import subprocess
14
15
  import sys
15
16
  from pathlib import Path
@@ -66,6 +67,19 @@ def _values(args: argparse.Namespace) -> dict[str, str]:
66
67
  return load(args.file, identity=args.identity, passphrase=passphrase)
67
68
 
68
69
 
70
+ # cmd.exe metacharacters, escaped with ^ (the same rules as the npm CLI and cross-spawn).
71
+ _CMD_META = re.compile(r"""([()\][%!^"`<>&|;, *?])""")
72
+
73
+
74
+ def _cmd_escape(arg: str, twice: bool) -> str:
75
+ # Backslashes before a quote, or at the end (before the closing quote), are doubled.
76
+ quoted = '"' + re.sub(r"(\\*)$", r"\1\1", re.sub(r'(\\*)"', r'\1\1\\"', arg)) + '"'
77
+ quoted = _CMD_META.sub(r"^\1", quoted)
78
+ # A batch file that forwards its arguments with %* has cmd.exe parse them a
79
+ # second time, which removes one more level of escaping.
80
+ return _CMD_META.sub(r"^\1", quoted) if twice else quoted
81
+
82
+
69
83
  def _run(command: list[str], env: dict[str, str]) -> int:
70
84
  if os.name == "posix":
71
85
  # Replace this process, so the command gets signals and keeps our PID
@@ -78,6 +92,13 @@ def _run(command: list[str], env: dict[str, str]) -> int:
78
92
  # Windows: resolve .exe/.cmd through PATHEXT, then wait for the child.
79
93
  exe = shutil.which(command[0], path=env.get("PATH")) or command[0]
80
94
  try:
95
+ if re.search(r"\.(cmd|bat)$", exe, re.IGNORECASE):
96
+ # Batch files run through cmd.exe, which would interpret & | % and
97
+ # friends in the arguments: escape them, twice, since launchers pass
98
+ # them on with %*.
99
+ line = " ".join([_CMD_META.sub(r"^\1", exe), *(_cmd_escape(a, True) for a in command[1:])])
100
+ comspec = env.get("COMSPEC") or os.environ.get("COMSPEC") or "cmd.exe"
101
+ return subprocess.call(f'{comspec} /d /s /c "{line}"', env=env)
81
102
  return subprocess.call([exe, *command[1:]], env=env)
82
103
  except OSError as err:
83
104
  print(f"envlockup: cannot run {command[0]}: {err.strerror}", file=sys.stderr)
@@ -141,6 +141,24 @@ def test_cli_run(env_file, cli_env):
141
141
  assert json.loads(r.stdout)["EMPTY"] == ""
142
142
 
143
143
 
144
+ TRICKY_ARGS = ["a&b", "50%", "%PATH%", "x^y", 'say "hi"', 'q"&"q', "back\\slash\\", "with space", "", "!bang!", "(p)", "a|b", "<lt>"]
145
+
146
+
147
+ @pytest.mark.skipif(os.name != "nt", reason="Windows only")
148
+ def test_cli_run_passes_arguments_unchanged_to_cmd_scripts(env_file, cli_env, tmp_path):
149
+ printer = tmp_path / "argv.py"
150
+ printer.write_text("import json, sys\nprint(json.dumps(sys.argv[1:]))\n", encoding="utf-8")
151
+ for sub in ("scripts", os.path.join("node_modules", ".bin")):
152
+ (tmp_path / sub).mkdir(parents=True, exist_ok=True)
153
+ script = tmp_path / sub / "argv.cmd"
154
+ script.write_text(f'@"{sys.executable}" "{printer}" %*\r\n', encoding="utf-8")
155
+ r = cli("run", "--", str(script), *TRICKY_ARGS, env=cli_env, cwd=env_file.parent)
156
+ assert r.returncode == 0, r.stderr
157
+ assert json.loads(r.stdout) == TRICKY_ARGS, sub
158
+ r = cli("run", "--", sys.executable, str(printer), *TRICKY_ARGS, env=cli_env, cwd=env_file.parent)
159
+ assert json.loads(r.stdout) == TRICKY_ARGS
160
+
161
+
144
162
  def test_cli_run_exit_code_and_missing_command(env_file, cli_env):
145
163
  r = cli("run", "--", sys.executable, "-c", "raise SystemExit(7)", env=cli_env, cwd=env_file.parent)
146
164
  assert r.returncode == 7
File without changes
File without changes
File without changes