envstack 1.0.3__py3-none-any.whl
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/__init__.py +40 -0
- envstack/cli.py +478 -0
- envstack/config.py +108 -0
- envstack/encrypt.py +361 -0
- envstack/env.py +997 -0
- envstack/envshell.py +143 -0
- envstack/exceptions.py +82 -0
- envstack/logger.py +61 -0
- envstack/node.py +410 -0
- envstack/path.py +448 -0
- envstack/util.py +800 -0
- envstack-1.0.3.dist-info/LICENSE +12 -0
- envstack-1.0.3.dist-info/METADATA +177 -0
- envstack-1.0.3.dist-info/RECORD +17 -0
- envstack-1.0.3.dist-info/WHEEL +5 -0
- envstack-1.0.3.dist-info/entry_points.txt +3 -0
- envstack-1.0.3.dist-info/top_level.txt +1 -0
envstack/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
Stacked environment variable management system.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
__prog__ = "envstack"
|
|
37
|
+
__version__ = "1.0.3"
|
|
38
|
+
|
|
39
|
+
from envstack.env import clear, init, revert, save # noqa: F401
|
|
40
|
+
from envstack.env import load_environ, resolve_environ # noqa: F401
|
envstack/cli.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
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
|
+
Command line interface for envstack: stacked environment variable management.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import os
|
|
38
|
+
import re
|
|
39
|
+
import sys
|
|
40
|
+
import traceback
|
|
41
|
+
from typing import List
|
|
42
|
+
|
|
43
|
+
from envstack import __version__, config
|
|
44
|
+
from envstack.env import (
|
|
45
|
+
bake_environ,
|
|
46
|
+
Env,
|
|
47
|
+
encrypt_environ,
|
|
48
|
+
export_env_to_shell,
|
|
49
|
+
export,
|
|
50
|
+
load_environ,
|
|
51
|
+
resolve_environ,
|
|
52
|
+
trace_var,
|
|
53
|
+
)
|
|
54
|
+
from envstack.envshell import EnvshellWrapper
|
|
55
|
+
from envstack.logger import setup_stream_handler
|
|
56
|
+
from envstack.wrapper import run_command
|
|
57
|
+
|
|
58
|
+
setup_stream_handler()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StoreOnce(argparse.Action):
|
|
62
|
+
"""Custom argparse action to ensure an option is only set once."""
|
|
63
|
+
|
|
64
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
65
|
+
# if we've already seen this option once, bail
|
|
66
|
+
if getattr(namespace, f"__seen_{self.dest}", False):
|
|
67
|
+
aliases = "/".join(self.option_strings)
|
|
68
|
+
parser.error(f"{aliases} specified more than once.")
|
|
69
|
+
setattr(namespace, f"__seen_{self.dest}", True)
|
|
70
|
+
setattr(namespace, self.dest, values)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_env_lines(lines):
|
|
74
|
+
"""Parse lines of environment variables from an iterable.
|
|
75
|
+
|
|
76
|
+
:param lines: An iterable of lines, such as a file or stdin.
|
|
77
|
+
:return: A dictionary of environment variables.
|
|
78
|
+
"""
|
|
79
|
+
ENV_LINE_RE = re.compile(
|
|
80
|
+
r"""
|
|
81
|
+
^\s*
|
|
82
|
+
(?:export\s+)?
|
|
83
|
+
(?P<key>(?:<<|[A-Za-z_][A-Za-z0-9_]*))
|
|
84
|
+
\s*
|
|
85
|
+
(?P<op>[:=])
|
|
86
|
+
\s*
|
|
87
|
+
(?P<val>.*)
|
|
88
|
+
\s*$
|
|
89
|
+
""",
|
|
90
|
+
re.VERBOSE,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def parse_line(line: str):
|
|
94
|
+
m = ENV_LINE_RE.match(line)
|
|
95
|
+
if not m:
|
|
96
|
+
return None
|
|
97
|
+
key = m.group("key")
|
|
98
|
+
val = m.group("val")
|
|
99
|
+
# strip matching quotes only if present
|
|
100
|
+
if len(val) >= 2 and val[0] == val[-1] and val[0] in ("'", '"'):
|
|
101
|
+
val = val[1:-1]
|
|
102
|
+
return key, val
|
|
103
|
+
|
|
104
|
+
data = {}
|
|
105
|
+
for raw in lines:
|
|
106
|
+
k, v = parse_line(raw.strip())
|
|
107
|
+
data[k] = v
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _parse_keyvals(items: dict):
|
|
112
|
+
"""Parse a list of key=value pairs.
|
|
113
|
+
|
|
114
|
+
:param items: A list of strings in the form "key=value" or "key:value".
|
|
115
|
+
:return: A dictionary mapping keys to values.
|
|
116
|
+
"""
|
|
117
|
+
out = {}
|
|
118
|
+
for kv in items:
|
|
119
|
+
if ":" in kv:
|
|
120
|
+
k, v = kv.split(":", 1)
|
|
121
|
+
elif "=" in kv:
|
|
122
|
+
k, v = kv.split("=", 1)
|
|
123
|
+
else:
|
|
124
|
+
k, v = kv, ""
|
|
125
|
+
out[k] = v
|
|
126
|
+
return out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_args():
|
|
130
|
+
"""Command line argument parser.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
tuple: (args, command)
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
if "--" in sys.argv:
|
|
137
|
+
dash_index = sys.argv.index("--")
|
|
138
|
+
args_after_dash = sys.argv[dash_index + 1 :] # noqa: E203
|
|
139
|
+
args_before_dash = sys.argv[1:dash_index]
|
|
140
|
+
else:
|
|
141
|
+
args_after_dash = []
|
|
142
|
+
args_before_dash = sys.argv[1:]
|
|
143
|
+
|
|
144
|
+
parser = argparse.ArgumentParser(
|
|
145
|
+
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"-v",
|
|
149
|
+
"--version",
|
|
150
|
+
action="version",
|
|
151
|
+
version=f"envstack {__version__}",
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"namespace",
|
|
155
|
+
metavar="STACK",
|
|
156
|
+
nargs="*",
|
|
157
|
+
default=[config.DEFAULT_NAMESPACE],
|
|
158
|
+
help="the environment stacks to use",
|
|
159
|
+
)
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"-b",
|
|
162
|
+
"--bare",
|
|
163
|
+
action="store_true",
|
|
164
|
+
help="create a bare environment",
|
|
165
|
+
)
|
|
166
|
+
encrypt_group = parser.add_argument_group("encryption options")
|
|
167
|
+
encrypt_group.add_argument(
|
|
168
|
+
"-e",
|
|
169
|
+
"--encrypt",
|
|
170
|
+
action="store_true",
|
|
171
|
+
help="encrypt environment values",
|
|
172
|
+
)
|
|
173
|
+
encrypt_group.add_argument(
|
|
174
|
+
"--keygen",
|
|
175
|
+
action="store_true",
|
|
176
|
+
help="generate encryption keys",
|
|
177
|
+
)
|
|
178
|
+
parser.add_argument_group(encrypt_group)
|
|
179
|
+
bake_group = parser.add_argument_group("bake options")
|
|
180
|
+
bake_group.add_argument(
|
|
181
|
+
"-o",
|
|
182
|
+
"--out",
|
|
183
|
+
metavar="FILENAME",
|
|
184
|
+
help="save the environment to an env file",
|
|
185
|
+
)
|
|
186
|
+
bake_group.add_argument(
|
|
187
|
+
"-d",
|
|
188
|
+
"--depth",
|
|
189
|
+
type=int,
|
|
190
|
+
default=0,
|
|
191
|
+
help="depth of environment stack to bake (default: 0 = flatten)",
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument_group(bake_group)
|
|
194
|
+
export_group = parser.add_argument_group("export options")
|
|
195
|
+
export_group.add_argument(
|
|
196
|
+
"--clear",
|
|
197
|
+
action="store_true",
|
|
198
|
+
help="generate unset commands for %s" % config.SHELL,
|
|
199
|
+
)
|
|
200
|
+
export_group.add_argument(
|
|
201
|
+
"--export",
|
|
202
|
+
action="store_true",
|
|
203
|
+
help="generate export commands for %s" % config.SHELL,
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument_group(export_group)
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"-p",
|
|
208
|
+
"--platform",
|
|
209
|
+
default=config.PLATFORM,
|
|
210
|
+
metavar="PLATFORM",
|
|
211
|
+
help="platform to resolve variables for (linux, darwin, windows)",
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"-s",
|
|
215
|
+
"--set",
|
|
216
|
+
nargs="*",
|
|
217
|
+
action=StoreOnce,
|
|
218
|
+
metavar="KEY=VALUE",
|
|
219
|
+
help="overlay KEY=VALUE pairs to envstack environments",
|
|
220
|
+
)
|
|
221
|
+
parser.add_argument(
|
|
222
|
+
"--scope",
|
|
223
|
+
metavar="SCOPE",
|
|
224
|
+
help="search scope for environment stack files",
|
|
225
|
+
)
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"-u",
|
|
228
|
+
"--unresolved",
|
|
229
|
+
action="store_true",
|
|
230
|
+
help="dump unresolved environment variables to stdout",
|
|
231
|
+
)
|
|
232
|
+
parser.add_argument(
|
|
233
|
+
"-r",
|
|
234
|
+
"--resolve",
|
|
235
|
+
nargs="*",
|
|
236
|
+
action=StoreOnce,
|
|
237
|
+
metavar="VAR",
|
|
238
|
+
help="resolve environment variables",
|
|
239
|
+
)
|
|
240
|
+
parser.add_argument(
|
|
241
|
+
"-t",
|
|
242
|
+
"--trace",
|
|
243
|
+
nargs="*",
|
|
244
|
+
action=StoreOnce,
|
|
245
|
+
metavar="VAR",
|
|
246
|
+
help="trace where environment variables are being set",
|
|
247
|
+
)
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--sources",
|
|
250
|
+
action="store_true",
|
|
251
|
+
help="list the env stack file sources",
|
|
252
|
+
)
|
|
253
|
+
export_group.add_argument(
|
|
254
|
+
"-q",
|
|
255
|
+
"--quiet",
|
|
256
|
+
action="store_true",
|
|
257
|
+
help="print the value of an environment variable only (no key)",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
args = parser.parse_args(args_before_dash)
|
|
261
|
+
|
|
262
|
+
return args, args_after_dash
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def envshell(namespace: List[str] = None, quiet: bool = False):
|
|
266
|
+
"""Run a shell in the given environment stack."""
|
|
267
|
+
|
|
268
|
+
if not quiet:
|
|
269
|
+
shell_name = os.path.basename(config.SHELL).lower()
|
|
270
|
+
|
|
271
|
+
if shell_name in ("cmd", "cmd.exe"):
|
|
272
|
+
exit_hint = 'type "exit" to quit'
|
|
273
|
+
elif shell_name in ("powershell", "pwsh"):
|
|
274
|
+
exit_hint = 'type "exit" to quit'
|
|
275
|
+
else:
|
|
276
|
+
# bash, zsh, sh, etc.
|
|
277
|
+
exit_hint = 'CTRL+D or "exit" to quit'
|
|
278
|
+
|
|
279
|
+
print(f"\U0001F680 Launching envstack shell... ({exit_hint})")
|
|
280
|
+
|
|
281
|
+
name = (namespace or [config.DEFAULT_NAMESPACE])[:]
|
|
282
|
+
shell = EnvshellWrapper(name)
|
|
283
|
+
return shell.launch()
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def whichenv():
|
|
287
|
+
"""Entry point for the whichenv command line tool. Finds {VAR}s."""
|
|
288
|
+
from .util import findenv
|
|
289
|
+
|
|
290
|
+
if len(sys.argv) != 2:
|
|
291
|
+
print("Usage: whichenv [VAR]")
|
|
292
|
+
return 2
|
|
293
|
+
|
|
294
|
+
var_name = sys.argv[1]
|
|
295
|
+
paths = findenv(var_name)
|
|
296
|
+
for path in paths:
|
|
297
|
+
print("{0}: {1}".format(var_name, path))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main():
|
|
301
|
+
"""Main thread."""
|
|
302
|
+
args, command = parse_args()
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
if command:
|
|
306
|
+
return run_command(command, args.namespace)
|
|
307
|
+
|
|
308
|
+
elif args.keygen:
|
|
309
|
+
from envstack.encrypt import generate_keys
|
|
310
|
+
|
|
311
|
+
data = generate_keys()
|
|
312
|
+
|
|
313
|
+
if args.export:
|
|
314
|
+
print(export_env_to_shell(data))
|
|
315
|
+
elif args.out:
|
|
316
|
+
Env(data).write(args.out)
|
|
317
|
+
else:
|
|
318
|
+
for key, value in data.items():
|
|
319
|
+
print(f"{key}={value}")
|
|
320
|
+
|
|
321
|
+
elif args.clear:
|
|
322
|
+
from envstack.env import clear
|
|
323
|
+
|
|
324
|
+
print(clear(args.namespace, shell=config.SHELL))
|
|
325
|
+
|
|
326
|
+
elif args.export and args.resolve is None and args.set is None:
|
|
327
|
+
print(export(args.namespace, shell=config.SHELL, encrypt=args.encrypt))
|
|
328
|
+
|
|
329
|
+
elif args.set is not None and args.resolve is None:
|
|
330
|
+
force_stdin = args.set == [] or args.set == ["-"]
|
|
331
|
+
using_pipe = args.set == [] and not sys.stdin.isatty()
|
|
332
|
+
|
|
333
|
+
# load the environment if not in bare mode
|
|
334
|
+
if args.bare:
|
|
335
|
+
env = Env()
|
|
336
|
+
else:
|
|
337
|
+
env = load_environ(args.namespace, platform=args.platform)
|
|
338
|
+
|
|
339
|
+
# interactive mode
|
|
340
|
+
if force_stdin and sys.stdin.isatty():
|
|
341
|
+
print(
|
|
342
|
+
"Enter ENV:VAR or ENV=VAR pairs. Press Ctrl+D or Ctrl+C to finish:",
|
|
343
|
+
file=sys.stderr,
|
|
344
|
+
)
|
|
345
|
+
lines = []
|
|
346
|
+
try:
|
|
347
|
+
while True:
|
|
348
|
+
lines.append(input() + "\n")
|
|
349
|
+
except (EOFError, KeyboardInterrupt):
|
|
350
|
+
pass
|
|
351
|
+
data = _parse_env_lines(lines)
|
|
352
|
+
if not data:
|
|
353
|
+
return 0
|
|
354
|
+
# pipe stdin (or '-' with non-tty stdin)
|
|
355
|
+
elif force_stdin or using_pipe:
|
|
356
|
+
data = _parse_env_lines(sys.stdin)
|
|
357
|
+
if not data:
|
|
358
|
+
raise ValueError("no KEY=VALUE pairs found on stdin")
|
|
359
|
+
# explicit args path
|
|
360
|
+
else:
|
|
361
|
+
data = _parse_keyvals(args.set)
|
|
362
|
+
|
|
363
|
+
# encrypt the new data only
|
|
364
|
+
if args.encrypt:
|
|
365
|
+
data = encrypt_environ(data)
|
|
366
|
+
|
|
367
|
+
# update the environment with the new data
|
|
368
|
+
env.update(data)
|
|
369
|
+
|
|
370
|
+
if args.export:
|
|
371
|
+
print(export_env_to_shell(env))
|
|
372
|
+
elif args.out:
|
|
373
|
+
env.write(args.out, depth=args.depth)
|
|
374
|
+
else:
|
|
375
|
+
for key, val in env.items():
|
|
376
|
+
if args.quiet:
|
|
377
|
+
if len(env) > 1:
|
|
378
|
+
print("error: --quiet requires exactly one KEY")
|
|
379
|
+
return 2
|
|
380
|
+
else:
|
|
381
|
+
print(val)
|
|
382
|
+
else:
|
|
383
|
+
print(f"{key}={val}")
|
|
384
|
+
|
|
385
|
+
elif args.out and args.resolve is None:
|
|
386
|
+
bake_environ(
|
|
387
|
+
args.namespace,
|
|
388
|
+
filename=args.out,
|
|
389
|
+
depth=args.depth,
|
|
390
|
+
encrypt=args.encrypt,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
elif args.resolve is not None:
|
|
394
|
+
if args.depth:
|
|
395
|
+
print("error: --depth is not valid with --resolve")
|
|
396
|
+
return 2
|
|
397
|
+
resolved = resolve_environ(
|
|
398
|
+
load_environ(args.namespace, platform=args.platform)
|
|
399
|
+
)
|
|
400
|
+
if args.set:
|
|
401
|
+
resolved.update(_parse_keyvals(args.set))
|
|
402
|
+
if args.encrypt:
|
|
403
|
+
resolved = encrypt_environ(resolved)
|
|
404
|
+
if args.out:
|
|
405
|
+
if len(args.resolve) == 0:
|
|
406
|
+
resolved.write(args.out, depth=0)
|
|
407
|
+
else:
|
|
408
|
+
keys = args.resolve or resolved.keys()
|
|
409
|
+
if args.set:
|
|
410
|
+
keys = set(keys).union(_parse_keyvals(args.set).keys())
|
|
411
|
+
env = Env({key: resolved[key] for key in keys})
|
|
412
|
+
env.write(args.out, depth=0)
|
|
413
|
+
elif args.export:
|
|
414
|
+
if len(args.resolve) == 0:
|
|
415
|
+
print(export_env_to_shell(resolved, shell=config.SHELL))
|
|
416
|
+
else:
|
|
417
|
+
keys = args.resolve or resolved.keys()
|
|
418
|
+
if args.set:
|
|
419
|
+
keys = set(keys).union(_parse_keyvals(args.set).keys())
|
|
420
|
+
env = Env({key: resolved[key] for key in keys})
|
|
421
|
+
print(export_env_to_shell(env, shell=config.SHELL))
|
|
422
|
+
else:
|
|
423
|
+
keys = args.resolve or resolved.keys()
|
|
424
|
+
if args.set:
|
|
425
|
+
keys = set(keys).union(_parse_keyvals(args.set).keys())
|
|
426
|
+
for key in sorted(str(k) for k in keys):
|
|
427
|
+
val = resolved.get(key)
|
|
428
|
+
if key in resolved:
|
|
429
|
+
if args.quiet:
|
|
430
|
+
if len(keys) > 1:
|
|
431
|
+
print("error: --quiet requires exactly one KEY")
|
|
432
|
+
return 2
|
|
433
|
+
else:
|
|
434
|
+
print(val)
|
|
435
|
+
else:
|
|
436
|
+
print(f"{key}={val}")
|
|
437
|
+
|
|
438
|
+
elif args.trace is not None:
|
|
439
|
+
if len(args.trace) == 0:
|
|
440
|
+
args.trace = load_environ(args.namespace).keys()
|
|
441
|
+
for trace in args.trace:
|
|
442
|
+
path = trace_var(*args.namespace, var=trace)
|
|
443
|
+
if path:
|
|
444
|
+
if args.quiet:
|
|
445
|
+
if len(args.trace) > 1:
|
|
446
|
+
print("error: --quiet requires exactly one KEY")
|
|
447
|
+
return 2
|
|
448
|
+
else:
|
|
449
|
+
print(path)
|
|
450
|
+
else:
|
|
451
|
+
print("{0}={1}".format(trace, path))
|
|
452
|
+
|
|
453
|
+
elif args.sources:
|
|
454
|
+
env = load_environ(args.namespace, platform=args.platform)
|
|
455
|
+
for source in env.sources:
|
|
456
|
+
print(source.path)
|
|
457
|
+
|
|
458
|
+
elif args.unresolved:
|
|
459
|
+
env = load_environ(
|
|
460
|
+
args.namespace, platform=args.platform, encrypt=args.encrypt
|
|
461
|
+
)
|
|
462
|
+
for k, v in sorted(env.items(), key=lambda x: str(x[0])):
|
|
463
|
+
print(f"{k}={v}")
|
|
464
|
+
|
|
465
|
+
else:
|
|
466
|
+
return envshell(args.namespace, quiet=args.quiet)
|
|
467
|
+
|
|
468
|
+
except KeyboardInterrupt:
|
|
469
|
+
print("Stopping...")
|
|
470
|
+
return 2
|
|
471
|
+
|
|
472
|
+
except Exception:
|
|
473
|
+
traceback.print_exc()
|
|
474
|
+
return 1
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
if __name__ == "__main__":
|
|
478
|
+
sys.exit(main())
|
envstack/config.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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 default configs and settings.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import os
|
|
37
|
+
import platform
|
|
38
|
+
import sys
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def detect_shell():
|
|
42
|
+
"""Detect the current shell."""
|
|
43
|
+
if PLATFORM == "windows":
|
|
44
|
+
comspec = os.environ.get("ComSpec")
|
|
45
|
+
if comspec:
|
|
46
|
+
if "cmd.exe" in comspec:
|
|
47
|
+
return "cmd"
|
|
48
|
+
elif "powershell.exe" in comspec:
|
|
49
|
+
return "pwsh"
|
|
50
|
+
else:
|
|
51
|
+
return "unknown"
|
|
52
|
+
else:
|
|
53
|
+
shell = os.environ.get("SHELL", "/bin/bash")
|
|
54
|
+
if shell:
|
|
55
|
+
return shell
|
|
56
|
+
else:
|
|
57
|
+
return "/usr/bin/bash"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# debug mode
|
|
61
|
+
DEBUG = os.getenv("DEBUG")
|
|
62
|
+
|
|
63
|
+
# default namespace
|
|
64
|
+
DEFAULT_NAMESPACE = os.getenv("DEFAULT_ENV_STACK", "default")
|
|
65
|
+
|
|
66
|
+
# allow embedded commands
|
|
67
|
+
ALLOW_COMMANDS = os.getenv("ALLOW_COMMANDS", "0") in ("1", "true", "True", "TRUE")
|
|
68
|
+
|
|
69
|
+
# embedded command timeout in seconds
|
|
70
|
+
try:
|
|
71
|
+
COMMAND_TIMEOUT = int(os.getenv("COMMAND_TIMEOUT", 5))
|
|
72
|
+
except ValueError:
|
|
73
|
+
COMMAND_TIMEOUT = 5
|
|
74
|
+
|
|
75
|
+
# default environment variables
|
|
76
|
+
ENV = os.getenv("ENV", "prod")
|
|
77
|
+
HOME = os.getenv("HOME")
|
|
78
|
+
|
|
79
|
+
# Ignore missing stack files when resolving environments
|
|
80
|
+
IGNORE_MISSING = os.getenv("IGNORE_MISSING", "1") in ("1", "true", "True", "TRUE")
|
|
81
|
+
|
|
82
|
+
# logging level
|
|
83
|
+
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO")
|
|
84
|
+
|
|
85
|
+
# platform and shell info
|
|
86
|
+
ON_POSIX = "posix" in sys.builtin_module_names
|
|
87
|
+
PLATFORM = platform.system().lower()
|
|
88
|
+
PYTHON_VERSION = sys.version_info[0]
|
|
89
|
+
SHELL = detect_shell()
|
|
90
|
+
USERNAME = os.getenv("USERNAME", os.getenv("USER"))
|
|
91
|
+
|
|
92
|
+
# set some default environment values
|
|
93
|
+
DEFAULT_ENV = {
|
|
94
|
+
"ENV": ENV,
|
|
95
|
+
"HOME": HOME,
|
|
96
|
+
"PLATFORM": PLATFORM,
|
|
97
|
+
"ROOT": os.getenv(
|
|
98
|
+
"ROOT",
|
|
99
|
+
{
|
|
100
|
+
"darwin": "{HOME}/Library/Application Support/pipe",
|
|
101
|
+
"linux": "{HOME}/.local/pipe",
|
|
102
|
+
"windows": "C:\\ProgramData\\pipe",
|
|
103
|
+
}
|
|
104
|
+
.get(PLATFORM)
|
|
105
|
+
.format(**locals()),
|
|
106
|
+
),
|
|
107
|
+
"USER": USERNAME,
|
|
108
|
+
}
|