Monday, November 9, 2020

Stack Abuse: Generating Command-Line Interfaces (CLI) with Fire in Python

Introduction

A Command-line interface (CLI) is a way to interact with computers using textual commands.

A lot of tools that don't require GUIs are written as CLI tools/utilities. Although Python has the built-in argparse module, other libraries with similar functionality do exist.

These libraries can help us in writing CLI scripts, providing services like parsing options and flags to much more advanced CLI functionality.

This article discusses the Python Fire library, written by Google Inc., a useful tool to create CLI with minimal code.

General Form of CLI Applications

Before we start with the Fire library let's try to understand the basics of command-line interface programs in general. Depending on the program and command, the general pattern of a CLI can be summed up as follows:

prompt command parameter1 parameter2 ... parameterN
  • prompt is a sequence of characters that prompts the user to input a command
  • command is the name of the program that the user is executing (e.g. ls)
  • parameters are optional tokens that augment or modify the command output

A CLI program is executed by typing the name of the program after the prompt appears, in this case the $ symbol.

Here we are using the ls command that returns a list of file names in a directory, the current directory being the default:

$ ls
README.md
python

You can modify the behavior or output of a command-line program by providing it with a list of tokens or parameters better known as flags. Let's try out a flag of the ls command:

$ ls -l
-rwxrwxrwx 1 pandeytapan pandeytapan  10 Sep 23 18:29 README.md
drwxrwxrwx 1 pandeytapan pandeytapan 512 Sep 23 18:29 python

As you can see, after passing the -l flag, we get additional information for each entry like the owner, group, and file size.

Flags which have a single hyphen (-) are called short options, while those with two hyphens (--) are called long options. Both kinds can be used together in a single command, like in the following example:

$ ls -l --time-style=full-iso
-rwxrwxrwx 1 pandeytapan pandeytapan  10 2020-09-23 18:29:25.501149000 +0530 README.md
drwxrwxrwx 1 pandeytapan pandeytapan 512 2020-09-23 18:29:25.506148600 +0530 python

The difference between short and long options:

  1. Short options can be chained together
    • If we want to use both the -l and -a short options we just type -al
  2. Short options are denoted by a single character while long options have a full hyphen-separated name and cannot be chained together.

The --time-style flag works with the -l flag and controls the display time format for a directory listing.

A CLI provides an easy way for the user to configure and run an application from the command line. Google's Python Fire library makes it easy to add a CLI processing component to any existing Python script.

Let's see how to make a command line application using Python Fire.

Installation

Let's go ahead and install the library using pip:

$ pip install fire

Python Fire works on any Python object i.e. functions, classes, dictionaries, lists etc. Let's try to understand the usage of the Python Fire library through some examples.

Generating CLI Application with Python Fire

Let's make a script, say, fire_cli.py and put a function within it:

def greet_mankind():
    """Greets you with Hello World"""
    return 'Hello World'

On running this program on Python shell the output is:

>>> from fire_cli import greet_mankind
>>> greet_mankind()
'Hello World'
>>>

We can easily turn this script into a CLI application using Python Fire:

import fire

def greet_mankind():
    """
    Returns a textual message
    """
    return 'Hello World'

if __name__ == '__main__':
    fire.Fire()

The fire.Fire() calls turns the module i.e. fire_cli.py into a Fire CLI application. Moreover it has exposed the greet_mankind() function as command, automatically.

Now we can save and run the above script as CLI as follows:

$ python fire_greet_mk_cli.py greet_mankind
Hello World

As a refresher, let's break down the call:

  • $ is the prompt
  • python is the command interpreter
  • fire_cli.py is the module that contains the CLI command
  • greet_mankind is the command

Passing Arguments to a Command

Let us make another CLI application that takes a name as a parameter and displays a custom greeting message:

import fire

def greetings(name):
    '''
    Returns a greeting message
    Parameters
    ----------
    name : string
        String that represents the addresses name 

    Returns
    -------
    string
        greeting message concatenated with name
    '''
    return 'Hello %s' % name

if __name__ == '__main__':
    fire.Fire()

Here, we've now got a function that accepts a string - name. Python Fire automatically picks this up and if we supply an argument after the greetings call, it'll bind that input to the name parameter. We've also added a comment as a sort of documentation for the --help command.

Here's how we can run this command from the command line:

$ python fire_greet_cli.py greetings Robin
Hello Robin

A Fire CLI application can use --help flags to check the command description generated from Python docs:

python fire_greet_cli.py greetings --help
NAME
    fire_greet_cli.py greetings - Returns a greeting message

SYNOPSIS
    fire_greet_cli.py greetings NAME

DESCRIPTION
    Returns a greetings message

