Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve quoting of values in set_key #330

Merged
merged 1 commit into from Jun 20, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
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
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
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
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.

else:
value_out = value_to_set
if export:
Expand Down
13 changes: 7 additions & 6 deletions tests/test_cli.py
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
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