1import os
2import sys
3from subprocess import Popen
4from typing import Any, Dict, List
5
6try:
7    import click
8except ImportError:
9    sys.stderr.write('It seems python-dotenv is not installed with cli option. \n'
10                     'Run pip install "python-dotenv[cli]" to fix this.')
11    sys.exit(1)
12
13from .main import dotenv_values, get_key, set_key, unset_key
14from .version import __version__
15
16
17@click.group()
18@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
19              type=click.Path(file_okay=True),
20              help="Location of the .env file, defaults to .env file in current working directory.")
21@click.option('-q', '--quote', default='always',
22              type=click.Choice(['always', 'never', 'auto']),
23              help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
24@click.option('-e', '--export', default=False,
25              type=click.BOOL,
26              help="Whether to write the dot file as an executable bash script.")
27@click.version_option(version=__version__)
28@click.pass_context
29def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None:
30    '''This script is used to set, get or unset values from a .env file.'''
31    ctx.obj = {}
32    ctx.obj['QUOTE'] = quote
33    ctx.obj['EXPORT'] = export
34    ctx.obj['FILE'] = file
35
36
37@cli.command()
38@click.pass_context
39def list(ctx: click.Context) -> None:
40    '''Display all the stored key/value.'''
41    file = ctx.obj['FILE']
42    if not os.path.isfile(file):
43        raise click.BadParameter(
44            'Path "%s" does not exist.' % (file),
45            ctx=ctx
46        )
47    dotenv_as_dict = dotenv_values(file)
48    for k, v in dotenv_as_dict.items():
49        click.echo('%s=%s' % (k, v))
50
51
52@cli.command()
53@click.pass_context
54@click.argument('key', required=True)
55@click.argument('value', required=True)
56def set(ctx: click.Context, key: Any, value: Any) -> None:
57    '''Store the given key/value.'''
58    file = ctx.obj['FILE']
59    quote = ctx.obj['QUOTE']
60    export = ctx.obj['EXPORT']
61    success, key, value = set_key(file, key, value, quote, export)
62    if success:
63        click.echo('%s=%s' % (key, value))
64    else:
65        exit(1)
66
67
68@cli.command()
69@click.pass_context
70@click.argument('key', required=True)
71def get(ctx: click.Context, key: Any) -> None:
72    '''Retrieve the value for the given key.'''
73    file = ctx.obj['FILE']
74    if not os.path.isfile(file):
75        raise click.BadParameter(
76            'Path "%s" does not exist.' % (file),
77            ctx=ctx
78        )
79    stored_value = get_key(file, key)
80    if stored_value:
81        click.echo(stored_value)
82    else:
83        exit(1)
84
85
86@cli.command()
87@click.pass_context
88@click.argument('key', required=True)
89def unset(ctx: click.Context, key: Any) -> None:
90    '''Removes the given key.'''
91    file = ctx.obj['FILE']
92    quote = ctx.obj['QUOTE']
93    success, key = unset_key(file, key, quote)
94    if success:
95        click.echo("Successfully removed %s" % key)
96    else:
97        exit(1)
98
99
100@cli.command(context_settings={'ignore_unknown_options': True})
101@click.pass_context
102@click.option(
103    "--override/--no-override",
104    default=True,
105    help="Override variables from the environment file with those from the .env file.",
106)
107@click.argument('commandline', nargs=-1, type=click.UNPROCESSED)
108def run(ctx: click.Context, override: bool, commandline: List[str]) -> None:
109    """Run command with environment variables present."""
110    file = ctx.obj['FILE']
111    if not os.path.isfile(file):
112        raise click.BadParameter(
113            'Invalid value for \'-f\' "%s" does not exist.' % (file),
114            ctx=ctx
115        )
116    dotenv_as_dict = {
117        k: v
118        for (k, v) in dotenv_values(file).items()
119        if v is not None and (override or k not in os.environ)
120    }
121
122    if not commandline:
123        click.echo('No command given.')
124        exit(1)
125    ret = run_command(commandline, dotenv_as_dict)
126    exit(ret)
127
128
129def run_command(command: List[str], env: Dict[str, str]) -> int:
130    """Run command in sub process.
131
132    Runs the command in a sub process with the variables from `env`
133    added in the current environment variables.
134
135    Parameters
136    ----------
137    command: List[str]
138        The command and it's parameters
139    env: Dict
140        The additional environment variables
141
142    Returns
143    -------
144    int
145        The return code of the command
146
147    """
148    # copy the current environment variables and add the vales from
149    # `env`
150    cmd_env = os.environ.copy()
151    cmd_env.update(env)
152
153    p = Popen(command,
154              universal_newlines=True,
155              bufsize=0,
156              shell=False,
157              env=cmd_env)
158    _, _ = p.communicate()
159
160    return p.returncode
161
162
163if __name__ == "__main__":
164    cli()
165