Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(idf.py): Allow adding arguments from file via @filename.txt (#11783) (IDFGH-10584) #11821

Merged
merged 1 commit into from
Aug 1, 2023
Merged
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
52 changes: 50 additions & 2 deletions tools/idf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import locale
import os
import os.path
import shlex
import subprocess
import sys
from collections import Counter, OrderedDict, _OrderedDictKeysView
Expand Down Expand Up @@ -695,7 +696,7 @@ def parse_project_dir(project_dir: str) -> Any:
return CLI(help=cli_help, verbose_output=verbose_output, all_actions=all_actions)


def main() -> None:
def main(argv=None) -> None:
# Check the environment only when idf.py is invoked regularly from command line.
checks_output = None if SHELL_COMPLETE_RUN else check_environment()

Expand All @@ -713,7 +714,54 @@ def main() -> None:
else:
raise
else:
cli(sys.argv[1:], prog_name=PROG, complete_var=SHELL_COMPLETE_VAR)
argv = expand_file_arguments(argv or sys.argv[1:])

cli(argv, prog_name=PROG, complete_var=SHELL_COMPLETE_VAR)


def expand_file_arguments(argv):
"""
Any argument starting with "@" gets replaced with all values read from a text file.
Text file arguments can be split by newline or by space.
Values are added "as-is", as if they were specified in this order
on the command line.
"""
visited = set()
expanded = False
nebkat marked this conversation as resolved.
Show resolved Hide resolved

def expand_args(args, parent_path, file_stack):
expanded_args = []
for arg in args:
if not arg.startswith("@"):
expanded_args.append(arg)
else:
nonlocal expanded, visited
expanded = True

file_name = arg[1:]
rel_path = os.path.normpath(os.path.join(parent_path, file_name))

if rel_path in visited:
file_stack_str = ' -> '.join(['@' + f for f in file_stack + [file_name]])
raise FatalError(f'Circular dependency in file argument expansion: {file_stack_str}')
visited.add(rel_path)

try:
with open(rel_path, "r") as f:
for line in f:
expanded_args.extend(expand_args(shlex.split(line), os.path.dirname(rel_path), file_stack + [file_name]))
except IOError:
file_stack_str = ' -> '.join(['@' + f for f in file_stack + [file_name]])
raise FatalError(f"File '{rel_path}' (expansion of {file_stack_str}) could not be opened. "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the " is okay, since ' are used in the string.

"Please ensure the file exists and you have the necessary permissions to read it.")
return expanded_args

argv = expand_args(argv, os.getcwd(), [])

if expanded:
print(f'Running: idf.py {" ".join(argv)}')

return argv


def _valid_unicode_config() -> Union[codecs.CodecInfo, bool]:
Expand Down
1 change: 1 addition & 0 deletions tools/test_idf_py/args_a
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-DAAA -DBBB
1 change: 1 addition & 0 deletions tools/test_idf_py/args_b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-DCCC -DDDD
1 change: 1 addition & 0 deletions tools/test_idf_py/args_circular_a
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-DAAA @args_circular_b
1 change: 1 addition & 0 deletions tools/test_idf_py/args_circular_b
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-DBBB @args_circular_a
1 change: 1 addition & 0 deletions tools/test_idf_py/args_recursive
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@args_a -DEEE -DFFF
53 changes: 53 additions & 0 deletions tools/test_idf_py/test_idf_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,5 +291,58 @@ def test_roms_validate_build_date(self):
self.assertTrue(build_date_str == k['build_date_str'])


class TestFileArgumentExpansion(TestCase):
def test_file_expansion(self):
"""Test @filename expansion functionality"""
try:
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_a'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version -DAAA -DBBB', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')

def test_multiple_file_arguments(self):
"""Test multiple @filename arguments"""
try:
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_a', '@args_b'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version -DAAA -DBBB -DCCC -DDDD', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')

def test_recursive_expansion(self):
"""Test recursive expansion of @filename arguments"""
try:
output = subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_recursive'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('Running: idf.py --version -DAAA -DBBB -DEEE -DFFF', output)
except subprocess.CalledProcessError as e:
self.fail(f'Process should have exited normally, but it exited with a return code of {e.returncode}')

def test_circular_dependency(self):
"""Test circular dependency detection in file argument expansion"""
with self.assertRaises(subprocess.CalledProcessError) as cm:
subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_circular_a'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('Circular dependency in file argument expansion', cm.exception.output.decode('utf-8', 'ignore'))

def test_missing_file(self):
"""Test missing file detection in file argument expansion"""
with self.assertRaises(subprocess.CalledProcessError) as cm:
subprocess.check_output(
[sys.executable, idf_py_path, '--version', '@args_non_existent'],
env=os.environ,
stderr=subprocess.STDOUT).decode('utf-8', 'ignore')
self.assertIn('(expansion of @args_non_existent) could not be opened', cm.exception.output.decode('utf-8', 'ignore'))


if __name__ == '__main__':
main()