200 lines
5.7 KiB
Python
200 lines
5.7 KiB
Python
|
import json
|
||
|
import os
|
||
|
import shlex
|
||
|
import sys
|
||
|
from contextlib import contextmanager
|
||
|
from subprocess import Popen
|
||
|
from typing import Any, Dict, IO, Iterator, List
|
||
|
|
||
|
try:
|
||
|
import click
|
||
|
except ImportError:
|
||
|
sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
|
||
|
'Run pip install "python-dotenv[cli]" to fix this.')
|
||
|
sys.exit(1)
|
||
|
|
||
|
from .main import dotenv_values, set_key, unset_key
|
||
|
from .version import __version__
|
||
|
|
||
|
|
||
|
def enumerate_env():
|
||
|
"""
|
||
|
Return a path for the ${pwd}/.env file.
|
||
|
|
||
|
If pwd does not exist, return None.
|
||
|
"""
|
||
|
try:
|
||
|
cwd = os.getcwd()
|
||
|
except FileNotFoundError:
|
||
|
return None
|
||
|
path = os.path.join(cwd, '.env')
|
||
|
return path
|
||
|
|
||
|
|
||
|
@click.group()
|
||
|
@click.option('-f', '--file', default=enumerate_env(),
|
||
|
type=click.Path(file_okay=True),
|
||
|
help="Location of the .env file, defaults to .env file in current working directory.")
|
||
|
@click.option('-q', '--quote', default='always',
|
||
|
type=click.Choice(['always', 'never', 'auto']),
|
||
|
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
|
||
|
@click.option('-e', '--export', default=False,
|
||
|
type=click.BOOL,
|
||
|
help="Whether to write the dot file as an executable bash script.")
|
||
|
@click.version_option(version=__version__)
|
||
|
@click.pass_context
|
||
|
def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
|
||
|
"""This script is used to set, get or unset values from a .env file."""
|
||
|
ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file}
|
||
|
|
||
|
|
||
|
@contextmanager
|
||
|
def stream_file(path: os.PathLike) -> Iterator[IO[str]]:
|
||
|
"""
|
||
|
Open a file and yield the corresponding (decoded) stream.
|
||
|
|
||
|
Exits with error code 2 if the file cannot be opened.
|
||
|
"""
|
||
|
|
||
|
try:
|
||
|
with open(path) as stream:
|
||
|
yield stream
|
||
|
except OSError as exc:
|
||
|
print(f"Error opening env file: {exc}", file=sys.stderr)
|
||
|
exit(2)
|
||
|
|
||
|
|
||
|
@cli.command()
|
||
|
@click.pass_context
|
||
|
@click.option('--format', default='simple',
|
||
|
type=click.Choice(['simple', 'json', 'shell', 'export']),
|
||
|
help="The format in which to display the list. Default format is simple, "
|
||
|
"which displays name=value without quotes.")
|
||
|
def list(ctx: click.Context, format: bool) -> None:
|
||
|
"""Display all the stored key/value."""
|
||
|
file = ctx.obj['FILE']
|
||
|
|
||
|
with stream_file(file) as stream:
|
||
|
values = dotenv_values(stream=stream)
|
||
|
|
||
|
if format == 'json':
|
||
|
click.echo(json.dumps(values, indent=2, sort_keys=True))
|
||
|
else:
|
||
|
prefix = 'export ' if format == 'export' else ''
|
||
|
for k in sorted(values):
|
||
|
v = values[k]
|
||
|
if v is not None:
|
||
|
if format in ('export', 'shell'):
|
||
|
v = shlex.quote(v)
|
||
|
click.echo(f'{prefix}{k}={v}')
|
||
|
|
||
|
|
||
|
@cli.command()
|
||
|
@click.pass_context
|
||
|
@click.argument('key', required=True)
|
||
|
@click.argument('value', required=True)
|
||
|
def set(ctx: click.Context, key: Any, value: Any) -> None:
|
||
|
"""Store the given key/value."""
|
||
|
file = ctx.obj['FILE']
|
||
|
quote = ctx.obj['QUOTE']
|
||
|
export = ctx.obj['EXPORT']
|
||
|
success, key, value = set_key(file, key, value, quote, export)
|
||
|
if success:
|
||
|
click.echo(f'{key}={value}')
|
||
|
else:
|
||
|
exit(1)
|
||
|
|
||
|
|
||
|
@cli.command()
|
||
|
@click.pass_context
|
||
|
@click.argument('key', required=True)
|
||
|
def get(ctx: click.Context, key: Any) -> None:
|
||
|
"""Retrieve the value for the given key."""
|
||
|
file = ctx.obj['FILE']
|
||
|
|
||
|
with stream_file(file) as stream:
|
||
|
values = dotenv_values(stream=stream)
|
||
|
|
||
|
stored_value = values.get(key)
|
||
|
if stored_value:
|
||
|
click.echo(stored_value)
|
||
|
else:
|
||
|
exit(1)
|
||
|
|
||
|
|
||
|
@cli.command()
|
||
|
@click.pass_context
|
||
|
@click.argument('key', required=True)
|
||
|
def unset(ctx: click.Context, key: Any) -> None:
|
||
|
"""Removes the given key."""
|
||
|
file = ctx.obj['FILE']
|
||
|
quote = ctx.obj['QUOTE']
|
||
|
success, key = unset_key(file, key, quote)
|
||
|
if success:
|
||
|
click.echo(f"Successfully removed {key}")
|
||
|
else:
|
||
|
exit(1)
|
||
|
|
||
|
|
||
|
@cli.command(context_settings={'ignore_unknown_options': True})
|
||
|
@click.pass_context
|
||
|
@click.option(
|
||
|
"--override/--no-override",
|
||
|
default=True,
|
||
|
help="Override variables from the environment file with those from the .env file.",
|
||
|
)
|
||
|
@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
|
||
|
def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
|
||
|
"""Run command with environment variables present."""
|
||
|
file = ctx.obj['FILE']
|
||
|
if not os.path.isfile(file):
|
||
|
raise click.BadParameter(
|
||
|
f'Invalid value for \'-f\' "{file}" does not exist.',
|
||
|
ctx=ctx
|
||
|
)
|
||
|
dotenv_as_dict = {
|
||
|
k: v
|
||
|
for (k, v) in dotenv_values(file).items()
|
||
|
if v is not None and (override or k not in os.environ)
|
||
|
}
|
||
|
|
||
|
if not commandline:
|
||
|
click.echo('No command given.')
|
||
|
exit(1)
|
||
|
ret = run_command(commandline, dotenv_as_dict)
|
||
|
exit(ret)
|
||
|
|
||
|
|
||
|
def run_command(command: List[str], env: Dict[str, str]) -> int:
|
||
|
"""Run command in sub process.
|
||
|
|
||
|
Runs the command in a sub process with the variables from `env`
|
||
|
added in the current environment variables.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
command: List[str]
|
||
|
The command and it's parameters
|
||
|
env: Dict
|
||
|
The additional environment variables
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
int
|
||
|
The return code of the command
|
||
|
|
||
|
"""
|
||
|
# copy the current environment variables and add the vales from
|
||
|
# `env`
|
||
|
cmd_env = os.environ.copy()
|
||
|
cmd_env.update(env)
|
||
|
|
||
|
p = Popen(command,
|
||
|
universal_newlines=True,
|
||
|
bufsize=0,
|
||
|
shell=False,
|
||
|
env=cmd_env)
|
||
|
_, _ = p.communicate()
|
||
|
|
||
|
return p.returncode
|