polyflag
Repeatable comma-separated set-style cli flags with - prefix removal.
Given a fixed list of known token names, parse one occurrence of a flag whose
value is a comma-separated list of those names, accumulating into a
HashSet<&'static str>. A - prefix on a token removes it from the set
instead of adding. Unknown tokens error.
Kitchensink example:
set -x MY_APP_DEFAULT_QUIRKS=foo
my-app --quirks=bar --quirks=-foo --quirks=baz
where the final state is {bar, baz}.
See examples/demo.rs for a runnable showcase (cargo run --example demo
or make demo).
Usage
use HashSet;
use ;
const KNOWN: & = &;
let mut set: = new;
apply.unwrap;
apply.unwrap;
apply.unwrap;
assert!;
assert!;
The caller owns the set, the loop over occurrences, and the error formatting.
polyflag only knows how to apply one occurrence's value.
Aliases
Each KnownToken has one canonical spelling plus zero or more aliases.
Aliases let multiple input spellings resolve to the same canonical entry --
the canonical is always what lives in the set, regardless of which spelling
the user typed.
use ;
const KNOWN: & = &;
let mut set = new;
apply.unwrap;
// canonicals only, regardless of spelling used:
assert!;
assert!;
Each alias has a status:
Alternative-- intentional alternate spelling, equally valid as the canonical. (Bare literal in thetoken!macro.)Deprecated-- still resolves but the caller may want to warn or prompt migration. Useapply_with_callbackto surface a warning. (deprecated <literal>in the macro.)Hidden-- resolves silently. Omitted from--helplistings and from any public enumeration of accepted spellings; for undocumented compat with an old typo or removed convention. (hidden <literal>in the macro.)
Add and remove are interchangeable across canonical and alias: e.g.
--quirks=nocolor then --quirks=-no-color adds and then removes the
same canonical entry; final state has nocolor absent.
Deprecation warnings
apply_with_callback invokes the callback once per input token that
resolves through a Deprecated alias:
apply_with_callback?;
apply is the no-callback convenience that calls the callback variant with
a no-op. The same split exists for env vars: apply_env_for_flag and
apply_env_for_flag_with_callback.
Resolving without mutating a set
canonicalize(input, known) returns a Resolved { canonical, kind } for any
canonical or alias spelling, where kind is Canonical | Alternative | Deprecated | Hidden. Useful when the caller wants to classify a match (e.g.
to decide between an inline warning and a structured deprecation report)
without re-walking the table.
Validating the known-token table
check_known(known) panics on a duplicate or empty spelling anywhere in the
table. The body is #[cfg(debug_assertions)]-gated, so release builds
compile to a no-op -- callers may invoke it unconditionally at startup, and
typos in the static table get caught by cargo test / dev builds.
The debug_check_known!(KNOWN) macro is a one-liner wrapper around
check_known for call sites that prefer the debug_assert!-shaped form;
identical runtime semantics.
Rendering the table for help / error output
format_known_for_help(known) produces a comma-separated string of
canonicals with non-Hidden aliases parenthesised, suitable for embedding
in --help text or unknown quirk X; known: ... error messages:
// "foo, bar (barre), baz (baz-alt, old-baz)"
let listing = format_known_for_help;
Hidden aliases are omitted (that's the point of Hidden).
On-by-default tokens
Mark canonicals with the default prefix in token! and seed the working
set with defaults(known). The table doubles as both the schema and the
on-by-default policy -- one source of truth.
use ;
const KNOWN: & = &;
let mut set = defaults;
apply.unwrap;
assert!;
assert!;
This pairs naturally with the recc convention of naming tokens as positive
switches for on-by-default booleans (color, animations, cookies)
rather than nocolor-style negations. Users then disable with the -
prefix (--quirks=-color) and the table alone tells you the starting
state.
Env-var defaults
apply_env_for_flag(prefix, flag, known, set) reads an env var derived from
the cli surface and applies it as if it were a leading occurrence of the flag:
prefix |
flag |
env var resolved |
|---|---|---|
"app" |
"quirks" |
APP_QUIRKS |
"app" |
"allow-create" |
APP_ALLOW_CREATE |
Mapping rule: {PREFIX}_{FLAG}, prefix uppercased, kebab-to-underscore on the
flag, uppercased. Env value semantics match apply (comma-list, -name
removal, unknown-token error). Unset / empty / non-utf-8 values are no-ops.
// Example: cli surface is `--quirks=...`, so the env surface is APP_QUIRKS.
apply_env_for_flag?;
// Then apply any cli occurrences -- they layer on top, so a cli `-name` can
// negate an entry the env contributed.
apply?;
The cli and env names stay in lock-step by construction -- no second string
to keep in sync. env_var_name(prefix, flag) is exposed if the caller wants
to surface the resolved name in error messages.
Semantics
- input split on
,; tokens trimmed; empty tokens skipped. - token
name->set.insert(name). - token
-name->set.remove(name). - unknown name (after stripping any
-) ->Err(UnknownToken). The set is left in its partial state -- clone first if you need atomic application. - order matters: across occurrences, and within an occurrence (left-to-right).
Compatibility with cli parsers
polyflag operates on the value of one flag occurrence ("foo,bar,-baz"),
not on the flag itself. It composes with any parser that hands you the raw
value(s) of a repeatable flag.
| Parser | Repeatable -> Vec<String>? |
Value starting with - ok? |
Integration |
|---|---|---|---|
clap (derive/builder) |
yes via ArgAction::Append + value_delimiter(',') |
only with allow_hyphen_values(true), or via --flag=value form |
collect to Vec<String> per occurrence, loop + polyflag::apply |
argh |
yes (#[argh(option)] repeated) |
via --flag=value form |
same |
lexopt |
manual loop -- you get each value | yes (you control parsing) | call polyflag::apply inline |
pico-args |
similar to lexopt |
yes | same |
gumdrop |
yes via Vec<String> |
via = form |
post-process |
manual env::args_os |
trivial | yes | direct |
The -foo tokens are the only friction. Most parsers accept them inside
--flag=... (the = form pins the value to the flag). The space-separated
form (--flag -foo) needs an opt-in on parsers that defend against stray
hyphen-tokens -- e.g. clap's allow_hyphen_values(true).
What polyflag deliberately doesn't do
- no flag-name recognition (
--quirks=). that's the parser's job. - no error formatting. caller emits the message in their app's voice.
- no value lifetime juggling.
&[KnownToken]returns&'static strkeys for the canonicals, since eachKnownToken.canonicalis itself&'static str.
Known friction
OsStringinput. Most parsers hand youOsString; polyflag wants&str. Caller does.to_str().ok_or(...)?. Fine for ascii-only tokens (the design assumption).- Static-only known list.
&[KnownToken]with&'static strcanonicals / aliases excludes runtime-loaded token sets (e.g. plugin names from a config file). AString-keyed variant could be added if needed. - No clap adapter. A separate
polyflag-clapcrate could wrap this as a clapvalue_parser. Out of scope for this crate.
License
MIT. See LICENSE.