|
1 | | -import json |
| 1 | +import codecs |
2 | 2 | import os |
3 | | -import shlex |
4 | 3 | import sys |
5 | | -from contextlib import contextmanager |
6 | | -from typing import IO, Any, Dict, Iterator, List, Optional |
| 4 | +from pathlib import Path |
| 5 | +from subprocess import Popen |
| 6 | +from typing import Any, Dict, List, Optional |
7 | 7 |
|
8 | | -if sys.platform == "win32": |
9 | | - from subprocess import Popen |
10 | | - |
11 | | -try: |
12 | | - import click |
13 | | -except ImportError: |
14 | | - sys.stderr.write( |
15 | | - "It seems python-dotenv is not installed with cli option. \n" |
16 | | - 'Run pip install "python-dotenv[cli]" to fix this.' |
17 | | - ) |
18 | | - sys.exit(1) |
| 8 | +import click |
19 | 9 |
|
20 | 10 | from .main import dotenv_values, set_key, unset_key |
21 | 11 | from .version import __version__ |
22 | 12 |
|
23 | 13 |
|
24 | | -def enumerate_env() -> Optional[str]: |
| 14 | +def enumerate_env(): |
25 | 15 | """ |
26 | | - Return a path for the ${pwd}/.env file. |
27 | | -
|
28 | | - If pwd does not exist, return None. |
| 16 | + Display a list of all the variables from the .env file. |
29 | 17 | """ |
30 | | - try: |
31 | | - cwd = os.getcwd() |
32 | | - except FileNotFoundError: |
33 | | - return None |
34 | | - path = os.path.join(cwd, ".env") |
35 | | - return path |
| 18 | + file = os.path.join(os.getcwd(), '.env') |
| 19 | + click.echo(file) |
| 20 | + if os.path.exists(file): |
| 21 | + click.echo('In your %s file:' % file) |
| 22 | + for k, v in dotenv_values(file).items(): |
| 23 | + click.echo('%s: %s' % (k, v)) |
| 24 | + else: |
| 25 | + click.echo('No %s file found.' % file) |
36 | 26 |
|
37 | 27 |
|
38 | 28 | @click.group() |
39 | 29 | @click.option( |
40 | | - "-f", |
41 | | - "--file", |
42 | | - default=enumerate_env(), |
| 30 | + '-f', |
| 31 | + '--file', |
| 32 | + default=os.path.join(os.getcwd(), '.env'), |
43 | 33 | type=click.Path(file_okay=True), |
44 | | - help="Location of the .env file, defaults to .env file in current working directory.", |
| 34 | + help='The .env file path.', |
| 35 | + required=False |
45 | 36 | ) |
46 | 37 | @click.option( |
47 | | - "-q", |
48 | | - "--quote", |
49 | | - default="always", |
50 | | - type=click.Choice(["always", "never", "auto"]), |
51 | | - help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.", |
| 38 | + '-q', |
| 39 | + '--quote', |
| 40 | + default='always', |
| 41 | + type=click.Choice(['always', 'never', 'auto']), |
| 42 | + help='Quote the values in the .env file.', |
| 43 | + required=False |
52 | 44 | ) |
53 | 45 | @click.option( |
54 | | - "-e", |
55 | | - "--export", |
| 46 | + '-e', |
| 47 | + '--export', |
56 | 48 | default=False, |
57 | 49 | type=click.BOOL, |
58 | | - help="Whether to write the dot file as an executable bash script.", |
| 50 | + help='Append the variables to the file in the form export VAR=value.', |
| 51 | + is_flag=True, |
| 52 | + required=False |
59 | 53 | ) |
60 | | -@click.version_option(version=__version__) |
| 54 | +@click.version_option(version=__version__, prog_name='dotenv') |
61 | 55 | @click.pass_context |
62 | | -def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: |
| 56 | +def cli(ctx: click.Context, file: str, quote: str, export: bool) -> None: |
63 | 57 | """This script is used to set, get or unset values from a .env file.""" |
64 | | - ctx.obj = {"QUOTE": quote, "EXPORT": export, "FILE": file} |
65 | | - |
| 58 | + ctx.obj = {} |
| 59 | + ctx.obj['QUOTE'] = quote |
| 60 | + ctx.obj['EXPORT'] = export |
| 61 | + ctx.obj['FILE'] = file |
66 | 62 |
|
67 | | -@contextmanager |
68 | | -def stream_file(path: os.PathLike) -> Iterator[IO[str]]: |
69 | | - """ |
70 | | - Open a file and yield the corresponding (decoded) stream. |
71 | | -
|
72 | | - Exits with error code 2 if the file cannot be opened. |
73 | | - """ |
74 | 63 |
|
75 | | - try: |
76 | | - with open(path) as stream: |
77 | | - yield stream |
78 | | - except OSError as exc: |
79 | | - print(f"Error opening env file: {exc}", file=sys.stderr) |
80 | | - sys.exit(2) |
81 | | - |
82 | | - |
83 | | -@cli.command(name="list") |
| 64 | +@cli.command() |
84 | 65 | @click.pass_context |
85 | | -@click.option( |
86 | | - "--format", |
87 | | - "output_format", |
88 | | - default="simple", |
89 | | - type=click.Choice(["simple", "json", "shell", "export"]), |
90 | | - help="The format in which to display the list. Default format is simple, " |
91 | | - "which displays name=value without quotes.", |
92 | | -) |
93 | | -def list_values(ctx: click.Context, output_format: str) -> None: |
| 66 | +def list(ctx: click.Context) -> None: |
94 | 67 | """Display all the stored key/value.""" |
95 | | - file = ctx.obj["FILE"] |
| 68 | + file = ctx.obj['FILE'] |
| 69 | + if not os.path.isfile(file): |
| 70 | + click.echo("File doesn't exist: %s" % file) |
| 71 | + sys.exit(1) |
| 72 | + dotenv_as_dict = dotenv_values(file) |
| 73 | + for k, v in dotenv_as_dict.items(): |
| 74 | + click.echo('%s=%s' % (k, v)) |
96 | 75 |
|
97 | | - with stream_file(file) as stream: |
98 | | - values = dotenv_values(stream=stream) |
99 | 76 |
|
100 | | - if output_format == "json": |
101 | | - click.echo(json.dumps(values, indent=2, sort_keys=True)) |
| 77 | +@cli.command() |
| 78 | +@click.pass_context |
| 79 | +def get(ctx: click.Context, key: str) -> None: |
| 80 | + """Display the stored value.""" |
| 81 | + file = ctx.obj['FILE'] |
| 82 | + if not os.path.isfile(file): |
| 83 | + click.echo("File doesn't exist: %s" % file) |
| 84 | + sys.exit(1) |
| 85 | + stored_value = dotenv_values(file).get(key) |
| 86 | + if stored_value: |
| 87 | + click.echo(stored_value) |
102 | 88 | else: |
103 | | - prefix = "export " if output_format == "export" else "" |
104 | | - for k in sorted(values): |
105 | | - v = values[k] |
106 | | - if v is not None: |
107 | | - if output_format in ("export", "shell"): |
108 | | - v = shlex.quote(v) |
109 | | - click.echo(f"{prefix}{k}={v}") |
| 89 | + click.echo("Key %s not found." % key) |
| 90 | + sys.exit(1) |
110 | 91 |
|
111 | 92 |
|
112 | | -@cli.command(name="set") |
113 | | -@click.pass_context |
114 | | -@click.argument("key", required=True) |
115 | | -@click.argument("value", required=True) |
116 | | -def set_value(ctx: click.Context, key: Any, value: Any) -> None: |
117 | | - """ |
118 | | - Store the given key/value. |
| 93 | +get.add_argument = click.argument # type: ignore |
| 94 | +get.add_argument('key', required=True) |
119 | 95 |
|
120 | | - This doesn't follow symlinks, to avoid accidentally modifying a file at a |
121 | | - potentially untrusted path. |
122 | | - """ |
123 | 96 |
|
124 | | - file = ctx.obj["FILE"] |
125 | | - quote = ctx.obj["QUOTE"] |
126 | | - export = ctx.obj["EXPORT"] |
| 97 | +@cli.command() |
| 98 | +@click.pass_context |
| 99 | +def set( |
| 100 | + ctx: click.Context, |
| 101 | + key: str, |
| 102 | + value: str, |
| 103 | +) -> None: |
| 104 | + """Store a new variable or update an existing variable.""" |
| 105 | + file = ctx.obj['FILE'] |
| 106 | + quote = ctx.obj['QUOTE'] |
| 107 | + export = ctx.obj['EXPORT'] |
127 | 108 | success, key, value = set_key(file, key, value, quote, export) |
128 | 109 | if success: |
129 | | - click.echo(f"{key}={value}") |
| 110 | + click.echo('%s=%s' % (key, value)) |
130 | 111 | else: |
| 112 | + click.echo("Key %s not set." % key) |
131 | 113 | sys.exit(1) |
132 | 114 |
|
133 | 115 |
|
134 | | -@cli.command() |
135 | | -@click.pass_context |
136 | | -@click.argument("key", required=True) |
137 | | -def get(ctx: click.Context, key: Any) -> None: |
138 | | - """Retrieve the value for the given key.""" |
139 | | - file = ctx.obj["FILE"] |
140 | | - |
141 | | - with stream_file(file) as stream: |
142 | | - values = dotenv_values(stream=stream) |
143 | | - |
144 | | - stored_value = values.get(key) |
145 | | - if stored_value: |
146 | | - click.echo(stored_value) |
147 | | - else: |
148 | | - sys.exit(1) |
| 116 | +set.add_argument = click.argument # type: ignore |
| 117 | +set.add_argument('key', required=True) |
| 118 | +set.add_argument('value', required=True) |
149 | 119 |
|
150 | 120 |
|
151 | 121 | @cli.command() |
152 | 122 | @click.pass_context |
153 | | -@click.argument("key", required=True) |
154 | | -def unset(ctx: click.Context, key: Any) -> None: |
155 | | - """ |
156 | | - Removes the given key. |
157 | | -
|
158 | | - This doesn't follow symlinks, to avoid accidentally modifying a file at a |
159 | | - potentially untrusted path. |
160 | | - """ |
161 | | - file = ctx.obj["FILE"] |
162 | | - quote = ctx.obj["QUOTE"] |
163 | | - success, key = unset_key(file, key, quote) |
| 123 | +def unset(ctx: click.Context, key: str) -> None: |
| 124 | + """Removes a variable.""" |
| 125 | + file = ctx.obj['FILE'] |
| 126 | + quote = ctx.obj['QUOTE'] |
| 127 | + success = unset_key(file, key, quote) |
164 | 128 | if success: |
165 | | - click.echo(f"Successfully removed {key}") |
| 129 | + click.echo("Key %s removed." % key) |
166 | 130 | else: |
| 131 | + click.echo("Key %s not found." % key) |
167 | 132 | sys.exit(1) |
168 | 133 |
|
169 | 134 |
|
170 | | -@cli.command( |
171 | | - context_settings={ |
172 | | - "allow_extra_args": True, |
173 | | - "allow_interspersed_args": False, |
174 | | - "ignore_unknown_options": True, |
175 | | - } |
176 | | -) |
| 135 | +unset.add_argument = click.argument # type: ignore |
| 136 | +unset.add_argument('key', required=True) |
| 137 | + |
| 138 | + |
| 139 | +@cli.command(context_settings={'ignore_unknown_options': True, 'allow_extra_args': True}) |
177 | 140 | @click.pass_context |
178 | | -@click.option( |
179 | | - "--override/--no-override", |
180 | | - default=True, |
181 | | - help="Override variables from the environment file with those from the .env file.", |
182 | | -) |
183 | | -@click.argument("commandline", nargs=-1, type=click.UNPROCESSED) |
184 | | -def run(ctx: click.Context, override: bool, commandline: tuple[str, ...]) -> None: |
185 | | - """Run command with environment variables present.""" |
186 | | - file = ctx.obj["FILE"] |
| 141 | +def run(ctx: click.Context, commandline: List[str]) -> None: |
| 142 | + """Run a command with the environment variables from the .env file.""" |
| 143 | + file = ctx.obj['FILE'] |
187 | 144 | if not os.path.isfile(file): |
188 | | - raise click.BadParameter( |
189 | | - f"Invalid value for '-f' \"{file}\" does not exist.", ctx=ctx |
190 | | - ) |
| 145 | + click.echo("File doesn't exist: %s" % file) |
| 146 | + sys.exit(1) |
| 147 | + |
191 | 148 | dotenv_as_dict = { |
192 | 149 | k: v |
193 | 150 | for (k, v) in dotenv_values(file).items() |
194 | | - if v is not None and (override or k not in os.environ) |
| 151 | + if v is not None |
195 | 152 | } |
196 | 153 |
|
197 | 154 | if not commandline: |
198 | | - click.echo("No command given.") |
| 155 | + click.echo('No command given.') |
199 | 156 | sys.exit(1) |
| 157 | + run_command(commandline, dotenv_as_dict) |
200 | 158 |
|
201 | | - run_command([*commandline, *ctx.args], dotenv_as_dict) |
202 | 159 |
|
| 160 | +run.add_argument = click.argument # type: ignore |
| 161 | +run.add_argument('commandline', nargs=-1) |
203 | 162 |
|
204 | | -def run_command(command: List[str], env: Dict[str, str]) -> None: |
205 | | - """Replace the current process with the specified command. |
206 | 163 |
|
207 | | - Replaces the current process with the specified command and the variables from `env` |
208 | | - added in the current environment variables. |
| 164 | +def run_command(command: List[str], env: Dict[str, str]) -> None: |
| 165 | + """Run the command and set the environment variables. |
209 | 166 |
|
210 | 167 | Parameters |
211 | 168 | ---------- |
212 | 169 | command: List[str] |
213 | | - The command and it's parameters |
| 170 | + The command and its parameters |
214 | 171 | env: Dict |
215 | 172 | The additional environment variables |
216 | 173 |
|
@@ -245,3 +202,7 @@ def run_command(command: List[str], env: Dict[str, str]) -> None: |
245 | 202 | except FileNotFoundError: |
246 | 203 | print(f"Command not found: {command[0]}", file=sys.stderr) |
247 | 204 | sys.exit(1) |
| 205 | + |
| 206 | + |
| 207 | +if __name__ == '__main__': |
| 208 | + cli() |
0 commit comments