Skip to content

Commit 6ca2e2a

Browse files
jadutterbbc2
authored andcommitted
Add --export to set and make it create env file
- Add `--export` option to `set` to make it prepend the binding with `export`. - Make `set` command create the `.env` file in the current directory if no `.env` file was found.
1 parent 92ec3b2 commit 6ca2e2a

File tree

6 files changed

+79
-25
lines changed

6 files changed

+79
-25
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `--export` option to `set` to make it prepend the binding with `export` (#270 by
13+
[@jadutter]).
14+
15+
### Changed
16+
17+
- Make `set` command create the `.env` file in the current directory if no `.env` file was
18+
found (#270 by [@jadutter]).
19+
1020
### Fixed
1121

1222
- Fix potentially empty expanded value for duplicate key (#260 by [@bbc]).
@@ -209,6 +219,7 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
209219
[@gergelyk]: https://github.com/gergelyk
210220
[@gongqingkui]: https://github.com/gongqingkui
211221
[@greyli]: https://github.com/greyli
222+
[@jadutter]: https://github.com/jadutter
212223
[@qnighy]: https://github.com/qnighy
213224
[@snobu]: https://github.com/snobu
214225
[@techalchemy]: https://github.com/techalchemy

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,23 @@ Usage: dotenv [OPTIONS] COMMAND [ARGS]...
186186
Options:
187187
-f, --file PATH Location of the .env file, defaults to .env
188188
file in current working directory.
189+
189190
-q, --quote [always|never|auto]
190191
Whether to quote or not the variable values.
191192
Default mode is always. This does not affect
192193
parsing.
194+
195+
-e, --export BOOLEAN
196+
Whether to write the dot file as an
197+
executable bash script.
198+
199+
--version Show the version and exit.
193200
--help Show this message and exit.
194201
195202
Commands:
196-
get Retrive the value for the given key.
203+
get Retrieve the value for the given key.
197204
list Display all the stored key/value.
198-
run Run command with environment variables from .env file present
205+
run Run command with environment variables present.
199206
set Store the given key/value.
200207
unset Removes the given key.
201208
```

src/dotenv/cli.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,23 @@
1919

2020
@click.group()
2121
@click.option('-f', '--file', default=os.path.join(os.getcwd(), '.env'),
22-
type=click.Path(exists=True),
22+
type=click.Path(file_okay=True),
2323
help="Location of the .env file, defaults to .env file in current working directory.")
2424
@click.option('-q', '--quote', default='always',
2525
type=click.Choice(['always', 'never', 'auto']),
2626
help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.")
27+
@click.option('-e', '--export', default=False,
28+
type=click.BOOL,
29+
help="Whether to write the dot file as an executable bash script.")
2730
@click.version_option(version=__version__)
2831
@click.pass_context
29-
def cli(ctx, file, quote):
30-
# type: (click.Context, Any, Any) -> None
32+
def cli(ctx, file, quote, export):
33+
# type: (click.Context, Any, Any, Any) -> None
3134
'''This script is used to set, get or unset values from a .env file.'''
3235
ctx.obj = {}
33-
ctx.obj['FILE'] = file
3436
ctx.obj['QUOTE'] = quote
37+
ctx.obj['EXPORT'] = export
38+
ctx.obj['FILE'] = file
3539

3640

3741
@cli.command()
@@ -40,6 +44,11 @@ def list(ctx):
4044
# type: (click.Context) -> None
4145
'''Display all the stored key/value.'''
4246
file = ctx.obj['FILE']
47+
if not os.path.isfile(file):
48+
raise click.BadParameter(
49+
'Path "%s" does not exist.' % (file),
50+
ctx=ctx
51+
)
4352
dotenv_as_dict = dotenv_values(file)
4453
for k, v in dotenv_as_dict.items():
4554
click.echo('%s=%s' % (k, v))
@@ -54,7 +63,8 @@ def set(ctx, key, value):
5463
'''Store the given key/value.'''
5564
file = ctx.obj['FILE']
5665
quote = ctx.obj['QUOTE']
57-
success, key, value = set_key(file, key, value, quote)
66+
export = ctx.obj['EXPORT']
67+
success, key, value = set_key(file, key, value, quote, export)
5868
if success:
5969
click.echo('%s=%s' % (key, value))
6070
else:
@@ -68,6 +78,11 @@ def get(ctx, key):
6878
# type: (click.Context, Any) -> None
6979
'''Retrieve the value for the given key.'''
7080
file = ctx.obj['FILE']
81+
if not os.path.isfile(file):
82+
raise click.BadParameter(
83+
'Path "%s" does not exist.' % (file),
84+
ctx=ctx
85+
)
7186
stored_value = get_key(file, key)
7287
if stored_value:
7388
click.echo('%s=%s' % (key, stored_value))
@@ -97,6 +112,11 @@ def run(ctx, commandline):
97112
# type: (click.Context, List[str]) -> None
98113
"""Run command with environment variables present."""
99114
file = ctx.obj['FILE']
115+
if not os.path.isfile(file):
116+
raise click.BadParameter(
117+
'Invalid value for \'-f\' "%s" does not exist.' % (file),
118+
ctx=ctx
119+
)
100120
dotenv_as_dict = {to_env(k): to_env(v) for (k, v) in dotenv_values(file).items() if v is not None}
101121

102122
if not commandline:

src/dotenv/main.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ def get_key(dotenv_path, key_to_get):
140140
def rewrite(path):
141141
# type: (_PathLike) -> Iterator[Tuple[IO[Text], IO[Text]]]
142142
try:
143+
if not os.path.isfile(path):
144+
with io.open(path, "w+") as source:
145+
source.write("")
143146
with tempfile.NamedTemporaryFile(mode="w+", delete=False) as dest:
144147
with io.open(path) as source:
145148
yield (source, dest) # type: ignore
@@ -151,18 +154,15 @@ def rewrite(path):
151154
shutil.move(dest.name, path)
152155

153156

154-
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
155-
# type: (_PathLike, Text, Text, Text) -> Tuple[Optional[bool], Text, Text]
157+
def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=False):
158+
# type: (_PathLike, Text, Text, Text, bool) -> Tuple[Optional[bool], Text, Text]
156159
"""
157160
Adds or Updates a key/value to the given .env
158161
159162
If the .env path given doesn't exist, fails instead of risking creating
160163
an orphan .env somewhere in the filesystem
161164
"""
162165
value_to_set = value_to_set.strip("'").strip('"')
163-
if not os.path.exists(dotenv_path):
164-
logger.warning("Can't write to %s - it doesn't exist.", dotenv_path)
165-
return None, key_to_set, value_to_set
166166

167167
if " " in value_to_set:
168168
quote_mode = "always"
@@ -171,7 +171,10 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always"):
171171
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
172172
else:
173173
value_out = value_to_set
174-
line_out = "{}={}\n".format(key_to_set, value_out)
174+
if export:
175+
line_out = 'export {}={}\n'.format(key_to_set, value_out)
176+
else:
177+
line_out = "{}={}\n".format(key_to_set, value_out)
175178

176179
with rewrite(dotenv_path) as (source, dest):
177180
replaced = False

tests/test_cli.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def test_list_non_existent_file(cli):
2020
result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list'])
2121

2222
assert result.exit_code == 2, result.output
23-
assert "Invalid value for '-f'" in result.output
23+
assert "does not exist" in result.output
2424

2525

2626
def test_list_no_file(cli):
@@ -48,7 +48,7 @@ def test_get_no_file(cli):
4848
result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'get', 'a'])
4949

5050
assert result.exit_code == 2
51-
assert "Invalid value for '-f'" in result.output
51+
assert "does not exist" in result.output
5252

5353

5454
def test_unset_existing_value(cli, dotenv_file):
@@ -77,10 +77,27 @@ def test_unset_non_existent_value(cli, dotenv_file):
7777
("auto", "HELLO", "HELLO WORLD", 'HELLO="HELLO WORLD"\n'),
7878
)
7979
)
80-
def test_set_options(cli, dotenv_file, quote_mode, variable, value, expected):
80+
def test_set_quote_options(cli, dotenv_file, quote_mode, variable, value, expected):
8181
result = cli.invoke(
8282
dotenv_cli,
83-
["--file", dotenv_file, "--quote", quote_mode, "set", variable, value]
83+
["--file", dotenv_file, "--export", "false", "--quote", quote_mode, "set", variable, value]
84+
)
85+
86+
assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value))
87+
assert open(dotenv_file, "r").read() == expected
88+
89+
90+
@pytest.mark.parametrize(
91+
"dotenv_file,export_mode,variable,value,expected",
92+
(
93+
(".nx_file", "true", "HELLO", "WORLD", "export HELLO=\"WORLD\"\n"),
94+
(".nx_file", "false", "HELLO", "WORLD", "HELLO=\"WORLD\"\n"),
95+
)
96+
)
97+
def test_set_export(cli, dotenv_file, export_mode, variable, value, expected):
98+
result = cli.invoke(
99+
dotenv_cli,
100+
["--file", dotenv_file, "--quote", "always", "--export", export_mode, "set", variable, value]
84101
)
85102

86103
assert (result.exit_code, result.output) == (0, "{}={}\n".format(variable, value))
@@ -97,7 +114,7 @@ def test_set_no_file(cli):
97114
result = cli.invoke(dotenv_cli, ["--file", "nx_file", "set"])
98115

99116
assert result.exit_code == 2
100-
assert "Invalid value for '-f'" in result.output
117+
assert "Missing argument" in result.output
101118

102119

103120
def test_get_default_path(tmp_path):

tests/test_main.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,11 @@ def test_set_key_no_file(tmp_path):
1818
nx_file = str(tmp_path / "nx")
1919
logger = logging.getLogger("dotenv.main")
2020

21-
with mock.patch.object(logger, "warning") as mock_warning:
21+
with mock.patch.object(logger, "warning"):
2222
result = dotenv.set_key(nx_file, "foo", "bar")
2323

24-
assert result == (None, "foo", "bar")
25-
assert not os.path.exists(nx_file)
26-
mock_warning.assert_called_once_with(
27-
"Can't write to %s - it doesn't exist.",
28-
nx_file,
29-
)
24+
assert result == (True, "foo", "bar")
25+
assert os.path.exists(nx_file)
3026

3127

3228
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)