Example Scripts¶
In the spirit of "show, don't tell", here are a few example scripts. Some of the scripts may be broken up to better show the code for a subcommand next to its output.
Hello, world!¶
A "Hello, world!" script. Can accept a different name to greet, and has a --shout
option. Because there is only one
command, it's automatically selected - no need to specify a subcommand.
If you're not familiar, the *
works similarly to *args
- it separates positional args from keyword-only args. In
arguably
, keyword-only args each appear as an --option
.
import arguably
@arguably.command
def hello(name="world", *, shout=False):
"""
says hello to someone
Args:
name: {who} to greet
shout: will only use uppercase
"""
message = f"Hello, {name}!"
if shout:
message = message.upper()
print(message)
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 example-hello.py -h
usage: example-hello.py [-h] [--shout] [WHO]
says hello to someone
positional arguments:
WHO who to greet (type: str, default: world)
options:
-h, --help show this help message and exit
--shout will only use uppercase (type: bool, default: False)
user@machine:~$ python3 example-hello.py
Hello, world!
user@machine:~$ python3 example-hello.py Python
Hello, Python!
user@machine:~$ python3 example-hello.py --shout Python
HELLO, PYTHON!
Subcommands¶
A simple script showing subcommands and multi-level subcommands being used. Outputs for each command are shown next to the its code.
Imports and hey_there
¶
#!/usr/bin/env python3
"""this docstring is the description for the script"""
import arguably
import builtins
@arguably.command
def hey_there(first_name, last_name: str | None = None):
"""
this will say hello to someone
arguments without annotations (`first_name`) default to `str`
... unless the type can be inferred from their default value
any union with `None` is removed, so `last_name` is parsed as `str`
Args:
first_name: the {first} name of the person to greet
last_name: their {last} name
"""
if last_name is None:
full_name = first_name
else:
full_name = f"{first_name} {last_name}"
print(f"Hello, {full_name}!")
user@machine:~$ python3 simple.py hey-there -h
usage: simple.py hey-there [-h] FIRST [LAST]
this will say hello to someone
positional arguments:
FIRST the first name of the person to greet (type: str)
LAST their last name (type: str, default: None)
options:
-h, --help show this help message and exit
user@machine:~$ python3 simple.py hey-there Monty
Hello, Monty!
user@machine:~$ python3 simple.py hey-there Monty Python
Hello, Monty Python!
good
¶
good
has two subcommands. The -s/--shout
option is able to be passed to good
any time one of these subcommands is
invoked.
@arguably.command(alias="g")
def good(*, shout=False):
"""
this is a command with two subcommands
everything after the `*` appears as an `--option`
`shout` is inferred to be a `bool` because of its default value
`bool` `--option`s take no value by design
Args:
shout: [-s] will shout out the greeting
"""
if shout:
# All prints are now UPPERCASE
global print
print = lambda msg: builtins.print(msg.upper())
user@machine:~$ python3 simple.py good -h
usage: simple.py good [-h] [-s] command ...
this is a command with two subcommands
positional arguments:
command
morning Greet someone early in the day
night Say goodbye at night
options:
-h, --help show this help message and exit
-s, --shout will shout out the greeting (type: bool, default: False)
good__morning
¶
@arguably.command
def good__morning(name):
"""Greet someone early in the day"""
print(f"Good morning, {name}!")
user@machine:~$ python3 simple.py good -s morning Monty
GOOD MORNING, MONTY!
good__night
¶
@arguably.command
def good__night(name):
"""Say goodbye at night"""
print(f"Good night, {name}!")
user@machine:~$ python3 simple.py good night Python
Good night, Python!
arguably.run()
¶
if __name__ == "__main__":
arguably.run()
user@machine:~$ python3 simple.py
usage: simple.py [-h] command ...
this docstring is the description for the script
positional arguments:
command
hey-there this will say hello to someone
good (g) this is a command with two subcommands
options:
-h, --help show this help message and exit
One of everything¶
This script includes one of every feature. arguably
is designed so that you don't have to reach for the tools hidden
behind Annotated[]
except in special cases, but this script makes heavy use of them.
It's a long script, so it's periodically broken up to show the results on the CLI.
Imports and __root__
¶
#!/usr/bin/env python3
"""
A demo script to show all features
"""
import enum
import operator
from dataclasses import dataclass
from pathlib import Path
from typing import Annotated
import arguably
# used if version_flag is set
__version__ = "1.0.0"
@arguably.command
def __root__(*, verbose: Annotated[int, arguably.arg.count()] = 0):
"""
__root__ is always called first, before any subcommand.
It's also run if no subcommand is specified.
Args:
verbose: [-v] the verbosity - flag occurrences are counted
"""
print(f"Verbosity: {verbose}")
if not arguably.is_target():
return
print("__root__ is the target!")
user@machine:~$ ./everything.py -vvvv
Verbosity: 4
__root__ is the target!
add
¶
@arguably.command
def add(
coords: tuple[int, int],
*values: Annotated[int, arguably.arg.required()],
include_z: bool = False,
):
"""
this is the CLI description for this command
Args:
coords: some coordinates {X,Y}
*values: scalar {value}s to add to the coords, requires one or more
include_z: [-z] whether to include a value for Z
"""
print(f"Coordinates: {coords}")
if include_z:
x, y = coords
z = 0
coords = (x, y, z)
for value in values:
value_arr = (value,) * len(coords)
coords = tuple(map(operator.add, coords, value_arr))
print(f"Added {value}: {coords}")
print(f"Result: {coords}")
user@machine:~$ ./everything.py add -h
usage: kitchen-sink add [-h] [-z] X,Y VALUE [VALUE ...]
this is the CLI description for this command
positional arguments:
X,Y some coordinates X,Y (type: (int,int))
VALUE scalar values to add to the coords, requires one or more (type: int)
options:
-h, --help show this help message and exit
-z, --include-z whether to include a value for Z (type: bool, default: False)
user@machine:~$ ./everything.py add -z 5,5 1 2 3 4
Verbosity: 0
Coordinates: (5, 5)
Added 1: (6, 6, 1)
Added 2: (8, 8, 3)
Added 3: (11, 11, 6)
Added 4: (15, 15, 10)
Result: (15, 15, 10)
chmod
¶
class Permissions(enum.Flag):
READ = 4
"""[-r] allows for reads"""
WRITE = 2
"""[-w] allows for writes"""
EXECUTE = 1
"""[-x] allows for execution"""
@arguably.command
def chmod(file: Path, *, flags: Permissions = Permissions(0)):
"""
flags break down into multiple --options, one for each flag member.
the docstring for each flag member is used.
Args:
file: the file to modify
flags: permission flags
"""
print(f"{file=}", f"{flags=}")
user@machine:~$ ./everything.py chmod -h
usage: kitchen-sink chmod [-h] [-r] [-w] [-x] file
flags break down into multiple --options, one for each flag member.
positional arguments:
file the file to modify (type: Path)
options:
-h, --help show this help message and exit
-r, --read allows for reads
-w, --write allows for writes
-x, --execute allows for execution
user@machine:~$ ./everything.py chmod foo.exe -rwx
Verbosity: 0
file=PosixPath('foo.exe') flags=<Permissions.READ|WRITE|EXECUTE: 7>
move
¶
class Direction(enum.Enum):
UP = (0, 1)
DOWN = (0, -1)
LEFT = (-1, 0)
RIGHT = (1, 0)
@arguably.command
def move(direction: Direction):
"""
enum values are entered by the enum value name
Args:
direction: the direction to move
"""
dx, dy = direction.value
print(f"Will move {dx}, {dy}")
user@machine:~$ ./everything.py move -h
usage: kitchen-sink move [-h] {up,down,left,right}
enum values are entered by the enum value name
positional arguments:
{up,down,left,right} the direction to move (type: Direction)
options:
-h, --help show this help message and exit
user@machine:~$ ./everything.py move up
Verbosity: 0
Will move 0, 1
make
¶
@arguably.command
def make(
target: Annotated[str, arguably.arg.choices("build", "install", "clean")],
*,
log: Annotated[Path | None, arguably.arg.missing("~/.log.txt")] = None,
):
"""
arguably.arg.choices restricts input values
arguably.arg.missing provides a value if the flag is specified but value omitted
Args:
target: the command to send to `make`
log: the path to log, if any
"""
print(f"Running `make {target}`")
if log is None:
print("Will not log")
else:
print(f"Logging to {log}")
user@machine:~$ ./everything.py make -h
usage: kitchen-sink make [-h] [--log [LOG]] {build,install,clean}
arguably.arg.choices restricts input values
positional arguments:
{build,install,clean} the command to send to `make` (type: str)
options:
-h, --help show this help message and exit
--log [LOG] the path to log, if any (type: Path, default: None)
user@machine:~$ ./everything.py make build
Verbosity: 0
Running `make build`
Will not log
user@machine:~$ ./everything.py make build --log
Verbosity: 0
Running `make build`
Logging to ~/.log.txt
user@machine:~$ ./everything.py make build --log foo.log
Verbosity: 0
Running `make build`
Logging to foo.log
arg
¶
@arguably.command
def arg():
"""
this has two subcommands. a double underscore __ is used when a space would appear
* arg__handler -> "arg handler"
* arg__builder -> "arg builder"
"""
print("Hello from arg()!")
if arguably.is_target():
print("arg() is the target!")
user@machine:~$ ./everything.py arg -h
usage: kitchen-sink arg [-h] command ...
this has two subcommands. a double underscore __ is used when a space would appear
positional arguments:
command
handler runs a custom handler for input
builder builds a complex class - can pick between subtypes of class
options:
-h, --help show this help message and exit
user@machine:~$ ./everything.py arg
Verbosity: 0
Hello from arg()!
arg() is the target!
arg__handler
¶
@arguably.command
def arg__handler(
version: Annotated[str, arguably.arg.handler(lambda s: s.removeprefix("Python-"))]
):
"""
runs a custom handler for input
arguably.arg.handler allows for arbitrary functions to handle inputs
this one removes a prefix of "Python-" before passing the value along
Args:
version: Python version, like 3.11
"""
print(f"Python version: {version}")
user@machine:~$ ./everything.py arg handler -h
usage: kitchen-sink arg handler [-h] version
runs a custom handler for input
positional arguments:
version Python version, like 3.11 (type: str)
options:
-h, --help show this help message and exit
user@machine:~$ ./everything.py arg handler Python-3.10
Verbosity: 0
Hello from arg()!
Python version: 3.10
arg__builder
¶
class Nic: ...
@arguably.subtype(alias="tap")
@dataclass
class TapNic(Nic):
model: str
@arguably.subtype(alias="user")
@dataclass
class UserNic(Nic):
hostfwd: str
@arguably.command
def arg__builder(
*,
nic: Annotated[list[Nic], arguably.arg.builder()]
):
"""
builds a complex class - can pick between subtypes of class
Args:
nic: network interfaces - will build subclasses of `Nic`
"""
print(f"Built nics: {nic}")
user@machine:~$ ./everything.py arg builder -h
usage: kitchen-sink arg builder [-h] --nic NIC
builds a complex class - can pick between subtypes of class
options:
-h, --help show this help message and exit
--nic NIC network interfaces - will build subclasses of `Nic` (type: list[Nic])
user@machine:~$ ./everything.py -vvv arg builder --nic tap,model=e1000 --nic user,hostfwd=tcp::10022-:22
Verbosity: 3
Hello from arg()!
Built nics: [TapNic(model='e1000'), UserNic(hostfwd='tcp::10022-:22')]
list_
¶
@arguably.command
def list_(files: list[Path], *, output: list[str]):
"""
lists are supported and use a comma to separate inputs
an empty list is a single dash `-`
if a list appears as an `--option`, it can be repeated
Args:
files: input files
output: outputs
"""
for file in files:
print(f"Resolved path: {file.resolve()}")
for out in output:
print(f"Will output to {out}")
user@machine:~$ ./everything.py list -h
usage: kitchen-sink list [-h] --output OUTPUT files
lists are supported and use a comma to separate inputs
positional arguments:
files input files (type: list[Path])
options:
-h, --help show this help message and exit
--output OUTPUT outputs (type: list[str])
user@machine:~$ ./everything.py list foo.txt,bar.bat --output wifi0,en0 --output en1
Verbosity: 0
Resolved path: .../arguably/etc/scripts/foo.txt
Resolved path: .../arguably/etc/scripts/bar.bat
Will output to wifi0
Will output to en0
Will output to en1
arguably.run()
¶
if __name__ == "__main__":
arguably.run(name="kitchen-sink", version_flag=True)
user@machine:~$ ./everything.py -h
usage: kitchen-sink [-h] [--version] [-v] command ...
A demo script to show all features
positional arguments:
command
add this is the CLI description for this command
chmod flags break down into multiple --options, one for each flag member.
move enum values are entered by the enum value name
make arguably.arg.choices restricts input values
arg this has two subcommands. a double underscore __ is used when a space would appear
list lists are supported and use a comma to separate inputs
options:
-h, --help show this help message and exit
--version show program's version number and exit
-v, --verbose the verbosity - flag occurrences are counted (type: int, default: 0)
user@machine:~$ ./everything.py --version
kitchen-sink 1.0.0