-
-
Notifications
You must be signed in to change notification settings - Fork 341
Expand file tree
/
Copy pathcmd.py
More file actions
145 lines (113 loc) · 4.11 KB
/
cmd.py
File metadata and controls
145 lines (113 loc) · 4.11 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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
from __future__ import annotations
import os
import subprocess
import warnings
from typing import TYPE_CHECKING, NamedTuple, overload
from charset_normalizer import from_bytes
from commitizen.exceptions import CharacterSetDecodeError
if TYPE_CHECKING:
from collections.abc import Mapping, Sequence
class Command(NamedTuple):
out: str
err: str
stdout: bytes
stderr: bytes
return_code: int
def _try_decode(bytes_: bytes) -> str:
try:
return bytes_.decode("utf-8")
except UnicodeDecodeError:
pass
charset_match = from_bytes(bytes_).best()
if charset_match is None:
raise CharacterSetDecodeError()
try:
return bytes_.decode(charset_match.encoding)
except UnicodeDecodeError as e:
raise CharacterSetDecodeError() from e
def _popen(
cmd: str | Sequence[str],
*,
shell: bool,
env: Mapping[str, str] | None = None,
) -> Command:
if env is not None:
env = {**os.environ, **env}
process = subprocess.Popen(
cmd,
shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
env=env,
)
stdout, stderr = process.communicate()
return_code = process.returncode
return Command(
_try_decode(stdout),
_try_decode(stderr),
stdout,
stderr,
return_code,
)
@overload
def run(cmd: str, env: Mapping[str, str] | None = None) -> Command: ...
@overload
def run(cmd: Sequence[str], env: Mapping[str, str] | None = None) -> Command: ...
def run(cmd: str | Sequence[str], env: Mapping[str, str] | None = None) -> Command:
"""Run a command safely without shell interpretation (shell=False).
Arguments are passed directly to the OS, preventing shell-injection
vulnerabilities (CWE-78).
Passing a string is deprecated and will be removed in a future version.
Use a list of arguments instead, or use run_shell() for shell features.
"""
if isinstance(cmd, str):
warnings.warn(
"Passing a string to cmd.run() is deprecated and will be removed in v5. "
"Use a list of arguments instead, or use cmd.run_shell() explicitly.",
DeprecationWarning,
stacklevel=2,
)
return _popen(cmd, shell=True, env=env)
return _popen(cmd, shell=False, env=env)
def run_shell(cmd: str, env: Mapping[str, str] | None = None) -> Command:
"""Run a command string via the system shell (shell=True).
Only use this for cases that intentionally require shell features
(e.g., user-defined hooks with pipes/redirects). Never pass
untrusted/user-controlled values into *cmd*.
Related: CWE-78 (OS Command Injection),
https://github.com/commitizen-tools/commitizen/issues/1918
"""
return _popen(cmd, shell=True, env=env)
def run_interactive(
cmd: str | Sequence[str], env: Mapping[str, str] | None = None
) -> int:
"""Run a command safely without shell interpretation and without redirecting stdin, stdout, or stderr
Args:
cmd: The command to run
env: Extra environment variables to define in the subprocess. Defaults to None.
Returns:
subprocess returncode
"""
if env is not None:
env = {**os.environ, **env}
if isinstance(cmd, str):
warnings.warn(
"Passing a string to cmd.run_interactive() is deprecated and will be removed in v5. "
"Use a list of arguments instead, or use cmd.run_interactive_shell() explicitly.",
DeprecationWarning,
stacklevel=2,
)
return subprocess.run(cmd, shell=True, env=env).returncode
return subprocess.run(cmd, shell=False, env=env).returncode
def run_interactive_shell(cmd: str, env: Mapping[str, str] | None = None) -> int:
"""Run a command without redirecting stdin, stdout, or stderr
Args:
cmd: The command to run
env: Extra environment variables to define in the subprocess. Defaults to None.
Returns:
subprocess returncode
"""
if env is not None:
env = {**os.environ, **env}
return subprocess.run(cmd, shell=True, env=env).returncode