JSPM

  • Created
  • Published
  • Downloads 1208
  • Score
    100M100P100Q108309F
  • 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 301 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 options --no-color options.color = false
-f flags -v flags.v = true
--opt=val options --output=file options.output = "file"
-o val options -o file options.o = "file"

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 }

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:
  data: Record<string, PrimitiveValue>
  meta: Record<string, PrimitiveValue>
  // ... custom categories
}

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
  coerceTyped,
  coerceToType,
  coerceToBoolean,
  coerceToNumber,
  stripQuotes,
  isNumericString,
  isBooleanString,
  inferType,

  // Help
  generateHelp,
  generateCLIHelp,
  wrapText,

  // Completion
  generateCompletion,

  // Errors
  ParseError,
  MissingRequiredError,
  InvalidValueError,
  UnknownOptionError,
  formatErrors,
  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 301
Dependencies 0
Bundle size ~59 KB

License

MIT