POSITIONAL ARGUMENTS
    NAME
        String that represents the addresses name

NOTES
    You can also use flags syntax for POSITIONAL ARGUMENTS

Setting a Function as Entry Point

With slight modification we can control the exposure of the greetings() function to the command-line and set it as the default entry-point:

import fire

def greetings(name):
    '''
    Returns a greeting message 
    :param name: string argument
    :return: greeting message appended with name
    '''
    return 'Hello %s' % name

if __name__ == '__main__':
    fire.Fire(greetings)

This is how we will run the command now:

$ python fire_greet_cli.py Robin
Hello Robin

So this time we no longer need to call the command as we have defined greetings implicitly as an entry point using Fire(). One thing to be noted here is that with this version, we can only pass a single argument :

$ python fire_greet_cli.py Robin Hood
ERROR: Could not consume arg: Hood
...
$ python fire_greet_cli.py Robin
Hello Robin

Arguments Parsing

The Fire library also works with classes. Let's define a class CustomSequence that generates and returns a list of numbers between start and end:

import fire

class CustomSequence:
    '''Class that generates a sequence of numbers'''
    def __init__(self, offset=1):
        '''
         Parameters
        ----------
        offset : int, optional
            Number controlling the difference between two generated values
        '''
        self.offset = offset

    def generate(self, start, stop):
        '''
        Generates the sequence of numbers

        Parameters
        ----------
        start : int
            Number that represents the elements lower bound
        stop : int
            Number that represents the elements upper bound

        Returns
        -------
        string
            a string that represents the generated sequence
        '''
        return ' '.join(str(item) for item in range(start, stop, self.offset))

if __name__ == '__main__':
    fire.Fire(CustomSequence)

This is how we generate a sequence using this command-line utility:

$ python fire_gen_cli.py generate 1 10
1 2 3 4 5 6 7 8 9

We used a class instead of a function because unlike functions if we want to pass an argument to the constructor, it always has to be represented as a command line flag with double hyphens (e.g. --offset=2).

Therefore, our CLI application supports an optional argument --offset that will be passed on to the class constructor. This modifies the output by controlling the difference between two consecutive generated values:

Here's the output with offset value of 2:

$ python fire_gen_cli.py generate 1 10 --offset=2
1 3 5 7 9

The constructor's arguments are always passed using the flag syntax whereas arguments to other methods or functions are passed positionally or by name :

$ python fire_gen_cli.py generate --start=10 --stop=20
10 11 12 13 14 15 16 17 18 19
$ python fire_gen_cli.py generate 10 20
10 11 12 13 14 15 16 17 18 19
$ python fire_gen_cli.py generate --start=10 --stop=20 --offset=2
10 12 14 16 18

We can check the usage of thegenerate command using the --help flag. This will give the usage information for the CLI :

$ python fire_gen_cli.py generate --help
INFO: Showing help with the command 'fire_gen_cli.py generate -- --help'.

NAME
    fire_gen_cli.py generate - Generates the sequence of numbers

SYNOPSIS
    fire_gen_cli.py generate START STOP

DESCRIPTION
    Generates the sequence of numbers

POSITIONAL ARGUMENTS
    START
        Number that represents the first value for the sequence
    STOP
        Number that represents the ending value for the sequence

NOTES
    You can also use flags syntax for POSITIONAL ARGUMENTS

Using --help with the module gives us its usage information:

$ python fire_gen_cli.py  --help
INFO: Showing help with the command 'fire_gen_cli.py -- --help'.

NAME
    fire_gen_cli.py - Class that generates a sequence of numbers

SYNOPSIS
    fire_gen_cli.py <flags>

DESCRIPTION
    Class that generates a sequence of numbers

FLAGS
    --offset=OFFSET

Fire Flags

Fire CLIs comes with many built-in flags. We have already seen --help, though, another useful flag is --interactive. Using this flag puts us in Python REPL mode, with the module already defined.

This is quite useful for testing commands:

$ python fire_greet_cli.py -- --interactive
Fire is starting a Python REPL with the following objects:
Modules: fire
Objects: component, fire_greet_cli.py, greetings, result, trace

Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.16.1 -- An enhanced Interactive Python. Type '?' for help.


In [1]: greetings("Robin")
Out[1]: 'Hello Robin'

Note that Fire flags should be separated from other options with two hyphens (--), so if we want to use both --help and --interactive flags in a command, it would look something like this:

$ python fire_greet_cli.py -- --help --interactive

Conclusion

Google's Python Fire library is a quick and easy way to generate command line interfaces (CLIs) for nearly any Python object.

In this article, we've gone over how to install Python Fire, as well as generate simple command-line interfaces.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...