Files
bun.sh/docs/cli-parser-design.md
Claude Bot 94c24d708e Add Bun.CLI flag parser and interactive system
Implemented comprehensive CLI functionality:
- Fast zero-allocation flag parser for common cases
- Support for multiple flag types (boolean, string, number, array)
- Interactive prompts with TTY detection and fallback
- Performance-optimized with single-pass parsing
- Full TypeScript API with type definitions

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 12:52:06 +00:00

7.0 KiB

Bun CLI Parser & Interactive System Design

Three Implementation Approaches

Approach 1: TypeScript-First with Zig Backend

Pros: Familiar API, easy to extend, good DX Cons: JS overhead for parsing

// User-facing API
const cli = Bun.CLI.create({
  name: "myapp",
  version: "1.0.0",
  flags: {
    verbose: { type: "boolean", short: "v", default: false },
    port: { type: "number", short: "p", default: 3000 },
    files: { type: "array", of: "string" },
    config: { type: "string", env: "MY_APP_CONFIG" }
  }
});

const args = cli.parse();

// Interactive mode
if (!args.config && cli.isTTY) {
  args.config = await cli.prompt.select({
    message: "Choose config",
    choices: ["dev", "prod", "test"]
  });
}

Approach 2: Pure Zig with Code Generation

Pros: Zero overhead, compile-time validation, fastest possible Cons: Less flexible, harder to maintain

// Generated from schema at build time
const MyAppCLI = generateCLI(.{
    .name = "myapp",
    .flags = .{
        .verbose = .{ .type = .boolean, .short = 'v' },
        .port = .{ .type = .number, .short = 'p', .default = 3000 },
        .files = .{ .type = .array, .of = .string },
    },
});

// Runtime usage
const args = try MyAppCLI.parse(std.os.argv);

Pros: Best of both worlds, progressive enhancement Cons: More complex implementation

// Fast path for simple cases
const args = Bun.CLI.parseSimple(); // Zero alloc for basic flags

// Full featured for complex cases
const cli = new Bun.CLI({
  // Schema definition
  schema: {
    commands: {
      serve: {
        flags: {
          port: { type: "number", short: "p" },
          host: { type: "string", short: "h" }
        }
      }
    }
  },

  // Performance hints
  hints: {
    maxArgs: 10,        // Pre-allocate buffers
    commonFlags: ["v", "h", "help"], // Optimize these
    lazyInteractive: true  // Load interactive only when needed
  }
});

Core Implementation Plan

Phase 1: Fast Flag Parser (Week 1)

// Core parser with zero allocations for common cases
export namespace Bun.CLI {
  export interface ParseOptions {
    // Stop at first non-flag
    stopEarly?: boolean;
    // Allow unknown flags
    allowUnknown?: boolean;
    // Parse numbers/booleans
    autoType?: boolean;
  }

  export function parse(
    args?: string[],
    options?: ParseOptions
  ): Record<string, any>;

  // Fast path for simple cases
  export function parseSimple(args?: string[]): {
    _: string[];
    [key: string]: any;
  };
}

Phase 2: Type-Safe Schema (Week 2)

// Type-safe flag definitions
export interface FlagSchema {
  type: "string" | "number" | "boolean" | "array" | "enum";
  short?: string;
  long?: string;
  default?: any;
  required?: boolean;
  env?: string;  // Environment variable fallback
  validate?: (value: any) => boolean | string;
  transform?: (value: any) => any;
}

// Advanced array handling
export interface ArrayFlagSchema extends FlagSchema {
  type: "array";
  of: "string" | "number";
  separator?: string;  // For comma-separated values
  accumulate?: boolean; // Multiple --flag values
}

Phase 3: Interactive System (Week 3)

export namespace Bun.CLI.Interactive {
  // TTY detection with fallback
  export const isTTY: boolean;

  // Core prompt types
  export interface PromptOptions {
    message: string;
    default?: any;
    validate?: (input: any) => boolean | string;
    // Non-TTY fallback
    fallback?: () => any;
  }

  export async function text(options: PromptOptions): Promise<string>;
  export async function confirm(options: PromptOptions): Promise<boolean>;
  export async function select<T>(options: SelectOptions<T>): Promise<T>;
  export async function multiselect<T>(options: MultiSelectOptions<T>): Promise<T[]>;

  // Advanced: Form with multiple fields
  export async function form<T>(schema: FormSchema): Promise<T>;
}

Phase 4: Performance Optimizations

// Zig backend for hot paths
pub const FastParser = struct {
    allocator: std.mem.Allocator,
    args_buffer: [256][]const u8, // Stack allocation for common case

    pub fn parse(args: []const []const u8) ParseResult {
        // Single pass parsing
        // Zero-copy string slicing
        // Compile-time type coercion
    }
};

// Incremental renderer for interactive mode
pub const IncrementalRenderer = struct {
    last_frame: []u8,
    dirty_regions: std.ArrayList(Region),

    pub fn render(content: []const u8) !void {
        // Diff-based updates
        // Batched escape sequences
        // Adaptive frame rate
    }
};

Performance Benchmarks Target

Simple flag parsing (10 args):
- Target: < 100ns
- Baseline (minimist): ~1μs

Complex parsing (100 args, nested commands):
- Target: < 1μs
- Baseline (yargs): ~50μs

Interactive prompt render:
- Target: < 1ms per frame
- 60fps for smooth animations

Memory usage:
- Zero allocations for < 16 args
- Single arena for complex parsing
- Reusable buffers for interactive

Edge Cases Handled

  1. No TTY: Graceful fallback to simple prompts or defaults
  2. Piped input: Detect and handle stdin/stdout pipes
  3. CI environment: Auto-detect CI and disable interactive
  4. Windows Terminal: Special handling for Windows console
  5. SSH sessions: Detect and adapt rendering
  6. Screen readers: Accessibility mode with plain text
  7. Partial args: Handle incomplete flag values
  8. Unicode: Full UTF-8 support in prompts
  9. Signals: Proper cleanup on SIGINT/SIGTERM
  10. Large inputs: Stream processing for huge arg lists

API Examples

Basic Usage

// Simple parsing
const args = Bun.CLI.parse();
console.log(args.verbose, args.port);

// With schema
const cli = Bun.CLI.create({
  flags: {
    verbose: { type: "boolean", short: "v" },
    port: { type: "number", default: 3000 }
  }
});
const { verbose, port } = cli.parse();

Interactive Usage

// Auto-detect TTY and fallback
const name = await Bun.CLI.prompt.text({
  message: "Your name?",
  fallback: () => process.env.USER || "anonymous"
});

// Complex form
const config = await Bun.CLI.prompt.form({
  fields: {
    database: { type: "select", choices: ["postgres", "mysql", "sqlite"] },
    port: { type: "number", default: 5432 },
    ssl: { type: "confirm", default: true }
  }
});

Advanced Subcommands

const cli = Bun.CLI.create({
  commands: {
    serve: {
      handler: (args) => startServer(args),
      flags: { port: { type: "number" } }
    },
    build: {
      handler: (args) => runBuild(args),
      flags: { watch: { type: "boolean" } }
    }
  }
});

await cli.run();

Testing Strategy

  1. Unit tests: Each parser component
  2. Integration tests: Full CLI flows
  3. Performance tests: Benchmark suite
  4. TTY simulation: Mock terminal tests
  5. Cross-platform: Windows, macOS, Linux
  6. Fuzzing: Random input generation
  7. Memory tests: Leak detection
  8. Stress tests: Large arg counts