clipanion

repository·master·Indexed 23 days ago

https://github.com/arcanis/clipanion

A type-safe CLI library and framework with no runtime dependencies, designed to support advanced typing, nested commands, and transparent option proxying. It allows developers to create commands by extending the Command abstract base class and provides helpers like run and runExit for execution. It integrates with Typanion for complex validation and coercion, and includes built-in commands for help, versioning, and JSON specification exports.

Tokens
10.3K
Snippets
36
Records
58
Agent score
78%

What's inside clipanion

  1. Overview of Clipanion CLI framework

    master
    Clipanion is a CLI framework designed to be correct, full-featured, and type-safe. It aims to provide consistent and predictable behaviors for CLI option definitions without requiring custom code for common CLI patterns. Unlike modular frameworks, Clipanion is distributed as a single package (clipanion) to ensure all features are readily available. It avoids using Domain-Specific Languages (DSL) in favor of standard JavaScript/TypeScript to maintain compatibility with existing tooling and language features.
  2. Use Counter options for verbosity levels

    master

    Counters are boolean options that track how many times they are enabled. You can increase the count by repeating the shorthand (e.g., -vvv) and reset the counter to zero using the --no- prefix.

    Example:

    -vvvv
        => Command {"v": 4}
    -vvvv --no-verbose
        => Command {"v": 0}
  3. Use Boolean options and Batches

    master

    Boolean options are enabled by their presence. Clipanion automatically supports "Batches" for boolean options: if you have multiple boolean options with shorthands (e.g., -p, -i, -e), you can combine them into a single argument.

    Example of boolean toggling:

    --foo
        => Command {"foo": true}
    --no-foo
        => Command {"foo": false}

    Example of batching shorthands:

    -pie
        => Command {"p": true, "i": true, "e": true}
  4. How commands work in Clipanion

    master

    Commands are created by extending the Command abstract base class. You must implement the execute method, which Clipanion calls when the command is run. The value returned by execute is used as the process exit code.

    class SuccessCommand extends Command {
        async execute() {
            return 0;
        }
    }
  5. Define command paths to structure CLI subcommands

    master

    By default, Clipanion treats all commands as top-level, meaning they can be executed from the first token. For complex CLI applications (like yarn), you should use the static paths property on your command class to define specific command hierarchies.

    A path is an array of strings that must appear in order for the command to be selected. You can provide multiple paths for a single command by using an array of arrays.

    import {Command} from 'clipanion';
    
    class InstallCommand extends Command {
        // This command matches either 'install' or 'i'
        static paths = [[`install`], [`i`]];
        async execute() {
            // ...
        }
    }
  6. Use Positional, Rest, and Proxy options

    master

    Clipanion provides three ways to handle arguments that are not tagged with flags:

    1. Positionals: Arguments that rely on a strict order. They can be required or optional.
    2. Rests: Arguments that accept an arbitrary amount of data. Unlike many frameworks, Clipanion allows you to place required positional arguments after a rest option.
    3. Proxies: Similar to rests, but they stop all further parsing once encountered. This is useful for commands that wrap other commands (e.g., yarn run).

    Examples:

    Rest option (aggregating everything following the command):

    yarn add webpack webpack-cli
        => Command {"rest": ["webpack", "webpack-cli"]}

    Rest option with trailing positionals (e.g., cp command):

    cp src1 src2 src3 dest/
        => Command {"srcs": ["src1", "src2", "src3"], "dest": "dest/"}

    Proxy option (stopping parsing to pass arguments to a sub-command):

    yarn run foo --hello --world
        => Command {"proxy": ["--hello", "--world"]}
  7. Handle overlapping command paths

    master

    Clipanion supports hierarchical command structures where one path is a prefix of another. It will select the most specific command that matches the input tokens.

    For example, if FooCommand has path ['foo'] and FooBarCommand has path ['foo', 'bar']:

    • Running foo bar executes FooBarCommand.
    • Running foo executes FooCommand.

    Ambiguity Note: You can have multiple commands with identical paths if they have different options. However, if the user invokes the command without specifying an option that distinguishes between them, Clipanion will throw an AmbiguousSyntaxError.

    import {Command} from 'clipanion';
    
    class FooCommand extends Command {
        static paths = [[`foo`]];
        async execute() {
            // ...
        }
    }
    
    class FooBarCommand extends Command {
        static paths = [[`foo`, `bar`]];
        async execute() {
            // ...
        }
    }
  8. Use Array options for multiple values or tuples

    master

    Array options allow a single option key to be set multiple times, aggregating the values into an array. If you define the option as a tuple, each occurrence will be an array of strings.

    Example of multiple string values:

    --email foo@baz --email bar@baz
        => Command {"email": ["foo@baz", "bar@baz"]}

    Example of tuples:

    --point x1 y1 --point x2 y2
        => Command {"point": [["x1", "y1"], ["x2", "y2"]]}
  9. Inherit options and positionals using class inheritance

    master

    Since Clipanion commands are standard ES6 classes, you can use class inheritance to share options and positionals across multiple commands.

    • Options: Subclasses inherit all options defined in the superclass.
    • Positionals: Positionals are inherited and consumed in order, starting from the superclass definitions. If a superclass defines a positional, it will be satisfied by the first argument provided before the subclass's positionals are evaluated.
    import {Command, Option} from 'clipanion';
    // ---cut---
    abstract class BaseCommand extends Command {
        foo = Option.String();
    
        abstract execute(): Promise<number | void>;
    }
    
    class FooCommand extends BaseCommand {
        bar = Option.String();
    
        async execute() {
            this.context.stdout.write(`This is foo: ${this.foo}.\n`);
            this.context.stdout.write(`This is bar: ${this.bar}.\n`);
        }
    }

    // Example usage: // hello world // => Command {"foo": "hello", "bar": "world"}

  10. Understand Execution Contexts in Clipanion

    master

    In Clipanion, a context is an arbitrary object provided to all commands via this.context during execution. This allows you to pass environment options like cwd, user auth tokens, or configuration throughout your command hierarchy.

    By default, the context includes standard I/O streams and environment information. Using streams instead of console.log allows commands to intercept and capture the output of other commands, enabling better composition.

    To automatically route console.log calls to the correct streams (even during parallel execution), set enableCapture: true in your CLI configuration.

    interface BaseContext {
        env: Record<string, string | undefined>;
        stdin: Readable;
        stdout: Writable;
        stderr: Writable;
        colorDepth: number;
    }
  11. Choose the right error strategy in Clipanion

    master

    Clipanion handles errors differently depending on whether you throw a standard Error, a UsageError, or return an exit code. Use the following rules of thumb to decide how to signal failure:

    • Unexpected internal errors: Throw a standard Error. Clipanion will catch it and print the full stacktrace.
    • Invalid user input/environment: Throw a UsageError. Clipanion will display only the provided message and the command usage line, suppressing the stacktrace.
    • Expected non-zero exit conditions: Return 1 (or another non-zero integer) from your command. This is for cases where the command executed correctly but signaled a specific state to the caller (e.g., a linter finding errors or grep finding no matches). Clipanion will not print any additional error messages in this case.
  12. Add options to existing commands via inheritance

    master

    You can extend an existing command to add new options or modify behavior. To do this, create a new class that extends the target Command class.

    Note: To add options to a command, you must have access to its specific Command class. If you are using a plugin system where multiple plugins register commands, you must ensure the commands are structured in an inheritance chain (e.g., CommandB extends CommandA) rather than all extending a single base class if you want to layer options.

    import {Command, Option} from 'clipanion';
    // ---cut---
    class GreetCommand extends Command {
      static paths = [[`greet`]];
    
      name = Option.String();
    
      greeting = Option.String(`--greeting`, `Hello`);
    
      async execute(): Promise<number | void> {
        this.context.stdout.write(`${this.greeting} ${this.name}!\n`);
      }
    }
    
    class GreetWithReverseCommand extends GreetCommand {
      reverse = Option.Boolean(`--reverse`, {required: true});
    
      async execute() {
        return await this.cli.run([`greet`, this.reverse ? this.name.split(``).reverse().join(``) : this.name, `--greeting`, this.greeting]);
      }
    }