qq_lib.core.click_format

GNU-style help formatting for Click commands.

This module defines GNUHelpColorsCommand, a Click command class that prints help text using GNU-style formatting with customizable colors, headings, and option layouts.

  1# Released under MIT License.
  2# Copyright (c) 2025-2026 Ladislav Bartos and Robert Vacha Lab
  3
  4"""
  5GNU-style help formatting for Click commands.
  6
  7This module defines `GNUHelpColorsCommand`, a Click command class that prints
  8help text using GNU-style formatting with customizable colors, headings, and
  9option layouts.
 10"""
 11
 12from collections.abc import Sequence
 13from pathlib import Path
 14
 15import click
 16from click import Context, HelpFormatter
 17from click_help_colors import HelpColorsCommand
 18
 19
 20class GNUHelpColorsCommand(HelpColorsCommand):
 21    """Custom formatter that prints options in GNU-style."""
 22
 23    def get_help(self, ctx: Context) -> str:
 24        class GNUHelpFormatter(HelpFormatter):
 25            def __init__(self, width=None, headers_color=None, options_color=None):
 26                super().__init__(width=width)
 27                self.headers_color = headers_color or "white"
 28                self.options_color = options_color or "white"
 29
 30            def write_heading(self, heading: str) -> None:
 31                styled_heading = click.style(heading, fg=self.headers_color, bold=True)
 32                self.write(f"{styled_heading}\n")
 33
 34            def write_usage(
 35                self, prog_name: str, args: str | None, prefix: str | None = None
 36            ) -> None:  # ty: ignore[invalid-method-override]
 37                """Override to make Usage: header bold"""
 38                if prefix is None:
 39                    prefix = "Usage:"
 40
 41                styled_prefix = click.style(prefix, fg=self.headers_color, bold=True)
 42                usage_line = f"{styled_prefix} {prog_name}"
 43
 44                if args:
 45                    usage_line += f" {args}"
 46
 47                self.write(f"{usage_line}\n")
 48
 49            def write_dl(
 50                self,
 51                rows: Sequence[tuple[str, str | None]],
 52                _col_max: int = 30,
 53                _col_spacing: int = 2,
 54            ) -> None:  # ty: ignore[invalid-method-override]
 55                for term, definition in rows:
 56                    colored_term = click.style(term, fg=self.options_color, bold=True)
 57                    self.write(f"  {colored_term}\n")
 58
 59                    if definition:
 60                        for line in definition.splitlines():
 61                            if line.strip():
 62                                self.write(f"      {line}\n")
 63                    self.write("\n")
 64
 65        formatter = GNUHelpFormatter(
 66            width=ctx.terminal_width,
 67            headers_color=getattr(self, "help_headers_color", "white"),
 68            options_color=getattr(self, "help_options_color", "white"),
 69        )
 70
 71        self.format_help(ctx, formatter)
 72        return formatter.getvalue()
 73
 74    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
 75        """
 76        Reassemble bash-split option tokens before delegating to Click's parser.
 77
 78        Bash splits tokens on `=` (which appears in `COMP_WORDBREAKS` by
 79        default) before invoking the completion handler. This means that an
 80        option like ``--option=foo=bar`` arrives as the fragments
 81        `["--option", "=", "foo", "=", "bar"]` rather than as a single token.
 82        Click's parser cannot handle this fragmented form, so autocompletion
 83        breaks whenever an option value contains `=`.
 84
 85        This override reassembles such fragments back into `["--option",
 86        "foo=bar"]` during resilient (completion) parsing, leaving normal
 87        invocations entirely unaffected.
 88
 89        Args:
 90            ctx: The current Click context.
 91            args: The raw argument list as received from the shell, potentially
 92                containing `=`-fragmented option tokens produced by bash's
 93                completion machinery.
 94
 95        Returns:
 96            The remaining unparsed arguments, after reassembly and delegation
 97            to the parent parser.
 98        """
 99
