Explore Books related to Python. Click Here

How to Create a Command-line Application with argparse

When you are creating an application, you will usually want to be able to tell your application how to do something. There are two popular methods for accomplishing this task. You can make your application accept command-line arguments or you can create a graphical user interface. Some applications support both.

Command-line interfaces are helpful when you need to run your code on a server. Most servers do not have a monitor hooked up, especially if they are Linux servers. In those cases, you might not be able to run a graphical user interface even if you wanted to.

Python comes with a built-in library called argparse that you can use to create a command-line interface. In this article, you will learn about the following:

  • Parsing Arguments
  • Creating Helpful Messages
  • Adding Aliases
  • Using Mutually Exclusive Arguments
  • Creating a Simple Search Utility

There is a lot more to the argparse module than what will be covered in this article. If you would like to know more about it, you can check out the documentation.

Now it's time to get started with parsing arguments from the command-line!

Parsing Arguments

Before you learn how to use argparse, it's good to know that there is another way to pass arguments to a Python script. You can pass any arguments to a Python script and access those arguments by using the sys module.

To see how that works, create a file named sys_args.py and enter the following code into it:

# sys_args.py

import sys

def main():
    print('You passed the following arguments:')
    print(sys.argv)

if __name__ == '__main__':
    main()

This code imports sys and prints out whatever is in sys.argv. The argv attribute contains a list of everything that was passed to the script with the first item being the script itself.

Here's an example of what happens when you run this code along with a couple of sample arguments:

$ python3 sys_args.py --s 45
You passed the following arguments:
['sys_args.py', '--s', '45']

The problem with using sys.argv is that you have no control over the arguments that can be passed to your application:

  • You can't ignore arguments
  • You can't create default arguments
  • You can't really tell what is a valid argument at all

This is why using argparse is the way to go when working with Python's standard library. The argparse module is very powerful and useful. Let's think about a common process that a command line application follows:

  • pass in a file
  • do something to that file in your program
  • output the result

Here is a generic example of how that might work. Go ahead and create file_parser.py and add the following code:

