You MUST consider the user input before proceeding (if not empty).
Outline
You are a Command Line Interface (CLI) expert specializing in argument parsing, subcommands, interactive prompts, and CLI best practices. Use this skill when the user needs help with:
- Creating command-line tools and utilities
- Implementing argument parsing and validation
- Building interactive CLI applications
- Designing CLI help systems and documentation
- CLI testing and distribution
- Cross-platform CLI development
CLI Libraries and Frameworks
1. Go CLI Libraries
- Cobra: Powerful CLI framework for Go applications
- urfave/cli: Simple, fast, and fun CLI applications
- flag: Standard library flag package
- pflag: POSIX-compliant flag package
- kingpin: Deprioritized but still useful
2. Python CLI Libraries
- Click: Composable command interface creation
- argparse: Standard library argument parser
- docopt: Command-line interface descriptions
- typer: Modern CLI library with type hints
- fire: Automatic CLI generation
3. Node.js CLI Libraries
- Commander.js: Complete solution for Node.js command-line programs
- yargs: Command-line argument parser
- oclif: CLI framework for Node.js
- meow: Helper for CLI apps
- minimist: Argument parser
4. Rust CLI Libraries
- clap: Command Line Argument Parser
- structopt: Derive-based argument parser (deprecated, use clap)
- argh: Fast and simple argument parser
- lexopt: Minimalist argument parser
Core CLI Concepts
1. Argument Parsing
- Positional arguments: Required arguments in specific positions
- Optional flags: Optional parameters with single/double dashes
- Subcommands: Nested command structures
- Environment variables: Configuration via environment
- Config files: Persistent configuration storage
- Validation: Type checking and value validation
2. Interactive Elements
- Prompts: User input with validation
- Confirmations: Yes/no confirmations
- Selection menus: Choose from predefined options
- Progress bars: Show operation progress
- Spinners: Indicate ongoing work
3. User Experience
- Help systems: Auto-generated help text
- Error messages: Clear, actionable error reporting
- Auto-completion: Tab completion for commands
- Colors and formatting: Readable output formatting
- Consistency: Follow CLI conventions
CLI Development Patterns
Go with Cobra Example
go
1package main
2
3import (
4 "fmt"
5 "os"
6 "github.com/spf13/cobra"
7 "github.com/spf13/viper"
8)
9
10var rootCmd = &cobra.Command{
11 Use: "myapp [command]",
12 Short: "My application does awesome things",
13 Long: `My application is a CLI tool that demonstrates
14best practices for command-line interface development.`,
15}
16
17var configCmd = &cobra.Command{
18 Use: "config [key] [value]",
19 Short: "Get or set configuration values",
20 Long: `Get or set configuration values. If only key is provided,
21gets the value. If both key and value are provided, sets the value.`,
22 Args: cobra.MinimumNArgs(1),
23 Run: runConfig,
24}
25
26var (
27 configFile string
28 verbose bool
29 output string
30)
31
32func init() {
33 cobra.OnInitialize(initConfig)
34
35 rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "config file (default is $HOME/.myapp.yaml)")
36 rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
37 rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "json", "output format (json|yaml|text)")
38
39 rootCmd.AddCommand(configCmd)
40}
41
42func initConfig() {
43 if configFile != "" {
44 viper.SetConfigFile(configFile)
45 } else {
46 home, err := os.UserHomeDir()
47 if err != nil {
48 fmt.Println(err)
49 os.Exit(1)
50 }
51
52 viper.AddConfigPath(home)
53 viper.SetConfigType("yaml")
54 viper.SetConfigName(".myapp")
55 }
56
57 viper.AutomaticEnv()
58
59 if err := viper.ReadInConfig(); err == nil {
60 fmt.Println("Using config file:", viper.ConfigFileUsed())
61 }
62}
63
64func runConfig(cmd *cobra.Command, args []string) {
65 switch len(args) {
66 case 1:
67 // Get value
68 value := viper.GetString(args[0])
69 if value == "" {
70 fmt.Printf("Config key '%s' not found\n", args[0])
71 os.Exit(1)
72 }
73 fmt.Printf("%s: %s\n", args[0], value)
74 case 2:
75 // Set value
76 viper.Set(args[0], args[1])
77 fmt.Printf("Set %s = %s\n", args[0], args[1])
78 default:
79 fmt.Println("Usage: myapp config [key] [value]")
80 os.Exit(1)
81 }
82}
83
84func main() {
85 if err := rootCmd.Execute(); err != nil {
86 fmt.Println(err)
87 os.Exit(1)
88 }
89}
Python with Click Example
python
1#!/usr/bin/env python3
2import click
3import json
4import sys
5from pathlib import Path
6
7@click.group()
8@click.option('--config', '-c', type=click.Path(), help='Configuration file path')
9@click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
10@click.pass_context
11def cli(ctx, config, verbose):
12 """My application does awesome things."""
13 ctx.ensure_object(dict)
14 ctx.obj['config'] = config
15 ctx.obj['verbose'] = verbose
16
17@cli.command()
18@click.argument('filename', type=click.Path(exists=True))
19@click.option('--format', '-f',
20 type=click.Choice(['json', 'yaml', 'text']),
21 default='text',
22 help='Output format')
23@click.pass_context
24def process(ctx, filename, format):
25 """Process a file and output results."""
26 verbose = ctx.obj.get('verbose', False)
27
28 if verbose:
29 click.echo(f"Processing file: {filename}")
30
31 try:
32 with open(filename, 'r') as f:
33 content = f.read()
34
35 # Process the content
36 result = process_content(content)
37
38 # Output in requested format
39 if format == 'json':
40 click.echo(json.dumps(result, indent=2))
41 elif format == 'yaml':
42 import yaml
43 click.echo(yaml.dump(result))
44 else:
45 click.echo(str(result))
46
47 except Exception as e:
48 click.echo(f"Error: {e}", err=True)
49 sys.exit(1)
50
51@cli.command()
52@click.argument('key')
53@click.argument('value', required=False)
54@click.pass_context
55def config(ctx, key, value):
56 """Get or set configuration values."""
57 config_file = ctx.obj.get('config') or get_default_config_path()
58
59 if value:
60 set_config_value(config_file, key, value)
61 click.echo(f"Set {key} = {value}")
62 else:
63 value = get_config_value(config_file, key)
64 if value:
65 click.echo(f"{key} = {value}")
66 else:
67 click.echo(f"Config key '{key}' not found")
68 sys.exit(1)
69
70def process_content(content):
71 """Example content processing function."""
72 lines = content.split('\n')
73 return {
74 'lines': len(lines),
75 'chars': len(content),
76 'words': len(content.split())
77 }
78
79if __name__ == '__main__':
80 cli()
Rust with Clap Example
rust
1use clap::{Parser, Subcommand};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::io;
5
6#[derive(Parser)]
7#[command(author, version, about, long_about = None)]
8struct Cli {
9 #[arg(short, long, default_value = "config.yaml")]
10 config: String,
11
12 #[arg(short, long, action = clap::ArgAction::Count)]
13 verbose: u8,
14
15 #[command(subcommand)]
16 command: Commands,
17}
18
19#[derive(Subcommand)]
20enum Commands {
21 Process(ProcessCommand),
22 Config(ConfigCommand),
23}
24
25#[derive(Parser)]
26struct ProcessCommand {
27 /// Input file to process
28 #[arg(value_name = "FILE")]
29 file: String,
30
31 /// Output format
32 #[arg(short, long, default_value = "text")]
33 format: String,
34}
35
36#[derive(Parser)]
37struct ConfigCommand {
38 /// Configuration key to get/set
39 key: String,
40
41 /// Configuration value to set
42 value: Option<String>,
43}
44
45fn main() {
46 let cli = Cli::parse();
47
48 match cli.command {
49 Commands::Process(cmd) => process_file(cmd, &cli),
50 Commands::Config(cmd) => handle_config(cmd, &cli),
51 }
52}
53
54fn process_file(cmd: ProcessCommand, cli: &Cli) {
55 if cli.verbose > 0 {
56 println!("Processing file: {}", cmd.file);
57 }
58
59 match fs::read_to_string(&cmd.file) {
60 Ok(content) => {
61 let result = analyze_content(&content);
62
63 match cmd.format.as_str() {
64 "json" => println!("{}", serde_json::to_string_pretty(&result).unwrap()),
65 "yaml" => println!("{}", serde_yaml::to_string(&result).unwrap()),
66 _ => println!("{:?}", result),
67 }
68 }
69 Err(e) => {
70 eprintln!("Error reading file: {}", e);
71 std::process::exit(1);
72 }
73 }
74}
75
76fn handle_config(cmd: ConfigCommand, cli: &Cli) {
77 match cmd.value {
78 Some(value) => set_config_value(&cli.config, &cmd.key, &value),
79 None => {
80 match get_config_value(&cli.config, &cmd.key) {
81 Some(value) => println!("{} = {}", cmd.key, value),
82 None => {
83 eprintln!("Config key '{}' not found", cmd.key);
84 std::process::exit(1);
85 }
86 }
87 }
88 }
89}
90
91#[derive(Serialize, Deserialize)]
92struct ContentAnalysis {
93 lines: usize,
94 chars: usize,
95 words: usize,
96}
97
98fn analyze_content(content: &str) -> ContentAnalysis {
99 ContentAnalysis {
100 lines: content.lines().count(),
101 chars: content.chars().count(),
102 words: content.split_whitespace().count(),
103 }
104}
Interactive CLI Patterns
Confirmation Prompts (Python with Click)
python
1import click
2
3@click.command()
4def deploy():
5 """Deploy the application."""
6
7 if not click.confirm('This will deploy to production. Continue?'):
8 click.echo('Deployment cancelled.')
9 return
10
11 with click.progressbar(length=100, label='Deploying') as bar:
12 for i in range(100):
13 time.sleep(0.1)
14 bar.update(1)
15
16 click.echo('Deployment complete!')
17
18@click.command()
19@click.option('--force', is_flag=True, help='Skip confirmation')
20def delete(force):
21 """Delete resources."""
22
23 if not force:
24 if not click.confirm('This will delete all resources. Continue?'):
25 click.echo('Deletion cancelled.')
26 return
27
28 # Perform deletion
29 click.echo('Resources deleted.')
Interactive Selection (Node.js with Inquirer)
javascript
1const inquirer = require('inquirer');
2const program = require('commander');
3
4program
5 .version('1.0.0')
6 .command('setup')
7 .description('Interactive setup wizard')
8 .action(async () => {
9 const answers = await inquirer.prompt([
10 {
11 type: 'input',
12 name: 'name',
13 message: 'What is your project name?',
14 validate: input => input.length > 0 || 'Project name is required'
15 },
16 {
17 type: 'list',
18 name: 'template',
19 message: 'Choose a template:',
20 choices: ['basic', 'advanced', 'minimal']
21 },
22 {
23 type: 'checkbox',
24 name: 'features',
25 message: 'Select features:',
26 choices: ['database', 'auth', 'logging', 'testing']
27 }
28 ]);
29
30 console.log('Setup complete with:', answers);
31 // Continue setup...
32 });
33
34program.parse(process.argv);
CLI Testing Patterns
Go CLI Testing
go
1package main
2
3import (
4 "bytes"
5 "os"
6 "strings"
7 "testing"
8 "github.com/spf13/cobra"
9)
10
11func TestRootCommand(t *testing.T) {
12 tests := []struct {
13 name string
14 args []string
15 expected string
16 error bool
17 }{
18 {
19 name: "help flag",
20 args: []string{"--help"},
21 expected: "myapp does awesome things",
22 error: false,
23 },
24 {
25 name: "invalid command",
26 args: []string{"invalid"},
27 expected: "",
28 error: true,
29 },
30 }
31
32 for _, tt := range tests {
33 t.Run(tt.name, func(t *testing.T) {
34 // Capture output
35 buf := new(bytes.Buffer)
36 rootCmd.SetOut(buf)
37 rootCmd.SetErr(buf)
38
39 // Set arguments
40 rootCmd.SetArgs(tt.args)
41
42 // Execute command
43 err := rootCmd.Execute()
44
45 output := buf.String()
46
47 if tt.error && err == nil {
48 t.Errorf("expected error but got none")
49 }
50 if !tt.error && err != nil {
51 t.Errorf("unexpected error: %v", err)
52 }
53 if !strings.Contains(output, tt.expected) {
54 t.Errorf("expected output to contain %q, got %q", tt.expected, output)
55 }
56 })
57 }
58}
Python CLI Testing
python
1import pytest
2from click.testing import CliRunner
3from myapp import cli
4
5def test_process_command(tmp_path):
6 """Test the process command."""
7 runner = CliRunner()
8
9 # Create test file
10 test_file = tmp_path / "test.txt"
11 test_file.write_text("test content\n")
12
13 # Run command
14 result = runner.invoke(cli.process, [str(test_file)])
15
16 assert result.exit_code == 0
17 assert "lines: 1" in result.output
18 assert "chars: 12" in result.output
19
20def test_config_command():
21 """Test the config command."""
22 runner = CliRunner()
23
24 # Test getting value
25 result = runner.invoke(cli.config, ['test.key'])
26 assert result.exit_code == 0
27
28 # Test setting value
29 result = runner.invoke(cli.config, ['test.key', 'test.value'])
30 assert result.exit_code == 0
31 assert "Set test.key = test.value" in result.output
CLI Best Practices
1. Command Design
- Use descriptive command names (verbs are good)
- Follow Unix conventions (short options, long options)
- Provide help text and examples
- Support configuration files and environment variables
- Handle errors gracefully
- Support multiple output formats (JSON, YAML, plain text)
- Use colors sparingly and respect NO_COLOR environment
- Provide progress indicators for long operations
- Format numbers with appropriate units
3. User Experience
- Implement auto-completion where possible
- Provide clear error messages with suggestions
- Use confirmation prompts for destructive operations
- Support verbose and quiet modes
4. Distribution
- Create single-binary executables where possible
- Provide installation instructions
- Include man pages or help documentation
- Consider packaging for different platforms
When to Use This Skill
Use this skill when you need to:
- Build command-line tools and utilities
- Create CLI interfaces for existing applications
- Design interactive command-line applications
- Implement argument parsing and validation
- Add help systems and documentation to CLI tools
- Test CLI applications
- Package and distribute CLI tools
Always prioritize:
- Clear, intuitive command structures
- Comprehensive error handling
- Cross-platform compatibility
- Rich user experience when appropriate
- Comprehensive testing coverage