100        if ctx.resilient_parsing:
101            processed = []
102            i = 0
103            while i < len(args):
104                arg = args[i]
105                if arg.startswith("-") and "=" in arg:
106                    # already combined: --option=foo=bar
107                    option, value = arg.split("=", 1)
108                    processed.append(option)
109                    if value:
110                        processed.append(value)
111                    i += 1
112                elif arg.startswith("-") and i + 1 < len(args) and args[i + 1] == "=":
113                    # bash pre-split starting with "=": ["--opt", "=", "foo", "=", "bar"]
114                    processed.append(arg)
115                    i += 2  # skip option and bare "="
116                    value_parts = []
117                    while i < len(args) and not args[i].startswith("-"):
118                        if args[i] == "=":
119                            value_parts.append("=")
120                        else:
121                            if value_parts and value_parts[-1] != "=":
122                                break  # previous wasn't "=", this is a new arg
123                            value_parts.append(args[i])
124                        i += 1
125                    if value_parts:
126                        processed.append("".join(value_parts))
127                elif (
128                    arg.startswith("-")
129                    and i + 1 < len(args)
130                    and not args[i + 1].startswith("-")
131                    and i + 2 < len(args)
132                    and args[i + 2] == "="
133                ):
134                    # bash pre-split starting with fragment: ["--opt", "foo", "=", "bar"]
135                    processed.append(arg)
136                    i += 1
137                    value_parts = [args[i]]
138                    i += 1
139                    while i < len(args) and not args[i].startswith("-"):
140                        if args[i] == "=":
141                            value_parts.append("=")
142                            i += 1
143                            if i < len(args) and not args[i].startswith("-"):
144                                value_parts.append(args[i])
145                                i += 1
146                        else:
147                            break
148                    processed.append("".join(value_parts))
149                else:
150                    processed.append(arg)
151                    i += 1
152            args = processed
153        return super().parse_args(ctx, args)
154
155
156class GlobDirectoryMixin:
157    """
158    Mixin that adds glob-expanding multi-value support for -d/--directory.
159
160    Click does not support nargs=-1 on options. This mixin pre-processes the
161    argument list before Click's parser sees it, greedily consuming all
162    non-flag tokens following -d/--directory, glob-expanding each one, and
163    rewriting them as repeated -d <path> pairs that Click's multiple=True
164    parser handles correctly.
165
166    Must appear before the Click command class in the MRO so that its
167    parse_args runs first.
168    """
169
170    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
171        """
172        Rewrite -d/--dir tokens to support multiple values and globs.
173
174        Greedily consumes all non-flag tokens following a -d/--dir flag,
175        glob-expands each one relative to its own parent directory, and emits
176        one -d <path> pair per expanded result.
177
178        Args:
179            ctx (click.Context): The current Click context.
180            args (list[str]): The raw argument list.
181
182        Returns:
183            list[str]: The remaining unparsed arguments after delegation to the
184                parent parser.
185        """
186        rewritten: list[str] = []
187        i = 0
188        while i < len(args):
189            if args[i] in ("-d", "--dir"):
190                i += 1
191                while i < len(args) and not args[i].startswith("-"):
192                    pattern = args[i]
193                    p = Path(pattern)
194                    if not p.name:
195                        # pattern is a bare directory (e.g. "." or "/some/dir/") - no glob needed
196                        rewritten.extend(["-d", pattern])
197                    else:
198                        expanded = sorted(p.parent.glob(p.name))
199                        if expanded:
200                            for path in expanded:
201                                rewritten.extend(["-d", str(path)])
202                        else:
203                            rewritten.extend(["-d", pattern])
204                    i += 1
205            else:
206                rewritten.append(args[i])
207                i += 1
208        return super().parse_args(ctx, rewritten)  # ty:ignore[unresolved-attribute]
209
210
211class QQOperatorCommand(GlobDirectoryMixin, GNUHelpColorsCommand):
212    """GNUHelpColorsCommand with glob-expanding -d/--dir support."""
213
214    pass
class GNUHelpColorsCommand(click_help_colors.core.HelpColorsCommand):
 21class GNUHelpColorsCommand(HelpColorsCommand):
 22    """Custom formatter that prints options in GNU-style."""
 23
 24    def get_help(self, ctx: Context) -> str:
 25        class GNUHelpFormatter(HelpFormatter):
 26            def __init__(self, width=None, headers_color=None, options_color=None):
 27                super().__init__(width=width)
 28                self.headers_color = headers_color or "white"
 29                self.options_color = options_color or "white"
 30
 31            def write_heading(self, heading: str) -> None:
 32                styled_heading = click.style(heading, fg=self.headers_color, bold=True)
 33                self.write(f"{styled_heading}\n")
 34
 35            def write_usage(
 36                self, prog_name: str, args: str | None, prefix: str | None = None
 37            ) -> None:  # ty: ignore[invalid-method-override]
 38                """Override to make Usage: header bold"""
 39                if prefix is None:
 40                    prefix = "Usage:"
 41
 42                styled_prefix = click.style(prefix, fg=self.headers_color, bold=True)
 43                usage_line = f"{styled_prefix} {prog_name}"
 44
 45                if args:
 46                    usage_line += f" {args}"
 47
 48                self.write(f"{usage_line}\n")
 49
 50            def write_dl(
 51                self,
 52                rows: Sequence[tuple[str, str | None]],
 53                _col_max: int = 30,
 54                _col_spacing: int = 2,
 55            ) -> None:  # ty: ignore[invalid-method-override]
 56                for term, definition in rows:
 57                    colored_term = click.style(term, fg=self.options_color, bold=True)
 58                    self.write(f"  {colored_term}\n")
 59
 60                    if definition:
 61                        for line in definition.splitlines():
 62                            if line.strip():
 63                                self.write(f"      {line}\n")
 64                    self.write("\n")
 65
 66        formatter = GNUHelpFormatter(
 67            width=ctx.terminal_width,
 68            headers_color=getattr(self, "help_headers_color", "white"),
 69            options_color=getattr(self, "help_options_color", "white"),
 70        )
 71
 72        self.format_help(ctx, formatter)
 73        return formatter.getvalue()
 74
 75    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
 76        """
 77        Reassemble bash-split option tokens before delegating to Click's parser.
 78
 79        Bash splits tokens on `=` (which appears in `COMP_WORDBREAKS` by
 80        default) before invoking the completion handler. This means that an
 81        option like ``--option=foo=bar`` arrives as the fragments
 82        `["--option", "=", "foo", "=", "bar"]` rather than as a single token.
 83        Click's parser cannot handle this fragmented form, so autocompletion
 84        breaks whenever an option value contains `=`.
 85
 86        This override reassembles such fragments back into `["--option",
 87        "foo=bar"]` during resilient (completion) parsing, leaving normal
 88        invocations entirely unaffected.
 89
 90        Args:
 91            ctx: The current Click context.
 92            args: The raw argument list as received from the shell, potentially
 93                containing `=`-fragmented option tokens produced by bash's
 94                completion machinery.
 95
 96        Returns:
 97            The remaining unparsed arguments, after reassembly and delegation
 98            to the parent parser.
 99        """
