Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Changed

- Raise `ValueError` if `quote_mode` isn't one of `always`, `auto` or `never` in
`set_key` (#330 by [@bbc2]).
- When writing a value to a .env file with `set_key` or `dotenv set <key> <value>` (#330
by [@bbc2]):
- Use single quotes instead of double quotes.
- Don't strip surrounding quotes.
- In `auto` mode, don't add quotes if the value is only made of alphanumeric characters
(as determined by `string.isalnum`).

## [0.17.1] - 2021-04-29

### Fixed
Expand Down
13 changes: 8 additions & 5 deletions src/dotenv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,16 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode="always", export=F
If the .env path given doesn't exist, fails instead of risking creating
an orphan .env somewhere in the filesystem
"""
value_to_set = value_to_set.strip("'").strip('"')
if quote_mode not in ("always", "auto", "never"):
raise ValueError("Unknown quote_mode: {}".format(quote_mode))

if " " in value_to_set:
quote_mode = "always"
quote = (
quote_mode == "always"
or (quote_mode == "auto" and not value_to_set.isalnum())
)

if quote_mode == "always":
value_out = '"{}"'.format(value_to_set.replace('"', '\\"'))
if quote:
value_out = "'{}'".format(value_to_set.replace("'", "\\'"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so you know, this is not the correct way to escape strings surrounded by single quotes when it comes to shells like Bash (this is the way).

I don't know if this project has a goal of making .env files compatible with Bash, if it does, than this code will fail in this edge-case. These commands in the Bash command-line will demonstrate my point:

$ printf %s 'one'"'"'two'
one'two
$ printf %s 'one\'two'
> 

(The last command doesn't run after pressing enter, but waits for a terminating single quote.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good point. I had actually forgotten about that rule. I'll see if we can do better with regards to matching Bash's behavior but I'm not sure it's worth it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bbc2 I believe we are facing this bug. We have single quotes inside value, it is being [wrongly] escaped as \' (which is not the right way) and source .env of resulting file fails

else:
value_out = value_to_set
if export:
Expand Down
13 changes: 7 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ def test_unset_non_existent_value(cli, dotenv_file):
@pytest.mark.parametrize(
"quote_mode,variable,value,expected",
(
("always", "HELLO", "WORLD", 'HELLO="WORLD"\n'),
("never", "HELLO", "WORLD", 'HELLO=WORLD\n'),
("auto", "HELLO", "WORLD", 'HELLO=WORLD\n'),
("auto", "HELLO", "HELLO WORLD", 'HELLO="HELLO WORLD"\n'),
("always", "a", "x", "a='x'\n"),
("never", "a", "x", 'a=x\n'),
("auto", "a", "x", "a=x\n"),
("auto", "a", "x y", "a='x y'\n"),
("auto", "a", "$", "a='$'\n"),
)
)
def test_set_quote_options(cli, dotenv_file, quote_mode, variable, value, expected):
Expand All @@ -92,8 +93,8 @@ def test_set_quote_options(cli, dotenv_file, quote_mode, variable, value, expect
@pytest.mark.parametrize(
"dotenv_file,export_mode,variable,value,expected",
(
(".nx_file", "true", "HELLO", "WORLD", "export HELLO=\"WORLD\"\n"),
(".nx_file", "false", "HELLO", "WORLD", "HELLO=\"WORLD\"\n"),
(".nx_file", "true", "a", "x", "export a='x'\n"),
(".nx_file", "false", "a", "x", "a='x'\n"),
)
)
def test_set_export(cli, dotenv_file, export_mode, variable, value, expected):
Expand Down
24 changes: 12 additions & 12 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ def test_set_key_no_file(tmp_path):
@pytest.mark.parametrize(
"before,key,value,expected,after",
[
("", "a", "", (True, "a", ""), 'a=""\n'),
("", "a", "b", (True, "a", "b"), 'a="b"\n'),
("", "a", "'b'", (True, "a", "b"), 'a="b"\n'),
("", "a", "\"b\"", (True, "a", "b"), 'a="b"\n'),
("", "a", "b'c", (True, "a", "b'c"), 'a="b\'c"\n'),
("", "a", "b\"c", (True, "a", "b\"c"), 'a="b\\\"c"\n'),
("a=b", "a", "c", (True, "a", "c"), 'a="c"\n'),
("a=b\n", "a", "c", (True, "a", "c"), 'a="c"\n'),
("a=b\n\n", "a", "c", (True, "a", "c"), 'a="c"\n\n'),
("a=b\nc=d", "a", "e", (True, "a", "e"), 'a="e"\nc=d'),
("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), 'a=b\nc="g"\ne=f'),
("a=b\n", "c", "d", (True, "c", "d"), 'a=b\nc="d"\n'),
("", "a", "", (True, "a", ""), "a=''\n"),
("", "a", "b", (True, "a", "b"), "a='b'\n"),
("", "a", "'b'", (True, "a", "'b'"), "a='\\'b\\''\n"),
("", "a", "\"b\"", (True, "a", '"b"'), "a='\"b\"'\n"),
("", "a", "b'c", (True, "a", "b'c"), "a='b\\'c'\n"),
("", "a", "b\"c", (True, "a", "b\"c"), "a='b\"c'\n"),
("a=b", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n", "a", "c", (True, "a", "c"), "a='c'\n"),
("a=b\n\n", "a", "c", (True, "a", "c"), "a='c'\n\n"),
("a=b\nc=d", "a", "e", (True, "a", "e"), "a='e'\nc=d"),
("a=b\nc=d\ne=f", "c", "g", (True, "c", "g"), "a=b\nc='g'\ne=f"),
("a=b\n", "c", "d", (True, "c", "d"), "a=b\nc='d'\n"),
],
)
def test_set_key(dotenv_file, before, key, value, expected, after):
Expand Down