Skip to content

Commit e6bf0f1

Browse files
authored
Merge pull request #142 from mattsb42-aws/dev-110
fix for Windows config file quote character handling
2 parents 8972db1 + fb0e7b9 commit e6bf0f1

File tree

6 files changed

+87
-52
lines changed

6 files changed

+87
-52
lines changed

CHANGELOG.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
Changelog
33
*********
44

5+
1.1.4
6+
=====
7+
8+
Bugfixes
9+
--------
10+
* Fixed config file handling of quotes in Windows
11+
`#110 <https://github.com/awslabs/aws-encryption-sdk-cli/issues/110>`_
12+
13+
514
1.1.3
615
=====
716

README.rst

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -355,12 +355,6 @@ Configuration Files
355355
As with any CLI where the configuration can get rather complex, you might want to use a configuration
356356
file to define some or all of your desired behavior.
357357

358-
.. warning::
359-
360-
There is a `known issue with config file parsing in Windows`_. Including single or double quote
361-
characters in a config file on Windows will fail until we fix this issue. Please let us know
362-
if this impacts you in the linked GitHub issue.
363-
364358
Configuration files are supported using Python's native `argparse file support`_, which allows
365359
you to write configuration files exactly as you would enter arguments in the shell. Configuration
366360
file references passed to ``aws-encryption-cli`` are identified by the ``@`` prefix and the
@@ -409,8 +403,9 @@ Configuration files can be referenced anywhere in ``aws-encryption-cli`` paramet
409403
410404
aws-encryption-cli -e -i $INPUT_DIR -o $OUTPUT_DIR @master-key.conf @caching.conf --recursive
411405
412-
Configuration files can have many lines, include comments using ``#``, and include
413-
references to other configuration files.
406+
Configuration files can have many lines, include comments using ``#``. Escape characters are
407+
platform-specific: ``\`` on Linux and MacOS and ````` on Windows. Configuration files may
408+
also include references to other configuration files.
414409

415410
**my-encrypt.config**
416411

@@ -568,4 +563,3 @@ Execution
568563
.. _named profile: http://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html
569564
.. _setuptools entry point: http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
570565
.. _you must not specify a key: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/crypto-cli-how-to.html#crypto-cli-master-key
571-
.. _known issue with config file parsing in Windows: https://github.com/awslabs/aws-encryption-sdk-cli/issues/110

src/aws_encryption_sdk_cli/internal/arg_parsing.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
import shlex
2121

2222
import aws_encryption_sdk
23+
import six
2324

24-
from aws_encryption_sdk_cli.exceptions import BadUserArgumentError, ParameterParseError
25+
from aws_encryption_sdk_cli.exceptions import ParameterParseError
2526
from aws_encryption_sdk_cli.internal.identifiers import __version__, ALGORITHM_NAMES, DEFAULT_MASTER_KEY_PROVIDER
2627
from aws_encryption_sdk_cli.internal.logging_utils import LOGGER_NAME
2728
from aws_encryption_sdk_cli.internal.metadata import MetadataWriter
@@ -49,7 +50,7 @@ def __init__(self, *args, **kwargs):
4950
# I would rather not duplicate the typeshed's effort keeping it up to date.
5051
# https://github.com/python/typeshed/blob/master/stdlib/2and3/argparse.pyi#L27-L39
5152
self.__dummy_arguments = []
52-
self.__is_posix = not any(platform.win32_ver())
53+
self.__is_windows = any(platform.win32_ver())
5354
super(CommentIgnoringArgumentParser, self).__init__(*args, **kwargs)
5455

5556
def add_dummy_redirect_argument(self, expected_name):
@@ -85,22 +86,34 @@ def add_argument(self, *args, **kwargs):
8586

8687
return super(CommentIgnoringArgumentParser, self).add_argument(*args, **kwargs)
8788

89+
def __parse_line(self, arg_line):
90+
# type: (ARGPARSE_TEXT) -> List[str]
91+
"""Parses a line of arguments into individual arguments intelligently for different platforms.
92+
This differs from standard shlex behavior in that is supports escaping both single and double
93+
quotes and on Windows platforms uses the Windows-native escape character "`".
94+
95+
:param str arg_line: Raw argument line
96+
:returns: Parsed line members
97+
:rtype: list of str
98+
"""
99+
shlexer = shlex.shlex(six.StringIO(arg_line), posix=True) # type: ignore # shlex confuses mypy
100+
shlexer.whitespace_split = True
101+
shlexer.escapedquotes = '\'"'
102+
if self.__is_windows:
103+
shlexer.escape = '`'
104+
return list(shlexer) # type: ignore # shlex confuses mypy
105+
88106
def convert_arg_line_to_args(self, arg_line):
89107
# type: (ARGPARSE_TEXT) -> List[str]
90-
"""Applies whitespace stripping to individual arguments in each line and
91-
drops both full-line and in-line comments.
108+
"""Converts a line of arguments into individual arguments, expanding user and environment variables.
109+
110+
:param str arg_line: Raw argument line
111+
:returns: Converted line members
112+
:rtype: list of str
92113
"""
93114
converted_line = []
94-
problematic_characters = ['\'', '"']
95-
if not self.__is_posix and any([val in arg_line for val in problematic_characters]):
96-
raise BadUserArgumentError(
97-
'Config files containing characters {} are not currently supported:'
98-
' https://github.com/awslabs/aws-encryption-sdk-cli/issues/110'.format(repr(problematic_characters))
99-
)
100-
for arg in shlex.split(str(arg_line), posix=self.__is_posix):
115+
for arg in self.__parse_line(arg_line):
101116
arg = arg.strip()
102-
if arg.startswith('#'):
103-
break
104117
user_arg = os.path.expanduser(arg)
105118
environ_arg = os.path.expandvars(user_arg)
106119
converted_line.append(environ_arg)