100
101        if ctx.resilient_parsing:
102            processed = []
103            i = 0
104            while i < len(args):
105                arg = args[i]
106                if arg.startswith("-") and "=" in arg:
107                    # already combined: --option=foo=bar
108                    option, value = arg.split("=", 1)
109                    processed.append(option)
110                    if value:
111                        processed.append(value)
112                    i += 1
113                elif arg.startswith("-") and i + 1 < len(args) and args[i + 1] == "=":
114                    # bash pre-split starting with "=": ["--opt", "=", "foo", "=", "bar"]
115                    processed.append(arg)
116                    i += 2  # skip option and bare "="
117                    value_parts = []
118                    while i < len(args) and not args[i].startswith("-"):
119                        if args[i] == "=":
120                            value_parts.append("=")
121                        else:
122                            if value_parts and value_parts[-1] != "=":
123                                break  # previous wasn't "=", this is a new arg
124                            value_parts.append(args[i])
125                        i += 1
126                    if value_parts:
127                        processed.append("".join(value_parts))
128                elif (
129                    arg.startswith("-")
130                    and i + 1 < len(args)
131                    and not args[i + 1].startswith("-")
132                    and i + 2 < len(args)
133                    and args[i + 2] == "="
134                ):
135                    # bash pre-split starting with fragment: ["--opt", "foo", "=", "bar"]
136                    processed.append(arg)
137                    i += 1
138                    value_parts = [args[i]]
139                    i += 1
140                    while i < len(args) and not args[i].startswith("-"):
141                        if args[i] == "=":
142                            value_parts.append("=")
143                            i += 1
144                            if i < len(args) and not args[i].startswith("-"):
145                                value_parts.append(args[i])
146                                i += 1
147                        else:
148                            break
149                    processed.append("".join(value_parts))
150                else:
151                    processed.append(arg)
152                    i += 1
153            args = processed
154        return super().parse_args(ctx, args)

