Caveats
This document describes important behaviors and caveats of Clerc's argument parser. Understanding these behaviors is essential for building reliable CLI applications and avoiding unexpected issues.
Non-Greedy Parsing
Important
The Clerc parser is non-greedy. It only reads arguments before the first flag to determine which command to execute.
Why Non-Greedy?
Clerc uses non-greedy parsing for the following reasons:
- Predictable behavior: Flags can appear anywhere after the command without affecting command resolution
- Compatibility: This matches the behavior of most Unix CLI tools
- Flexibility: Allows flags to be placed in any order after the command
- Simplicity: Makes the parsing logic straightforward and easier to understand
How It Works
When parsing command-line arguments, the parser follows this logic:
- Read arguments from left to right
- Stop reading command/subcommand tokens when encountering the first flag (starting with
-or--) - Everything after the first flag is treated as flags and their values, or as parameters
Examples
# Command is "build", --verbose is a flag
cli build --verbose
# Command is "build", --help is a flag, "foo" is a parameter (NOT a subcommand)
cli build --help foo
# NO command is matched! --help is encountered first, so "build" becomes a parameter
cli --help build
# Command is "deploy staging", --force is a flag
cli deploy staging --force
# Command is "deploy", --env is a flag, "staging" is a parameter (NOT a subcommand)
cli deploy --env stagingImpact on Plugins
This non-greedy behavior affects how certain plugins work:
Help Plugin
The --help flag shows CLI help only when it immediately follows the CLI name with no additional arguments:
# ✅ Shows CLI help (--help immediately follows cli, no extra arguments)
cli --help
# ✅ Shows help for "build" command (command comes before --help)
cli build --help
# ❌ Throws error!
cli --help buildWARNING
cli --help build will throw an error because:
- The parser encounters
--helpfirst, so no command is matched (tries to match root command) - No root command is registered
- Therefore, an error is thrown
The key difference:
cli --help→ Help plugin intercepts and shows CLI helpcli --help build→ Tries to execute root command (which doesn't exist), throws error
If you want to show help for a specific command, always place the command name before the --help flag:
# ✅ Correct: Shows help for "build"
cli build --helpAlternatively, use the help command:
# ✅ Always shows help for "build"
cli help buildVersion Plugin
Similarly, for version flags:
# Shows version (no command matched)
cli --version
# Command "build" is matched, but --version flag may be ignored by the command
cli build --versionParsing Order
The parser processes arguments in the following order:
- Command Resolution: Identify the command from arguments before the first flag
- Flag Parsing: Parse all flags (both global and command-specific)
- Parameter Collection: Remaining non-flag arguments become parameters
- Double Dash Handling: Everything after
--is collected as-is
Double Dash (--)
The double dash -- is a special marker that tells the parser to stop interpreting flags:
# "--foo" is passed as a parameter, not parsed as a flag
cli build -- --foo --barFlag Value Resolution
Flags can receive values in multiple ways:
# Space-separated
cli build --output dist
# Equals sign
cli build --output=dist
# Colon (useful when value contains =)
cli build --define:KEY=VALUEDot-Notation for Object Flags
For flags with type: Object, you can use dot notation to set nested values:
# Sets config.port to "8080"
cli --config.port 8080
# Sets config.server.host to "localhost"
cli --config.server.host localhostBoolean Value Handling
For dot-notation flags, special values are automatically converted:
| Input | Result |
|---|---|
--config.enabled true | { enabled: true } |
--config.enabled false | { enabled: false } |
--config.enabled (no value) | { enabled: true } |
--config.enabled=true | { enabled: true } |
--config.enabled=false | { enabled: false } |
--config.enabled= (empty) | { enabled: true } |
The conversion rules are:
"true"or empty string →true"false"→false- Other values remain as strings
Path Conflicts
When a path has already been set to a primitive value, subsequent nested paths will be silently ignored:
# --config.port.internal is ignored because config.port is already "8080"
cli --config.port 8080 --config.port.internal 9090
# Result: { config: { port: "8080" } }To avoid this, ensure your paths don't conflict (i.e., don't set both a.b and a.b.c).
Default Values for Object Flags
Not Recommended
We do not recommend using default values with Object flags that use dot-notation.
Object flags follow an all-or-nothing default behavior:
- If no dot-notation values are provided for the flag, the
defaultvalue is used entirely - If any dot-notation value is provided, the
defaultis completely ignored (no merging)
# Example: env flag with default { NODE_ENV: "development", PORT: "3000" }
# No --env flags provided → Uses entire default
cli build
# Result: { NODE_ENV: "development", PORT: "3000" }
# Any --env flag provided → Default is completely ignored
cli build --env.PORT 8080
# Result: { PORT: "8080" } ← NODE_ENV is NOT included!Why Not Use Defaults with Dot-Notation?
Dot-notation is designed for user-defined, runtime configuration values (like environment variables, define macros, etc.) where:
- The keys are not known in advance
- Users specify exactly what they need
- There's no "standard set" of expected keys
This semantic mismatch with defaults causes several issues:
- Complex merge logic: Shallow merge? Deep merge? User-configurable merge function? Each approach adds complexity
- Type inference complexity: Merging object types requires intersection types and sophisticated type-level logic
- Unexpected behavior: Users might expect
defaultto act as "fallback values" for missing keys, but implementing this is non-trivial
Recommended Approach
Instead of using defaults with dot-notation, handle default values in your command handler:
cli
.command("build", "Build the project")
.flags({
env: Object,
})
.on((context) => {
const env = {
NODE_ENV: "development",
PORT: "3000",
...context.flags.env, // User-provided values override defaults
};
// Use env...
});This gives you full control over the merge logic and keeps type inference simple.
Short Flag Combinations
Short flags can be combined:
# Equivalent to: -a -b -c
cli -abc
# -a and -b are boolean flags, -c takes "value"
cli -abc valueBest Practices
Place commands before flags: Always write
cli command --flaginstead ofcli --flag commandUse explicit help command: When in doubt, use
cli help commandinstead ofcli --help commandQuote special characters: Use quotes for values containing spaces or special characters
Use
--for pass-through arguments: When passing arguments to child processes, use--to prevent parsing
# Pass "--watch" to the underlying tool, not to cli
cli build -- --watch
