diff --git a/src/azure-cli-core/azure/cli/core/_output.py b/src/azure-cli-core/azure/cli/core/_output.py index 5cf98b38d8b..0038af2d4c0 100644 --- a/src/azure-cli-core/azure/cli/core/_output.py +++ b/src/azure-cli-core/azure/cli/core/_output.py @@ -3,7 +3,13 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- +import errno + import knack.output +from knack.log import get_logger +from knack.util import CommandResultItem + +logger = get_logger(__name__) class AzOutputProducer(knack.output.OutputProducer): @@ -11,6 +17,29 @@ class AzOutputProducer(knack.output.OutputProducer): def check_valid_format_type(self, format_type): return format_type in self._FORMAT_DICT + def out(self, obj, formatter=None, out_file=None): + if formatter == knack.output.format_tsv and hasattr(out_file, 'buffer'): + if not isinstance(obj, CommandResultItem): + raise TypeError('Expected {} got {}'.format(CommandResultItem.__name__, type(obj))) + + output = formatter(obj) + try: + out_file.buffer.write(output.encode(out_file.encoding or 'utf-8')) + out_file.flush() + except IOError as ex: + if ex.errno == errno.EPIPE: + pass + else: + raise + except UnicodeEncodeError: + logger.warning("Unable to encode the output with %s encoding. Unsupported characters are discarded.", + out_file.encoding) + out_file.buffer.write(output.encode('ascii', 'ignore')) + out_file.flush() + return + + super().out(obj, formatter=formatter, out_file=out_file) + def get_output_format(cli_ctx): return cli_ctx.invocation.data.get("output", None) diff --git a/src/azure-cli-core/azure/cli/core/tests/test_output.py b/src/azure-cli-core/azure/cli/core/tests/test_output.py index b114e888ff2..41a4a81613b 100644 --- a/src/azure-cli-core/azure/cli/core/tests/test_output.py +++ b/src/azure-cli-core/azure/cli/core/tests/test_output.py @@ -7,6 +7,11 @@ class TestCoreCLIOutput(unittest.TestCase): + @staticmethod + def _create_translating_stream(): + import io + return io.TextIOWrapper(io.BytesIO(), encoding='utf-8', newline='\r\n') + def test_create_AzOutputProducer(self): from azure.cli.core._output import AzOutputProducer from azure.cli.core.mock import DummyCli @@ -41,6 +46,35 @@ def test_yaml_output_with_ordered_dict(self): yaml_output = output_producer.get_formatter('yaml')(CommandResultItem(result=OrderedDict(account_dict))) self.assertEqual(account_dict, yaml.safe_load(yaml_output)) + def test_tsv_output_uses_lf_line_endings(self): + from azure.cli.core._output import AzOutputProducer + from azure.cli.core.mock import DummyCli + from knack.util import CommandResultItem + + output_producer = AzOutputProducer(DummyCli()) + out_file = self._create_translating_stream() + + output_producer.out(CommandResultItem(result=['topic1', 'topic2']), + formatter=output_producer.get_formatter('tsv'), + out_file=out_file) + + self.assertEqual(b'topic1\ntopic2\n', out_file.buffer.getvalue()) + + def test_json_output_preserves_stream_translation(self): + from azure.cli.core._output import AzOutputProducer + from azure.cli.core.mock import DummyCli + from knack.util import CommandResultItem + + output_producer = AzOutputProducer(DummyCli()) + out_file = self._create_translating_stream() + + output_producer.out(CommandResultItem(result=[{'name': 'topic1'}]), + formatter=output_producer.get_formatter('json'), + out_file=out_file) + + out_file.flush() + self.assertIn(b'\r\n', out_file.buffer.getvalue()) + if __name__ == '__main__': unittest.main()