Skip to content

Subcommands

Introduction

Marking multiple functions with @arguably.command will make them show up as subcommands on the CLI:

import arguably

@arguably.command
def hello(name):
    """this will say hello to someone"""
    print(f"Hello, {name}!")

@arguably.command
def goodbye(name):
    """this will say goodbye to someone"""
    print(f"Goodbye, {name}!")

if __name__ == "__main__":
    arguably.run()

user@machine:~$ python3 goodbye-1.py -h
usage: goodbye-1.py [-h] command ...

positional arguments:
  command
    hello     this will say hello to someone
    goodbye   this will say goodbye to someone

options:
  -h, --help  show this help message and exit
user@machine:~$ python3 goodbye-1.py hello Python
Hello, Python!
user@machine:~$ python3 goodbye-1.py goodbye Python
Goodbye, Python!

Name normalization

Function names are first converted to lowercase. Single underscores _ in a function name are converted to a dash -. Also, any leading or trailing underscores are stripped.

  • def FOOBAR()foobar
  • def foo_bar():foo-bar
  • def list_():list
  • def _asdf():asdf
  • def __foo__():foo
  • def ___really_really_long_name():really-really-long-name

Multi-level subcommands

To add a subcommand to a parent command, separate their names with two underscores __. For example:

  • s3__lss3 ls
  • ec2__start_instancesec2 start-instances

Continuing this example:

import arguably

@arguably.command
def ec2__start_instances(*instances):
    """
    start instances
    Args:
        *instances: {instance}s to start
    """
    for inst in instances:
        print(f"Starting {inst}")

@arguably.command
def ec2__stop_instances(*instances):
    """
    stop instances
    Args:
        *instances: {instance}s to stop
    """
    for inst in instances:
        print(f"Stopping {inst}")

@arguably.command
def s3__ls(path="/"):
    """
    list objects
    Args:
        path: path to list under
    """
    print(f"Listing objects under {path}")

@arguably.command
def s3__cp(src, dst):
    """
    copy objects
    Args:
        src: source object
        dst: destination path
    """
    print(f"Copy {src} to {dst}")

if __name__ == "__main__":
    arguably.run()

user@machine:~$ python3 aws-1.py -h
usage: aws-1.py [-h] command ...

positional arguments:
  command
    ec2
    s3

options:
  -h, --help  show this help message and exit
user@machine:~$ python3 aws-1.py s3 -h
usage: aws-1.py s3 [-h] command ...

positional arguments:
  command
    ls        list objects
    cp        copy objects

options:
  -h, --help  show this help message and exit
user@machine:~$ python3 aws-1.py s3 ls -h
usage: aws-1.py s3 ls [-h] [path]

list objects

positional arguments:
  path        path to list under (type: str, default: /)

options:
  -h, --help  show this help message and exit
user@machine:~$ python3 aws-1.py s3 ls /foo/bar
Listing objects under /foo/bar

Hierarchy

You may have noticed that ec2 and s3 had no description. This is because they are automatically created stubs. We can define them ourselves and attach arguments to them:

@arguably.command
def s3(*, bucket):
    """
    s3 commands
    Args:
        bucket: the bucket to use
    """
    print(f"Using bucket: {bucket}")

user@machine:~$ python3 aws-2.py s3 -h
usage: aws-2.py s3 [-h] [--bucket BUCKET] command ...

s3 commands

positional arguments:
  command
    ls             list objects
    cp             copy objects

options:
  -h, --help       show this help message and exit
  --bucket BUCKET  the bucket to use (type: str)
user@machine:~$ python3 aws-2.py s3 --bucket mybucket ls
Using bucket: mybucket
Listing objects under /

As you can see, def s3(*, bucket) was called first and printed the bucket name to use. After that, def s3__ls(path="/") was invoked. This is because all ancestors are invoked before the target command is invoked. For a more complex example:

import arguably

@arguably.command
def first():
    print("first")

@arguably.command
def first__second():
    print("second")

@arguably.command
def first__second__third():
    print("third")

if __name__ == "__main__":
    arguably.run(always_subcommand=True)
user@machine:~$ python3 nested-1.py first second third
first
second
third

The __root__ function

If a function named __root__ is marked with @arguably.command, it always appears as the highest ancestor for commands in the script. This allows global options and actions to be placed at the root of the script.

import arguably

@arguably.command
def __root__():
    print("__root__")

@arguably.command
def hi():
    print("hi")

@arguably.command
def bye():
    print("bye")

if __name__ == "__main__":
    arguably.run()
user@machine:~$ python3 root-1.py hi
__root__
hi

Checking arguably.is_target()

Sometimes you'll want to allow a command in the heirarchy to process its input arguments, but bail if it wasn't the target. For that, you can use arguably.is_target(). This returns False if the currently-executing function was called as an ancestor of the target command, and True every other time.

import arguably

@arguably.command
def __root__(*, config_file=None):
    print(f"Using config {config_file}")
    if not arguably.is_target():
        return
    print("__root__ is the target!")

@arguably.command
def hi():
    print("hi is the target!")

@arguably.command
def bye():
    print("bye is the target!")

if __name__ == "__main__":
    arguably.run()

user@machine:~$ python3 root-2.py --config-file foo.yml
Using config foo.yml
__root__ is the target!
user@machine:~$ python3 root-2.py --config-file foo.yml hi
Using config foo.yml
hi is the target!