JSPM

  • Created
  • Published
  • Downloads 1208
  • Score
    100M100P100Q108319F
  • License MIT

Expressive CLI argument parser with HTTPie-style syntax

Package Exports

  • cli-args-parser

Readme

cli-args-parser

Expressive CLI Argument Parser

Expressive syntax meets powerful schema validation.
Zero dependenciesTypeScript-firstNested subcommandsCustom validation
Parse key=value, key:=typed, Key:Meta — with fully customizable separators.

npm version npm downloads TypeScript Node.js License Zero Dependencies

Quick Start · Syntax · Schema · Validation · CLI · API


Table of Contents


Quick Start

npm install cli-args-parser
# or
pnpm add cli-args-parser
import { parse } from 'cli-args-parser'

const result = parse([
  'https://api.example.com',
  'name=Filipe',
  'age:=35',
  'active:=true',
  'Authorization:Bearer TOKEN',
  '--verbose',
  '-o', 'output.json'
])

// {
//   positional: ['https://api.example.com'],
//   data: { name: 'Filipe', age: 35, active: true },
//   meta: { Authorization: 'Bearer TOKEN' },
//   flags: { verbose: true },
//   options: { o: 'output.json' },
//   errors: []
// }

Features

Category Features
Syntax Expressive key=value, key:=typed, Key:Value patterns
Separators Fully customizable — define your own patterns and categories
Schema Type coercion, required fields, choices, defaults, env vars
Validation Built-in + custom validation functions
Commands Nested subcommands with unlimited depth
Options Auto-short generation, aliases, negation (--no-flag)
Output Shell completion (Bash, Zsh, Fish), help generation
Quality 392 tests, zero dependencies, TypeScript-first

Syntax Reference

Default Separators

Syntax Category Example Result
key=value data name=Filipe data.name = "Filipe"
key:=value data (typed) age:=35 data.age = 35
Key:Value meta Auth:Bearer X meta.Auth = "Bearer X"
--flag flags --verbose flags.verbose = true
--no-flag flags* --no-color flags.color = false
-f flags -v flags.v = true
--opt=val options --output=file options.output = "file"
-o val options -o file options.o = "file"
-- marker -- --help End of options, rest is positional

Notes:

  • The Key:Value meta syntax requires the key to start with an uppercase letter (e.g., Authorization:Bearer). Lowercase keys like nginx:latest are treated as positional to avoid conflicts with docker images and similar patterns.
  • Keys containing / are rejected for = and := separators to avoid matching URL-like patterns (e.g., data:text/plain;base64,x=). Use positional arguments for paths: myapp -- path/to/file instead of path/to=file. Namespaced keys like aws:region=value work because they don't contain /.
  • * --no-flag behavior differs between APIs:
    • parse(): Always goes to flags as flags.flag = false
    • createParser()/createCLI(): Goes to options as options.flag = false (only for defined boolean options with negatable !== false)

End of Options (--)

The -- marker signals the end of options. Everything after it is treated as positional, even if it looks like a flag or separator:

const result = parse(['--verbose', '--', '--help', 'name=value'])
// result.flags = { verbose: true }
// result.positional = ['--help', 'name=value']  // Treated as positional, not parsed

This is useful for passing arguments to subprocesses or handling filenames that start with -.

Behavior with createParser() and allowUnknown:

When using createParser() with positional definitions:

const parser = createParser({
  positional: [{ name: 'file' }],
  allowUnknown: true  // default
})

// Without --: unknown flags go to options
parser.parse(['--unknown'])
// { options: { unknown: true }, positional: {}, rest: [] }

// With --: everything after is positional, fills schema positions first, then rest
parser.parse(['--', '--unknown', 'extra'])
// { positional: { file: '--unknown' }, rest: ['extra'], options: {} }

With allowUnknown: false, extra positionals after -- that don't fit schema positions are silently ignored:

const parser = createParser({
  positional: [{ name: 'file' }],
  allowUnknown: false
})
parser.parse(['--', 'file.txt', 'extra'])
// { positional: { file: 'file.txt' }, rest: [], options: {} }
// 'extra' is ignored, not stored in rest

