Skip to content

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:

  1. Predictable behavior: Flags can appear anywhere after the command without affecting command resolution
  2. Compatibility: This matches the behavior of most Unix CLI tools
  3. Flexibility: Allows flags to be placed in any order after the command
  4. Simplicity: Makes the parsing logic straightforward and easier to understand

How It Works

When parsing command-line arguments, the parser follows this logic:

  1. Read arguments from left to right
  2. Stop reading command/subcommand tokens when encountering the first flag (starting with - or --)
  3. Everything after the first flag is treated as flags and their values, or as parameters

Examples

bash
# 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 staging

Impact 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:

bash
# ✅ 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 build

WARNING

cli --help build will throw an error because:

  1. The parser encounters --help first, so no command is matched (tries to match root command)
  2. No root command is registered
  3. Therefore, an error is thrown

The key difference:

  • cli --help → Help plugin intercepts and shows CLI help
  • cli --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:

bash
# ✅ Correct: Shows help for "build"
cli build --help

Alternatively, use the help command:

bash
# ✅ Always shows help for "build"
cli help build

Version Plugin

Similarly, for version flags:

bash
# Shows version (no command matched)
cli --version

# Command "build" is matched, but --version flag may be ignored by the command
cli build --version

Parsing Order

The parser processes arguments in the following order:

  1. Command Resolution: Identify the command from arguments before the first flag
  2. Flag Parsing: Parse all flags (both global and command-specific)
  3. Parameter Collection: Remaining non-flag arguments become parameters
  4. 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:

bash
# "--foo" is passed as a parameter, not parsed as a flag
cli build -- --foo --bar

Flag Value Resolution

Flags can receive values in multiple ways:

bash
# Space-separated
cli build --output dist

# Equals sign
cli build --output=dist

# Colon (useful when value contains =)
cli build --define:KEY=VALUE

Dot-Notation for Object Flags

For flags with type: Object, you can use dot notation to set nested values:

bash
# Sets config.port to "8080"
cli --config.port 8080

# Sets config.server.host to "localhost"
cli --config.server.host localhost

Boolean Value Handling

For dot-notation flags, special values are automatically converted:

InputResult
--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:

bash
# --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 default value is used entirely
  • If any dot-notation value is provided, the default is completely ignored (no merging)
bash
# 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:

  1. Complex merge logic: Shallow merge? Deep merge? User-configurable merge function? Each approach adds complexity
  2. Type inference complexity: Merging object types requires intersection types and sophisticated type-level logic
  3. Unexpected behavior: Users might expect default to act as "fallback values" for missing keys, but implementing this is non-trivial

Instead of using defaults with dot-notation, handle default values in your command handler:

typescript
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:

bash
# Equivalent to: -a -b -c
cli -abc

# -a and -b are boolean flags, -c takes "value"
cli -abc value

Best Practices

  1. Place commands before flags: Always write cli command --flag instead of cli --flag command

  2. Use explicit help command: When in doubt, use cli help command instead of cli --help command

  3. Quote special characters: Use quotes for values containing spaces or special characters

  4. Use -- for pass-through arguments: When passing arguments to child processes, use -- to prevent parsing

bash
# Pass "--watch" to the underlying tool, not to cli
cli build -- --watch

Released under the MIT license