|
| 1 | +# CommandLine internals |
| 2 | + |
| 3 | +Two small classes; most of it is clear from signatures. This is the thin |
| 4 | +"conventions & traps" layer — the few facts that are expensive to rediscover. |
| 5 | + |
| 6 | +## Help text is the schema (`addFromHelp`) |
| 7 | + |
| 8 | +The clever, non-obvious part: `addFromHelp()` **parses formatted help text into |
| 9 | +option definitions** with two regexes. The outer one extracts each option line and |
| 10 | +its `(default: …)`; the inner one parses the option syntax itself, and its result |
| 11 | +determines everything: |
| 12 | + |
| 13 | +- `--long` / `-s, --long` → the option name is the **last** matched flag (`--long`), |
| 14 | + the alias the first (`-s`) when they differ; |
| 15 | +- a value spec `<file>` (required), `[type]` (optional) or a bare word (`--width N`, |
| 16 | + required) → whether the option takes a value; a trailing `...` → **repeatable**; |
| 17 | + `<a|b|c>` → an **enum** (split on `|`); |
| 18 | +- a `(default: …)` (or a `[…]` value spec) → the option is optional with that |
| 19 | + fallback. |
| 20 | + |
| 21 | +The whole matched flag string must be fully consumed by the inner regex or it throws |
| 22 | +`Unable to parse '…'`. The `$defaults` array passed alongside **merges over** the |
| 23 | +parsed values (it supplies what help text can't express: `RealPath`, `Normalizer`, |
| 24 | +`Enum`, `Repeatable`). `RealPath` is desugared into a `Normalizer` that resolves the |
| 25 | +path (composing with any user normalizer). The `ValueType` (`None`/`Optional`/ |
| 26 | +`Required`) is then derived: no value spec → `None`; optional or has a default → |
| 27 | +`Optional`; else `Required`. |
| 28 | + |
| 29 | +## `parse()` and the `OptionPresent` sentinel |
| 30 | + |
| 31 | +`parse()` walks the args with a **positional cursor** (`current()`/`next()` over the |
| 32 | +positional options) alongside named-option lookup. A token starting with `-` is an |
| 33 | +option, anything else is a positional argument — so **options and arguments may |
| 34 | +interleave freely** (an argument can precede an option). |
| 35 | + |
| 36 | +The trap is the sentinel `OptionPresent = true`. A `--flag` with no `=value` yields |
| 37 | +this literal `true` as its "value". Then: |
| 38 | + |
| 39 | +- if the option takes a value and the sentinel is still present, the **next token is |
| 40 | + consumed as the value only if it does not start with `-`**; otherwise a `Required` |
| 41 | + option throws and an `Optional` one keeps the `true`. |
| 42 | +- so an optional-value option used bare (`--format`) parses as **`true`**, not its |
| 43 | + fallback. The **fallback is used only when the option is absent entirely** (the |
| 44 | + final fill-in pass). `enum` validation skips the `true` sentinel — but a |
| 45 | + `normalizer` does **not**: it receives the raw `true`. And fallbacks are assigned |
| 46 | + as-is, never normalized. |
| 47 | + |
| 48 | +The fill-in pass at the end assigns fallbacks to unused non-`Required` options, |
| 49 | +throws for a missing required **positional** argument (a missing required *option* |
| 50 | +just becomes `null`), and casts a repeatable value to an array. A repeatable option |
| 51 | +accumulates into a list; a non-repeatable one keeps the **last** occurrence. |
| 52 | + |
| 53 | +## `parseOnly()` is a deliberately dumb early-exit path |
| 54 | + |
| 55 | +`parseOnly(['--help', '--version'])` exists so info options work even when a required |
| 56 | +argument is missing. It parses **only** the named options, **ignores everything |
| 57 | +else, never validates, never throws**, and returns `null` for anything unused (but |
| 58 | +respects aliases). Do not add validation to it — that would defeat the "`--help` |
| 59 | +before the missing-argument error" use case. |
| 60 | + |
| 61 | +## Console: two distinct terminal checks |
| 62 | + |
| 63 | +`detectColors()` and `detectTerminal()` are **separate on purpose**. `detectTerminal` |
| 64 | +is the pure CLI+TTY check (`stream_isatty(STDOUT)`); `detectColors` builds on it and |
| 65 | +adds `NO_COLOR` (disables) and `FORCE_COLOR` (overrides the TTY check). The rule: |
| 66 | +gate **color** on `detectColors`, but gate **interactive-only features** (progress |
| 67 | +bars, line-rewriting, prompts) on `detectTerminal` — a user may set `NO_COLOR` yet |
| 68 | +still be on a real terminal that should get a progress indicator. `color($color)` |
| 69 | +with the string omitted emits the escape code with **no reset**, for building a |
| 70 | +sequence manually (it is the null *string*, not a null color, that skips the reset). |
0 commit comments