Note: createCLI() uses strict instead of allowUnknown. With strict: true, unknown options cause errors, but extras after -- always go to rest (useful for passing args to subprocesses).

Type Coercion (:=)

The := separator automatically coerces values to their JavaScript types:

parse(['count:=42'])         // data.count = 42 (number)
parse(['active:=true'])      // data.active = true (boolean)
parse(['active:=false'])     // data.active = false (boolean)
parse(['value:=null'])       // data.value = null
parse(['items:=[1,2,3]'])    // data.items = [1, 2, 3] (JSON array)
parse(['config:={"a":1}'])   // data.config = { a: 1 } (JSON object)
parse(['tags:=a,b,c'])       // data.tags = ["a", "b", "c"] (comma-separated)
parse(['ids:=1,2,3'])        // data.ids = [1, 2, 3] (comma-separated with type coercion)

Customizable Separators

The separator system is fully customizable. Define your own patterns and category names:

import { parse } from 'cli-args-parser'

// Default separators
const defaults = {
  '=': 'data',                      // key=value → data.key
  ':=': { to: 'data', typed: true }, // key:=value → data.key (with type coercion)
  ':': 'meta'                        // Key:Value → meta.Key
}

// Custom separators for your use case
const result = parse(['file@data.json', 'name->Filipe', 'count::42'], {
  separators: {
    '@': 'files',                      // file@path → files.file
    '->': 'body',                      // key->value → body.key
    '::': { to: 'body', typed: true }  // key::value → body.key (typed)
  }
})

// {
//   positional: [],
//   files: { file: 'data.json' },
//   body: { name: 'Filipe', count: 42 },
//   flags: {},
//   options: {},
//   errors: []
// }

Separator Configuration:

Format Description Example
string Category name shorthand '@': 'files'
{ to, typed? } Full config with type coercion ':=': { to: 'data', typed: true }
{ to, prefix: true } Prefix separator (starts the arg) '@': { to: 'tags', prefix: true }

Prefix Separators

Regular separators expect key<sep>value format. Prefix separators start at position 0 and use : as the key/value delimiter:

import { parse } from 'cli-args-parser'

const result = parse(['@tag:important', '@env:production', 'name=test'], {
  separators: {
    '=': 'data',
    '@': { to: 'tags', prefix: true }  // @key:value format
  }
})

// {
//   positional: [],
//   data: { name: 'test' },
//   tags: { tag: 'important', env: 'production' },
//   flags: {},
//   options: {},
//   errors: []
// }

Prefix separators are useful for tag-like syntax where you want a marker character to distinguish a category.

Schema Parser

Add validation, defaults, and help generation:

import { createParser } from 'cli-args-parser'

const parser = createParser({
  positional: [
    { name: 'url', required: true, description: 'Target URL' }
  ],
  options: {
    output: {
      short: 'o',
      type: 'string',
      description: 'Output file'
    },
    verbose: {
      short: 'v',
      type: 'boolean',
      default: false
    },
    format: {
      type: 'string',
      choices: ['json', 'yaml', 'xml'],
      default: 'json'
    },
    retries: {
      type: 'number',
      default: 3,
      env: 'MAX_RETRIES'  // Fallback to env var
    }
  }
})

const result = parser.parse(['https://api.com', '-v', '--format=yaml'])
console.log(parser.help())

Option Definition

interface OptionDefinition {
  short?: string              // Short flag (-o)
  aliases?: string[]          // Alternative names
  type?: 'string' | 'number' | 'boolean' | 'array'
  default?: any               // Default value
  description?: string        // Help text
  required?: boolean          // Required option
  choices?: any[]             // Allowed values
  env?: string                // Environment variable fallback
  hidden?: boolean            // Hide from help
  negatable?: boolean         // Allow --no-flag (default: true)
  validate?: ValidateFn       // Custom validation function
}

Positional Definition

