envstack 0.9.3__tar.gz → 0.9.4__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-0.9.3/lib/envstack.egg-info → envstack-0.9.4}/PKG-INFO +2 -1
- {envstack-0.9.3 → envstack-0.9.4}/README.md +1 -0
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/__init__.py +2 -2
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/cli.py +14 -13
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/config.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/encrypt.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/env.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/envshell.py +20 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/exceptions.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/logger.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/node.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/path.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/util.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack/wrapper.py +62 -57
- {envstack-0.9.3 → envstack-0.9.4/lib/envstack.egg-info}/PKG-INFO +2 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/SOURCES.txt +2 -1
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/entry_points.txt +0 -1
- {envstack-0.9.3 → envstack-0.9.4}/setup.py +2 -3
- {envstack-0.9.3 → envstack-0.9.4}/tests/test_cmds.py +23 -70
- {envstack-0.9.3 → envstack-0.9.4}/tests/test_encrypt.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/tests/test_env.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/tests/test_node.py +1 -1
- {envstack-0.9.3 → envstack-0.9.4}/tests/test_util.py +1 -1
- envstack-0.9.4/tests/test_wrapper.py +141 -0
- {envstack-0.9.3 → envstack-0.9.4}/LICENSE +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/dist.json +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/dependency_links.txt +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/not-zip-safe +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/requires.txt +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/lib/envstack.egg-info/top_level.txt +0 -0
- {envstack-0.9.3 → envstack-0.9.4}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: envstack
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Stacked environment variable management system
|
|
5
5
|
Home-page: http://github.com/rsgalloway/envstack
|
|
6
6
|
Author: Ryan Galloway
|
|
@@ -616,6 +616,7 @@ The following environment variables are used to help manage functionality:
|
|
|
616
616
|
| DEFAULT_ENV_STACK | Name of the default environment stack (default) |
|
|
617
617
|
| ENVPATH | Colon-separated paths to search for stack files |
|
|
618
618
|
| IGNORE_MISSING | Ignore missing stack files when resolving environments |
|
|
619
|
+
| INTERACTIVE | Run one-off commands in an interactive shell |
|
|
619
620
|
| STACK | Stores the name of the current environment stack |
|
|
620
621
|
|
|
621
622
|
# Tests
|
|
@@ -592,6 +592,7 @@ The following environment variables are used to help manage functionality:
|
|
|
592
592
|
| DEFAULT_ENV_STACK | Name of the default environment stack (default) |
|
|
593
593
|
| ENVPATH | Colon-separated paths to search for stack files |
|
|
594
594
|
| IGNORE_MISSING | Ignore missing stack files when resolving environments |
|
|
595
|
+
| INTERACTIVE | Run one-off commands in an interactive shell |
|
|
595
596
|
| STACK | Stores the name of the current environment stack |
|
|
596
597
|
|
|
597
598
|
# Tests
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -34,6 +34,6 @@ Stacked environment variable management system.
|
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
36
|
__prog__ = "envstack"
|
|
37
|
-
__version__ = "0.9.
|
|
37
|
+
__version__ = "0.9.4"
|
|
38
38
|
|
|
39
39
|
from envstack.env import clear, init, revert, save # noqa: F401
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -161,11 +161,6 @@ def parse_args():
|
|
|
161
161
|
action="store_true",
|
|
162
162
|
help="create a bare environment",
|
|
163
163
|
)
|
|
164
|
-
parser.add_argument(
|
|
165
|
-
"--shell",
|
|
166
|
-
action="store_true",
|
|
167
|
-
help="drop into a shell with the environment loaded",
|
|
168
|
-
)
|
|
169
164
|
encrypt_group = parser.add_argument_group("encryption options")
|
|
170
165
|
encrypt_group.add_argument(
|
|
171
166
|
"-e",
|
|
@@ -226,6 +221,12 @@ def parse_args():
|
|
|
226
221
|
metavar="SCOPE",
|
|
227
222
|
help="search scope for environment stack files",
|
|
228
223
|
)
|
|
224
|
+
parser.add_argument(
|
|
225
|
+
"-u",
|
|
226
|
+
"--unresolved",
|
|
227
|
+
action="store_true",
|
|
228
|
+
help="dump unresolved environment variables to stdout",
|
|
229
|
+
)
|
|
229
230
|
parser.add_argument(
|
|
230
231
|
"-r",
|
|
231
232
|
"--resolve",
|
|
@@ -263,11 +264,11 @@ def envshell(namespace: List[str] = None):
|
|
|
263
264
|
"""Run a shell in the given environment stack."""
|
|
264
265
|
from .envshell import EnvshellWrapper
|
|
265
266
|
|
|
266
|
-
print("\U0001F680 Launching
|
|
267
|
+
print("\U0001F680 Launching envstack shell... CTRL+D to exit")
|
|
267
268
|
|
|
268
269
|
name = (namespace or [config.DEFAULT_NAMESPACE])[:]
|
|
269
|
-
|
|
270
|
-
return
|
|
270
|
+
shell = EnvshellWrapper(name)
|
|
271
|
+
return shell.launch()
|
|
271
272
|
|
|
272
273
|
|
|
273
274
|
def whichenv():
|
|
@@ -292,9 +293,6 @@ def main():
|
|
|
292
293
|
if command:
|
|
293
294
|
return run_command(command, args.namespace)
|
|
294
295
|
|
|
295
|
-
elif args.shell:
|
|
296
|
-
return envshell(args.namespace)
|
|
297
|
-
|
|
298
296
|
elif args.keygen:
|
|
299
297
|
from envstack.encrypt import generate_keys
|
|
300
298
|
|
|
@@ -445,13 +443,16 @@ def main():
|
|
|
445
443
|
for source in env.sources:
|
|
446
444
|
print(source.path)
|
|
447
445
|
|
|
448
|
-
|
|
446
|
+
elif args.unresolved:
|
|
449
447
|
env = load_environ(
|
|
450
448
|
args.namespace, platform=args.platform, encrypt=args.encrypt
|
|
451
449
|
)
|
|
452
450
|
for k, v in sorted(env.items(), key=lambda x: str(x[0])):
|
|
453
451
|
print(f"{k}={v}")
|
|
454
452
|
|
|
453
|
+
else:
|
|
454
|
+
return envshell(args.namespace)
|
|
455
|
+
|
|
455
456
|
except KeyboardInterrupt:
|
|
456
457
|
print("Stopping...")
|
|
457
458
|
return 2
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -122,3 +122,22 @@ class EnvshellWrapper(Wrapper):
|
|
|
122
122
|
Override to return argv list for subprocess.Popen(..., shell=False).
|
|
123
123
|
"""
|
|
124
124
|
return _detect_shell_argv()
|
|
125
|
+
|
|
126
|
+
def get_shell_prompt(self) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Return the environment variable that controls the shell prompt and its
|
|
129
|
+
desired value.
|
|
130
|
+
"""
|
|
131
|
+
if os.name == "nt":
|
|
132
|
+
return ("PROMPT", "$E[32m(${ENV:=${STACK}})$E[0m $P$G ")
|
|
133
|
+
else:
|
|
134
|
+
return ("PS1", "\[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$ ")
|
|
135
|
+
|
|
136
|
+
def get_subprocess_env(self):
|
|
137
|
+
"""
|
|
138
|
+
Override to inject PS1/PROMPT if not already set.
|
|
139
|
+
"""
|
|
140
|
+
prompt_env, prompt_value = self.get_shell_prompt()
|
|
141
|
+
if prompt_env not in self.env:
|
|
142
|
+
self.env[prompt_env] = prompt_value
|
|
143
|
+
return super().get_subprocess_env()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -37,7 +37,6 @@ import os
|
|
|
37
37
|
import re
|
|
38
38
|
import shlex
|
|
39
39
|
import subprocess
|
|
40
|
-
import sys
|
|
41
40
|
import traceback
|
|
42
41
|
|
|
43
42
|
from envstack import config, logger
|
|
@@ -93,6 +92,7 @@ class Wrapper(object):
|
|
|
93
92
|
|
|
94
93
|
tool.log = MyLogger()
|
|
95
94
|
"""
|
|
95
|
+
shell: bool = False
|
|
96
96
|
|
|
97
97
|
def __init__(self, namespace, args=[]):
|
|
98
98
|
"""Initializes the wrapper with the given namespace and args.
|
|
@@ -103,7 +103,7 @@ class Wrapper(object):
|
|
|
103
103
|
super(Wrapper, self).__init__()
|
|
104
104
|
self.args = args
|
|
105
105
|
self.name = namespace
|
|
106
|
-
self.shell =
|
|
106
|
+
self.shell = self.shell
|
|
107
107
|
self.log = logger.log
|
|
108
108
|
self.env = load_environ(namespace)
|
|
109
109
|
|
|
@@ -113,38 +113,34 @@ class Wrapper(object):
|
|
|
113
113
|
|
|
114
114
|
def launch(self):
|
|
115
115
|
"""Launches the wrapped tool in a subprocess with env."""
|
|
116
|
-
exitcode = 0
|
|
117
116
|
env = self.get_subprocess_env()
|
|
118
117
|
command = self.get_subprocess_command(env)
|
|
119
118
|
|
|
120
119
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
bufsize=0,
|
|
120
|
+
proc = subprocess.run(
|
|
121
|
+
command,
|
|
124
122
|
env=encode(env),
|
|
125
123
|
shell=self.shell,
|
|
124
|
+
check=False,
|
|
126
125
|
)
|
|
126
|
+
return proc.returncode
|
|
127
127
|
except Exception:
|
|
128
128
|
traceback.print_exc()
|
|
129
|
-
|
|
130
|
-
else:
|
|
131
|
-
stdout, stderr = process.communicate() # dead code, kept for reference
|
|
132
|
-
while stdout and stderr:
|
|
133
|
-
self.log.info(stdout)
|
|
134
|
-
self.log.error(stderr)
|
|
135
|
-
exitcode = process.poll()
|
|
136
|
-
|
|
137
|
-
return exitcode
|
|
129
|
+
return 1
|
|
138
130
|
|
|
139
131
|
def get_subprocess_args(self, cmd):
|
|
140
132
|
"""Returns the arguments to be passed to the subprocess."""
|
|
141
133
|
return self.args
|
|
142
134
|
|
|
143
135
|
def get_subprocess_command(self, env):
|
|
144
|
-
"""Returns
|
|
136
|
+
"""Returns argv (preferred) or a command string if shell=True."""
|
|
145
137
|
cmd = evaluate_modifiers(self.executable(), env)
|
|
146
138
|
args = self.get_subprocess_args(cmd)
|
|
147
|
-
|
|
139
|
+
|
|
140
|
+
if self.shell:
|
|
141
|
+
return " ".join([cmd] + args)
|
|
142
|
+
|
|
143
|
+
return [cmd] + args
|
|
148
144
|
|
|
149
145
|
def get_subprocess_env(self):
|
|
150
146
|
"""
|
|
@@ -173,52 +169,39 @@ class CommandWrapper(Wrapper):
|
|
|
173
169
|
:param args: command and arguments as a list.
|
|
174
170
|
"""
|
|
175
171
|
super(CommandWrapper, self).__init__(namespace, args)
|
|
176
|
-
self.cmd = args
|
|
172
|
+
self.cmd = list(args)
|
|
177
173
|
|
|
178
174
|
def executable(self):
|
|
179
175
|
"""Returns the command to run."""
|
|
180
|
-
return
|
|
176
|
+
return self.cmd[0]
|
|
181
177
|
|
|
178
|
+
def get_subprocess_args(self, cmd):
|
|
179
|
+
return self.cmd[1:]
|
|
182
180
|
|
|
183
|
-
class ShellWrapper(CommandWrapper):
|
|
184
|
-
"""Wrapper class for running wrapped commands in bash, sh, or zsh."""
|
|
185
181
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
class ShellWrapper(Wrapper):
|
|
183
|
+
"""
|
|
184
|
+
Runs a *string expression* under the user's shell using -c.
|
|
185
|
+
Useful for wrapping shell expressions that need variable expansion:
|
|
190
186
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
bash
|
|
194
|
-
>>> print(cmd.args)
|
|
195
|
-
['-i', '-c', 'ls -l']
|
|
187
|
+
>>> shell = ShellWrapper('default', 'echo {HOME}')
|
|
188
|
+
"""
|
|
196
189
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
"""
|
|
201
|
-
super(ShellWrapper, self).__init__(namespace, args)
|
|
190
|
+
def __init__(self, namespace=config.DEFAULT_NAMESPACE, expr: str = ""):
|
|
191
|
+
super().__init__(namespace, args=[])
|
|
192
|
+
self.expr = expr
|
|
202
193
|
|
|
203
194
|
def get_interactive(self, env: dict = os.environ):
|
|
204
|
-
"""Returns whether to run the command in an interactive shell."""
|
|
205
195
|
override = env.get("INTERACTIVE")
|
|
206
196
|
if override is not None:
|
|
207
|
-
return override.lower() in {"1", "true", "yes", "on"}
|
|
208
|
-
return
|
|
197
|
+
return str(override).lower() in {"1", "true", "yes", "on"}
|
|
198
|
+
return False # default safe
|
|
209
199
|
|
|
210
200
|
def get_subprocess_command(self, env: dict = os.environ):
|
|
211
|
-
"""Returns the command to be passed to the shell in a subprocess."""
|
|
212
201
|
interactive = self.get_interactive(env)
|
|
213
|
-
if
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return f'{config.SHELL} -c "{self.cmd}"'
|
|
217
|
-
else:
|
|
218
|
-
escaped_command = shlex.quote(self.cmd)
|
|
219
|
-
if interactive:
|
|
220
|
-
return f"{config.SHELL} -i -c {escaped_command}"
|
|
221
|
-
return f"{config.SHELL} -c {escaped_command}"
|
|
202
|
+
if interactive:
|
|
203
|
+
return [config.SHELL, "-i", "-c", self.expr]
|
|
204
|
+
return [config.SHELL, "-c", self.expr]
|
|
222
205
|
|
|
223
206
|
def executable(self):
|
|
224
207
|
"""Returns the shell command to run the original command."""
|
|
@@ -276,12 +259,34 @@ def run_command(command: str, namespace: str = config.DEFAULT_NAMESPACE):
|
|
|
276
259
|
"""
|
|
277
260
|
logger.setup_stream_handler()
|
|
278
261
|
shellname = os.path.basename(config.SHELL)
|
|
262
|
+
|
|
263
|
+
# normalize to argv list
|
|
264
|
+
argv = list(command) if isinstance(command, (list, tuple)) else to_args(command)
|
|
265
|
+
|
|
266
|
+
needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
|
|
267
|
+
if needs_shell:
|
|
268
|
+
expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
|
|
269
|
+
expr = shell_join(expr_argv)
|
|
270
|
+
return ShellWrapper(namespace, expr).launch()
|
|
271
|
+
|
|
279
272
|
if shellname in ["bash", "sh", "zsh"]:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
273
|
+
# 1) if user explicitly invoked a shell (bash/sh/zsh), do not wrap again
|
|
274
|
+
if argv and os.path.basename(argv[0]) in ["bash", "sh", "zsh"]:
|
|
275
|
+
return CommandWrapper(namespace, argv).launch()
|
|
276
|
+
|
|
277
|
+
# 2) if command contains {VARS}, convert to ${VARS} and run as a shell expression
|
|
278
|
+
needs_shell = any(re.search(r"\{(\w+)\}", a) for a in argv)
|
|
279
|
+
if needs_shell:
|
|
280
|
+
expr_argv = [re.sub(r"\{(\w+)\}", r"${\1}", a) for a in argv]
|
|
281
|
+
expr = shell_join(expr_argv)
|
|
282
|
+
return ShellWrapper(namespace, expr).launch()
|
|
283
|
+
|
|
284
|
+
# 3) otherwise run direct argv (best behavior)
|
|
285
|
+
return CommandWrapper(namespace, argv).launch()
|
|
286
|
+
|
|
287
|
+
if shellname in ["cmd"]:
|
|
288
|
+
# windows behavior preserved (if you need it)
|
|
289
|
+
expr = re.sub(r"\{(\w+)\}", r"%\1%", " ".join(argv))
|
|
290
|
+
return CmdWrapper(namespace, expr).launch()
|
|
291
|
+
|
|
292
|
+
return CommandWrapper(namespace, argv).launch()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: envstack
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
4
4
|
Summary: Stacked environment variable management system
|
|
5
5
|
Home-page: http://github.com/rsgalloway/envstack
|
|
6
6
|
Author: Ryan Galloway
|
|
@@ -616,6 +616,7 @@ The following environment variables are used to help manage functionality:
|
|
|
616
616
|
| DEFAULT_ENV_STACK | Name of the default environment stack (default) |
|
|
617
617
|
| ENVPATH | Colon-separated paths to search for stack files |
|
|
618
618
|
| IGNORE_MISSING | Ignore missing stack files when resolving environments |
|
|
619
|
+
| INTERACTIVE | Run one-off commands in an interactive shell |
|
|
619
620
|
| STACK | Stores the name of the current environment stack |
|
|
620
621
|
|
|
621
622
|
# Tests
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -40,7 +40,7 @@ with open(os.path.join(here, "README.md")) as f:
|
|
|
40
40
|
|
|
41
41
|
setup(
|
|
42
42
|
name="envstack",
|
|
43
|
-
version="0.9.
|
|
43
|
+
version="0.9.4",
|
|
44
44
|
description="Stacked environment variable management system",
|
|
45
45
|
long_description=long_description,
|
|
46
46
|
long_description_content_type="text/markdown",
|
|
@@ -69,7 +69,6 @@ setup(
|
|
|
69
69
|
entry_points={
|
|
70
70
|
"console_scripts": [
|
|
71
71
|
"envstack=envstack.cli:main",
|
|
72
|
-
"envshell=envstack.cli:envshell",
|
|
73
72
|
"whichenv=envstack.cli:whichenv",
|
|
74
73
|
],
|
|
75
74
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
2
|
#
|
|
3
3
|
# Copyright (c) 2024-2025, Ryan Galloway (ryan@rsgalloway.com)
|
|
4
4
|
#
|
|
@@ -67,9 +67,7 @@ class TestUnresolved(unittest.TestCase):
|
|
|
67
67
|
"""Tests unresolved environment variables from the cli."""
|
|
68
68
|
|
|
69
69
|
def setUp(self):
|
|
70
|
-
self.envstack_bin =
|
|
71
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
72
|
-
)
|
|
70
|
+
self.envstack_bin = "envstack -u"
|
|
73
71
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
74
72
|
self.root = {
|
|
75
73
|
"linux": "/mnt/pipe",
|
|
@@ -86,15 +84,14 @@ ENVPATH=${DEPLOY_ROOT}/env:${ENVPATH}
|
|
|
86
84
|
HELLO=${HELLO:=world}
|
|
87
85
|
LOG_LEVEL=${LOG_LEVEL:=INFO}
|
|
88
86
|
PATH=${DEPLOY_ROOT}/bin:${PATH}
|
|
89
|
-
PS1=\[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
87
|
+
PS1=\[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$
|
|
90
88
|
PYTHONPATH=${DEPLOY_ROOT}/lib/python:${PYTHONPATH}
|
|
91
89
|
ROOT=%s
|
|
92
90
|
STACK=default
|
|
93
91
|
"""
|
|
94
92
|
% self.root
|
|
95
93
|
)
|
|
96
|
-
|
|
97
|
-
output = subprocess.check_output(command, shell=True, universal_newlines=True)
|
|
94
|
+
output = subprocess.check_output(self.envstack_bin, shell=True, universal_newlines=True)
|
|
98
95
|
self.assertEqual(output, expected_output)
|
|
99
96
|
|
|
100
97
|
def test_dev(self):
|
|
@@ -105,7 +102,7 @@ ENVPATH=${ROOT}/dev/env:${ROOT}/prod/env:${ENVPATH}
|
|
|
105
102
|
HELLO=${HELLO:=world}
|
|
106
103
|
LOG_LEVEL=DEBUG
|
|
107
104
|
PATH=${ROOT}/dev/bin:${ROOT}/prod/bin:${PATH}
|
|
108
|
-
PS1=\[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
105
|
+
PS1=\[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$
|
|
109
106
|
PYTHONPATH=${ROOT}/dev/lib/python:${ROOT}/prod/lib/python:${PYTHONPATH}
|
|
110
107
|
ROOT=%s
|
|
111
108
|
STACK=dev
|
|
@@ -116,25 +113,6 @@ STACK=dev
|
|
|
116
113
|
output = subprocess.check_output(command, shell=True, universal_newlines=True)
|
|
117
114
|
self.assertEqual(output, expected_output)
|
|
118
115
|
|
|
119
|
-
def test_distman(self):
|
|
120
|
-
expected_output = (
|
|
121
|
-
"""DEPLOY_ROOT=${ROOT}/${ENV}
|
|
122
|
-
ENV=${ENV:=prod}
|
|
123
|
-
ENVPATH=${DEPLOY_ROOT}/env:${ENVPATH}
|
|
124
|
-
HELLO=${HELLO:=world}
|
|
125
|
-
LOG_LEVEL=INFO
|
|
126
|
-
PATH=${DEPLOY_ROOT}/bin:${PATH}
|
|
127
|
-
PS1=\[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
128
|
-
PYTHONPATH=${DEPLOY_ROOT}/lib/python:${PYTHONPATH}
|
|
129
|
-
ROOT=%s
|
|
130
|
-
STACK=distman
|
|
131
|
-
"""
|
|
132
|
-
% self.root
|
|
133
|
-
)
|
|
134
|
-
command = "%s distman" % self.envstack_bin
|
|
135
|
-
output = subprocess.check_output(command, shell=True, universal_newlines=True)
|
|
136
|
-
self.assertEqual(output, expected_output)
|
|
137
|
-
|
|
138
116
|
def test_hello(self):
|
|
139
117
|
expected_output = (
|
|
140
118
|
"""DEPLOY_ROOT=${ROOT}/dev
|
|
@@ -167,7 +145,7 @@ INT=5
|
|
|
167
145
|
LOG_LEVEL=${LOG_LEVEL:=INFO}
|
|
168
146
|
NUMBER_LIST=[1, 2, 3]
|
|
169
147
|
PATH=${DEPLOY_ROOT}/bin:${PATH}
|
|
170
|
-
PS1=\[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
148
|
+
PS1=\[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$
|
|
171
149
|
PYTHONPATH=${DEPLOY_ROOT}/lib/python:${PYTHONPATH}
|
|
172
150
|
ROOT=${HOME}/.local/pipe
|
|
173
151
|
STACK=thing
|
|
@@ -181,9 +159,7 @@ class TestEncrypt(unittest.TestCase):
|
|
|
181
159
|
"""Tests encrypting environment variables from the cli."""
|
|
182
160
|
|
|
183
161
|
def setUp(self):
|
|
184
|
-
self.envstack_bin =
|
|
185
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
186
|
-
)
|
|
162
|
+
self.envstack_bin = "envstack -u"
|
|
187
163
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
188
164
|
self.root = {
|
|
189
165
|
"linux": "/mnt/pipe",
|
|
@@ -208,7 +184,7 @@ ENVPATH=JHtERVBMT1lfUk9PVH0vZW52OiR7RU5WUEFUSH0=
|
|
|
208
184
|
HELLO=JHtIRUxMTzo9d29ybGR9
|
|
209
185
|
LOG_LEVEL=JHtMT0dfTEVWRUw6PUlORk99
|
|
210
186
|
PATH=JHtERVBMT1lfUk9PVH0vYmluOiR7UEFUSH0=
|
|
211
|
-
PS1=
|
|
187
|
+
PS1=XFtcZVszMm1cXSgke0VOVjo9JHtTVEFDS319KVxbXGVbMG1cXSBcd1wkIA==
|
|
212
188
|
PYTHONPATH=JHtERVBMT1lfUk9PVH0vbGliL3B5dGhvbjoke1BZVEhPTlBBVEh9
|
|
213
189
|
ROOT=L21udC9waXBl
|
|
214
190
|
STACK=ZGVmYXVsdA==
|
|
@@ -257,7 +233,7 @@ ENVPATH=JHtST09UfS9kZXYvZW52OiR7Uk9PVH0vcHJvZC9lbnY6JHtFTlZQQVRIfQ==
|
|
|
257
233
|
HELLO=JHtIRUxMTzo9d29ybGR9
|
|
258
234
|
LOG_LEVEL=REVCVUc=
|
|
259
235
|
PATH=JHtST09UfS9kZXYvYmluOiR7Uk9PVH0vcHJvZC9iaW46JHtQQVRIfQ==
|
|
260
|
-
PS1=
|
|
236
|
+
PS1=XFtcZVszMm1cXSgke0VOVjo9JHtTVEFDS319KVxbXGVbMG1cXSBcd1wkIA==
|
|
261
237
|
PYTHONPATH=JHtST09UfS9kZXYvbGliL3B5dGhvbjoke1JPT1R9L3Byb2QvbGliL3B5dGhvbjoke1BZVEhPTlBBVEh9
|
|
262
238
|
ROOT=L21udC9waXBl
|
|
263
239
|
STACK=ZGV2
|
|
@@ -278,9 +254,7 @@ class TestResolved(unittest.TestCase):
|
|
|
278
254
|
"""Tests resolved environment variables."""
|
|
279
255
|
|
|
280
256
|
def setUp(self):
|
|
281
|
-
self.envstack_bin =
|
|
282
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
283
|
-
)
|
|
257
|
+
self.envstack_bin = "envstack"
|
|
284
258
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
285
259
|
self.root = {
|
|
286
260
|
"linux": "/mnt/pipe",
|
|
@@ -310,15 +284,6 @@ STACK=dev
|
|
|
310
284
|
output = subprocess.check_output(command, shell=True, universal_newlines=True)
|
|
311
285
|
self.assertEqual(output, expected_output)
|
|
312
286
|
|
|
313
|
-
def test_distman(self):
|
|
314
|
-
expected_output = f"""DEPLOY_ROOT={self.root}/{os.getenv('ENV', 'prod')}
|
|
315
|
-
ROOT={self.root}
|
|
316
|
-
STACK=distman
|
|
317
|
-
"""
|
|
318
|
-
command = "%s distman -r DEPLOY_ROOT ROOT STACK" % self.envstack_bin
|
|
319
|
-
output = subprocess.check_output(command, shell=True, universal_newlines=True)
|
|
320
|
-
self.assertEqual(output, expected_output)
|
|
321
|
-
|
|
322
287
|
def test_dev_distman(self):
|
|
323
288
|
expected_output = f"""DEPLOY_ROOT={self.root}/dev
|
|
324
289
|
ROOT={self.root}
|
|
@@ -373,9 +338,7 @@ class TestBake(unittest.TestCase):
|
|
|
373
338
|
self.filename = "baketest.env"
|
|
374
339
|
if os.path.exists(self.filename):
|
|
375
340
|
os.remove(self.filename)
|
|
376
|
-
self.envstack_bin =
|
|
377
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
378
|
-
)
|
|
341
|
+
self.envstack_bin = "envstack"
|
|
379
342
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
380
343
|
self.root = {
|
|
381
344
|
"linux": "/mnt/pipe",
|
|
@@ -408,15 +371,15 @@ all: &all
|
|
|
408
371
|
PYTHONPATH: ${DEPLOY_ROOT}/lib/python:${PYTHONPATH}
|
|
409
372
|
darwin:
|
|
410
373
|
<<: *all
|
|
411
|
-
PS1: \[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
374
|
+
PS1: \[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$
|
|
412
375
|
ROOT: /Volumes/pipe
|
|
413
376
|
linux:
|
|
414
377
|
<<: *all
|
|
415
|
-
PS1: \[\e[32m\](${ENV})\[\e[0m\] \w\$
|
|
378
|
+
PS1: \[\e[32m\](${ENV:=${STACK}})\[\e[0m\] \w\$
|
|
416
379
|
ROOT: /mnt/pipe
|
|
417
380
|
windows:
|
|
418
381
|
<<: *all
|
|
419
|
-
PROMPT: $E[32m(${ENV})$E[0m $P$G
|
|
382
|
+
PROMPT: $E[32m(${ENV:=${STACK}})$E[0m $P$G
|
|
420
383
|
ROOT: //tools/pipe
|
|
421
384
|
"""
|
|
422
385
|
output = subprocess.check_output(
|
|
@@ -499,15 +462,15 @@ all: &all
|
|
|
499
462
|
PYTHONPATH: !encrypt JHtERVBMT1lfUk9PVH0vbGliL3B5dGhvbjoke1BZVEhPTlBBVEh9
|
|
500
463
|
darwin:
|
|
501
464
|
<<: *all
|
|
502
|
-
PS1: !encrypt
|
|
465
|
+
PS1: !encrypt XFtcZVszMm1cXSgke0VOVjo9JHtTVEFDS319KVxbXGVbMG1cXSBcd1wkIA==
|
|
503
466
|
ROOT: !encrypt L1ZvbHVtZXMvcGlwZQ==
|
|
504
467
|
linux:
|
|
505
468
|
<<: *all
|
|
506
|
-
PS1: !encrypt
|
|
469
|
+
PS1: !encrypt XFtcZVszMm1cXSgke0VOVjo9JHtTVEFDS319KVxbXGVbMG1cXSBcd1wkIA==
|
|
507
470
|
ROOT: !encrypt L21udC9waXBl
|
|
508
471
|
windows:
|
|
509
472
|
<<: *all
|
|
510
|
-
PROMPT: !encrypt
|
|
473
|
+
PROMPT: !encrypt JEVbMzJtKCR7RU5WOj0ke1NUQUNLfX0pJEVbMG0gJFAkRw==
|
|
511
474
|
ROOT: !encrypt Ly90b29scy9waXBl
|
|
512
475
|
"""
|
|
513
476
|
output = subprocess.check_output(
|
|
@@ -575,9 +538,7 @@ class TestCommands(unittest.TestCase):
|
|
|
575
538
|
"""Tests various envstack commands."""
|
|
576
539
|
|
|
577
540
|
def setUp(self):
|
|
578
|
-
self.envstack_bin =
|
|
579
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
580
|
-
)
|
|
541
|
+
self.envstack_bin = "envstack"
|
|
581
542
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
582
543
|
self.root = {
|
|
583
544
|
"linux": "/mnt/pipe",
|
|
@@ -639,9 +600,7 @@ class TestSet(unittest.TestCase):
|
|
|
639
600
|
|
|
640
601
|
def setUp(self):
|
|
641
602
|
self.filename = "settest.env"
|
|
642
|
-
self.envstack_bin =
|
|
643
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
644
|
-
)
|
|
603
|
+
self.envstack_bin = "envstack"
|
|
645
604
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
646
605
|
self.root = {
|
|
647
606
|
"linux": "/mnt/pipe",
|
|
@@ -762,9 +721,7 @@ class TestVarFlow(unittest.TestCase):
|
|
|
762
721
|
"""Tests the flow of environment variables through stacks."""
|
|
763
722
|
|
|
764
723
|
def setUp(self):
|
|
765
|
-
self.envstack_bin =
|
|
766
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
767
|
-
)
|
|
724
|
+
self.envstack_bin = "envstack"
|
|
768
725
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
769
726
|
os.environ["ENVPATH"] = envpath
|
|
770
727
|
|
|
@@ -805,9 +762,7 @@ class TestDistman(unittest.TestCase):
|
|
|
805
762
|
"""Tests value for $DEPLOY_ROOT under various environment configurations."""
|
|
806
763
|
|
|
807
764
|
def setUp(self):
|
|
808
|
-
self.envstack_bin =
|
|
809
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
810
|
-
)
|
|
765
|
+
self.envstack_bin = "envstack"
|
|
811
766
|
self.python_cmd = """python -c \"import os,envstack;envstack.init('distman');print(os.getenv('DEPLOY_ROOT'))\""""
|
|
812
767
|
envpath = os.path.join(os.path.dirname(__file__), "..", "env")
|
|
813
768
|
self.root = {
|
|
@@ -858,9 +813,7 @@ class TestDistman(unittest.TestCase):
|
|
|
858
813
|
class TestIssues(unittest.TestCase):
|
|
859
814
|
def setUp(self):
|
|
860
815
|
self.root = create_test_root()
|
|
861
|
-
self.envstack_bin =
|
|
862
|
-
os.path.dirname(__file__), "..", "bin", "envstack"
|
|
863
|
-
)
|
|
816
|
+
self.envstack_bin = "envstack"
|
|
864
817
|
os.environ["ENVPATH"] = os.path.join(self.root, "prod", "env")
|
|
865
818
|
|
|
866
819
|
def tearDown(self):
|
|
@@ -988,7 +941,7 @@ class TestIssues(unittest.TestCase):
|
|
|
988
941
|
cmd = [
|
|
989
942
|
"bash",
|
|
990
943
|
"-lc",
|
|
991
|
-
'envstack --
|
|
944
|
+
'envstack -- echo "hello from envstack"',
|
|
992
945
|
]
|
|
993
946
|
|
|
994
947
|
proc = subprocess.Popen(
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright (c) 2024-2025, 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 unit tests for running commands.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
import pytest
|
|
38
|
+
|
|
39
|
+
import envstack.wrapper as wrapper_mod
|
|
40
|
+
from envstack.wrapper import Wrapper, CommandWrapper, run_command
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.fixture
|
|
44
|
+
def stub_env(monkeypatch):
|
|
45
|
+
"""
|
|
46
|
+
Make wrapper tests independent of real envstack stack files.
|
|
47
|
+
|
|
48
|
+
We monkeypatch load_environ(namespace) to return a deterministic env.
|
|
49
|
+
Adjust ROOT/ENV values as needed.
|
|
50
|
+
"""
|
|
51
|
+
def _fake_load_environ(namespace):
|
|
52
|
+
# minimal env for tests; include PATH so subprocess can find tools
|
|
53
|
+
env = dict(os.environ)
|
|
54
|
+
env["ENV"] = namespace or "prod"
|
|
55
|
+
env["ROOT"] = "/tmp/envstack-root"
|
|
56
|
+
return env
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(wrapper_mod, "load_environ", _fake_load_environ)
|
|
59
|
+
return _fake_load_environ
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class HelloWrapper(Wrapper):
|
|
63
|
+
"""
|
|
64
|
+
Example wrapper that prints an env var passed as argv[0].
|
|
65
|
+
Uses shell=True because executable() returns a command-line string.
|
|
66
|
+
"""
|
|
67
|
+
shell = True
|
|
68
|
+
|
|
69
|
+
def executable(self):
|
|
70
|
+
# prints env var named by first arg
|
|
71
|
+
return "python3 -c 'import os,sys;print(os.getenv(sys.argv[1]))'"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_wrapper_shell_true_allows_command_string(stub_env, capfd):
|
|
75
|
+
"""Test that Wrapper with shell=True runs command string."""
|
|
76
|
+
w = HelloWrapper("hello", ["ROOT"])
|
|
77
|
+
rc = w.launch()
|
|
78
|
+
out, err = capfd.readouterr()
|
|
79
|
+
assert rc == 0
|
|
80
|
+
assert out.strip() == "/tmp/envstack-root"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_commandwrapper_runs_argv_without_shell(stub_env, capfd):
|
|
84
|
+
"""Test that CommandWrapper runs argv mode without shell."""
|
|
85
|
+
w = CommandWrapper("hello", ["echo", "hi there"])
|
|
86
|
+
rc = w.launch()
|
|
87
|
+
out, err = capfd.readouterr()
|
|
88
|
+
assert rc == 0
|
|
89
|
+
assert out.strip() == "hi there"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_run_command_preserves_quoted_arg_in_argv_mode(stub_env):
|
|
93
|
+
"""Test that run_command preserves quoted args when shell=False."""
|
|
94
|
+
# Regression for: envstack -- bash -c "sleep 5" breaking into bash -c sleep 5
|
|
95
|
+
# Here we use something observable instead of sleep.
|
|
96
|
+
rc = run_command(["bash", "-c", "printf '%s\n' \"sleep 5\""], namespace="hello")
|
|
97
|
+
assert rc == 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_run_command_brace_expands_to_env_value(stub_env, capfd):
|
|
101
|
+
"""Test that {VAR} in command args expands to env var value."""
|
|
102
|
+
rc = run_command(["echo", "{ROOT}"], namespace="hello")
|
|
103
|
+
out, err = capfd.readouterr()
|
|
104
|
+
assert rc == 0
|
|
105
|
+
assert out.strip() == "/tmp/envstack-root"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_run_command_two_in_series_no_stop(stub_env, capfd):
|
|
109
|
+
"""Test that two commands run in series without stopping the process."""
|
|
110
|
+
rc1 = run_command(["bash", "-c", "echo one"], namespace="hello")
|
|
111
|
+
rc2 = run_command(["bash", "-c", "echo two"], namespace="hello")
|
|
112
|
+
out, err = capfd.readouterr()
|
|
113
|
+
lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
|
|
114
|
+
assert rc1 == 0 and rc2 == 0
|
|
115
|
+
assert lines[-2:] == ["one", "two"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.parametrize(
|
|
119
|
+
"override, expected",
|
|
120
|
+
[
|
|
121
|
+
("1", True),
|
|
122
|
+
("true", True),
|
|
123
|
+
("yes", True),
|
|
124
|
+
("on", True),
|
|
125
|
+
("0", False),
|
|
126
|
+
("false", False),
|
|
127
|
+
("no", False),
|
|
128
|
+
("off", False),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
def test_interactive_env_override_parsing(stub_env, monkeypatch, override, expected):
|
|
132
|
+
env = dict(os.environ)
|
|
133
|
+
env["INTERACTIVE"] = override
|
|
134
|
+
|
|
135
|
+
# example: if you implemented ShellWrapper.get_interactive(env)
|
|
136
|
+
ShellWrapper = getattr(wrapper_mod, "ShellWrapper", None)
|
|
137
|
+
if ShellWrapper is None:
|
|
138
|
+
pytest.skip("ShellWrapper not present in envstack.wrapper")
|
|
139
|
+
|
|
140
|
+
sw = ShellWrapper("hello", "echo test")
|
|
141
|
+
assert sw.get_interactive(env) is expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|