src/aws_encryption_sdk_cli/internal/identifiers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
'DEFAULT_MASTER_KEY_PROVIDER',
3131
'OperationResult'
3232
)
33-
__version__ = '1.1.3' # type: str
33+
__version__ = '1.1.4' # type: str
3434

3535
#: Suffix added to output files if specific output filename is not specified.
3636
OUTPUT_SUFFIX = {

test/unit/test_arg_parsing.py

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,8 @@
2121
from pytest_mock import mocker # noqa pylint: disable=unused-import
2222

2323
import aws_encryption_sdk_cli
24-
from aws_encryption_sdk_cli.exceptions import BadUserArgumentError, ParameterParseError
24+
from aws_encryption_sdk_cli.exceptions import ParameterParseError
2525
from aws_encryption_sdk_cli.internal import arg_parsing, identifiers, metadata
26-
from .unit_test_utils import is_windows
2726

2827
pytestmark = [pytest.mark.unit, pytest.mark.local]
2928

@@ -121,24 +120,64 @@ def test_comment_ignoring_argument_parser_convert_filename(patch_platform_win32_
121120
parser = arg_parsing.CommentIgnoringArgumentParser()
122121

123122
if any(win32_version):
124-
assert not parser._CommentIgnoringArgumentParser__is_posix
123+
assert parser._CommentIgnoringArgumentParser__is_windows
125124
else:
126-
assert parser._CommentIgnoringArgumentParser__is_posix
125+
assert not parser._CommentIgnoringArgumentParser__is_windows
127126

128127
parsed_line = [arg for arg in parser.convert_arg_line_to_args(expected_transform[0])]
129128
assert expected_transform[1] == parsed_line
130129

131130

131+
def build_convert_special_cases():
132+
test_cases = []
133+
escape_chars = {
134+
False: '\\',
135+
True: '`'
136+
}
137+
for plat_is_windows in (True, False):
138+
test_cases.append((
139+
'-o "example file with spaces surrounded by double quotes"',
140+
['-o', 'example file with spaces surrounded by double quotes'],
141+
plat_is_windows
142+
))
143+
test_cases.append((
144+
"-o 'example file with spaces surrounded by single quotes'",
145+
['-o', 'example file with spaces surrounded by single quotes'],
146+
plat_is_windows
147+
))
148+
test_cases.append((
149+
'-o "example with an inner {}" double quote"'.format(escape_chars[plat_is_windows]),
150+
['-o', 'example with an inner " double quote'],
151+
plat_is_windows
152+
))
153+
test_cases.append((
154+
"-o 'example with an inner {}' single quote'".format(escape_chars[plat_is_windows]),
155+
['-o', "example with an inner ' single quote"],
156+
plat_is_windows
157+
))
158+
return test_cases
159+
160+
161+
@pytest.mark.parametrize('arg_line, expected_args, plat_is_windows', build_convert_special_cases())
162+
def test_comment_ignoring_argument_parser_convert_special_cases(arg_line, expected_args, plat_is_windows):
163+
parser = arg_parsing.CommentIgnoringArgumentParser()
164+
parser._CommentIgnoringArgumentParser__is_windows = plat_is_windows
165+
166+
actual_args = parser.convert_arg_line_to_args(arg_line)
167+
168+
assert actual_args == expected_args
169+
170+
132171
@pytest.mark.functional
133172
def test_f_comment_ignoring_argument_parser_convert_filename():
134173
# Actually checks against the current local system
135174
parser = arg_parsing.CommentIgnoringArgumentParser()
136175

137176
if any(platform.win32_ver()):
138-
assert not parser._CommentIgnoringArgumentParser__is_posix
177+
assert parser._CommentIgnoringArgumentParser__is_windows
139178
expected_transform = NON_POSIX_FILEPATH
140179
else:
141-
assert parser._CommentIgnoringArgumentParser__is_posix
180+
assert not parser._CommentIgnoringArgumentParser__is_windows
142181
expected_transform = POSIX_FILEPATH
143182

144183
parsed_line = [arg for arg in parser.convert_arg_line_to_args(expected_transform[0])]
@@ -218,15 +257,10 @@ def build_expected_good_args(from_file=False): # pylint: disable=too-many-local
218257
{'some': 'data', 'not': 'secret'}
219258
))
220259
if from_file:
221-
good_args.append(pytest.param(
260+
good_args.append((
222261
default_encrypt + ' -c "key with a space=value with a space"',
223262
'encryption_context',
224-
{'key with a space': 'value with a space'},
225-
marks=pytest.mark.xfail(
226-
is_windows(),
227-
reason='https://github.com/awslabs/aws-encryption-sdk-cli/issues/110',
228-
strict=True
229-
)
263+
{'key with a space': 'value with a space'}
230264
))
231265
else:
232266
good_args.append((
@@ -660,17 +694,3 @@ def test_process_encryption_context_encrypt_required_key_fail():
660694
raw_encryption_context=['encryption=context', 'with=values', 'key_3'],
661695
raw_required_encryption_context_keys=['key_1', 'key_2']
662696
)
663-
664-
665-
@pytest.mark.parametrize('arg_line', (
666-
'single-quote \' line',
667-
'double-quote " line'
668-
))
669-
def test_line_contains_problematic_characters(arg_line):
670-
parser = arg_parsing.CommentIgnoringArgumentParser()
671-
parser._CommentIgnoringArgumentParser__is_posix = False
672-
673-
with pytest.raises(BadUserArgumentError) as excinfo:
674-
parser.convert_arg_line_to_args(arg_line)
675-
676-
excinfo.match(r'Config files containing characters *')

tox.ini

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ passenv =
2828
sitepackages = False
2929
deps =
3030
mock
31-
pytest!=3.3.0
32-
pytest-catchlog
31+
pytest>3.3.0
3332
pytest-cov
3433
pytest-mock
3534
pytest-sugar

0 commit comments

Comments
 (0)