Skip to content
Open
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
21 changes: 19 additions & 2 deletions awscli/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ def __init__(
argument_model=None,
synopsis='',
const=None,
aliases=None,
):
self._name = name
self._help = help_text
Expand All @@ -235,6 +236,9 @@ def __init__(
choices = []
self._choices = choices
self._synopsis = synopsis
if positional_arg and aliases:
raise ValueError("A positional argument cannot have aliases")
self._aliases = aliases

# These are public attributes that are ok to access from external
# objects.
Expand Down Expand Up @@ -271,13 +275,22 @@ def cli_name(self):
else:
return '--' + self._name

@property
def cli_flags(self):
if self._aliases is None:
return (self.cli_name,)
return (
*(("-" if len(a) == 1 else "--") + a for a in self._aliases),
self.cli_name,
)

def add_to_parser(self, parser):
"""

See the ``BaseCLIArgument.add_to_parser`` docs for more information.

"""
cli_name = self.cli_name
cli_flags = self.cli_flags
kwargs = {}
if self._dest is not None:
kwargs['dest'] = self._dest
Expand All @@ -293,7 +306,7 @@ def add_to_parser(self, parser):
kwargs['nargs'] = self._nargs
if self._const is not None:
kwargs['const'] = self._const
parser.add_argument(cli_name, **kwargs)
parser.add_argument(*cli_flags, **kwargs)

@property
def required(self):
Expand Down Expand Up @@ -349,6 +362,10 @@ def positional_arg(self):
def nargs(self):
return self._nargs

@property
def aliases(self):
return self._aliases


class CLIArgument(BaseCLIArgument):
"""Represents a CLI argument that maps to a service parameter."""
Expand Down
24 changes: 17 additions & 7 deletions awscli/completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ def _get_command(self, command_help, command_args):
return command_name, cmd_obj.create_help_command()
return None, None

def _get_documented_completions(self, table, startswith=None):
def _get_documented_completions(
self, table, startswith=None, include_aliases=False
):
names = []
for key, command in table.items():
if getattr(command, '_UNDOCUMENTED', False):
Expand All @@ -117,25 +119,33 @@ def _get_documented_completions(self, table, startswith=None):
if getattr(command, 'positional_arg', False):
continue
names.append(key)
if include_aliases and getattr(command, 'aliases', None):
names.extend(command.aliases)
return names

def _find_possible_options(self, current_arg, opts, subcmd_help=None):
all_options = copy.copy(self.main_options)
if subcmd_help is not None:
all_options += self._get_documented_completions(
subcmd_help.arg_table
subcmd_help.arg_table, include_aliases=True
)

# Prefix with hyphens to match arg (assume single-letter options are
# short)
all_options_prefixed = {
('-' if len(o) == 1 else '--') + o for o in all_options
}

for option in opts:
# Look through list of options on cmdline. If there are
# options that have already been specified and they are
# not the current word, remove them from list of possibles.
if option != current_arg:
stripped_opt = option.lstrip('-')
if stripped_opt in all_options:
all_options.remove(stripped_opt)
cw = current_arg.lstrip('-')
possibilities = ['--' + n for n in all_options if n.startswith(cw)]
if option in all_options_prefixed:
all_options_prefixed.remove(option)
possibilities = [
n for n in all_options_prefixed if n.startswith(current_arg)
]
if len(possibilities) == 1 and possibilities[0] == current_arg:
return self._complete_option(possibilities[0])
return possibilities
Expand Down
7 changes: 4 additions & 3 deletions awscli/customizations/s3/subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,22 +43,23 @@


RECURSIVE = {'name': 'recursive', 'action': 'store_true', 'dest': 'dir_op',
'aliases': ['r'],
'help_text': (
"Command is performed on all files or objects "
"under the specified directory or prefix.")}


HUMAN_READABLE = {'name': 'human-readable', 'action': 'store_true',
HUMAN_READABLE = {'name': 'human-readable', 'action': 'store_true', 'aliases': ['H'],
'help_text': "Displays file sizes in human readable format."}


SUMMARIZE = {'name': 'summarize', 'action': 'store_true',
SUMMARIZE = {'name': 'summarize', 'action': 'store_true', 'aliases': ['s'],
'help_text': (
"Displays summary information "
"(number of objects, total size).")}


DRYRUN = {'name': 'dryrun', 'action': 'store_true',
DRYRUN = {'name': 'dryrun', 'action': 'store_true', 'aliases': ['n'],
'help_text': (
"Displays the operations that would be performed using the "
"specified command without actually running them.")}
Expand Down
10 changes: 8 additions & 2 deletions tests/functional/s3/test_cp_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,17 @@ def test_operations_used_in_download_file(self):
self.assertEqual(self.operations_called[1][0].name, 'GetObject')

def test_operations_used_in_recursive_download(self):
self._test_operations_used_in_recursive_download(arg='--recursive')

def test_operations_used_in_recursive_download_short_option(self):
self._test_operations_used_in_recursive_download(arg='-r')

def _test_operations_used_in_recursive_download(self, arg):
self.parsed_responses = [
{'ETag': '"foo-1"', 'Contents': [], 'CommonPrefixes': []},
]
cmdline = '%s s3://bucket/key.txt %s --recursive' % (
self.prefix, self.files.rootdir)
cmdline = '%s s3://bucket/key.txt %s %s' % (
self.prefix, self.files.rootdir, arg)
self.run_cmd(cmdline, expected_rc=0)
# We called ListObjectsV2 but had no objects to download, so
# we only have a single ListObjectsV2 operation being called.
Expand Down
24 changes: 21 additions & 3 deletions tests/functional/s3/test_ls_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
class TestLSCommand(BaseS3TransferCommandTest):

def test_operations_used_in_recursive_list(self):
self._test_operations_used_in_recursive_list(arg='--recursive')

def test_operations_used_in_recursive_list_short_option(self):
self._test_operations_used_in_recursive_list(arg='-r')

def _test_operations_used_in_recursive_list(self, arg):
time_utc = "2014-01-09T20:45:49.000Z"
self.parsed_responses = [{"CommonPrefixes": [], "Contents": [
{"Key": "foo/bar.txt", "Size": 100,
"LastModified": time_utc}]}]
stdout, _, _ = self.run_cmd('s3 ls s3://bucket/ --recursive', expected_rc=0)
stdout, _, _ = self.run_cmd(f's3 ls s3://bucket/ {arg}', expected_rc=0)
call_args = self.operations_called[0][1]
# We should not be calling the args with any delimiter because we
# want a recursive listing.
Expand Down Expand Up @@ -123,6 +129,12 @@ def test_fail_rc_no_objects_nor_prefixes(self):
self.run_cmd('s3 ls s3://bucket/foo', expected_rc=1)

def test_human_readable_file_size(self):
self._test_human_readable_file_size(arg='--human-readable')

def test_human_readable_file_size_short_option(self):
self._test_human_readable_file_size(arg='-H')

def _test_human_readable_file_size(self, arg):
time_utc = "2014-01-09T20:45:49.000Z"
self.parsed_responses = [{"CommonPrefixes": [], "Contents": [
{"Key": "onebyte.txt", "Size": 1, "LastModified": time_utc},
Expand All @@ -131,7 +143,7 @@ def test_human_readable_file_size(self):
{"Key": "onegigabyte.txt", "Size": 1024 ** 3, "LastModified": time_utc},
{"Key": "oneterabyte.txt", "Size": 1024 ** 4, "LastModified": time_utc},
{"Key": "onepetabyte.txt", "Size": 1024 ** 5, "LastModified": time_utc} ]}]
stdout, _, _ = self.run_cmd('s3 ls s3://bucket/ --human-readable',
stdout, _, _ = self.run_cmd(f's3 ls s3://bucket/ {arg}',
expected_rc=0)
call_args = self.operations_called[0][1]
# Time is stored in UTC timezone, but the actual time displayed
Expand All @@ -146,6 +158,12 @@ def test_human_readable_file_size(self):
self.assertIn('%s 1.0 PiB onepetabyte.txt\n' % time_fmt, stdout)

def test_summarize(self):
self._test_summarize(arg='--summarize')

def test_summarize_short_option(self):
self._test_summarize(arg='-s')

def _test_summarize(self, arg):
time_utc = "2014-01-09T20:45:49.000Z"
self.parsed_responses = [{"CommonPrefixes": [], "Contents": [
{"Key": "onebyte.txt", "Size": 1, "LastModified": time_utc},
Expand All @@ -154,7 +172,7 @@ def test_summarize(self):
{"Key": "onegigabyte.txt", "Size": 1024 ** 3, "LastModified": time_utc},
{"Key": "oneterabyte.txt", "Size": 1024 ** 4, "LastModified": time_utc},
{"Key": "onepetabyte.txt", "Size": 1024 ** 5, "LastModified": time_utc} ]}]
stdout, _, _ = self.run_cmd('s3 ls s3://bucket/ --summarize', expected_rc=0)
stdout, _, _ = self.run_cmd(f's3 ls s3://bucket/ {arg}', expected_rc=0)
call_args = self.operations_called[0][1]
# Time is stored in UTC timezone, but the actual time displayed
# is specific to your tzinfo, so shift the timezone to your local's.
Expand Down
51 changes: 51 additions & 0 deletions tests/functional/s3/test_mv_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,57 @@ def test_metadata_directive_copy(self):
self.assertEqual(self.operations_called[1][1]['MetadataDirective'],
'REPLACE')

def test_recursive(self):
self._test_recursive(arg='--recursive')

def test_recursive_short_option(self):
self._test_recursive(arg='-r')

def _test_recursive(self, arg):
self.parsed_responses = [
self.list_objects_response(
['foo/a/1.txt', 'foo/a/2.txt', 'foo/b/3.txt'],
),
self.copy_object_response(),
self.copy_object_response(),
self.copy_object_response(),
self.delete_object_response(),
self.delete_object_response(),
self.delete_object_response(),
]
cmdline = (
f'{self.prefix} s3://bucket/foo/ s3://bucket/bar/ {arg}'
)
self.run_cmd(cmdline, expected_rc=0)

self.assertEqual(len(self.operations_called), 7,
self.operations_called)

self.assertEqual(self.operations_called[0][0].name, 'ListObjectsV2')
self.assertEqual(self.operations_called[0][1]['Prefix'], 'foo/')
self.assertEqual(self.operations_called[1][0].name, 'CopyObject')
self.assertEqual(self.operations_called[2][0].name, 'DeleteObject')
self.assertEqual(self.operations_called[3][0].name, 'CopyObject')
self.assertEqual(self.operations_called[4][0].name, 'DeleteObject')
self.assertEqual(self.operations_called[5][0].name, 'CopyObject')
self.assertEqual(self.operations_called[6][0].name, 'DeleteObject')

self.assertEqual(
{
(
self.operations_called[i][1]['CopySource']['Key'],
self.operations_called[i][1]['Key'],
self.operations_called[i + 1][1]['Key'], # delete
)
for i in [1, 3, 5]
},
{
('foo/a/1.txt', 'bar/a/1.txt', 'foo/a/1.txt'),
('foo/a/2.txt', 'bar/a/2.txt', 'foo/a/2.txt'),
('foo/b/3.txt', 'bar/b/3.txt', 'foo/b/3.txt'),
},
) # set-equal doesn't care about order

def test_no_metadata_directive_for_non_copy(self):
full_path = self.files.create_file('foo.txt', 'mycontent')
cmdline = '%s %s s3://bucket --metadata-directive REPLACE' % \
Expand Down
8 changes: 7 additions & 1 deletion tests/functional/s3/test_rm_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ def test_delete_with_request_payer(self):
)

def test_recursive_delete_with_requests(self):
cmdline = '%s s3://mybucket/ --recursive --request-payer' % self.prefix
self._test_recursive_delete_with_requests(arg='--recursive')

def test_recursive_delete_with_requests_short_option(self):
self._test_recursive_delete_with_requests(arg='-r')

def _test_recursive_delete_with_requests(self, arg):
cmdline = '%s s3://mybucket/ %s --request-payer' % (self.prefix, arg)
self.parsed_responses = [
self.list_objects_response(['mykey']),
self.empty_response(),
Expand Down
8 changes: 7 additions & 1 deletion tests/functional/s3/test_sync_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,13 @@ def test_website_redirect_ignore_paramfile(self):
)

def test_no_recursive_option(self):
cmdline = '. s3://mybucket --recursive'
self._test_no_recursive_option(arg='--recursive')

def test_no_recursive_short_option(self):
self._test_no_recursive_option(arg='-r')

def _test_no_recursive_option(self, arg):
cmdline = '. s3://mybucket %s' % arg
# Return code will be 2 for invalid parameter ``--recursive``
self.run_cmd(cmdline, expected_rc=2)

Expand Down
8 changes: 7 additions & 1 deletion tests/integration/customizations/s3/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1431,11 +1431,17 @@ class TestDryrun(BaseS3IntegrationTest):
This ensures that dryrun works.
"""
def test_dryrun(self):
self._test_dryrun(arg='--dryrun')

def test_dryrun_short_option(self):
self._test_dryrun(arg='-n')

def _test_dryrun(self, arg):
bucket_name = _SHARED_BUCKET
foo_txt = self.files.create_file('foo.txt', 'foo contents')

# Copy file into bucket.
p = aws('s3 cp %s s3://%s/ --dryrun' % (foo_txt, bucket_name))
p = aws('s3 cp %s s3://%s/ %s' % (foo_txt, bucket_name, arg))
self.assertEqual(p.rc, 0)
self.assert_no_errors(p)
self.assertTrue(self.key_not_exists(bucket_name, 'foo.txt'))
Expand Down
6 changes: 5 additions & 1 deletion tests/unit/test_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ class TestCompleteCustomCommands(BaseCompleterTest):
def setUp(self):
super(TestCompleteCustomCommands, self).setUp()
custom_arguments = [
{'name': 'recursive'},
{'name': 'recursive', 'aliases': ['r']},
{'name': 'sse'}
]
custom_commands = [
Expand Down Expand Up @@ -338,6 +338,10 @@ def test_complete_custom_command_arguments(self):
self.assert_completion(self.completer, 'aws s3 cp --', [
'--bar', '--recursive', '--sse'])

def test_complete_custom_command_short_arguments(self):
self.assert_completion(self.completer, 'aws s3 cp -', [
'-r', '--bar', '--recursive', '--sse'])

def test_complete_custom_command_arguments_with_arg_already_used(self):
self.assert_completion(self.completer, 'aws s3 cp --recursive --', [
'--bar', '--sse'])
Expand Down