diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 71c4f094886546..72eda5b47e0c6f 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -645,6 +645,27 @@ are set. .. versionadded:: 3.14 +To highlight command examples in your description or epilog text, you can use +``[cmd]...[/cmd]`` markup:: + + >>> parser = argparse.ArgumentParser( + ... formatter_class=argparse.RawDescriptionHelpFormatter, + ... epilog='''Examples: + ... [cmd]python -m myapp --verbose[/cmd] + ... [cmd]python -m myapp --config settings.json[/cmd] + ... ''') + +When colors are enabled, the text inside ``[cmd]...[/cmd]`` tags will be +displayed in a distinct color to help examples stand out. When colors are +disabled, no transformation is applied and the tags remain as-is. + +.. note:: + + The ``[cmd]`` markup only applies to description and epilog text. It does + not apply to individual argument ``help`` strings. + +.. versionadded:: 3.15 + The add_argument() method ------------------------- diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 1bd82545e588fa..65fc11017bc129 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -407,6 +407,10 @@ argparse default to ``True``. This enables suggestions for mistyped arguments by default. (Contributed by Jakob Schluse in :gh:`140450`.) +* Added ``[cmd]...[/cmd]`` markup support in description and epilog text to + highlight command examples when color output is enabled. + (Contributed by Savannah Ostrowski in :gh:`142390`.) + calendar -------- diff --git a/Lib/argparse.py b/Lib/argparse.py index 398825508f5917..8d55ddc639085d 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -517,7 +517,28 @@ def _format_text(self, text): text = text % dict(prog=self._prog) text_width = max(self._width - self._current_indent, 11) indent = ' ' * self._current_indent - return self._fill_text(text, text_width, indent) + '\n\n' + text = self._fill_text(text, text_width, indent) + text = self._apply_text_markup(text) + return text + '\n\n' + + def _apply_text_markup(self, text): + """Apply color markup to text. + + Supported markup: + [cmd]...[/cmd] - command/shell example (single color) + + When colors are disabled, no transformation is applied. + """ + t = self._theme + if not t.reset: + return text + text = _re.sub( + r'\[cmd\](.*?)\[/cmd\]', + rf'{t.prog_extra}\1{t.reset}', + text, + flags=_re.DOTALL + ) + return text def _format_action(self, action): # determine the required width and the entry label diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 7c5eed21219de0..db13c78886d721 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -7558,6 +7558,154 @@ def test_error_and_warning_not_colorized_when_disabled(self): self.assertNotIn('\x1b[', warn) self.assertIn('warning:', warn) + def test_cmd_markup_in_epilog(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Example: [cmd]python -m myapp --verbose[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}', + help_text) + self.assertNotIn('[cmd]', help_text) + self.assertNotIn('[/cmd]', help_text) + + def test_cmd_markup_in_description(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + description='Run [cmd]python -m myapp[/cmd] to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Run {prog_extra}python -m myapp{reset} to start.', + help_text) + + def test_cmd_markup_multiline(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog='Example:\n[cmd]python -m myapp \\\n --verbose[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}python -m myapp \\\n --verbose{reset}', + help_text) + + def test_cmd_markup_multiple_tags(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Try [cmd]app run[/cmd] or [cmd]app test[/cmd].', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}app run{reset}', help_text) + self.assertIn(f'{prog_extra}app test{reset}', help_text) + + def test_cmd_markup_not_applied_when_color_disabled(self): + # When color is disabled, markup is not transformed (tags remain as-is) + parser = argparse.ArgumentParser( + prog='PROG', + color=False, + epilog='Example: [cmd]python -m myapp[/cmd]', + ) + + help_text = parser.format_help() + self.assertIn('[cmd]python -m myapp[/cmd]', help_text) + self.assertNotIn('\x1b[', help_text) + + def test_cmd_markup_unclosed_tag_unchanged(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Example: [cmd]python -m myapp without closing tag', + ) + + help_text = parser.format_help() + self.assertIn('[cmd]', help_text) + + def test_cmd_markup_empty_tag(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='Before [cmd][/cmd] after', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'Before {prog_extra}{reset} after', help_text) + + def test_cmd_markup_with_format_string(self): + parser = argparse.ArgumentParser( + prog='myapp', + color=True, + epilog='Run [cmd]%(prog)s --help[/cmd] for more info.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}myapp --help{reset}', help_text) + + def test_cmd_markup_case_sensitive(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='[CMD]uppercase[/CMD] vs [cmd]lowercase[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn('[CMD]uppercase[/CMD]', help_text) + self.assertIn(f'{prog_extra}lowercase{reset}', help_text) + + def test_cmd_markup_in_subparser(self): + parser = argparse.ArgumentParser(prog='PROG', color=True) + subparsers = parser.add_subparsers() + sub = subparsers.add_parser( + 'sub', + description='Run [cmd]PROG sub --foo[/cmd] to start.', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = sub.format_help() + self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text) + + def test_cmd_markup_special_regex_chars(self): + parser = argparse.ArgumentParser( + prog='PROG', + color=True, + epilog='[cmd]grep "foo.*bar" | sort[/cmd]', + ) + + prog_extra = self.theme.prog_extra + reset = self.theme.reset + + help_text = parser.format_help() + self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text) + class TestModule(unittest.TestCase): def test_deprecated__version__(self): diff --git a/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst b/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst new file mode 100644 index 00000000000000..49ef9f3bbb303c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-07-22-13-28.gh-issue-142389.J9v904.rst @@ -0,0 +1 @@ +Add ``[cmd]...[/cmd]`` markup support in :mod:`argparse` description and epilog text to highlight command examples when color output is enabled.