Skip to content

Commit a38087e

Browse files
Add support for [cmd] colorization in argparse help text
1 parent ff2577f commit a38087e

File tree

4 files changed

+193
-1
lines changed

4 files changed

+193
-1
lines changed

Doc/library/argparse.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,28 @@ are set.
645645

646646
.. versionadded:: 3.14
647647

648+
To highlight command examples in your description or epilog text, you can use
649+
``[cmd]...[/cmd]`` markup::
650+
651+
>>> parser = argparse.ArgumentParser(
652+
... formatter_class=argparse.RawDescriptionHelpFormatter,
653+
... epilog='''Examples:
654+
... [cmd]python -m myapp --verbose[/cmd]
655+
... [cmd]python -m myapp --config settings.json[/cmd]
656+
... ''')
657+
658+
When colors are enabled, the text inside ``[cmd]...[/cmd]`` tags will be
659+
displayed in a distinct color to help examples stand out. When colors are
660+
disabled, the tags are stripped and the content is displayed as plain text.
661+
662+
.. note::
663+
664+
The ``[cmd]`` markup only applies to description and epilog text processed
665+
by :meth:`HelpFormatter._format_text`. It does not apply to individual
666+
argument ``help`` strings.
667+
668+
.. versionadded:: 3.15
669+
648670

649671
The add_argument() method
650672
-------------------------

Doc/whatsnew/3.15.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ argparse
407407
default to ``True``. This enables suggestions for mistyped arguments by default.
408408
(Contributed by Jakob Schluse in :gh:`140450`.)
409409

410+
* Added ``[cmd]...[/cmd]`` markup support in description and epilog text to
411+
highlight command examples when color output is enabled.
412+
(Contributed by Savannah Ostrowski in :gh:`XXXXX`.)
413+
410414
calendar
411415
--------
412416

Lib/argparse.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,24 @@ def _format_text(self, text):
517517
text = text % dict(prog=self._prog)
518518
text_width = max(self._width - self._current_indent, 11)
519519
indent = ' ' * self._current_indent
520-
return self._fill_text(text, text_width, indent) + '\n\n'
520+
text = self._fill_text(text, text_width, indent)
521+
text = self._apply_text_markup(text)
522+
return text + '\n\n'
523+
524+
def _apply_text_markup(self, text):
525+
"""Apply color markup to text.
526+
527+
Supported markup:
528+
[cmd]...[/cmd] - command/shell example (single color)
529+
"""
530+
t = self._theme
531+
text = _re.sub(
532+
r'\[cmd\](.*?)\[/cmd\]',
533+
rf'{t.prog_extra}\1{t.reset}',
534+
text,
535+
flags=_re.DOTALL
536+
)
537+
return text
521538

522539
def _format_action(self, action):
523540
# determine the required width and the entry label

Lib/test/test_argparse.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7558,6 +7558,155 @@ def test_error_and_warning_not_colorized_when_disabled(self):
75587558
self.assertNotIn('\x1b[', warn)
75597559
self.assertIn('warning:', warn)
75607560

