StackPractices
beginner By Mathias Paulenko

Parse Command Line Arguments: argparse, Commander & picocli

How to parse command line arguments in Python, Java, and Node.js CLI applications.

Topics: data

Overview

Argument parsing is the first thing users hit when they run your tool. A good CLI exposes clear flags, typed options, automatic help text, and subcommands that feel like git push or docker run.

Mermaid flowchart LR diagram

When to Use

Reach for a CLI framework when your script needs configurable inputs — build tools, deployment scripts, data pipelines, or anything where hard-coding values would make reuse painful. Subcommand-style interfaces (like git push vs git pull) also fit here.

Prerequisites

  • Python 3.8+ — argparse is part of the standard library, no install needed. For Click, run pip install click; for typer, pip install typer.
  • Node.js 16+ — install commander with npm install commander, or grab yargs (npm install yargs) if you need something more extensible.
  • Java 8+ — add info.picocli:picocli:4.7.6 to your Maven pom.xml or the Gradle equivalent.

Solution

Python

# argparse is the standard library for Python CLI
import argparse

parser = argparse.ArgumentParser(description='Process some files.')
parser.add_argument('input', help='Input file path')
parser.add_argument('-o', '--output', default='out.txt', help='Output file path')
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose logging')

args = parser.parse_args()
print(f'Input: {args.input}, Output: {args.output}, Verbose: {args.verbose}')
# Click is a popular third-party alternative
# pip install click
import click

@click.command()
@click.argument('input')
@click.option('--output', '-o', default='out.txt', help='Output file')
@click.option('--verbose', '-v', is_flag=True, help='Verbose mode')
def cli(input, output, verbose):
    click.echo(f'Input: {input}, Output: {output}, Verbose: {verbose}')

if __name__ == '__main__':
    cli()
# Subcommands with argparse (like git push / git pull)
import argparse

parser = argparse.ArgumentParser(prog='mytool')
sub = parser.add_subparsers(dest='command', required=True)

# push subcommand
push = sub.add_parser('push', help='Push data to remote')
push.add_argument('--force', action='store_true', help='Force push')

# pull subcommand
pull = sub.add_parser('pull', help='Pull data from remote')
pull.add_argument('--depth', type=int, default=1, help='Clone depth')

args = parser.parse_args()
print(f'Command: {args.command}, Force: {getattr(args, "force", False)}')

JavaScript

// Node.js built-in process.argv is the raw array
const args = process.argv.slice(2);
console.log(args);
// Commander.js is the most popular CLI framework for Node.js
// npm install commander
import { Command } from 'commander';
const program = new Command();

program
  .argument('<input>', 'Input file path')
  .option('-o, --output <file>', 'Output file path', 'out.txt')
  .option('-v, --verbose', 'Enable verbose logging')
  .action((input, options) => {
    console.log(`Input: ${input}, Output: ${options.output}, Verbose: ${options.verbose}`);
  });

program.parse();

Java

// picocli is the modern standard for Java CLI
// Maven: info.picocli:picocli
import picocli.CommandLine;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;

@CommandLine.Command(name = "process", mixinStandardHelpOptions = true)
public class ProcessFile implements Callable<Integer> {
    @Parameters(index = "0", description = "Input file path")
    private String input;

    @Option(names = {"-o", "--output"}, defaultValue = "out.txt")
    private String output;

    @Option(names = {"-v", "--verbose"})
    private boolean verbose;

    @Override
    public Integer call() {
        System.out.printf("Input: %s, Output: %s, Verbose: %b%n", input, output, verbose);
        return 0;
    }

    public static void main(String[] args) {
        int exitCode = new CommandLine(new ProcessFile()).execute(args);
        System.exit(exitCode);
    }
}

Explanation

CLI frameworks take the raw argument array (sys.argv, process.argv, or String[] args) and turn it into typed values. They generate help text, validate required arguments, and cast values like --count 5 to integers. Boolean flags, positional arguments, variadic inputs, subcommands — all handled.

Python’s argparse ships with the standard library and covers most scripts. Click takes a decorator-based approach that composes more cleanly. For JavaScript, commander is the go-to choice with its chainable API. picocli uses Java annotations and plays well with GraalVM native-image compilation for fast-startup CLIs.

