Skip to content

Subcommands

In all of the examples so far, we've had a single command, with a single set of parameters. This works fine for smaller applications, but once a tool starts to grow in scope, it is useful to start breaking up it's functionality into descrete units using subcommands.

What is a Subcommand?

A subcommand is an arc command object that lives underneath another command object. Each subcommand will have it's own callback function and it's own set of parameters. When ran from the commandline it will look a little something like this

$ python example.py <global-options> <subcommand> <options>

Example

There are a couple of ways to define subcommands, in this doc we'll focus on the most common

examples/subcommand.py
import arc


@arc.command()
def command():
    print("hello there!")


@command.subcommand()
def sub1():
    print("This is sub 1")


@command.subcommand()
def sub2():
    print("This is sub 2")


command()

We can then execute each subcommand by referring to them by name.

$ python subcommand.py sub1
hello there!
This is sub 1
$ python subcommand.py sub2
hello there!
This is sub 2

Documentation

Subcommands also get their own --help

$ python subcommand.py --help
USAGE
    subcommand.py [-h]
    subcommand.py <subcommand> [ARGUMENTS ...]

OPTIONS
    --help (-h)  Displays this help message

SUBCOMMANDS
    sub1
    sub2

$ python subcommand.py sub1 --help
hello there!
USAGE
    subcommand.py sub1 [-h]

OPTIONS
    --help (-h)  Displays this help message

$ python subcommand.py sub2 --help
hello there!
USAGE
    subcommand.py sub2 [-h]

OPTIONS
    --help (-h)  Displays this help message
They're all pretty bare-bones right now, but they will fill out as the application grows.

Nesting Subcommands

Subcommands can be arbitrarly nested

examples/nested_subcommands.py
import arc


@arc.command()
def command():
    print("hello there!")


@command.subcommand()
def sub1():
    print("This is sub 1")


@sub1.subcommand()
def nested1():
    print("This is nested 1")


@command.subcommand()
def sub2():
    print("This is sub 2")


@sub2.subcommand()
def nested2():
    print("This is nested 2")


command()

$ python nested_subcommands.py sub1 nested1
hello there!
This is nested 1
$ python nested_subcommands.py sub2 nested2
hello there!
This is nested 2

Global Parameters

When you define subcommands for you application, the root @arc.command() object becomes a callback for global parameters. This is useful for defining things that can apply to the entirety of your applcation. For example, logging configuration.

examples/global_params.py
import logging
import arc


@arc.command()
def command(*, verbose: bool):
    logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO)


@command.subcommand()
def sub():
    logging.info("This is an info message")
    logging.debug("This is a debug message")


command()
$ python global_params.py sub
INFO:root:This is an info message
$ python global_params.py --verbose sub
INFO:root:This is an info message
DEBUG:root:This is a debug message

Global arguments must come before the name of the command for them to be interpreted correctly. You can see this reflected in the --help

$ python global_params.py --help
USAGE
    global_params.py [-h]
    global_params.py [--verbose] <subcommand> [ARGUMENTS ...]

OPTIONS
    --help (-h)  Displays this help message
    --verbose

SUBCOMMANDS
    sub

Things to be aware of

  • The root object can no longer define argument parameters, because arc doesn't have a way of knowing what is a positional parameter and what is the name of a command. Note: This may change in the future if I have time / care to change it.
  • The root callback will be executed before every subcommand, regardless of whether or not you actually provide any global arguments. If this behavior isn't desired, it can be changed with arc.configure(global_callback_execution="args_present")

Naming Subcommands

By default, commands are the kebab-case version of the function they decorate. This can be modified to change the name

import arc

@arc.command()
def command():
    ...

@command.subcommand("some-other-name")
def sub():
    ...

command()

or provide multiple names, for a command.

import arc

@arc.command()
def command():
    ...

@command.subcommand(("some-other-name", "another-name", "a-third-name"))
def sub():
    ...

command()

Subcommands in other Files

Breaking up your CLI interface into multiple files in arc is a very straightforward process.

import arc

@arc.command()
def sub():
    print("This is the subcommand")

# Notice, no call to sub()
import arc
from subcommand import sub


@arc.command()
def cli():
    print('hello there!')

# Here we add sub as a subcommand to cli
cli.add_command(sub)

cli()