interface PositionalDefinition {
  name: string                // Argument name
  description?: string        // Help text
  required?: boolean          // Required argument
  type?: 'string' | 'number' | 'boolean' | 'array'
  default?: any               // Default value
  variadic?: boolean          // Capture remaining args
  validate?: ValidateFn       // Custom validation function
}

Custom Validation

Add custom validation logic to options and positionals:

import { createParser, ValidateFn } from 'cli-args-parser'

const parser = createParser({
  options: {
    port: {
      type: 'number',
      validate: (value) => {
        const num = value as number
        if (num < 1 || num > 65535) {
          return `Port must be between 1 and 65535, got ${num}`
        }
        return true
      }
    },
    email: {
      type: 'string',
      validate: (value) => {
        const str = value as string
        if (!str.includes('@')) {
          return 'Invalid email format'
        }
        return true
      }
    }
  },
  positional: [
    {
      name: 'url',
      required: true,
      validate: (value) => {
        const str = value as string
        if (!str.startsWith('https://')) {
          return 'URL must use HTTPS'
        }
        return true
      }
    }
  ]
})

const result = parser.parse(['http://example.com', '--port=70000'])
// result.errors = [
//   'URL must use HTTPS',
//   'Port must be between 1 and 65535, got 70000'
// ]

Validation Function

// Return true if valid, or error message string if invalid
type ValidationResult = true | string
type ValidateFn<T = PrimitiveValue | PrimitiveValue[]> = (value: T) => ValidationResult

Validation Behavior:

  • Runs after type coercion
  • Only runs if value is defined (not on undefined)
  • Combines with choices validation (choices checked first)
  • Errors are collected in result.errors

Combining Validations

const parser = createParser({
  options: {
    level: {
      type: 'number',
      choices: [1, 2, 3, 4, 5],  // First check: must be in choices
      validate: (value) => {     // Second check: custom logic
        if (value === 3) return 'Level 3 is temporarily disabled'
        return true
      }
    }
  }
})

parser.parse(['--level=10'])  // Error: Invalid value (choices)
parser.parse(['--level=3'])   // Error: Level 3 is temporarily disabled
parser.parse(['--level=2'])   // OK

Auto-Short Generation

Automatically assigns short flags to options:

const parser = createParser({
  autoShort: true,
  options: {
    verbose: { type: 'boolean' },  // Gets -v
    output: { type: 'string' },    // Gets -o
    format: { type: 'string' }     // Gets -f
  }
})

parser.parse(['-v', '-o', 'file.txt', '-f', 'json'])

Algorithm:

  1. Options sorted alphabetically (deterministic assignment)
  2. Try first letter lowercase (verbosev)
  3. If taken, try uppercase (verboseV)
  4. If taken, try next letter (verbosee)

CLI with Subcommands

Build complex CLIs with nested command routing:

import { createCLI } from 'cli-args-parser'

const cli = createCLI({
  name: 'myapp',
  version: '1.0.0',
  autoShort: true,
  options: {
    verbose: { type: 'boolean', description: 'Verbose output' }
  },
  commands: {
    get: {
      description: 'Fetch a resource',
      positional: [{ name: 'url', required: true }],
      options: {
        output: { short: 'o', type: 'string' }
      }
    },
    post: {
      description: 'Create a resource',
      positional: [{ name: 'url', required: true }]
    }
  }
})

cli.parse(['get', 'https://api.com', '-o', 'result.json', '--verbose'])

Nested Subcommands

Commands can be nested to any depth:

const cli = createCLI({
  name: 'kubectl',
  commands: {
    config: {
      description: 'Manage configuration',
      commands: {
        get: {
          description: 'Get config value',
          positional: [{ name: 'key', required: true }]
        },
        context: {
          description: 'Manage contexts',
          commands: {
            list: { description: 'List all contexts' },
            use: {
              description: 'Switch context',
              positional: [{ name: 'name', required: true }]
            }
          }
        }
      }
    }
  }
})

cli.parse(['config', 'get', 'theme'])
// → command: ['config', 'get'], positional: { key: 'theme' }

cli.parse(['config', 'context', 'use', 'production'])
// → command: ['config', 'context', 'use'], positional: { name: 'production' }

