Tutorial

Part I: Say

The field of overdone versions of echo has been too long dominated by Big GNU. Today, we start taking back the power. We will implement the say command.

Positional arguments

While face offers a Parser interface underneath, the canonical way to create even the simplest CLI is with the Command object.

To demonstrate, we’ll start with the basics, positional arguments. say hello world should print hello world:

from face import Command, echo


def main():
    cmd = Command(say, posargs=True)  # posargs=True means we accept positional arguments
    cmd.run()


def say(posargs_):  # positional arguments are passed through the posargs_ parameter
    echo(' '.join(posargs_))  # our business logic


if __name__ == '__main__':  # standard fare Python: https://stackoverflow.com/questions/419163
    main()

A basic Command takes a single function entrypoint, in our case, the say function.

Note

Face’s echo() function is a version of print() with improved options and handling of console states, ideal for CLIs.

Flags

Let’s give say some options:

say --upper hello world or say -U hello world should print HELLO WORLD.

...

 def main():
     cmd = Command(say, posargs=True)
     cmd.add('--upper', char='-U', parse_as=True, doc='uppercase all output')
     cmd.run()


 def say(posargs_, upper):  # our --upper flag is bound to the upper parameter
     args = posargs_
     if upper:
         args = [a.upper() for a in args]
     echo(' '.join(args))

 ...

The parse_as keyword argument being set to True means that the presence of the flag results in the True value itself. As we’ll see below, flags can take arguments, too.

Flags with values

Let’s add more flags, this time ones that take values.

say --separator . hello world will print hello.world. Likewise, say --count 2 hello world will repeat it twice: hello world hello world

...

def main():
    cmd = Command(say, posargs=True)
    cmd.add('--upper', char='-U', parse_as=True, doc='uppercase all output')
    cmd.add('--separator', missing=' ', doc='text to put between arguments')
    cmd.add('--count', parse_as=int, missing=1, doc='how many times to repeat')
    cmd.run()


 def say(posargs_, upper, separator, count):
     args = posargs_ * count
     if upper:
         args = [a.upper() for a in args]
     echo(separator.join(args))

 ...

Now we can see that parse_as:

  • Can take a value (e.g., True), which make the flag no-argument
  • Can take a callable (e.g., int), which is used to convert the single argument
  • Defaults to str (as used by separator)

We can also see the missing keyword argument, which specifies the value to be passed to the Command’s handler function if the flag is absent. Without this, None is passed.

Note

Face also supports required flags, though they are not an ideal CLI UX best practice. Simply set missing to face.ERROR.

More Interesting Flag Types

say --multi-separator=@,# hello wonderful world prints hello@wonderful#world (The separators repeat)

say --from-file=fname reads the file and adds all words from it to its output

say --animal=dog|cat|cow will prepend “woof”, “meow”, or “moo” respectively.

Part II: Calc

(Details TBD!)

With echo having met its match, we are on to bigger and better: this time, with math

$ num
<Big help text>

Add and Multiply

$ num add 1 2
3
$ num mul 3 5
15

Subtract

$ num sub 10 5
5
$ num sub 5 10
Error: can't substract
$ num --allow-negatives 5 10
-5

Divide

$ num div 2 3
0.6666666666666666
$ num div --int 2 3
0

Precision support

$ num add 0.1 0.2
0.30000000000000004
$ num add --precision=3 0.1 0.2
0.3

Oh, now let’s add it to all subcommands.

Part III: Middleware

(Details TBD!)

Doing math locally is all well and good, but sometimes we need to use the web.

We will add an “expression” sub-command to num that uses https://api.mathjs.org/v4/. But since we want to unit test it, we will create the httpx.Client in a middleware.

$ num expression "1 + (2 * 3)"
7

But we can also write a unit test that does not touch the web:

$ pytest test_num.py

Part IV: Examples

There are more realistic examples of face usage out there, that can serve as a reference.

Cut MP4

The script cut_mp4 is a quick but useful tool to cut recordings using ffmpeg. I use it to slice and dice the Python meetup recordings. It does not have subcommands or middleware, just a few flags.

Glom

Glom is a command-line interface front end for the glom library. It does not have any subcommands, but does have some middleware usage.

Pocket Protector

Pocket Protector is a secrets management tool. It is a medium-sized application with quite a few subcommands for manipulating a YAML file.

Montage Admin Tools

Montage Admin Tools is a larger application. It has nested subcommands and a database connection. It is used to administer a web application.