# file_parser.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser('File parser')
    parser.add_argument('--infile', help='Input file')
    parser.add_argument('--out', help='Output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

The file_parser() function is where the logic for the parsing would go. For this example, it only takes in a file name and prints it back out. The output_file argument defaults to an empty string.

The meat of the program is in main() though. Here you create an instance of argparse.ArgumentParser() and give your parser a name. Then you add two arguments, --infile and --out. To use the parser, you need to call parse_args(), which will return whatever valid arguments were passed to your program. Finally, you check to see if the user used the --infile flag. If they did, then you run file_parser().

Here is how you might run the code in your terminal:

$ python file_parser.py --infile something.txt
Processing something.txt
Finished processing

Here you run your script with the --infile flag along with a file name. This will run main() which in turns calls file_parser().

The next step is to try your application using both command-line arguments you declared in your code:

$ python file_parser.py --infile something.txt --out output.txt
Processing something.txt
Finished processing
Creating output.txt

This time around, you get an extra line of output that mentions the output file name. This represents a branch in your code logic. When you specify an output file, you can have your code go through the process of generating that file using a new block of code or a function. If you do not specify an output file, then that block of code would not run.

When you create your command-line tool using argparse, you can easily add messages that help your users when they are unsure of how to correctly interact with your program.

Now it's time to find out how to get help from your application!

Creating Helpful Messages

The argparse library will automatically create a helpful message for your application using the information that you provided when you create each argument. Here is your code again:

# file_parser.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser('File parser')
    parser.add_argument('--infile', help='Input file')
    parser.add_argument('--out', help='Output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

Now try running this code with the -h flag and you should see the following:

$ file_parser.py -h
usage: File parser [-h] [--infile INFILE] [--out OUT]

optional arguments:
  -h, --help       show this help message and exit
  --infile INFILE  Input file
  --out OUT        Output file

The help parameter to add_argument() is used to create the help message above. The -h and --help options are added automatically by argparse. You can make your help more informative by giving it a description and an epilog.

Let's use them to improve your help messages. Start by copying the code from above into a new file named file_parser_with_description.py, then modify it to look like this:

# file_parser_with_description.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser(
            'File parser',
            description='PyParse - The File Processor',
            epilog='Thank you for choosing PyParse!',
            )
    parser.add_argument('--infile', help='Input file for conversion')
    parser.add_argument('--out', help='Converted output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

Here you pass in the description and epilog arguments to ArgumentParser. You also update the help arguments to add_argument() to be more descriptive.

When you run this script with -h or --help after making these changes, you will see the following output:

$ python file_parser_with_description.py -h
usage: File parser [-h] [--infile INFILE] [--out OUT]

PyParse - The File Processor

optional arguments:
  -h, --help       show this help message and exit
  --infile INFILE  Input file for conversion
  --out OUT        Converted output file

Thank you for choosing PyParse!

Now you can see the new description and epilog in your help output. This gives your command-line application some extra polish.

You can also disable help entirely in your application via the add_help argument to ArgumentParser. If you think that your help text is too wordy, you can disable it like this:

# file_parser_no_help.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser(
            'File parser',
            description='PyParse - The File Processor',
            epilog='Thank you for choosing PyParse!',
            add_help=False,
            )
    parser.add_argument('--infile', help='Input file for conversion')
    parser.add_argument('--out', help='Converted output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

By setting add_help to False, you are disabling the -h and --help flags.

You can see this demonstrated below:

$ python file_parser_no_help.py --help
usage: File parser [--infile INFILE] [--out OUT]
File parser: error: unrecognized arguments: --help

In the next section, you'll learn about adding aliases to your arguments!

Adding Aliases

An alias is a fancy word for using an alternate flag that does the same thing. For example, you learned that you can use both -h and --help to access your program's help message. -h is an alias for --help, and vice-versa

Look for the changes in the parser.add_argument() methods inside of main():

# file_parser_aliases.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser(
            'File parser',
            description='PyParse - The File Processor',
            epilog='Thank you for choosing PyParse!',
            add_help=False,
            )
    parser.add_argument('-i', '--infile', help='Input file for conversion')
    parser.add_argument('-o', '--out', help='Converted output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

Here you change the first add_argument() to accept -i in addition to --infile and you also added -o to the second add_argument(). This allows you to run your code using two new shortcut flags.

Here's an example:

$ python3 file_parser_aliases.py -i something.txt -o output.txt
Processing something.txt
Finished processing
Creating output.txt

If you go looking through the argparse documentation, you will find that you can add aliases to subparsers too. A subparser is a way to create sub-commands in your application so that it can do other things. A good example is Docker, a virtualization or container application. It has a series of commands that you can run under docker as well as docker compose and more. Each of these commands has separate sub-commands that you can use.

Here is a typical docker command to run a container:

docker exec -it container_name bash

This will launch a container with docker. Whereas if you were to use docker compose, you would use a different set of commands. The exec and compose are examples of subparsers.

The topic of subparsers are outside the scope of this tutorial. If you are interested in more details dive right into the documentation.

Using Mutually Exclusive Arguments

Sometimes you need to have your application accept some arguments but not others. For example, you might want to limit your application so that it can only create or delete files, but not both at once.

The argparse module provides the add_mutually_exclusive_group() method that does just that!

Change your two arguments to be mutually exclusive by adding them to a group object like in the example below:

# file_parser_exclusive.py

import argparse

def file_parser(input_file, output_file=''):
    print(f'Processing {input_file}')
    print('Finished processing')
    if output_file:
        print(f'Creating {output_file}')

def main():
    parser = argparse.ArgumentParser(
            'File parser',
            description='PyParse - The File Processor',
            epilog='Thank you for choosing PyParse!',
            add_help=False,
            )
    group = parser.add_mutually_exclusive_group()
    group.add_argument('-i', '--infile', help='Input file for conversion')
    group.add_argument('-o', '--out', help='Converted output file')
    args = parser.parse_args()
    if args.infile:
        file_parser(args.infile, args.out)

if __name__ == '__main__':
    main()

First, you created a mutually exclusive group. Then, you added the -i and -o arguments to the group instead of to the parser object. Now these two arguments are mutually exclusive.

Here is what happens when you try to run your code with both arguments:

$ python3 file_parser_exclusive.py -i something.txt -o output.txt
usage: File parser [-i INFILE | -o OUT]
File parser: error: argument -o/--out: not allowed with argument -i/--infile

Running your code with both arguments causes your parser to show the user an error message that explains what they did wrong.

After covering all this information related to using argparse, you are ready to apply your new skills to create a simple search tool!

Creating a Simple Search Utility

Before starting to create an application, it is always good to figure out what you are trying to accomplish. The application you want to build in this section should be able to search for files of a specific file type. To make it more interesting, you can add an additional argument that allows you to optionally search for specific file sizes as well.

You can use Python's glob module for searching for file types. You can read all about this module here:

There is also the fnmatch module, which glob itself uses. You should use glob for now as it is easier to use, but if you're interested in writing something more specialized, then fnmatch may be what you are looking for.

However, since you want to be able to optionally filter the files returned by the file size, you can use pathlib which includes a glob-like interface. The glob module itself does not provide file size information.

You can start by creating a file named pysearch.py and entering the following code:

# pysearch.py

import argparse
import pathlib


def search_folder(path, extension, file_size=None):
    """
    Search folder for files
    """
    folder = pathlib.Path(path)
    files = list(folder.rglob(f'*.{extension}'))

    if not files:
        print(f'No files found with {extension=}')
        return

    if file_size is not None:
        files = [
                f
                for f in files
                if f.stat().st_size >= file_size
                ]

    print(f'{len(files)} *.{extension} files found:')
    for file_path in files:
        print(file_path)

You start the code snippet above by importing argparse and pathlib. Next you create the search_folder() function which takes in three arguments:

  • path - The folder to search within
  • extension - The file extension to look for
  • file_size - What file size to filter on in bytes

You turn the path into a pathlib.Path object and then use its rglob() method to search in the folder for the extension that the user passed in. If no files are found, you print out a meaningful message to the user and exit.

If any files are found, you check to see whether file_size has been set. If it was set, you use a list comprehension to filter out the files that are smaller than the specified file_size.

Next, you print out the number of files that were found and finally loop over these files to print out their names.

To make this all work correctly, you need to create a command-line interface. You can do that by adding a main() function that contains your argparse code like this:

def main():
    parser = argparse.ArgumentParser(
            'PySearch',
            description='PySearch - The Python Powered File Searcher',
            )
    parser.add_argument('-p', '--path',
                        help='The path to search for files',
                        required=True,
                        dest='path')
    parser.add_argument('-e', '--ext',
                        help='The extension to search for',
                        required=True,
                        dest='extension')
    parser.add_argument('-s', '--size',
                        help='The file size to filter on in bytes',
                        type=int,
                        dest='size',
                        default=None)

    args = parser.parse_args()
    search_folder(args.path, args.extension, args.size)

if __name__ == '__main__':
    main()

This ArgumentParser() has three arguments added to it that correspond to the arguments that you pass to search_folder(). You make the --path and --ext arguments required while leaving the --size argument optional. Note that the --size argument is set to type=int, which means that you cannot pass it a string.

There is a new argument to the add_argument() function. It is the dest argument which you use to tell your argument parser where to save the arguments that are passed to them.

Here is an example run of the script:

$ python3 pysearch.py -p /Users/michael/Dropbox/python101code/chapter32_argparse -e py -s 650
6 *.py files found:
/Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_aliases2.py
/Users/michael/Dropbox/python101code/chapter32_argparse/pysearch.py
/Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_aliases.py
/Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_with_description.py
/Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_exclusive.py
/Users/michael/Dropbox/python101code/chapter32_argparse/file_parser_no_help.py

That worked quite well! Now try running it with -s and a string:

$ python3 pysearch.py -p /Users/michael/Dropbox/python101code/chapter32_argparse -e py -s python
usage: PySearch [-h] -p PATH -e EXTENSION [-s SIZE]
PySearch: error: argument -s/--size: invalid int value: 'python'

This time, you received an error because -s and --size only accept integers. Go try this code on your own machine and see if it works the way you want when you use -s with an integer.

Here are some ideas you can use to improve your version of the code:

  • Handle the extensions better. Right now it will accept *.py which won't work the way you might expect
  • Update the code so you can search for multiple extensions at once
  • Update the code to filter on a range of file sizes (Ex. 1 MB - 5MB)

There are lots of other features and enhancements you can add to this code, such as adding error handling or unittests.

Wrapping Up

The argparse module is full featured and can be used to create great, flexible command-line applications. In this chapter, you learned about the following:

  • Parsing Arguments
  • Creating Helpful Messages
  • Adding Aliases
  • Using Mutually Exclusive Arguments
  • Creating a Simple Search Utility

You can do a lot more with the argparse module than what was covered in this chapter. Be sure to check out the documentation for full details. Now go ahead and give it a try yourself. You will find that once you get the hang of using argparse, you can create some really neat applications!

Copyright © 2022 Mouse Vs Python | Powered by Pythonlibrary