A pattern you’ll see often: pair CLI flags with a config file. Read --config settings.yaml first, then let flags from the command line override specific values. See Parse YAML Files in Python and JavaScript and Parse TOML Files in Python and Java for examples of reading config files that pair well with CLI parsing.

If your CLI processes structured input, validate it against a schema before running. Check Validate JSON Schema in Python and JavaScript for schema validation patterns that work alongside CLI argument parsing.

Variants

TechnologyLibraryApproachNotes
PythonargparseStandard libraryZero dependencies, auto-generated help
PythonClickDecoratorsComposable, supports progress bars and prompts
PythontyperType hintsBuilt on Click, uses Python 3.6+ annotations
JavaScriptcommanderFluent APIMost popular, supports subcommands
JavaScriptyargsMiddleware chainHighly extensible, good for complex CLIs
JavapicocliAnnotationsAuto-completion scripts, native-image support
JavaApache Commons CLIBuilder patternOlder but widely used in enterprise

Best Practices

Use standard libraries first (argparse, process.argv) for simple scripts to avoid dependency bloat. Add -h and --help flags to every CLI, because the frameworks generate them automatically. Validate required arguments early and show friendly errors, not raw stack traces.

Support --version so users and CI/CD pipelines can pin tooling versions. Exit with 0 for success and a non-zero code on failure, so calling shell scripts can spot problems.

Common Mistakes

Parsing process.argv manually instead of using a framework leads to brittle, unmaintainable code. When required arguments go missing, users should see help text, not a stack trace.

Mutating global state in CLI handlers makes both testing and composition harder. Ignoring exit codes means CI/CD pipelines can’t detect failures when the CLI always exits with 0. Over-engineering subcommands is also common: one script with flags is usually simpler than a multi-level CLI.

When Not to Use

A script that only needs a single fixed input doesn’t need a CLI — use a function or an environment variable. If a tool is only run by other scripts, a configuration file or environment variables may be cleaner than flags. For hot paths where parsing overhead matters, prefer pre-parsed or compiled options.

Summary

  • Use argparse for Python scripts with zero dependencies; reach for Click when you need composition and decorators.
  • commander is the standard choice for Node.js CLIs, with yargs as a more extensible alternative.
  • picocli is the modern Java standard, with annotation-based config and GraalVM native-image support.
  • Always provide --help and --version flags; frameworks generate them automatically.
  • Exit 0 on success, non-zero on failure — that’s how CI/CD pipelines detect problems.
  • Combine CLI flags with config files (YAML, TOML) for tools that need persistent or complex configuration.
  • Full companion code with tests: stack-practices-resources.

See Also

Frequently Asked Questions

How do I handle environment variables alongside CLI arguments?

Use libraries that natively support env var fallbacks. Click exposes envvar=, and picocli exposes defaultValue = "${ENV_VAR}". Environment variables are ideal for secrets and deployment-specific values that shouldn't appear in shell history.

What is the best way to test CLI applications?

Invoke the CLI entry point as a function rather than spawning subprocesses. Python Click has runner.invoke(), picocli has CommandLine.execute() in-process, and commander can be tested by calling .parse() with a mock argv array. That runs much faster than shell-based testing.

How do I build a CLI with subcommands?

Every major framework supports subcommands. Use add_subparsers() in argparse, .command() in commander, and @Command on nested classes in picocli. Put shared options in a parent class or mixin to avoid duplication.

How do I validate argument types beyond what the framework provides?

Each framework lets you plug in custom validation. argparse accepts any callable as type=. Click gives you click.IntRange and custom ParamType subclasses. commander takes a parse function as the third argument to .option(). picocli uses @Option(type=...) with built-in or custom ITypeConverter implementations. Validate early and fail with a clear error message.

What exit codes should I use?

Follow the POSIX convention: 0 for success, 1 for general errors, 2 for argument parsing errors. argparse exits with 2 on parse failures by default. picocli returns the value from call() as the exit code. commander sets process.exitCode rather than calling process.exit() directly, so async handlers can finish first.

How do I add autocomplete to my CLI?

argparse doesn't support autocomplete natively, but argcomplete adds it with a single decorator. Click produces completion scripts through click.completion. commander handles completion via the commander-completion plugin. picocli includes picocli.AutoComplete for generating completion scripts.