Custom formatter that prints options in GNU-style.

def get_help(self, ctx: click.core.Context) -> str:
24    def get_help(self, ctx: Context) -> str:
25        class GNUHelpFormatter(HelpFormatter):
26            def __init__(self, width=None, headers_color=None, options_color=None):
27                super().__init__(width=width)
28                self.headers_color = headers_color or "white"
29                self.options_color = options_color or "white"
30
31            def write_heading(self, heading: str) -> None:
32                styled_heading = click.style(heading, fg=self.headers_color, bold=True)
33                self.write(f"{styled_heading}\n")
34
35            def write_usage(
36                self, prog_name: str, args: str | None, prefix: str | None = None
37            ) -> None:  # ty: ignore[invalid-method-override]
38                """Override to make Usage: header bold"""
39                if prefix is None:
40                    prefix = "Usage:"
41
42                styled_prefix = click.style(prefix, fg=self.headers_color, bold=True)
43                usage_line = f"{styled_prefix} {prog_name}"
44
45                if args:
46                    usage_line += f" {args}"
47
48                self.write(f"{usage_line}\n")
49
50            def write_dl(
51                self,
52                rows: Sequence[tuple[str, str | None]],
53                _col_max: int = 30,
54                _col_spacing: int = 2,
55            ) -> None:  # ty: ignore[invalid-method-override]
56                for term, definition in rows:
57                    colored_term = click.style(term, fg=self.options_color, bold=True)
58                    self.write(f"  {colored_term}\n")
59
60                    if definition:
61                        for line in definition.splitlines():
62                            if line.strip():
63                                self.write(f"      {line}\n")
64                    self.write("\n")
65
66        formatter = GNUHelpFormatter(
67            width=ctx.terminal_width,
68            headers_color=getattr(self, "help_headers_color", "white"),
69            options_color=getattr(self, "help_options_color", "white"),
70        )
71
72        self.format_help(ctx, formatter)
73        return formatter.getvalue()

Formats the help into a string and returns it.

Calls format_help() internally.

