envstack 1.0.2__tar.gz → 1.0.3__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.
- {envstack-1.0.2/lib/envstack.egg-info → envstack-1.0.3}/PKG-INFO +6 -2
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/__init__.py +1 -1
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/encrypt.py +14 -4
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/env.py +1 -1
- {envstack-1.0.2 → envstack-1.0.3/lib/envstack.egg-info}/PKG-INFO +6 -2
- envstack-1.0.3/lib/envstack.egg-info/requires.txt +7 -0
- {envstack-1.0.2 → envstack-1.0.3}/pyproject.toml +6 -2
- envstack-1.0.2/lib/envstack/wrapper.py +0 -335
- envstack-1.0.2/lib/envstack.egg-info/requires.txt +0 -2
- {envstack-1.0.2 → envstack-1.0.3}/LICENSE +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/README.md +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/dist.json +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/cli.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/config.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/envshell.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/exceptions.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/logger.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/node.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/path.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack/util.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/SOURCES.txt +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/dependency_links.txt +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/entry_points.txt +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/not-zip-safe +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/lib/envstack.egg-info/top_level.txt +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/setup.cfg +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_cmds.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_encrypt.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_env.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_node.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_path.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_util.py +0 -0
- {envstack-1.0.2 → envstack-1.0.3}/tests/test_wrapper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: envstack
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Environment variable composition layer for tools and processes.
|
|
5
5
|
Author-email: Ryan Galloway <ryan@rsgalloway.com>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -20,11 +20,15 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
26
|
Requires-Python: >=3.6
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
License-File: LICENSE
|
|
26
29
|
Requires-Dist: PyYAML>=5.1.2
|
|
27
|
-
Requires-Dist: cryptography
|
|
30
|
+
Requires-Dist: cryptography<47,>=43.0.1; python_version < "3.9"
|
|
31
|
+
Requires-Dist: cryptography>=43.0.1; python_version >= "3.9"
|
|
28
32
|
|
|
29
33
|
envstack
|
|
30
34
|
========
|
|
@@ -34,7 +34,7 @@ Stacked environment variable management system.
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
__prog__ = "envstack"
|
|
37
|
-
__version__ = "1.0.
|
|
37
|
+
__version__ = "1.0.3"
|
|
38
38
|
|
|
39
39
|
from envstack.env import clear, init, revert, save # noqa: F401
|
|
40
40
|
from envstack.env import load_environ, resolve_environ # noqa: F401
|
|
@@ -37,6 +37,7 @@ import base64
|
|
|
37
37
|
import binascii
|
|
38
38
|
import os
|
|
39
39
|
import secrets
|
|
40
|
+
import warnings
|
|
40
41
|
from base64 import b64decode, b64encode
|
|
41
42
|
from functools import wraps
|
|
42
43
|
|
|
@@ -53,10 +54,19 @@ Cipher = None
|
|
|
53
54
|
algorithms = None
|
|
54
55
|
modes = None
|
|
55
56
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
# cryptography emits a Python 3.8 deprecation warning during import.
|
|
58
|
+
with warnings.catch_warnings():
|
|
59
|
+
warnings.filterwarnings(
|
|
60
|
+
"ignore",
|
|
61
|
+
message=r"Python 3\.8 is no longer supported by the Python core team.*",
|
|
62
|
+
category=DeprecationWarning,
|
|
63
|
+
module=r"cryptography(\..*)?$",
|
|
64
|
+
)
|
|
65
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
66
|
+
from cryptography.exceptions import InvalidTag
|
|
67
|
+
from cryptography.hazmat.primitives import padding
|
|
68
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
69
|
+
|
|
60
70
|
CRYPTOGRAPHY_AVAILABLE = True
|
|
61
71
|
except ImportError as err:
|
|
62
72
|
log.debug("cryptography module not available: %s", err)
|
|
@@ -248,7 +248,7 @@ class EnvVar(string.Template, str):
|
|
|
248
248
|
>>> v.vars()
|
|
249
249
|
['FOO', 'BAR']
|
|
250
250
|
"""
|
|
251
|
-
matches =
|
|
251
|
+
matches = type(self).pattern.findall(str(self.template))
|
|
252
252
|
return [key for match in matches for key in match if key]
|
|
253
253
|
|
|
254
254
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: envstack
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Environment variable composition layer for tools and processes.
|
|
5
5
|
Author-email: Ryan Galloway <ryan@rsgalloway.com>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -20,11 +20,15 @@ Classifier: Programming Language :: Python :: 3.8
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
26
|
Requires-Python: >=3.6
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
License-File: LICENSE
|
|
26
29
|
Requires-Dist: PyYAML>=5.1.2
|
|
27
|
-
Requires-Dist: cryptography
|
|
30
|
+
Requires-Dist: cryptography<47,>=43.0.1; python_version < "3.9"
|
|
31
|
+
Requires-Dist: cryptography>=43.0.1; python_version >= "3.9"
|
|
28
32
|
|
|
29
33
|
envstack
|
|
30
34
|
========
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "envstack"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Environment variable composition layer for tools and processes."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
requires-python = ">=3.6"
|
|
@@ -12,7 +12,8 @@ license = { text = "BSD 3-Clause License" }
|
|
|
12
12
|
authors = [{ name = "Ryan Galloway", email = "ryan@rsgalloway.com" }]
|
|
13
13
|
dependencies = [
|
|
14
14
|
"PyYAML>=5.1.2",
|
|
15
|
-
|
|
15
|
+
'cryptography>=43.0.1,<47; python_version < "3.9"',
|
|
16
|
+
'cryptography>=43.0.1; python_version >= "3.9"',
|
|
16
17
|
]
|
|
17
18
|
classifiers = [
|
|
18
19
|
"Development Status :: 4 - Beta",
|
|
@@ -25,6 +26,9 @@ classifiers = [
|
|
|
25
26
|
"Programming Language :: Python :: 3.9",
|
|
26
27
|
"Programming Language :: Python :: 3.10",
|
|
27
28
|
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
28
32
|
]
|
|
29
33
|
keywords = [
|
|
30
34
|
"environment",
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
#
|
|
3
|
-
# Copyright (c) 2024-2026, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
|
-
#
|
|
5
|
-
# Redistribution and use in source and binary forms, with or without
|
|
6
|
-
# modification, are permitted provided that the following conditions are met:
|
|
7
|
-
#
|
|
8
|
-
# - Redistributions of source code must retain the above copyright notice,
|
|
9
|
-
# this list of conditions and the following disclaimer.
|
|
10
|
-
#
|
|
11
|
-
# - Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
-
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
-
# and/or other materials provided with the distribution.
|
|
14
|
-
#
|
|
15
|
-
# - Neither the name of the software nor the names of its contributors
|
|
16
|
-
# may be used to endorse or promote products derived from this software
|
|
17
|
-
# without specific prior written permission.
|
|
18
|
-
#
|
|
19
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
-
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
-
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
-
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
-
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
-
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
-
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
-
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
-
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
__doc__ = """
|
|
33
|
-
Contains executable wrapper classes and functions.
|
|
34
|
-
"""
|
|
35
|
-
|
|
36
|
-
import os
|
|
37
|
-
import re
|
|
38
|
-
import shlex
|
|
39
|
-
import subprocess
|
|
40
|
-
import traceback
|
|
41
|
-
|
|
42
|
-
from envstack import config, logger
|
|
43
|
-
from envstack.env import load_environ, resolve_environ
|
|
44
|
-
from envstack.util import encode, evaluate_modifiers
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def to_args(cmd: str):
|
|
48
|
-
"""
|
|
49
|
-
Converts a command line string to an arg list to be passed to
|
|
50
|
-
subprocess.Popen that preserves args with quotes.
|
|
51
|
-
|
|
52
|
-
:param cmd: command line string.
|
|
53
|
-
"""
|
|
54
|
-
return shlex.split(cmd)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def shell_join(args):
|
|
58
|
-
"""
|
|
59
|
-
Joins a list of arguments into a single quoted shell string.
|
|
60
|
-
|
|
61
|
-
:param args: list of arguments.
|
|
62
|
-
:returns: shell string.
|
|
63
|
-
"""
|
|
64
|
-
argstr = ".".join(args)
|
|
65
|
-
if '"' in argstr or "'" in argstr:
|
|
66
|
-
try:
|
|
67
|
-
return shlex.join(args)
|
|
68
|
-
except AttributeError:
|
|
69
|
-
return " ".join(shlex.quote(arg) for arg in args)
|
|
70
|
-
else:
|
|
71
|
-
return " ".join(args)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class Wrapper(object):
|
|
75
|
-
"""Wrapper class for executables. Subprocessed with preconfigured environment.
|
|
76
|
-
|
|
77
|
-
Subclasses should override executable():
|
|
78
|
-
|
|
79
|
-
class ToolWrapper(Wrapper):
|
|
80
|
-
def __init__(self, namespace, args):
|
|
81
|
-
super(ToolWrapper, self).__init__(namespace, args)
|
|
82
|
-
def executable(self):
|
|
83
|
-
return '$TOOL_ROOT/bin/tool'
|
|
84
|
-
|
|
85
|
-
To launch 'tool', create an instance of the wrapper, and pass as the first
|
|
86
|
-
argument the namespace continaining config options, then call launch():
|
|
87
|
-
|
|
88
|
-
tool = ToolWrapper('tool', sys.argv[1:])
|
|
89
|
-
tool.launch()
|
|
90
|
-
|
|
91
|
-
The log attribute can be set to a custom logger:
|
|
92
|
-
|
|
93
|
-
tool.log = MyLogger()
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
shell: bool = False
|
|
97
|
-
|
|
98
|
-
def __init__(self, namespace, args=[]):
|
|
99
|
-
"""Initializes the wrapper with the given namespace and args.
|
|
100
|
-
|
|
101
|
-
:param namespace: environment stack namespace.
|
|
102
|
-
:param args: command line arguments.
|
|
103
|
-
"""
|
|
104
|
-
super(Wrapper, self).__init__()
|
|
105
|
-
self.args = args
|
|
106
|
-
self.name = namespace
|
|
107
|
-
self.shell = self.shell
|
|
108
|
-
self.log = logger.log
|
|
109
|
-
self.env = load_environ(namespace)
|
|
110
|
-
|
|
111
|
-
def executable(self):
|
|
112
|
-
"""Returns the path to the executable."""
|
|
113
|
-
raise NotImplementedError
|
|
114
|
-
|
|
115
|
-
def launch(self):
|
|
116
|
-
"""Launches the wrapped tool in a subprocess with env."""
|
|
117
|
-
env = self.get_subprocess_env()
|
|
118
|
-
command = self.get_subprocess_command(env)
|
|
119
|
-
|
|
120
|
-
try:
|
|
121
|
-
proc = subprocess.run(
|
|
122
|
-
command,
|
|
123
|
-
env=encode(env),
|
|
124
|
-
shell=self.shell,
|
|
125
|
-
check=False,
|
|
126
|
-
)
|
|
127
|
-
return proc.returncode
|
|
128
|
-
except Exception:
|
|
129
|
-
traceback.print_exc()
|
|
130
|
-
return 1
|
|
131
|
-
|
|
132
|
-
def get_subprocess_args(self, cmd):
|
|
133
|
-
"""Returns the arguments to be passed to the subprocess."""
|
|
134
|
-
return self.args
|
|
135
|
-
|
|
136
|
-
def get_subprocess_command(self, env):
|
|
137
|
-
"""Returns argv (preferred) or a command string if shell=True."""
|
|
138
|
-
cmd = evaluate_modifiers(self.executable(), env)
|
|
139
|
-
args = self.get_subprocess_args(cmd)
|
|
140
|
-
|
|
141
|
-
if self.shell:
|
|
142
|
-
return " ".join([cmd] + args)
|
|
143
|
-
|
|
144
|
-
return [cmd] + args
|
|
145
|
-
|
|
146
|
-
def get_subprocess_env(self):
|
|
147
|
-
"""
|
|
148
|
-
Returns the environment that gets passed to the subprocess when launch()
|
|
149
|
-
is called on the wrapper.
|
|
150
|
-
"""
|
|
151
|
-
env = os.environ.copy()
|
|
152
|
-
env.update(resolve_environ(self.env))
|
|
153
|
-
return env
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class CommandWrapper(Wrapper):
|
|
157
|
-
"""Wrapper class for running wrapped commands from the command-line."""
|
|
158
|
-
|
|
159
|
-
def __init__(self, namespace=config.DEFAULT_NAMESPACE, args=[]):
|
|
160
|
-
"""
|
|
161
|
-
Initializes the command wrapper with the given namespace and args, e.g.:
|
|
162
|
-
|
|
163
|
-
>>> cmd = CommandWrapper(stack, ['ls', '-al'])
|
|
164
|
-
>>> print(cmd.executable())
|
|
165
|
-
ls
|
|
166
|
-
>>> print(cmd.args)
|
|
167
|
-
['-al']
|
|
168
|
-
|
|
169
|
-
:param namespace: environment stack name (default: 'default').
|
|
170
|
-
:param args: command and arguments as a list.
|
|
171
|
-
"""
|
|
172
|
-
super(CommandWrapper, self).__init__(namespace, args)
|
|
173
|
-
self.cmd = list(args)
|
|
174
|
-
|
|
175
|
-
def executable(self):
|
|
176
|
-
"""Returns the command to run."""
|
|
177
|
-
return self.cmd[0]
|
|
178
|
-
|
|
179
|
-
def get_subprocess_args(self, cmd):
|
|
180
|
-
return self.cmd[1:]
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
class ShellWrapper(Wrapper):
|
|
184
|
-
"""
|
|
185
|
-
Runs a *string expression* under the user's shell using -c.
|
|
186
|
-
Useful for wrapping shell expressions that need variable expansion:
|
|
187
|
-
|
|
188
|
-
>>> shell = ShellWrapper('default', 'echo {HOME}')
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
def __init__(self, namespace=config.DEFAULT_NAMESPACE, expr: str = ""):
|
|
192
|
-
super().__init__(namespace, args=[])
|
|
193
|
-
self.expr = expr
|
|
194
|
-
|
|
195
|
-
def get_interactive(self, env: dict = os.environ):
|
|
196
|
-
override = env.get("INTERACTIVE")
|
|
197
|
-
if override is not None:
|
|
198
|
-
return str(override).lower() in {"1", "true", "yes", "on"}
|
|
199
|
-
return False # default safe
|
|
200
|
-
|
|
201
|
-
def get_subprocess_command(self, env: dict = os.environ):
|
|
202
|
-
interactive = self.get_interactive(env)
|
|
203
|
-
if interactive:
|
|
204
|
-
return [config.SHELL, "-i", "-c", self.expr]
|
|
205
|
-
return [config.SHELL, "-c", self.expr]
|
|
206
|
-
|
|
207
|
-
def executable(self):
|
|
208
|
-
"""Returns the shell command to run the original command."""
|
|
209
|
-
return self.cmd
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
class CmdWrapper(CommandWrapper):
|
|
213
|
-
"""Wrapper class for running wrapped commands in Windows cmd.exe."""
|
|
214
|
-
|
|
215
|
-
def __init__(self, namespace=config.DEFAULT_NAMESPACE, args=[]):
|
|
216
|
-
super().__init__(namespace, args)
|
|
217
|
-
|
|
218
|
-
# Always run through cmd.exe explicitly
|
|
219
|
-
self.shell = False
|
|
220
|
-
self._cmd_exe = config.SHELL # expected: "cmd" or "cmd.exe"
|
|
221
|
-
|
|
222
|
-
# Join the intended argv into one command-line string for /c
|
|
223
|
-
cmdline = shell_join(self.cmd)
|
|
224
|
-
|
|
225
|
-
# cmd.exe /c <command>
|
|
226
|
-
self._subprocess_argv = [self._cmd_exe, "/c", cmdline]
|
|
227
|
-
|
|
228
|
-
def executable(self):
|
|
229
|
-
return self._cmd_exe
|
|
230
|
-
|
|
231
|
-
def get_subprocess_args(self, cmd):
|
|
232
|
-
# Not used (we override get_subprocess_command)
|
|
233
|
-
return []
|
|
234
|
-
|
|
235
|
-
def get_subprocess_command(self, env):
|
|
236
|
-
return list(self._subprocess_argv)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def capture_output(
|
|
240
|
-
command: str,
|
|
241
|
-
namespace: str = config.DEFAULT_NAMESPACE,
|
|
242
|
-
timeout: int = config.COMMAND_TIMEOUT,
|
|
243
|
-
):
|
|
244
|
-
"""
|
|
245
|
-
Runs a command (string or argv) with the given stack namespace and captures stdout/stderr.
|
|
246
|
-
|
|
247
|
-
Returns: (returncode, stdout, stderr)
|
|
248
|
-
"""
|
|
249
|
-
import errno
|
|
250
|
-
|
|
251
|
-
shellname = os.path.basename(config.SHELL).lower()
|
|
252
|
-
argv = list(command) if isinstance(command, (list, tuple)) else to_args(command)
|
|
253
|
-
|
|
254
|
-
# build env exactly like Wrapper.launch()
|
|
255
|
-
env = os.environ.copy()
|
|
256
|
-
env.update(resolve_environ(load_environ(namespace)))
|
|
257
|
-
|
|
258
|
-
# prefer argv execution where possible
|
|
259
|
-
if shellname in ["bash", "sh", "zsh"]:
|
|
260
|
-
needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
|
|
261
|
-
if needs_shell:
|
|
262
|
-
expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
|
|
263
|
-
expr = shell_join(expr_argv)
|
|
264
|
-
cmd = [config.SHELL, "-c", expr]
|
|
265
|
-
else:
|
|
266
|
-
cmd = argv
|
|
267
|
-
|
|
268
|
-
# for cmd always use original command string
|
|
269
|
-
elif shellname in ["cmd"]:
|
|
270
|
-
cmd = command
|
|
271
|
-
|
|
272
|
-
else:
|
|
273
|
-
cmd = argv
|
|
274
|
-
|
|
275
|
-
try:
|
|
276
|
-
proc = subprocess.run(
|
|
277
|
-
cmd,
|
|
278
|
-
env=encode(env),
|
|
279
|
-
shell=False,
|
|
280
|
-
check=False,
|
|
281
|
-
capture_output=True,
|
|
282
|
-
text=True,
|
|
283
|
-
timeout=timeout,
|
|
284
|
-
)
|
|
285
|
-
return proc.returncode, proc.stdout, proc.stderr
|
|
286
|
-
except FileNotFoundError as e:
|
|
287
|
-
# no process ran; synthesize a bash-like error and code
|
|
288
|
-
# 127 is the conventional "command not found" code in shells
|
|
289
|
-
missing = e.filename or (
|
|
290
|
-
cmd[0] if isinstance(cmd, list) and cmd else str(command)
|
|
291
|
-
)
|
|
292
|
-
return 127, "", f"{missing}: command not found"
|
|
293
|
-
except OSError as e:
|
|
294
|
-
# Other OS-level execution errors (permission, exec format, etc.)
|
|
295
|
-
rc = 126 if getattr(e, "errno", None) in (errno.EACCES,) else 1
|
|
296
|
-
return rc, "", str(e)
|
|
297
|
-
except subprocess.TimeoutExpired as e:
|
|
298
|
-
return 124, "", f"Command timed out after {timeout} seconds"
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def run_command(command: str, namespace: str = config.DEFAULT_NAMESPACE):
|
|
302
|
-
"""
|
|
303
|
-
Runs a given command with the given stack namespace.
|
|
304
|
-
|
|
305
|
-
>>> run_command(['ls', '-l'])
|
|
306
|
-
|
|
307
|
-
Or to run in a specific stack namespace:
|
|
308
|
-
|
|
309
|
-
>>> run_command(['ls', '-l'], 'my-stack')
|
|
310
|
-
|
|
311
|
-
- Automatically detects the shell to use.
|
|
312
|
-
- Converts {VAR} to $VAR for bash, sh, zsh, and %VAR% for cmd.
|
|
313
|
-
|
|
314
|
-
:param command: command to run as a list of arguments.
|
|
315
|
-
:param namespace: environment stack name (default: 'default').
|
|
316
|
-
:returns: command exit code
|
|
317
|
-
"""
|
|
318
|
-
logger.setup_stream_handler()
|
|
319
|
-
shellname = os.path.basename(config.SHELL).lower()
|
|
320
|
-
argv = list(command) if isinstance(command, (list, tuple)) else to_args(command)
|
|
321
|
-
|
|
322
|
-
if shellname in ["bash", "sh", "zsh"]:
|
|
323
|
-
needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
|
|
324
|
-
if needs_shell:
|
|
325
|
-
expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
|
|
326
|
-
expr = shell_join(expr_argv)
|
|
327
|
-
return ShellWrapper(namespace, expr).launch()
|
|
328
|
-
|
|
329
|
-
return CommandWrapper(namespace, argv).launch()
|
|
330
|
-
|
|
331
|
-
if shellname in ["cmd"]:
|
|
332
|
-
expr = [re.sub(r"\{(\w+)\}", r"%\1%", a) for a in argv]
|
|
333
|
-
return CmdWrapper(namespace, expr).launch()
|
|
334
|
-
|
|
335
|
-
return CommandWrapper(namespace, argv).launch()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|