Command Handlers

Execute code when a command is matched:

const cli = createCLI({
  name: 'deploy',
  commands: {
    staging: {
      description: 'Deploy to staging',
      handler: async (result) => {
        console.log('Deploying to staging...')
        // Your deploy logic
      }
    },
    production: {
      description: 'Deploy to production',
      options: {
        force: { type: 'boolean', default: false }
      },
      handler: async (result) => {
        if (!result.options.force) {
          console.log('Use --force to deploy to production')
          return
        }
        console.log('Deploying to production...')
      }
    }
  }
})

await cli.run(process.argv.slice(2))

Command Aliases

commands: {
  install: {
    aliases: ['i', 'add'],
    description: 'Install packages'
  }
}

// All equivalent:
// myapp install lodash
// myapp i lodash
// myapp add lodash

Validation in Commands

Custom validation works in subcommands too:

const cli = createCLI({
  name: 'server',
  commands: {
    start: {
      description: 'Start the server',
      options: {
        port: {
          type: 'number',
          default: 3000,
          validate: (value) => {
            if (value < 1024) return 'Port must be >= 1024 (non-privileged)'
            return true
          }
        }
      },
      positional: [
        {
          name: 'config',
          validate: (value) => {
            if (!value.endsWith('.json')) return 'Config must be a .json file'
            return true
          }
        }
      ]
    }
  }
})

Shell Completion

Generate completion scripts for popular shells:

const cli = createCLI({ /* ... */ })

// Generate for your shell
const bashScript = cli.completion('bash')
const zshScript = cli.completion('zsh')
const fishScript = cli.completion('fish')
# Add to ~/.bashrc or ~/.zshrc
eval "$(myapp completion bash)"

Additional Features

Environment Variable Fallback

const parser = createParser({
  options: {
    apiKey: {
      type: 'string',
      env: 'API_KEY',      // Falls back to $API_KEY
      required: true
    },
    debug: {
      type: 'boolean',
      env: 'DEBUG',
      default: false
    }
  }
})

Priority: CLI arg > Environment variable > Default value

Variadic Positionals

Capture all remaining arguments:

const parser = createParser({
  positional: [
    { name: 'command', required: true },
    { name: 'files', variadic: true }
  ]
})

parser.parse(['build', 'src/a.ts', 'src/b.ts', 'src/c.ts'])
// positional: { command: 'build', files: ['src/a.ts', 'src/b.ts', 'src/c.ts'] }

Strict Mode

Reject unknown options:

const parser = createParser({
  strict: true,
  options: { verbose: { type: 'boolean' } }
})

parser.parse(['--unknown'])
// errors: ['Unknown option: --unknown']

Negation with Aliases

Negation works with aliases and normalizes to the canonical option name:

const parser = createParser({
  options: {
    verbose: {
      type: 'boolean',
      default: true,
      aliases: ['debug']
    }
  }
})

parser.parse(['--no-debug'])
// options: { verbose: false }
// 'debug' is not set — normalized to canonical name 'verbose'

API Reference

parse(args, options?)

Basic parsing without schema validation.

import { parse } from 'cli-args-parser'

