Skip to main content
โšก Calmops

Building CLI Tools: Command-Line Applications with Modern Tools

Introduction

Command-line tools remain essential for developer productivity. A well-designed CLI can become a daily driver for users. This guide covers building CLI applications with modern frameworks, handling arguments, and creating delightful experiences.

A great CLI is discoverable, consistent, and provides helpful feedback.

Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    CLI Architecture                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚   โ”‚   Entry     โ”‚                                           โ”‚
โ”‚   โ”‚   Point     โ”‚                                           โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚          โ”‚                                                   โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚   โ”‚   Command   โ”‚                                           โ”‚
โ”‚   โ”‚   Parser    โ”‚                                           โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚          โ”‚                                                   โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚   โ”‚   Commands  โ”‚                                           โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚          โ”‚                                                   โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”                                           โ”‚
โ”‚   โ”‚   Business  โ”‚                                           โ”‚
โ”‚   โ”‚    Logic    โ”‚                                           โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                           โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Python CLI with Click

import click
from typing import Optional

@click.group()
@click.version_option(version="1.0.0")
def cli():
    """A sample CLI application."""
    pass

@cli.command()
@click.argument("name")
@click.option(
    "--count", "-c",
    default=1,
    type=int,
    help="Number of greetings"
)
@click.option(
    "--excited",
    is_flag=True,
    help="Add excitement"
)
def hello(name: str, count: int, excited: bool):
    """Greet NAME with enthusiasm."""
    exclamation = "!" if excited else "."
    
    for _ in range(count):
        if excited:
            click.echo(f"Hello, {name}{exclamation * 3}")
        else:
            click.echo(f"Hello, {name}{exclamation}")

@cli.command()
@click.argument("file", type=click.Path(exists=True))
@click.option("--lines", "-n", type=int, help="Number of lines")
def tail(file: str, lines: Optional[int]):
    """Display the last lines of FILE."""
    with open(file) as f:
        content = f.readlines()
    
    start = -lines if lines else -10
    for line in content[start:]:
        click.echo(line.rstrip())

@cli.group()
def config():
    """Manage configuration."""
    pass

@config.command("set")
@click.argument("key")
@click.argument("value")
def config_set(key: str, value: str):
    """Set a configuration value."""
    click.echo(f"Setting {key}={value}")

@config.command("get")
@click.argument("key")
def config_get(key: str):
    """Get a configuration value."""
    click.echo(f"{key}=some_value")

if __name__ == "__main__":
    cli()

Go CLI with Cobra

package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

var (
	rootCmd = &cobra.Command{
		Use:   "app",
		Short: "A sample CLI application",
		Long:  `A longer description that spans multiple lines.`,
	}

	name    string
	excited bool
	count   int
)

func init() {
	rootCmd.AddCommand(helloCmd)
	
	helloCmd.Flags().StringVarP(&name, "name", "n", "World", "Name to greet")
	helloCmd.Flags().BoolVarP(&excited, "excited", "e", false, "Add excitement")
	helloCmd.Flags().IntVarP(&count, "count", "c", 1, "Number of greetings")
}

var helloCmd = &cobra.Command{
	Use:   "hello",
	Short: "Greet someone",
	Run: func(cmd *cobra.Command, args []string) {
		exclamation := "."
		if excited {
			exclamation = "!!!"
		}
		
		for i := 0; i < count; i++ {
			fmt.Printf("Hello, %s%s\n", name, exclamation)
		}
	},
}

func main() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Interactive Prompts

import click
from click import prompt

@cli.command()
def create_project():
    """Create a new project."""
    name = prompt("Project name", default="my-project")
    
    # With validation
    framework = prompt(
        "Framework",
        type=click.Choice(["react", "vue", "angular", "svelte"]),
        default="react"
    )
    
    # Boolean confirmation
    use_typescript = click.confirm("Use TypeScript?", default=True)
    
    # Password input
    api_key = click.prompt("API Key", hide_input=True)
    
    # Display summary
    click.echo(f"\nCreating project: {name}")
    click.echo(f"Framework: {framework}")
    click.echo(f"TypeScript: {use_typescript}")

Progress and Feedback

import click
import time

@cli.command()
def process_files():
    """Process multiple files with progress."""
    files = [f"file_{i}.txt" for i in range(10)]
    
    with click.progressbar(files, label="Processing files") as bar:
        for file in bar:
            # Simulate processing
            time.sleep(0.1)

@cli.command()
def long_task():
    """A long-running task with spinner."""
    with click_spinner.spinner():
        # Do work
        time.sleep(3)
    
    click.echo("Done!")

@cli.command()
def download():
    """Download with progress bar."""
    with click.progressbar(length=100, label="Downloading") as bar:
        for i in range(10):
            time.sleep(0.1)
            bar.update(10)

Configuration

import click
from click import Config

@click.command()
@click.option("--config", type=click.Path(), help="Config file path")
@click.option("--debug", is_flag=True, help="Enable debug mode")
def main(config: str, debug: bool):
    """CLI application with config."""
    pass

# Using click's built-in config
# Create ~/.myapp.yaml
# Automatically loads from config file

Best Practices

  1. Use subcommands: Organize functionality
  2. Provide defaults: Sensible defaults reduce friction
  3. Validate input: Clear error messages
  4. Show progress: For long operations
  5. Color output: Use colors for errors and success
  6. Document thoroughly: Help text for every command

Conclusion

Great CLI tools empower users to be productive. By following these patterns and using modern frameworks, you can create CLI applications that are a joy to use.

Comments