diff --git a/tools/idf.py b/tools/idf.py index 9003a05de2..7941f3b981 100755 --- a/tools/idf.py +++ b/tools/idf.py @@ -37,8 +37,8 @@ import python_version_checker # noqa: E402 try: from idf_py_actions.errors import FatalError # noqa: E402 - from idf_py_actions.tools import (PropertyDict, executable_exists, idf_version, merge_action_lists, # noqa: E402 - realpath) + from idf_py_actions.tools import (PropertyDict, executable_exists, get_target, idf_version, # noqa: E402 + merge_action_lists, realpath) except ImportError: # For example, importing click could cause this. print('Please use idf.py only in an ESP-IDF shell environment.', file=sys.stderr) @@ -376,6 +376,7 @@ def init_cli(verbose_output: List=None) -> Any: chain=True, invoke_without_command=True, result_callback=self.execute_tasks, + no_args_is_help=True, context_settings={'max_content_width': 140}, help=help, ) @@ -530,10 +531,6 @@ def init_cli(verbose_output: List=None) -> Any: ctx = click.get_current_context() global_args = PropertyDict(kwargs) - def _help_and_exit() -> None: - print(ctx.get_help()) - ctx.exit() - # Show warning if some tasks are present several times in the list dupplicated_tasks = sorted( [item for item, count in Counter(task.name for task in tasks).items() if count > 1]) @@ -546,10 +543,6 @@ def init_cli(verbose_output: List=None) -> Any: 'Only first occurrence will be executed.') for task in tasks: - # Show help and exit if help is in the list of commands - if task.name == 'help': - _help_and_exit() - # Set propagated global options. # These options may be set on one subcommand, but available in the list of global arguments for key in list(task.action_args): @@ -577,10 +570,6 @@ def init_cli(verbose_output: List=None) -> Any: for action_callback in ctx.command.global_action_callbacks: action_callback(ctx, global_args, tasks) - # Always show help when command is not provided - if not tasks: - _help_and_exit() - # Build full list of tasks to and deal with dependencies and order dependencies tasks_to_run: OrderedDict = OrderedDict() while tasks: @@ -634,7 +623,9 @@ def init_cli(verbose_output: List=None) -> Any: if task.aliases: name_with_aliases += ' (aliases: %s)' % ', '.join(task.aliases) - print('Executing action: %s' % name_with_aliases) + # When machine-readable json format for help is printed, don't show info about executing action so the output is deserializable + if name_with_aliases != 'help' or not task.action_args.get('json_option', False): + print('Executing action: %s' % name_with_aliases) task(ctx, global_args, task.action_args) self._print_closing_message(global_args, tasks_to_run.keys()) @@ -715,7 +706,8 @@ def init_cli(verbose_output: List=None) -> Any: cli_help = ( 'ESP-IDF CLI build management tool. ' - 'For commands that are not known to idf.py an attempt to execute it as a build system target will be made.') + 'For commands that are not known to idf.py an attempt to execute it as a build system target will be made. ' + 'Selected target: {}'.format(get_target(project_dir))) return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions) diff --git a/tools/idf_py_actions/core_ext.py b/tools/idf_py_actions/core_ext.py index d31ea02d01..074c41316f 100644 --- a/tools/idf_py_actions/core_ext.py +++ b/tools/idf_py_actions/core_ext.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import fnmatch +import json import locale import os import re @@ -193,7 +194,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any: for target in SUPPORTED_TARGETS: print(target) - if 'preview' in ctx.params: + if ctx.params.get('preview'): for target in PREVIEW_TARGETS: print(target) @@ -238,6 +239,24 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any: language = 'en' return language + def help_and_exit(action: str, ctx: Context, param: List, json_option: bool, add_options: bool) -> None: + if json_option: + output_dict = {} + output_dict['target'] = get_target(param.project_dir) # type: ignore + output_dict['actions'] = [] + actions = ctx.to_info_dict().get('command').get('commands') + for a in actions: + action_info = {} + action_info['name'] = a + action_info['description'] = actions[a].get('help') + if add_options: + action_info['options'] = actions[a].get('params') + output_dict['actions'].append(action_info) + print(json.dumps(output_dict, sort_keys=True, indent=4)) + else: + print(ctx.get_help()) + ctx.exit() + root_options = { 'global_options': [ { @@ -286,6 +305,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any: { 'names': ['--preview'], 'help': 'Enable IDF features that are still in preview.', + 'is_eager': True, 'is_flag': True, 'default': False, }, @@ -530,4 +550,26 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any: } } - return merge_action_lists(root_options, build_actions, clean_actions) + help_action = { + 'actions': { + 'help': { + 'callback': help_and_exit, + 'help': 'Show help message and exit.', + 'hidden': True, + 'options': [ + { + 'names': ['--json', 'json_option'], + 'is_flag': True, + 'help': 'Print out actions in machine-readable format for selected target.' + }, + { + 'names': ['--add-options'], + 'is_flag': True, + 'help': 'Add options about actions to machine-readable format.' + } + ], + } + } + } + + return merge_action_lists(root_options, build_actions, clean_actions, help_action) diff --git a/tools/test_idf_py/idf_py_help_schema.json b/tools/test_idf_py/idf_py_help_schema.json new file mode 100644 index 0000000000..b28ea6fc97 --- /dev/null +++ b/tools/test_idf_py/idf_py_help_schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/espressif/esp-idf/blob/master/tools/test_idf_py/idf_py_help_schema.json", + "type": "object", + "properties": { + "target": { + "type": ["string", "null"], + "description": "Selected target" + }, + "actions": { + "type": "array", + "description": "List of supported actions", + "items": { + "$ref": "#/definitions/actionInfo" + } + } + }, + "required": [ + "target", + "actions" + ], + "definitions": { + "actionInfo": { + "type": "object", + "description": "Information about one action", + "properties": { + "name" : { + "description": "Action name", + "type": "string" + }, + "description" : { + "description": "Description of the action", + "type": "string" + }, + "options": { + "description": "Additional info about action's options", + "type": "array" + } + }, + "required": [ + "name", + "description" + ] + } + } +} diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index 0dfa5e71db..4a14faa687 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -3,11 +3,14 @@ # SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 +import json import os import subprocess import sys from unittest import TestCase, main, mock +import jsonschema + try: from StringIO import StringIO except ImportError: @@ -227,5 +230,21 @@ class TestDeprecations(TestWithoutExtensions): self.assertNotIn('"test_0" is deprecated', output) +class TestHelpOutput(TestWithoutExtensions): + def test_output(self): + def action_test(commands, schema): + output_file = 'idf_py_help_output.json' + with open(output_file, 'w') as outfile: + subprocess.run(commands, env=os.environ, stdout=outfile) + with open(output_file, 'r') as outfile: + help_obj = json.load(outfile) + self.assertIsNone(jsonschema.validate(help_obj, schema)) + + with open(os.path.join(current_dir, 'idf_py_help_schema.json'), 'r') as schema_file: + schema_json = json.load(schema_file) + action_test(['idf.py', 'help', '--json'], schema_json) + action_test(['idf.py', 'help', '--json', '--add-options'], schema_json) + + if __name__ == '__main__': main()