Skip to content

Commit c152dc7

Browse files
yarikopticclaude
andcommitted
Add colored examples in --help with smart color stripping
- Add ANSI color codes to examples in show-paths docstring: * Context/path lines shown in dark/dim (\x1b[2m) * Match/hit lines shown in red (\x1b[31m) - Implement ColoredHelpFormatter that strips ANSI codes when not a TTY - Add test to verify color codes are stripped from --help without TTY - Examples now visually demonstrate how output appears with colors All 23 tests pass. Users with color terminals will see colored examples in --help, while piped/redirected output remains clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent bc55bee commit c152dc7

File tree

2 files changed

+80
-47
lines changed

2 files changed

+80
-47
lines changed

bin/show-paths

Lines changed: 65 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -16,69 +16,69 @@ Examples from my pains of the past two days
1616
defined
1717
1818
❯ ~/bin/show-paths -f full-lines -e 'name="relatedIdentifier"' metadata.xsd
19-
16 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" targetNamespace="http://datacite.org/schema/kernel-4" elementFormDefault="qualified" xml:lang="EN">
20-
28 <xs:element name="resource">
21-
35 <xs:complexType>
22-
36 <xs:all>
23-
236: <xs:element name="relatedIdentifiers" minOccurs="0">
24-
237 <xs:complexType>
25-
238 <xs:sequence>
26-
239: <xs:element name="relatedIdentifier" minOccurs="0" maxOccurs="unbounded">
19+
\x1b[2m16 <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://datacite.org/schema/kernel-4" targetNamespace="http://datacite.org/schema/kernel-4" elementFormDefault="qualified" xml:lang="EN">\x1b[0m
20+
\x1b[2m28 <xs:element name="resource">\x1b[0m
21+
\x1b[2m35 <xs:complexType>\x1b[0m
22+
\x1b[2m36 <xs:all>\x1b[0m
23+
\x1b[31m236: <xs:element name="relatedIdentifiers" minOccurs="0">\x1b[0m
24+
\x1b[2m237 <xs:complexType>\x1b[0m
25+
\x1b[2m238 <xs:sequence>\x1b[0m
26+
\x1b[31m239: <xs:element name="relatedIdentifier" minOccurs="0" maxOccurs="unbounded">\x1b[0m
2727
2828
❯ ~/bin/show-paths -f full-lines -e '\brelatedIdentifier\b' ~/proj/datacite/inveniosoftware-datacite/datacite/schemas/datacite-v4.5.json
29-
304 "properties": {
30-
419 "relatedIdentifiers": {
31-
421 "items": {
32-
425 "properties": {
33-
426: "relatedIdentifier": {"type": "string"},
34-
429: "required": ["relatedIdentifier", "relatedIdentifierType", "relationType"],
29+
\x1b[2m304 "properties": {\x1b[0m
30+
\x1b[2m419 "relatedIdentifiers": {\x1b[0m
31+
\x1b[2m421 "items": {\x1b[0m
32+
\x1b[2m425 "properties": {\x1b[0m
33+
\x1b[31m426: "relatedIdentifier": {"type": "string"},\x1b[0m
34+
\x1b[31m429: "required": ["relatedIdentifier", "relatedIdentifierType", "relationType"],\x1b[0m
3535
3636
- Dig out where enhanced DICOM contains RepetitionTime in contrast to regular
3737
or interoperable one (this was the drop which overfilled the cup and
3838
needed to trigger coding!)
3939
4040
4141
❯ dcmdump dcm_qa_xa30i/In/7001_func-bold_task-fa_run-1/7001001_1.3.12.2.1107.5.2.43.67093.30000023011319484937100000688.dcm | show-paths -e RepetitionTime
42-
77: (0018,0080) DS [1000] # 4, 1 RepetitionTime
42+
\x1b[31m77: (0018,0080) DS [1000] # 4, 1 RepetitionTime\x1b[0m
4343
4444
❯ dcmdump dcm_qa_xa30/In/7_func-bold_task-fa_run-1/0001_1.3.12.2.1107.5.2.43.67093.2022071112090640678703211.dcm | show-paths -e RepetitionTime
45-
467: (5200,9229).(fffe,e000).(0018,9112).(fffe,e000) (0018,0080) DS [1000] # 4, 1 RepetitionTime
45+
\x1b[2m467: (5200,9229).(fffe,e000).(0018,9112).(fffe,e000)\x1b[0m \x1b[31m(0018,0080) DS [1000] # 4, 1 RepetitionTime\x1b[0m
4646
4747
❯ dcmdump dcm_qa_xa30/In/7_func-bold_task-fa_run-1/0001_1.3.12.2.1107.5.2.43.67093.2022071112090640678703211.dcm | show-paths -f full-lines -e RepetitionTime
48-
221 (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence
49-
222 (fffe,e000) na (Item with undefined length #=11) # u/l, 1 Item
50-
465 (0018,9112) SQ (Sequence with undefined length #=1) # u/l, 1 MRTimingAndRelatedParametersSequence
51-
466 (fffe,e000) na (Item with undefined length #=9) # u/l, 1 Item
52-
467: (0018,0080) DS [1000] # 4, 1 RepetitionTime
48+
\x1b[2m221 (5200,9229) SQ (Sequence with undefined length #=1) # u/l, 1 SharedFunctionalGroupsSequence\x1b[0m
49+
\x1b[2m222 (fffe,e000) na (Item with undefined length #=11) # u/l, 1 Item\x1b[0m
50+
\x1b[2m465 (0018,9112) SQ (Sequence with undefined length #=1) # u/l, 1 MRTimingAndRelatedParametersSequence\x1b[0m
51+
\x1b[2m466 (fffe,e000) na (Item with undefined length #=9) # u/l, 1 Item\x1b[0m
52+
\x1b[31m467: (0018,0080) DS [1000] # 4, 1 RepetitionTime\x1b[0m
5353
5454
- In Python code (better visible in terminal with colors), e.g.
5555
5656
❯ show-paths -f full-lines -e '\bcolored\b' $(which show-paths)
57-
57: 29: from termcolor import colored
58-
59: 31: colored = None
59-
63: 63: path_str = colored(path_str, attrs=["dark"])
60-
64: 64: line = colored(line, "red")
61-
68: 74: line_colored = colored(lines[i], attrs=["dark"]) if use_color else lines[i]
62-
69: 78: line_colored = colored(lines[line_num], "red") if use_color else lines[line_num]
63-
71: 111: (args.color == "auto" and sys.stdout.isatty() and colored)
64-
72: 113: if use_color and not colored:
65-
81 try:
66-
82: from termcolor import colored
67-
83 except ImportError as exc:
68-
84: colored = None
69-
110 def print_inline(paths, lines, use_color):
70-
112 for line_num, path in paths:
71-
115 if use_color:
72-
116: path_str = colored(path_str, attrs=["dark"])
73-
117: line = colored(line, "red")
74-
120 def print_full_lines(paths, lines, use_color):
75-
124 for line_num, path in paths:
76-
126 for key, indent, i in path[:-1]:
77-
127: line_colored = colored(lines[i], attrs=["dark"]) if use_color else lines[i]
78-
131: line_colored = colored(lines[line_num], "red") if use_color else lines[line_num]
79-
162 use_color = (
80-
164: (args.color == "auto" and sys.stdout.isatty() and colored)
81-
166: if use_color and not colored:
57+
\x1b[31m57: 29: from termcolor import colored\x1b[0m
58+
\x1b[31m59: 31: colored = None\x1b[0m
59+
\x1b[31m63: 63: path_str = colored(path_str, attrs=["dark"])\x1b[0m
60+
\x1b[31m64: 64: line = colored(line, "red")\x1b[0m
61+
\x1b[31m68: 74: line_colored = colored(lines[i], attrs=["dark"]) if use_color else lines[i]\x1b[0m
62+
\x1b[31m69: 78: line_colored = colored(lines[line_num], "red") if use_color else lines[line_num]\x1b[0m
63+
\x1b[31m71: 111: (args.color == "auto" and sys.stdout.isatty() and colored)\x1b[0m
64+
\x1b[31m72: 113: if use_color and not colored:\x1b[0m
65+
\x1b[2m81 try:\x1b[0m
66+
\x1b[31m82: from termcolor import colored\x1b[0m
67+
\x1b[2m83 except ImportError as exc:\x1b[0m
68+
\x1b[31m84: colored = None\x1b[0m
69+
\x1b[2m110 def print_inline(paths, lines, use_color):\x1b[0m
70+
\x1b[2m112 for line_num, path in paths:\x1b[0m
71+
\x1b[2m115 if use_color:\x1b[0m
72+
\x1b[31m116: path_str = colored(path_str, attrs=["dark"])\x1b[0m
73+
\x1b[31m117: line = colored(line, "red")\x1b[0m
74+
\x1b[2m120 def print_full_lines(paths, lines, use_color):\x1b[0m
75+
\x1b[2m124 for line_num, path in paths:\x1b[0m
76+
\x1b[2m126 for key, indent, i in path[:-1]:\x1b[0m
77+
\x1b[31m127: line_colored = colored(lines[i], attrs=["dark"]) if use_color else lines[i]\x1b[0m
78+
\x1b[31m131: line_colored = colored(lines[line_num], "red") if use_color else lines[line_num]\x1b[0m
79+
\x1b[2m162 use_color = (\x1b[0m
80+
\x1b[31m164: (args.color == "auto" and sys.stdout.isatty() and colored)\x1b[0m
81+
\x1b[31m166: if use_color and not colored:\x1b[0m
8282
8383
8484
""" # noqa: E501
@@ -93,6 +93,24 @@ except ImportError:
9393
colored = None
9494

9595

96+
class ColoredHelpFormatter(argparse.RawDescriptionHelpFormatter):
97+
"""Formatter that strips ANSI color codes from help text if terminal doesn't support colors."""
98+
99+
def _strip_ansi(self, text):
100+
"""Strip ANSI escape codes from text."""
101+
# Match ANSI escape sequences
102+
ansi_escape = re.compile(r"\x1b\[[0-9;]*m")
103+
return ansi_escape.sub("", text)
104+
105+
def format_help(self):
106+
"""Format help, stripping colors if stdout is not a TTY."""
107+
help_text = super().format_help()
108+
# Strip colors if not outputting to a TTY
109+
if not sys.stdout.isatty():
110+
help_text = self._strip_ansi(help_text)
111+
return help_text
112+
113+
96114
def get_paths(lines):
97115
"""Generate indentation-based paths for given lines."""
98116
paths = []
@@ -146,7 +164,7 @@ def print_full_lines(paths, lines, use_color):
146164

147165
def main():
148166
parser = argparse.ArgumentParser(
149-
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
167+
description=__doc__, formatter_class=ColoredHelpFormatter
150168
)
151169
parser.add_argument(
152170
"file",

tests/test_show_paths.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,21 @@ def test_show_paths_help_output():
215215
assert len(example_lines) > 1, "Examples should span multiple lines"
216216

217217

218+
@pytest.mark.ai_generated
219+
def test_show_paths_help_colors_stripped_without_tty():
220+
"""Test that ANSI color codes are stripped from --help when not a TTY."""
221+
result = run_show_paths("--help")
222+
assert result.returncode == 0
223+
help_output = result.stdout
224+
225+
# Since we're running in subprocess (no TTY), ANSI codes should be stripped
226+
assert "\x1b[" not in help_output, "ANSI color codes should be stripped from help without TTY"
227+
228+
# But the content should still be there
229+
assert "RepetitionTime" in help_output or "relatedIdentifier" in help_output
230+
assert "Examples" in help_output
231+
232+
218233
@pytest.mark.ai_generated
219234
def test_show_paths_no_matches():
220235
"""Test show-paths when regex doesn't match anything."""

0 commit comments

Comments
 (0)