const result = parse(process.argv.slice(2), {
  separators: { /* custom separators */ },
  strict: false,
  stopEarly: false,
  excludePatterns: [/^https?:\/\//]
})

Returns: ParsedArgs

interface ParsedArgs {
  positional: string[]
  flags: Record<string, boolean>
  options: Record<string, PrimitiveValue>
  errors: string[]
  // Dynamic categories based on separators:
  // Category value type depends on separator config:
  // - typed: false (default) → string
  // - typed: true → TypedValue (string | number | boolean | null | array | object)
  data: Record<string, TypedValue>   // ':=' is typed by default
  meta: Record<string, string>       // ':' is not typed by default
  // Note: if you configure ':' with { typed: true }, meta would be Record<string, TypedValue>
}

type PrimitiveValue = string | number | boolean | null
type TypedValue = PrimitiveValue | TypedValue[] | { [key: string]: TypedValue }

createEmptyResult(separators?)

Create an empty result object with categories initialized.

import { createEmptyResult, DEFAULT_SEPARATORS } from 'cli-args-parser'

const empty = createEmptyResult()
// { positional: [], flags: {}, options: {}, errors: [], data: {}, meta: {} }

const custom = createEmptyResult({ '=': 'params', '@': 'files' })
// { positional: [], flags: {}, options: {}, errors: [], params: {}, files: {} }

mergeResults(base, overlay)

Merge two parse results, with overlay values taking precedence.

import { parse, mergeResults } from 'cli-args-parser'

const defaults = parse(['name=default', '--verbose'])
const userArgs = parse(['name=custom', 'age:=30'])

const merged = mergeResults(defaults, userArgs)
// data: { name: 'custom', age: 30 }, flags: { verbose: true }

createParser(schema)

Create a parser with validation, defaults, and help generation.

import { createParser } from 'cli-args-parser'

const parser = createParser({
  positional: [/* ... */],
  options: { /* ... */ },
  separators: { /* ... */ },
  strict: false,
  stopEarly: false,
  allowUnknown: true,
  autoShort: false
})

parser.parse(args)    // Parse arguments
parser.help()         // Generate help text
parser.schema         // Access schema

Returns: Parser

interface Parser {
  parse(args: string[]): SchemaParseResult
  help(): string
  schema: ParserSchema
}

createCLI(schema)

Create a CLI with subcommands, handlers, and completion.

import { createCLI } from 'cli-args-parser'

const cli = createCLI({
  name: 'myapp',
  version: '1.0.0',
  description: 'My awesome CLI',
  options: { /* global options */ },
  commands: { /* ... */ },
  separators: { /* ... */ },
  strict: false,
  autoShort: false,
  config: {
    files: ['.myapprc', 'myapp.config.json'],
    searchPlaces: ['cwd', 'home']
  }
})

cli.parse(args)              // Parse arguments
await cli.run(args)          // Parse and execute handler
cli.help(['config'])         // Help for specific command
cli.completion('bash')       // Shell completion script
cli.schema                   // Access schema

Returns: CLI

interface CLI {
  parse(args: string[]): CommandParseResult
  run(args: string[]): Promise<void>
  help(command?: string[]): string
  completion(shell: 'bash' | 'zsh' | 'fish'): string
  schema: CLISchema
}

Exported Types

import type {
  // Core types
  PrimitiveValue,
  ParsedArgs,
  Token,
  TokenType,
  Separators,
  ParserOptions,

  // Schema types
  OptionType,
  OptionDefinition,
  PositionalDefinition,
  ParserSchema,
  SchemaParseResult,
  Parser,
  ValidateFn,
  ValidationResult,

  // CLI types
  CommandDefinition,
  CLISchema,
  ConfigOptions,
  CommandParseResult,
  CLI,

  // Internal types
  ResolvedOption
} from 'cli-args-parser'

Utility Functions

import {
  // Tokenizer
  tokenize,
  looksLikeValue,
  groupTokens,

  // Coercion
  coercePrimitive,
  coerceTyped,
  coerceToType,
  coerceToBoolean,
  coerceToNumber,
  stripQuotes,
  isNumericString,
  isBooleanString,
  inferType,

  // Help
  generateHelp,
  generateCLIHelp,
  wrapText,

  // Completion
  generateCompletion,

  // Errors
  ParseError,
  MissingRequiredError,
  MissingPositionalError,
  InvalidValueError,
  UnknownOptionError,
  TypeCoercionError,
  UnknownCommandError,
  formatErrors,
  createErrorMessage,
  hasErrors,
  throwIfErrors,

  // Constants
  DEFAULT_SEPARATORS,
  URL_PROTOCOLS
} from 'cli-args-parser'

Metrics

Metric Value
Syntax patterns 8 (data, meta, flags, options, negation...)
Type coercions 6 (string, number, boolean, null, array, object)
Shell completions 3 (Bash, Zsh, Fish)
Tests 383
Dependencies 0
Bundle size ~63 KB

License

MIT