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
7 changes: 7 additions & 0 deletions .changes/next-release/cli-input-stdin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"category": "``cli``",
"description": "Add support for reading ``--cli-input-json`` and ``--cli-input-yaml`` from standard input by passing ``-`` as the value (e.g. ``aws s3api head-object --cli-input-json -``). This is consistent with the existing ``aws s3 cp -`` convention.",
"type": "feature"
}
]
12 changes: 10 additions & 2 deletions awscli/customizations/cliinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import json
import sys

from ruamel.yaml import YAML
from ruamel.yaml.error import YAMLError
Expand Down Expand Up @@ -85,6 +86,11 @@ def _get_arg_value(self, parsed_args):
'Only one --cli-input- parameter may be specified.'
)

# Read from stdin when '-' is passed, consistent with
# 'aws s3 cp - s3://bucket/key'.
if arg_value == '-':
return sys.stdin.read()

# If the value starts with file:// or fileb://, return the contents
# from the file.
paramfile_data = get_paramfile(arg_value, LOCAL_PREFIX_MAP)
Expand Down Expand Up @@ -122,7 +128,8 @@ class CliInputJSONArgument(CliInputArgument):
'will override the JSON-provided values. It is not possible to '
'pass arbitrary binary values using a JSON-provided value as the '
'string will be taken literally. This may not be specified along '
'with ``--cli-input-yaml``.'
'with ``--cli-input-yaml``. If the value is ``-``, the JSON '
'string will be read from standard input.'
),
}

Expand All @@ -143,7 +150,8 @@ class CliInputYAMLArgument(CliInputArgument):
'``--generate-cli-skeleton yaml-input``. '
'If other arguments are provided on the command line, those '
'values will override the YAML-provided values. This may not be '
'specified along with ``--cli-input-json``.'
'specified along with ``--cli-input-json``. If the value is '
'``-``, the YAML string will be read from standard input.'
),
}

Expand Down
38 changes: 37 additions & 1 deletion tests/functional/test_cliinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import io
import os

from awscli.testutils import BaseAWSCommandParamsTest, FileCreator
from awscli.testutils import BaseAWSCommandParamsTest, FileCreator, mock
from awscli.utils import dump_yaml_to_str


Expand Down Expand Up @@ -77,6 +78,26 @@ def test_cli_input_json_has_extra_unknown_args(self):
cmdline, expected_rc=252, stderr_contains='Unknown'
)

def test_cli_input_json_from_stdin(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(self.input_json),
):
cmdline = 's3api head-object --cli-input-json -'
self.assert_params_for_cmd(
cmdline, params={'Bucket': 'bucket', 'Key': 'key'}
)

def test_cli_input_json_stdin_can_override(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(self.input_json),
):
cmdline = 's3api head-object --key bar --cli-input-json -'
self.assert_params_for_cmd(
cmdline, params={'Bucket': 'bucket', 'Key': 'bar'}
)


class TestCLIInputYAML(BaseCLIInputArgumentTest):
def test_input_yaml(self):
Expand Down Expand Up @@ -148,6 +169,21 @@ def test_input_yaml_ignores_comments(self):
command, {'Bucket': 'test-bucket', 'EncodingType': 'url'}
)

def test_input_yaml_from_stdin(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO('Bucket: test-bucket\nEncodingType: url'),
):
command = [
's3api',
'list-objects-v2',
'--cli-input-yaml',
'-',
]
self.assert_params_for_cmd(
command, {'Bucket': 'test-bucket', 'EncodingType': 'url'}
)

def test_errors_when_both_yaml_and_json_provided(self):
command = [
's3api',
Expand Down
74 changes: 74 additions & 0 deletions tests/unit/customizations/test_cliinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import io
import os
import shutil
import tempfile
Expand Down Expand Up @@ -122,6 +123,64 @@ def test_input_is_not_a_map(self):
parsed_globals=None,
)

def test_add_to_call_parameters_from_stdin(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(self.input_json),
):
parsed_args = self.create_args('-')
call_parameters = {}
self.argument.add_to_call_parameters(
service_operation=None,
call_parameters=call_parameters,
parsed_args=parsed_args,
parsed_globals=None,
)
self.assertEqual(call_parameters, {'A': 'foo', 'B': 'bar'})

def test_stdin_does_not_clobber(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(self.input_json),
):
parsed_args = self.create_args('-')
call_parameters = {'A': 'baz'}
self.argument.add_to_call_parameters(
service_operation=None,
call_parameters=call_parameters,
parsed_args=parsed_args,
parsed_globals=None,
)
self.assertEqual(call_parameters, {'A': 'baz', 'B': 'bar'})

def test_stdin_bad_json(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO('not valid json'),
):
parsed_args = self.create_args('-')
with self.assertRaises(ParamError):
self.argument.add_to_call_parameters(
service_operation=None,
call_parameters={},
parsed_args=parsed_args,
parsed_globals=None,
)

def test_stdin_empty(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(''),
):
parsed_args = self.create_args('-')
with self.assertRaises(ParamError):
self.argument.add_to_call_parameters(
service_operation=None,
call_parameters={},
parsed_args=parsed_args,
parsed_globals=None,
)


class TestCliInputYAMLArgument(TestCliInputJSONArgument):
def setUp(self):
Expand Down Expand Up @@ -191,3 +250,18 @@ def test_yaml_does_not_overwrite(self):
parsed_globals=None,
)
self.assertEqual(call_parameters, {'A': 'baz', 'B': 'bar'})

def test_input_yaml_from_stdin(self):
with mock.patch(
'awscli.customizations.cliinput.sys.stdin',
io.StringIO(self.input_yaml),
):
parsed_args = self.create_args('-')
call_parameters = {}
self.argument.add_to_call_parameters(
service_operation=None,
call_parameters=call_parameters,
parsed_args=parsed_args,
parsed_globals=None,
)
self.assertEqual(call_parameters, {'A': 'foo', 'B': 'bar'})