From 8e881c66286f93940c519983397b5373b7cdba3b Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Thu, 19 May 2022 12:46:25 -0700 Subject: [PATCH 1/5] feat(cli) add --format=(simple,json) option to list command The default is simple, which is backwards compatible. --format=json will display the list as a json dict resolves #405 --- README.md | 5 +++++ src/dotenv/cli.py | 14 +++++++++++--- tests/test_cli.py | 11 +++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 70de7e09..2921f177 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,11 @@ $ dotenv set EMAIL foo@example.org $ dotenv list USER=foo EMAIL=foo@example.org +$ dotenv list --format=json +{ + "USER": "foo", + "EMAIL": "foo@example.org" +} $ dotenv run -- python foo.py ``` diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 3411e346..ec4546f0 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,3 +1,4 @@ +import json import os import sys from subprocess import Popen @@ -36,7 +37,11 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: @cli.command() @click.pass_context -def list(ctx: click.Context) -> None: +@click.option('--format', default='simple', + type=click.Choice(['simple', 'json']), + help="The format in which to display the list. Default format is simple, " + "which displays name=value without quotes.") +def list(ctx: click.Context, format: bool) -> None: '''Display all the stored key/value.''' file = ctx.obj['FILE'] if not os.path.isfile(file): @@ -45,8 +50,11 @@ def list(ctx: click.Context) -> None: ctx=ctx ) dotenv_as_dict = dotenv_values(file) - for k, v in dotenv_as_dict.items(): - click.echo('%s=%s' % (k, v)) + if format == 'json': + print(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) + else: + for k, v in dotenv_as_dict.items(): + click.echo('%s=%s' % (k, v)) @cli.command() diff --git a/tests/test_cli.py b/tests/test_cli.py index 223476fe..70051783 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import json import os import pytest @@ -17,6 +18,16 @@ def test_list(cli, dotenv_file): assert (result.exit_code, result.output) == (0, result.output) +def test_list_json(cli, dotenv_file): + with open(dotenv_file, "w") as f: + f.write("a=b") + + result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'list', '--format=json']) + assert result.exit_code == 0 + result_obj = json.loads(result.output) + assert (len(result_obj), result_obj['a']) == (1, 'b') + + def test_list_non_existent_file(cli): result = cli.invoke(dotenv_cli, ['--file', 'nx_file', 'list']) From 01ec947901e955cf33e128563453595692f63bbd Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Fri, 20 May 2022 10:35:18 -0700 Subject: [PATCH 2/5] feat(cli) add --format= option to list command Allows dumping of all variables in various formats. Currently defined formats: simple: Each variable is output as = with no quoting or escaping. The output is not parseable. This is the default format, for backwards compatibility. shell: Each variable is output as =, where is quoted/escaped with shell-compatible rules, the result may be imported into a shell script with eval "$(dotenv list --format=shell)" export: Similar to "shell", but prefixes each line with "export ", so that when imported into a shell script, the variables are exported. json: The entire set of variables is output as a JSON-serialized object --- src/dotenv/cli.py | 11 ++++++++--- tests/test_cli.py | 33 ++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index ec4546f0..80af2ae9 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -1,5 +1,6 @@ import json import os +import shlex import sys from subprocess import Popen from typing import Any, Dict, List @@ -38,7 +39,7 @@ def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: @cli.command() @click.pass_context @click.option('--format', default='simple', - type=click.Choice(['simple', 'json']), + type=click.Choice(['simple', 'json', 'shell', 'export']), help="The format in which to display the list. Default format is simple, " "which displays name=value without quotes.") def list(ctx: click.Context, format: bool) -> None: @@ -53,8 +54,12 @@ def list(ctx: click.Context, format: bool) -> None: if format == 'json': print(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) else: - for k, v in dotenv_as_dict.items(): - click.echo('%s=%s' % (k, v)) + prefix = 'export ' if format == 'export' else '' + for k in sorted(dotenv_as_dict): + v = dotenv_as_dict[k] + if format in ('export', 'shell'): + v = shlex.quote(v) + click.echo('%s%s=%s' % (prefix, k, v)) @cli.command() diff --git a/tests/test_cli.py b/tests/test_cli.py index 70051783..2586d03b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,31 +1,34 @@ -import json import os import pytest import sh - +from typing import Optional import dotenv from dotenv.cli import cli as dotenv_cli from dotenv.version import __version__ -def test_list(cli, dotenv_file): +@pytest.mark.parametrize( + "format,content,expected", + ( + (None, "x='a b c'", '''x=a b c\n'''), + ("simple", "x='a b c'", '''x=a b c\n'''), + ("json", "x='a b c'", '''{\n "x": "a b c"\n}\n'''), + ("shell", "x='a b c'", '''x='a b c'\n'''), + ("export", "x='a b c'", '''export x='a b c'\n'''), + ) +) +def test_list(cli, dotenv_file, format: Optional[str], content: str, expected: str): with open(dotenv_file, "w") as f: - f.write("a=b") + f.write(content + '\n') - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'list']) + args = ['--file', dotenv_file, 'list'] + if format is not None: + args.extend(['--format', format]) - assert (result.exit_code, result.output) == (0, result.output) + result = cli.invoke(dotenv_cli, args) - -def test_list_json(cli, dotenv_file): - with open(dotenv_file, "w") as f: - f.write("a=b") - - result = cli.invoke(dotenv_cli, ['--file', dotenv_file, 'list', '--format=json']) - assert result.exit_code == 0 - result_obj = json.loads(result.output) - assert (len(result_obj), result_obj['a']) == (1, 'b') + assert (result.exit_code, result.output) == (0, expected) def test_list_non_existent_file(cli): From 0041e4eb5c9c773749549768ad152284a22dd30a Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Fri, 20 May 2022 11:01:21 -0700 Subject: [PATCH 3/5] use click.echo consistently --- src/dotenv/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index 80af2ae9..a2cdf74d 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -52,7 +52,7 @@ def list(ctx: click.Context, format: bool) -> None: ) dotenv_as_dict = dotenv_values(file) if format == 'json': - print(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) + click.echo(json.dumps(dotenv_as_dict, indent=2, sort_keys=True)) else: prefix = 'export ' if format == 'export' else '' for k in sorted(dotenv_as_dict): From bc671e7e3609f8f24980fd2be6bcd092012ca7fb Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Sat, 4 Jun 2022 16:02:21 -0700 Subject: [PATCH 4/5] Filter None values from 'list' command for shell formats dotenv_as_dict has Optional[str] values. 'None' values cannot be represented in shell-formatted listings, so they are omitted. --- src/dotenv/cli.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dotenv/cli.py b/src/dotenv/cli.py index a2cdf74d..b845b95e 100644 --- a/src/dotenv/cli.py +++ b/src/dotenv/cli.py @@ -57,9 +57,10 @@ def list(ctx: click.Context, format: bool) -> None: prefix = 'export ' if format == 'export' else '' for k in sorted(dotenv_as_dict): v = dotenv_as_dict[k] - if format in ('export', 'shell'): - v = shlex.quote(v) - click.echo('%s%s=%s' % (prefix, k, v)) + if v is not None: + if format in ('export', 'shell'): + v = shlex.quote(v) + click.echo('%s%s=%s' % (prefix, k, v)) @cli.command() From 9f0e4191821f90527e58df2b5ef2062be942caa7 Mon Sep 17 00:00:00 2001 From: Sam McKelvie Date: Wed, 8 Jun 2022 13:45:40 -0700 Subject: [PATCH 5/5] add test cases to distinguish simple/shell list formats --- tests/test_cli.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 2586d03b..ca5ba2a1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -13,8 +13,13 @@ ( (None, "x='a b c'", '''x=a b c\n'''), ("simple", "x='a b c'", '''x=a b c\n'''), + ("simple", """x='"a b c"'""", '''x="a b c"\n'''), + ("simple", '''x="'a b c'"''', '''x='a b c'\n'''), ("json", "x='a b c'", '''{\n "x": "a b c"\n}\n'''), - ("shell", "x='a b c'", '''x='a b c'\n'''), + ("shell", "x='a b c'", "x='a b c'\n"), + ("shell", """x='"a b c"'""", '''x='"a b c"'\n'''), + ("shell", '''x="'a b c'"''', '''x=''"'"'a b c'"'"''\n'''), + ("shell", "x='a\nb\nc'", "x='a\nb\nc'\n"), ("export", "x='a b c'", '''export x='a b c'\n'''), ) )