Files
2025-11-30 09:04:14 +08:00

7.7 KiB

Nested Subcommands

Multi-level command hierarchies like git config get or kubectl config view.

Template Reference

templates/nested-subparser.py

Overview

Create deep command structures:

  • mycli config get key
  • mycli config set key value
  • mycli deploy start production
  • mycli deploy stop production

Quick Start

# Two-level commands
python nested-subparser.py config get database_url
python nested-subparser.py config set api_key abc123
python nested-subparser.py config list

# Deploy subcommands
python nested-subparser.py deploy start production --replicas 3
python nested-subparser.py deploy stop staging

Architecture

mycli
├── config
│   ├── get <key>
│   ├── set <key> <value>
│   ├── list
│   └── delete <key>
└── deploy
    ├── start <environment>
    ├── stop <environment>
    └── restart <environment>

Implementation Pattern

1. Main Parser

parser = argparse.ArgumentParser(description='Multi-level CLI')
subparsers = parser.add_subparsers(dest='command', required=True)

2. First-Level Subcommand

# Create 'config' command group
config_parser = subparsers.add_parser(
    'config',
    help='Manage configuration'
)

# Create second-level subparsers under 'config'
config_subparsers = config_parser.add_subparsers(
    dest='config_command',
    required=True
)

3. Second-Level Subcommands

# config get
config_get = config_subparsers.add_parser('get', help='Get value')
config_get.add_argument('key', help='Configuration key')
config_get.set_defaults(func=config_get_handler)

# config set
config_set = config_subparsers.add_parser('set', help='Set value')
config_set.add_argument('key', help='Configuration key')
config_set.add_argument('value', help='Configuration value')
config_set.add_argument('--force', action='store_true')
config_set.set_defaults(func=config_set_handler)

Complete Example

#!/usr/bin/env python3
import argparse
import sys


# Config handlers
def config_get(args):
    print(f"Getting: {args.key}")
    return 0


def config_set(args):
    print(f"Setting: {args.key} = {args.value}")
    return 0


# Deploy handlers
def deploy_start(args):
    print(f"Starting deployment to {args.environment}")
    return 0


def main():
    parser = argparse.ArgumentParser(description='Nested CLI')
    subparsers = parser.add_subparsers(dest='command', required=True)

    # === Config group ===
    config_parser = subparsers.add_parser('config', help='Configuration')
    config_subs = config_parser.add_subparsers(
        dest='config_command',
        required=True
    )

    # config get
    get_parser = config_subs.add_parser('get')
    get_parser.add_argument('key')
    get_parser.set_defaults(func=config_get)

    # config set
    set_parser = config_subs.add_parser('set')
    set_parser.add_argument('key')
    set_parser.add_argument('value')
    set_parser.set_defaults(func=config_set)

    # === Deploy group ===
    deploy_parser = subparsers.add_parser('deploy', help='Deployment')
    deploy_subs = deploy_parser.add_subparsers(
        dest='deploy_command',
        required=True
    )

    # deploy start
    start_parser = deploy_subs.add_parser('start')
    start_parser.add_argument('environment')
    start_parser.set_defaults(func=deploy_start)

    # Parse and dispatch
    args = parser.parse_args()
    return args.func(args)


if __name__ == '__main__':
    sys.exit(main())

Accessing Nested Commands

args = parser.parse_args()

# Top-level command
print(args.command)  # 'config' or 'deploy'

# Second-level command
if args.command == 'config':
    print(args.config_command)  # 'get', 'set', 'list', 'delete'
elif args.command == 'deploy':
    print(args.deploy_command)  # 'start', 'stop', 'restart'

Help Output

Top-Level Help

usage: mycli [-h] {config,deploy} ...

positional arguments:
  {config,deploy}
    config         Manage configuration
    deploy         Manage deployments

Second-Level Help

$ python mycli.py config --help

usage: mycli config [-h] {get,set,list,delete} ...

positional arguments:
  {get,set,list,delete}
    get              Get configuration value
    set              Set configuration value
    list             List all configuration
    delete           Delete configuration value

Third-Level Help

$ python mycli.py config set --help

usage: mycli config set [-h] [-f] key value

positional arguments:
  key          Configuration key
  value        Configuration value

optional arguments:
  -h, --help   show this help message and exit
  -f, --force  Overwrite existing value

Dispatch Pattern

Option 1: Manual Switch

args = parser.parse_args()

if args.command == 'config':
    if args.config_command == 'get':
        config_get(args)
    elif args.config_command == 'set':
        config_set(args)
elif args.command == 'deploy':
    if args.deploy_command == 'start':
        deploy_start(args)
# Set handlers when creating parsers
config_get.set_defaults(func=config_get_handler)
config_set.set_defaults(func=config_set_handler)
deploy_start.set_defaults(func=deploy_start_handler)

# Simple dispatch
args = parser.parse_args()
return args.func(args)

Best Practices

1. Consistent Naming

# ✓ Good - consistent dest naming
config_parser.add_subparsers(dest='config_command')
deploy_parser.add_subparsers(dest='deploy_command')

2. Set Required

# ✓ Good - require subcommand
config_subs = config_parser.add_subparsers(
    dest='config_command',
    required=True
)

3. Provide Help

# ✓ Good - descriptive help at each level
config_parser = subparsers.add_parser(
    'config',
    help='Manage configuration',
    description='Configuration management commands'
)

4. Use set_defaults

# ✓ Good - easy dispatch
get_parser.set_defaults(func=config_get)

How Deep Should You Go?

✓ Good: 2-3 Levels

mycli config get key
mycli deploy start production

⚠️ Consider alternatives: 4+ Levels

mycli server database config get key  # Too deep

Alternatives:

  • Flatten: mycli db-config-get key
  • Split: Separate CLI tools
  • Use flags: mycli config get key --scope=server --type=database

Common Mistakes

Wrong: Same dest name

# Both use 'command' - second overwrites first
config_subs = config_parser.add_subparsers(dest='command')
deploy_subs = deploy_parser.add_subparsers(dest='command')
# ✓ Correct - unique dest names
config_subs = config_parser.add_subparsers(dest='config_command')
deploy_subs = deploy_parser.add_subparsers(dest='deploy_command')

Wrong: Accessing wrong level

args = parser.parse_args(['config', 'get', 'key'])

print(args.command)         # ✓ 'config'
print(args.config_command)  # ✓ 'get'
print(args.deploy_command)  # ✗ Error - not set

Wrong: Not checking hierarchy

# ✗ Assumes deploy command
print(args.deploy_command)
# ✓ Check first
if args.command == 'deploy':
    print(args.deploy_command)

Real-World Examples

Git-style

git config --global user.name "Name"
git remote add origin url
git branch --list

Kubectl-style

kubectl config view
kubectl get pods --namespace default
kubectl logs pod-name --follow

Docker-style

docker container ls
docker image build -t name .
docker network create name

Next Steps

  • Validation: See validation-patterns.md
  • Advanced: See advanced-parsing.md
  • Compare frameworks: See templates for Click/Typer equivalents