def parse_args(self, ctx: click.core.Context, args: list[str]) -> list[str]:
 75    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
 76        """
 77        Reassemble bash-split option tokens before delegating to Click's parser.
 78
 79        Bash splits tokens on `=` (which appears in `COMP_WORDBREAKS` by
 80        default) before invoking the completion handler. This means that an
 81        option like ``--option=foo=bar`` arrives as the fragments
 82        `["--option", "=", "foo", "=", "bar"]` rather than as a single token.
 83        Click's parser cannot handle this fragmented form, so autocompletion
 84        breaks whenever an option value contains `=`.
 85
 86        This override reassembles such fragments back into `["--option",
 87        "foo=bar"]` during resilient (completion) parsing, leaving normal
 88        invocations entirely unaffected.
 89
 90        Args:
 91            ctx: The current Click context.
 92            args: The raw argument list as received from the shell, potentially
 93                containing `=`-fragmented option tokens produced by bash's
 94                completion machinery.
 95
 96        Returns:
 97            The remaining unparsed arguments, after reassembly and delegation
 98            to the parent parser.
 99        """
100
101        if ctx.resilient_parsing:
102            processed = []
103            i = 0
104            while i < len(args):
105                arg = args[i]
106                if arg.startswith("-") and "=" in arg:
107                    # already combined: --option=foo=bar
108                    option, value = arg.split("=", 1)
109                    processed.append(option)
110                    if value:
111                        processed.append(value)
112                    i += 1
113                elif arg.startswith("-") and i + 1 < len(args) and args[i + 1] == "=":
114                    # bash pre-split starting with "=": ["--opt", "=", "foo", "=", "bar"]
115                    processed.append(arg)
116                    i += 2  # skip option and bare "="
117                    value_parts = []
118                    while i < len(args) and not args[i].startswith("-"):
119                        if args[i] == "=":
120                            value_parts.append("=")
121                        else:
122                            if value_parts and value_parts[-1] != "=":
123                                break  # previous wasn't "=", this is a new arg
124                            value_parts.append(args[i])
125                        i += 1
126                    if value_parts:
127                        processed.append("".join(value_parts))
128                elif (
129                    arg.startswith("-")
130                    and i + 1 < len(args)
131                    and not args[i + 1].startswith("-")
132                    and i + 2 < len(args)
133                    and args[i + 2] == "="
134                ):
135                    # bash pre-split starting with fragment: ["--opt", "foo", "=", "bar"]
136                    processed.append(arg)
137                    i += 1
138                    value_parts = [args[i]]
139                    i += 1
140                    while i < len(args) and not args[i].startswith("-"):
141                        if args[i] == "=":
142                            value_parts.append("=")
143                            i += 1
144                            if i < len(args) and not args[i].startswith("-"):
145                                value_parts.append(args[i])
146                                i += 1
147                        else:
148                            break
149                    processed.append("".join(value_parts))
150                else:
151                    processed.append(arg)
152                    i += 1
153            args = processed
154        return super().parse_args(ctx, args)

Reassemble bash-split option tokens before delegating to Click's parser.

Bash splits tokens on = (which appears in COMP_WORDBREAKS by default) before invoking the completion handler. This means that an option like --option=foo=bar arrives as the fragments ["--option", "=", "foo", "=", "bar"] rather than as a single token. Click's parser cannot handle this fragmented form, so autocompletion breaks whenever an option value contains =.

This override reassembles such fragments back into ["--option", "foo=bar"] during resilient (completion) parsing, leaving normal invocations entirely unaffected.

Arguments:
  • ctx: The current Click context.
  • args: The raw argument list as received from the shell, potentially containing =-fragmented option tokens produced by bash's completion machinery.
Returns:

The remaining unparsed arguments, after reassembly and delegation to the parent parser.

