From 17be25a1f2a606a78c3fe0ff095467f6e847617f Mon Sep 17 00:00:00 2001 From: Alan Date: Thu, 1 Oct 2026 10:55:24 +0800 Subject: [PATCH] Fix command-line macro option ordering --- pcpp/pcmd.py | 22 +++++++++++---- tests/issue0105.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/issue0105.py diff --git a/pcpp/pcmd.py b/pcpp/pcmd.py index 55da3d2..dc914b9 100644 --- a/pcpp/pcmd.py +++ b/pcpp/pcmd.py @@ -12,6 +12,15 @@ __all__ = [] +class MacroAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + items = copy.copy(getattr(namespace, self.dest) or []) + items.append(values) + setattr(namespace, self.dest, items) + actions = copy.copy(getattr(namespace, 'macro_actions', [])) + actions.append((option_string, values[0])) + namespace.macro_actions = actions + class FileAction(argparse.Action): def __init__(self, option_strings, dest, **kwargs): super(FileAction, self).__init__(option_strings, dest, **kwargs) @@ -38,8 +47,8 @@ def __init__(self, argv): ignores any arguments it does not understand.''') argp.add_argument('inputs', metavar = 'input', default = [sys.stdin], nargs = '*', action = FileAction, help = 'Files to preprocess (use \'-\' for stdin)') argp.add_argument('-o', dest = 'output', metavar = 'path', type = argparse.FileType('wt'), default=sys.stdout, nargs = '?', help = 'Output to a file instead of stdout') - argp.add_argument('-D', dest = 'defines', metavar = 'macro[=val]', nargs = 1, action = 'append', help = 'Predefine name as a macro [with value]') - argp.add_argument('-U', dest = 'undefines', metavar = 'macro', nargs = 1, action = 'append', help = 'Pre-undefine name as a macro') + argp.add_argument('-D', dest = 'defines', metavar = 'macro[=val]', nargs = 1, action = MacroAction, help = 'Predefine name as a macro [with value]') + argp.add_argument('-U', dest = 'undefines', metavar = 'macro', nargs = 1, action = MacroAction, help = 'Pre-undefine name as a macro') argp.add_argument('-N', dest = 'nevers', metavar = 'macro', nargs = 1, action = 'append', help = 'Never define name as a macro, even if defined during the preprocessing.') argp.add_argument('-I', dest = 'includes', metavar = 'path', nargs = 1, action = 'append', help = "Path to search for unfound #include's") #argp.add_argument('--passthru', dest = 'passthru', action = 'store_true', help = 'Pass through everything unexecuted except for #include and include guards (which need to be the first thing in an include file') @@ -117,14 +126,15 @@ def __init__(self, argv): if self.args.defines: self.args.defines = [x[0] for x in self.args.defines] - for d in self.args.defines: + if self.args.undefines: + self.args.undefines = [x[0] for x in self.args.undefines] + for option, d in getattr(self.args, 'macro_actions', []): + if option == '-D': if '=' not in d: d += '=1' d = d.replace('=', ' ', 1) self.define(d) - if self.args.undefines: - self.args.undefines = [x[0] for x in self.args.undefines] - for d in self.args.undefines: + else: self.undef(d) if self.args.nevers: self.args.nevers = [x[0] for x in self.args.nevers] diff --git a/tests/issue0105.py b/tests/issue0105.py new file mode 100644 index 0000000..3f4d1fc --- /dev/null +++ b/tests/issue0105.py @@ -0,0 +1,68 @@ +import unittest +from io import StringIO +from unittest.mock import patch + +from pcpp import CmdPreprocessor + + +class macro_option_order(unittest.TestCase): + def preprocess(self, options, source='FOO\n'): + output = StringIO() + source_file = StringIO(source) + source_file.name = '' + with patch('sys.stdin', source_file), patch('sys.stdout', output): + preprocessor = CmdPreprocessor( + ['pcpp', '--line-directive', 'none'] + options + ['-']) + self.assertEqual(preprocessor.return_code, 0) + return output.getvalue(), preprocessor.args + + def test_undefine_then_define(self): + output, _ = self.preprocess(['-U', 'FOO', '-D', 'FOO=2']) + self.assertEqual(output, '2\n') + + def test_redefine_after_undefine(self): + output, args = self.preprocess( + ['-D', 'FOO=1', '-U', 'FOO', '-D', 'FOO=2']) + self.assertEqual(output, '2\n') + self.assertEqual(args.defines, ['FOO=1', 'FOO=2']) + self.assertEqual(args.undefines, ['FOO']) + + def test_attached_arguments(self): + output, _ = self.preprocess(['-DFOO=1', '-UFOO', '-DFOO=2']) + self.assertEqual(output, '2\n') + + def test_default_definition_after_undefine(self): + output, _ = self.preprocess(['-UFOO', '-DFOO']) + self.assertEqual(output, '1\n') + + def test_final_undefine(self): + output, _ = self.preprocess( + ['-DFOO=1', '-UFOO', '-DFOO=2', '-UFOO']) + self.assertEqual(output, 'FOO\n') + + def test_multiple_macros(self): + output, _ = self.preprocess( + ['-DFOO=1', '-UBAR', '-DBAR=3', '-UFOO'], 'FOO BAR\n') + self.assertEqual(output, 'FOO 3\n') + + def test_only_definitions(self): + output, args = self.preprocess(['-DFOO=1', '-DFOO=2']) + self.assertEqual(output, '2\n') + self.assertIsNone(args.undefines) + + def test_only_undefinitions(self): + output, args = self.preprocess(['-UFOO']) + self.assertEqual(output, 'FOO\n') + self.assertIsNone(args.defines) + + def test_no_macro_options(self): + output, _ = self.preprocess(['--compress']) + self.assertEqual(output, 'FOO\n') + + def test_equals_in_value(self): + output, _ = self.preprocess(['-DFOO=1==1']) + self.assertEqual(output, '1==1\n') + + +if __name__ == '__main__': + unittest.main()