Cutting edge technology

How to write a perfect Python command line program?

Introduction: As a Python developer, we often write command-line programs. For example, in my data science project, I want to run a script from the command line to train the model, and calculate the accuracy of the algorithm.

As a Python developer, we often write command-line programs. For example, in my data science project, I want to run a script from the command line to train the model, and calculate the accuracy of the algorithm.

Therefore, more convenient and easier-to-use scripts can improve productivity, especially when there are multiple developers working on the same project.

Therefore, I suggest you follow the following four rules:

Provide default parameter values ​​whenever possible. All error conditions must be handled (for example, missing parameters, type errors, files not found) all parameters and Options must have documents that are not completed immediately. Tasks should be displayed.

For a simple example

we apply these rules to a specific example. This script can encrypt and decrypt messages using Caesar encryption.

Assuming there is already a written encrypt function (implemented below), we need to create a simple script to encrypt and decrypt the message. We want the user to select the encryption mode (default) and the decryption mode via command line parameters and select a secret key (the default is 1).

defencrypt(plaintext, key): cyphertext = ''for character in plaintext:if character. Isalpha(): number = ord(character) number += keyif character. Isupper():if number > ord('Z'): number -= 26elif number < ord('A'):number += 26elif character. Islower():if number > ord('z'): number -= 26elif number < ord('a'): number += 26 character = chr(number) cyphertext += characterreturn cyphertext

The first thing our script needs to do is to get the value of the command line argument. When I searched for "python command line arguments", the first result that appeared was about sys. Argv, so let's try this method...

"Beginner" method

sys. Argv is a list of all the parameters (including the script name itself) that the user entered when running the script.

For example, if I type:

> pythoncaesar_script. Py--key 23 --decryptmysecretmessagepbvhfuhwphvvdjh

This list will contain:

['caesar_script. Py', '--key', '23', '--decrypt', 'my', 'secret', 'message']

So just traverse the parameter list and find '--key '(or '-k') to get the secret key value, find '--decrypt' to set the decryption mode (actually only need to use the reversal of the secret key as the secret key).

Finally our script is roughly as follows:

import sysfrom caesar_encryption import encryptdefcaesar(): key = 1is_error = Falsefor index, arg in enumerate(sys.argv):if arg in ['-- Key', '-k'] and len(sys.argv) > index + 1: key = int(sys.argv[index + 1])del sys. Argv[Index]del sys. Argv[index]breakfor index, arg in enumerate(sys.argv):if arg in ['--encrypt', '-e']:del sys. Argv[index]breakif arg in ['--decrypt', '-d']: key = -keydel sys. Argv[index]breakif len(sys.argv) == 1: is_error = Trueelse:for arg in sys. Argv:if arg. Startswith('-'): is_error = Trueif is_error: print(f'Usage: python {sys.argv[0]} [ --key ] [ --encrypt|decrypt ] ')else: print (encrypt(' '.join(sys.argv[1:]), key))if __name__ == '__main__': caesar()

This script follows some of the rules we recommended earlier: [123 ]

Supports default key and default mode basic error handling (when no input text is provided, and unrecognized parameters are provided) The document is displayed when an error occurs or when the script is called without any arguments: > pythoncaesar_script_using_sys_argv. pyUsage: pythoncaesar. Py[ --key ][ --encrypt|decrypt ]

However, this Caesar cryptography script is too long (39 lines, not even including the encrypted code itself), and Hard to read.

There should be a better way to parse command line arguments...

Try argparse?

argparSe is the standard library that Python uses to parse command line arguments.

Let's take a look at how to write Caesar encrypted scripts with argparse:

import argparsefrom caesar_encryption import encryptdef caesar(): parser = argparse. ArgumentParser()group = parser. Add_mutually_exclusive_group()group. Add_argument('-e', '--encrypt', action='store_true') group. Add_argument('-d', '--decrypt', action='store_true')parser. Add_argument('text', nargs='*') parser. Add_argument('-k', '--key', type=int, default=1) args = parser. Parse_args() text_string = ' '. Join(args.text)key = args. Keyif args. Decrypt: key = -key cyphertext = encrypt(text_string, key) print(cyphertext)if __name__ == '__main__': caesar()

This code also follows the above rules, and is manually written with the previous one. Compared to scripts, it provides more accurate documentation and more interactive error handling:

> pythoncaesar_script_using_argparse. Py--encodeMymessageusage: caesar_script_using_argparse. Py[-h][-e |-d][-k KEY][text [text . . . ]]caesar_script_using_argparse. Py: error: unrecognizedarguments: --encode> pythoncaesar_script_using_argparse. Py--helpusage: caesar_script_using_argparse. Py[-h][-e | -d][-k KEY][text [text . . . ]]

positional arguments:textoptional arguments: -h, --help show this help message andexit -e, --encrypt -d, --decrypt -k KEY, --keyKEY

However, after carefully reading this code, I found that (although somewhat subjective) the first few lines of the function (from 7 lines to 13 lines) define the parameters, but the definition is not very elegant: it is too bloated, and it is completely Stylized. There should be a more descriptive, more concise approach.

click can do better!

Fortunately, there is a Python library that provides the same functionality as argparse (and even more), and its code style is more elegant. The name of this library is called click.

Here is the third version of Caesar's encryption script, using click:

import clickfrom caesar_encryption import [email protected] Command()@click. Argument('text', nargs=-1)@click. Option('--decrypt/--encrypt', '-d/-e')@click. Option('--key', '-k', default=1)def caesar(tExt, decrypt, key): text_string = ' '. Join(text)if decrypt: key = -key cyphertext = encrypt(text_string, key) click. Echo(cyphertext)if __name__ == '__main__': caesar()

Note that the parameters and options are now defined in the decorator, and the defined parameters are provided directly as function arguments.

Let me explain some of the above code:

The nargs parameter in the script parameter definition specifies the number of words expected by the parameter (a string enclosed in quotes) word). The default is 1. Here nargs=-1 allows to receive any number of words. The --encrypt/--decrypt method can define completely mutually exclusive options (similar to the add_mutually_exclusive_group function in argparse), which will generate a boolean argument. Click. Echo is a utility function provided by the library. It functions the same as print, but is compatible with Python 2 and Python 3, and has other functions (such as processing colors, etc.).

Add some privacy

The parameters of this script (encrypted messages) should be top secret. And it is not ironic that we ask the user to enter text directly in the terminal so that the text is recorded in the command history. One of the

workarounds is to use hidden hints. Or you can read the text from the input file, which is more practical for longer text. Or you can simply let the user choose. The

output is also the same: the user can save to a file or output to the terminal. This gives the last version of the Caesar script:

import clickfrom caesar_encryption import [email protected] Command()@click. Option('--inPut_file', type=click. File('r'),help='File in which there is the text you want to encrypt/decrypt. ''If not provided, a prompt will allow you to type the input text. ',) @click. Option('--output_file', type=click.File('w'), help='File in which the encrypted / decrypted text will be written.''If not provided, the output text will just be printed.', )@click. Option('--decrypt/--encrypt','-d/-e', help='Whether you want to encrypt the input text or decrypt it.')@click. Option('--key','-k',default=1,help='The numeric key to use for the caesar encryption / decryption.')def caesar(input_file, output_file, decrypt, key):if input_file:text = input_file. Read()else:text = click. Prompt('Enter a text', hide_input=not decrypt)if decrypt:key = -key cyphertext = encrypt(text, key)if output_file:output_file. Write(cyphertext)else: click. Echo(cyphertext)if __name__ == '__main__': caesar()