class GlobDirectoryMixin:
157class GlobDirectoryMixin:
158    """
159    Mixin that adds glob-expanding multi-value support for -d/--directory.
160
161    Click does not support nargs=-1 on options. This mixin pre-processes the
162    argument list before Click's parser sees it, greedily consuming all
163    non-flag tokens following -d/--directory, glob-expanding each one, and
164    rewriting them as repeated -d <path> pairs that Click's multiple=True
165    parser handles correctly.
166
167    Must appear before the Click command class in the MRO so that its
168    parse_args runs first.
169    """
170
171    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
172        """
173        Rewrite -d/--dir tokens to support multiple values and globs.
174
175        Greedily consumes all non-flag tokens following a -d/--dir flag,
176        glob-expands each one relative to its own parent directory, and emits
177        one -d <path> pair per expanded result.
178
179        Args:
180            ctx (click.Context): The current Click context.
181            args (list[str]): The raw argument list.
182
183        Returns:
184            list[str]: The remaining unparsed arguments after delegation to the
185                parent parser.
186        """
187        rewritten: list[str] = []
188        i = 0
189        while i < len(args):
190            if args[i] in ("-d", "--dir"):
191                i += 1
192                while i < len(args) and not args[i].startswith("-"):
193                    pattern = args[i]
194                    p = Path(pattern)
195                    if not p.name:
196                        # pattern is a bare directory (e.g. "." or "/some/dir/") - no glob needed
197                        rewritten.extend(["-d", pattern])
198                    else:
199                        expanded = sorted(p.parent.glob(p.name))
200                        if expanded:
201                            for path in expanded:
202                                rewritten.extend(["-d", str(path)])
203                        else:
204                            rewritten.extend(["-d", pattern])
205                    i += 1
206            else:
207                rewritten.append(args[i])
208                i += 1
209        return super().parse_args(ctx, rewritten)  # ty:ignore[unresolved-attribute]

Mixin that adds glob-expanding multi-value support for -d/--directory.

Click does not support nargs=-1 on options. This mixin pre-processes the argument list before Click's parser sees it, greedily consuming all non-flag tokens following -d/--directory, glob-expanding each one, and rewriting them as repeated -d pairs that Click's multiple=True parser handles correctly.

Must appear before the Click command class in the MRO so that its parse_args runs first.

def parse_args(self, ctx: click.core.Context, args: list[str]) -> list[str]:
171    def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
172        """
173        Rewrite -d/--dir tokens to support multiple values and globs.
174
175        Greedily consumes all non-flag tokens following a -d/--dir flag,
176        glob-expands each one relative to its own parent directory, and emits
177        one -d <path> pair per expanded result.
178
179        Args:
180            ctx (click.Context): The current Click context.
181            args (list[str]): The raw argument list.
182
183        Returns:
184            list[str]: The remaining unparsed arguments after delegation to the
185                parent parser.
186        """
187        rewritten: list[str] = []
188        i = 0
189        while i < len(args):
190            if args[i] in ("-d", "--dir"):
191                i += 1
192                while i < len(args) and not args[i].startswith("-"):
193                    pattern = args[i]
194                    p = Path(pattern)
195                    if not p.name:
196                        # pattern is a bare directory (e.g. "." or "/some/dir/") - no glob needed
197                        rewritten.extend(["-d", pattern])
198                    else:
199                        expanded = sorted(p.parent.glob(p.name))
200                        if expanded:
201                            for path in expanded:
202                                rewritten.extend(["-d", str(path)])
203                        else:
204                            rewritten.extend(["-d", pattern])
205                    i += 1
206            else:
207                rewritten.append(args[i])
208                i += 1
209        return super().parse_args(ctx, rewritten)  # ty:ignore[unresolved-attribute]

Rewrite -d/--dir tokens to support multiple values and globs.

Greedily consumes all non-flag tokens following a -d/--dir flag, glob-expands each one relative to its own parent directory, and emits one -d pair per expanded result.

Arguments:
  • ctx (click.Context): The current Click context.
  • args (list[str]): The raw argument list.
Returns:

list[str]: The remaining unparsed arguments after delegation to the parent parser.

class QQOperatorCommand(GlobDirectoryMixin, GNUHelpColorsCommand):
212class QQOperatorCommand(GlobDirectoryMixin, GNUHelpColorsCommand):
213    """GNUHelpColorsCommand with glob-expanding -d/--dir support."""
214
215    pass

GNUHelpColorsCommand with glob-expanding -d/--dir support.