7561+
def test_cmd_markup_in_epilog(self):
7562+
parser = argparse.ArgumentParser(
7563+
prog='PROG',
7564+
color=True,
7565+
epilog='Example: [cmd]python -m myapp --verbose[/cmd]',
7566+
)
7567+
7568+
prog_extra = self.theme.prog_extra
7569+
reset = self.theme.reset
7570+
7571+
help_text = parser.format_help()
7572+
self.assertIn(f'Example: {prog_extra}python -m myapp --verbose{reset}',
7573+
help_text)
7574+
self.assertNotIn('[cmd]', help_text)
7575+
self.assertNotIn('[/cmd]', help_text)
7576+
7577+
def test_cmd_markup_in_description(self):
7578+
parser = argparse.ArgumentParser(
7579+
prog='PROG',
7580+
color=True,
7581+
description='Run [cmd]python -m myapp[/cmd] to start.',
7582+
)
7583+
7584+
prog_extra = self.theme.prog_extra
7585+
reset = self.theme.reset
7586+
7587+
help_text = parser.format_help()
7588+
self.assertIn(f'Run {prog_extra}python -m myapp{reset} to start.',
7589+
help_text)
7590+
7591+
def test_cmd_markup_multiline(self):
7592+
parser = argparse.ArgumentParser(
7593+
prog='PROG',
7594+
color=True,
7595+
formatter_class=argparse.RawDescriptionHelpFormatter,
7596+
epilog='Example:\n[cmd]python -m myapp \\\n --verbose[/cmd]',
7597+
)
7598+
7599+
prog_extra = self.theme.prog_extra
7600+
reset = self.theme.reset
7601+
7602+
help_text = parser.format_help()
7603+
self.assertIn(f'{prog_extra}python -m myapp \\\n --verbose{reset}',
7604+
help_text)
7605+
7606+
def test_cmd_markup_multiple_tags(self):
7607+
parser = argparse.ArgumentParser(
7608+
prog='PROG',
7609+
color=True,
7610+
epilog='Try [cmd]app run[/cmd] or [cmd]app test[/cmd].',
7611+
)
7612+
7613+
prog_extra = self.theme.prog_extra
7614+
reset = self.theme.reset
7615+
7616+
help_text = parser.format_help()
7617+
self.assertIn(f'{prog_extra}app run{reset}', help_text)
7618+
self.assertIn(f'{prog_extra}app test{reset}', help_text)
7619+
7620+
def test_cmd_markup_not_applied_when_color_disabled(self):
7621+
parser = argparse.ArgumentParser(
7622+
prog='PROG',
7623+
color=False,
7624+
epilog='Example: [cmd]python -m myapp[/cmd]',
7625+
)
7626+
7627+
help_text = parser.format_help()
7628+
self.assertNotIn('[cmd]', help_text)
7629+
self.assertNotIn('[/cmd]', help_text)
7630+
self.assertIn('python -m myapp', help_text)
7631+
self.assertNotIn('\x1b[', help_text)
7632+
7633+
def test_cmd_markup_unclosed_tag_unchanged(self):
7634+
parser = argparse.ArgumentParser(
7635+
prog='PROG',
7636+
color=True,
7637+
epilog='Example: [cmd]python -m myapp without closing tag',
7638+
)
7639+
7640+
help_text = parser.format_help()
7641+
self.assertIn('[cmd]', help_text)
7642+
7643+
def test_cmd_markup_empty_tag(self):
7644+
parser = argparse.ArgumentParser(
7645+
prog='PROG',
7646+
color=True,
7647+
epilog='Before [cmd][/cmd] after',
7648+
)
7649+
7650+
prog_extra = self.theme.prog_extra
7651+
reset = self.theme.reset
7652+
7653+
help_text = parser.format_help()
7654+
self.assertIn(f'Before {prog_extra}{reset} after', help_text)
7655+
7656+
def test_cmd_markup_with_format_string(self):
7657+
parser = argparse.ArgumentParser(
7658+
prog='myapp',
7659+
color=True,
7660+
epilog='Run [cmd]%(prog)s --help[/cmd] for more info.',
7661+
)
7662+
7663+
prog_extra = self.theme.prog_extra
7664+
reset = self.theme.reset
7665+
7666+
help_text = parser.format_help()
7667+
self.assertIn(f'{prog_extra}myapp --help{reset}', help_text)
7668+
7669+
def test_cmd_markup_case_sensitive(self):
7670+
parser = argparse.ArgumentParser(
7671+
prog='PROG',
7672+
color=True,
7673+
epilog='[CMD]uppercase[/CMD] vs [cmd]lowercase[/cmd]',
7674+
)
7675+
7676+
prog_extra = self.theme.prog_extra
7677+
reset = self.theme.reset
7678+
7679+
help_text = parser.format_help()
7680+
self.assertIn('[CMD]uppercase[/CMD]', help_text)
7681+
self.assertIn(f'{prog_extra}lowercase{reset}', help_text)
7682+
7683+
def test_cmd_markup_in_subparser(self):
7684+
parser = argparse.ArgumentParser(prog='PROG', color=True)
7685+
subparsers = parser.add_subparsers()
7686+
sub = subparsers.add_parser(
7687+
'sub',
7688+
description='Run [cmd]PROG sub --foo[/cmd] to start.',
7689+
)
7690+
7691+
prog_extra = self.theme.prog_extra
7692+
reset = self.theme.reset
7693+
7694+
help_text = sub.format_help()
7695+
self.assertIn(f'{prog_extra}PROG sub --foo{reset}', help_text)
7696+
7697+
def test_cmd_markup_special_regex_chars(self):
7698+
parser = argparse.ArgumentParser(
7699+
prog='PROG',
7700+
color=True,
7701+
epilog='[cmd]grep "foo.*bar" | sort[/cmd]',
7702+
)
7703+
7704+
prog_extra = self.theme.prog_extra
7705+
reset = self.theme.reset
7706+
7707+
help_text = parser.format_help()
7708+
self.assertIn(f'{prog_extra}grep "foo.*bar" | sort{reset}', help_text)
7709+
75617710

75627711
class TestModule(unittest.TestCase):
75637712
def test_deprecated__version__(self):

0 commit comments

Comments
 (0)