Home > Article > Backend Development > Use click to create the perfect Python command line program
The main job of Python programmers is to write command line programs, that is, scripts that run directly in the terminal. As the project grows in size, we hope to create an effective command line interface that can solve different problems by providing different parameters instead of modifying the source code every time.
In order to achieve this goal, I have summarized four principles, I hope it will be helpful to everyone:
Let's apply these rules to a concrete case: a script that uses the Caesar cipher to encrypt and decrypt messages.
Suppose we write an encrypt function as shown below. Now let's create a script to encrypt and decrypt messages.
The script allows the user to select: mode (encryption or decryption), key. The default value for the former is encryption, and the default value for the latter is 1. This is all achieved through command line parameters.
def encrypt(plaintext, key): cyphertext = '' for character in plaintext: if character.isalpha(): number = ord(character) number += key if character.isupper(): if number > ord('Z'): number -= 26 elif number < ord('A'): number += 26 elif character.islower(): if number > ord('z'): number -= 26 elif number < ord('a'): number += 26 character = chr(number) cyphertext += character return cyphertext
The script needs to get the value of the command line parameter first, let’s use the simplest sys.argv to implement it first.
sys.argv is a list that contains all the parameters entered by the user when running the script (including the script name itself).
Enter the following command in the terminal:
> python caesar_script.py --key 23 --decrypt my secret message pb vhfuhw phvvdjh
sys.argv list includes:
['caesar_script.py', '--key', '23', '--decrypt', 'my', 'secret', 'message']
In order to obtain the parameter value, you need Loop through the argument list, looking for a '--key' (or '-k' ) to get the key value, and a '--decrypt' to get the mode.
import sys from caesar_encryption import encryp def caesar(): key = 1 is_error = False for 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] break for index, arg in enumerate(sys.argv): if arg in ['--encrypt', '-e']: del sys.argv[index] break if arg in ['--decrypt', '-d']: key = -key del sys.argv[index] break if len(sys.argv) == 1: is_error = True else: for arg in sys.argv: if arg.startswith('-'): is_error = True if is_error: print(f'Usage: python {sys.argv[0]} [ --key <key> ] [ --encrypt|decrypt ] <text>') else: print(encrypt(' '.join(sys.argv[1:]), key)) if __name__ == '__main__': caesar()
The code follows the principles we proposed at the beginning:
Have a default key value and a default mode
Handle basic errors (no input text or unknown parameters provided)
Print a concise prompt message when the parameters are wrong or the script is called without parameters
> python caesar_script_using_sys_argv.py Usage: python caesar.py [ --key <key> ] [ --encrypt|decrypt ] <text>
But this version of the script is quite long (39 lines, not Including encryption functions), and the code is very ugly.
Is there a better way to parse command line arguments?
argparse is a Python standard library module used to parse command line arguments.
Modify the script to use argparse to parse command line arguments:
import argparse from caesar_encryption import encrypt def 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.key if args.decrypt: key = -key cyphertext = encrypt(text_string, key) print(cyphertext) if __name__ == '__main__': caesar()
The code still adheres to the principles we proposed and provides more precise documentation and More interactive error handling.
> python caesar_script_using_argparse.py --encode My message usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]] caesar_script_using_argparse.py: error: unrecognized arguments: --encode > python caesar_script_using_argparse.py --help usage: caesar_script_using_argparse.py [-h] [-e | -d] [-k KEY] [text [text ...]]positional arguments: text optional arguments: -h, --help show this help message and exit -e, --encrypt -d, --decrypt -k KEY, --key KEY
Lines 7 to 13 of the script define command line arguments, but they are not very elegant: too verbose and procedural, we could use something more compact and declarative way completed.
Fortunately, there is a third-party library click for creating a command line interface. It not only provides more functions than argparse, but also has a more code style. pretty. Replace argparse with click and continue optimizing the script.
import click from caesar_encryption import encrypt @click.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 command line arguments and options are declared in decorators, which makes them directly accessible as function arguments.
Let us analyze the above code carefully:
nargs defines the number of values received by the command line parameter. The default value is 1, and nargs=-1 allows any number of words to be provided.
--encrypt/--decrypt defines mutually exclusive options, which are ultimately passed to the program as a Boolean value.
click.echo is the basic function provided by the click library. Its function is similar to print, but it provides more powerful functions, such as adjusting the color of text printed to the console.
The value received by the command line parameter is a top secret message that will be encrypted, so it may raise security concerns if the user is asked to enter plain text directly into the terminal .
A safer approach is to use hidden hints, or read the text from a local file, which is more practical for long texts.
The same idea applies to output: the user can save it to a file, or print it out in the terminal. Let's continue optimizing the script.
import click from caesar_encryption import encrypt @click.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()
As the script becomes more complex, we create a parameter document (implemented by defining the help parameter of the click.option decorator) to explain the function of the parameter in detail. The effect is as follows.
> python caesar_script_v2.py --help Usage: caesar_script_v2.py [OPTIONS] Options: --input_file FILENAMEFile in which there is the text you want to encrypt/decrypt. If not provided, a prompt will allow you to type the input text. --output_file FILENAME File in which the encrypted/decrypted text will be written. If not provided, the output text will just be printed. -d, --decrypt / -e, --encryptWhether you want to encrypt the input text or decrypt it. -k, --key INTEGERThe numeric key to use for the caesar encryption / decryption. --help Show this message and exit.
We have two new parameters input_file and output_file, the type is click.File, click will open the file in the correct mode and handle possible errors. For example, the file cannot be found:
> python caesar_script_v2.py --decrypt --input_file wrong_file.txt Usage: caesar_script_v2.py [OPTIONS] Error: Invalid value for "--input_file": Could not open file: wrong_file.txt: No such file or directory
If input_file is not provided, we use click.prompt to create a prompt window on the command line to allow the user to directly enter text. This prompt will be used in encryption mode. is hidden. The effect is as follows:
> python caesar_script_v2.py --encrypt --key 2 Enter a text: ************** yyy.ukectc.eqo
假设你是一名黑客:想要解密一个用凯撒加密过的密文,但你不知道秘钥是什么。最简单的策略就是用所有可能的秘钥调用解密函数 25 次,阅读解密结果,看看哪个是合理的。但你很聪明,而且也很懒,所以你想让整个过程自动化。确定解密后的 25 个文本哪个最可能是原始文本的方法之一,就是统计所有这些文本中的英文单词的个数。这可以使用 PyEnchant 模块实现:
import click import enchant from caesar_encryption import encrypt @click.command() @click.option( '--input_file', type=click.File('r'), required=True, ) @click.option( '--output_file', type=click.File('w'), required=True, ) def caesar_breaker(input_file, output_file): cyphertext = input_file.read() english_dictionnary = enchant.Dict("en_US") max_number_of_english_words = 0 for key in range(26): plaintext = encrypt(cyphertext, -key) number_of_english_words = 0 for word in plaintext.split(' '): if word and english_dictionnary.check(word): number_of_english_words += 1 if number_of_english_words > max_number_of_english_words: max_number_of_english_words = number_of_english_words best_plaintext = plaintext best_key = key click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:nn{best_plaintext[:1000]}...') output_file.write(best_plaintext) if __name__ == '__main__': caesar_breaker()
示例中的文本包含10^4个单词,因此该脚本需要大约5秒才能解密。这很正常,因为它需要检查所有25个秘钥,每个秘钥都要检查10^4个单词是否出现在英文字典中。
假设你要解密的文本包括10^5个单词,那么就要花费50秒才能输出结果,用户可能会非常着急。因此我建议这种任务一定要显示进度条。特别是,显示进度条还非常容易实现。下面是个显示进度条的例子:
import click import enchant from tqdm import tqdm from caesar_encryption import encrypt @click.command() @click.option( '--input_file', type=click.File('r'), required=True, ) @click.option( '--output_file', type=click.File('w'), required=True, ) def caesar_breaker(input_file, output_file): cyphertext = input_file.read() english_dictionnary = enchant.Dict("en_US") best_number_of_english_words = 0 for key in tqdm(range(26)): plaintext = encrypt(cyphertext, -key) number_of_english_words = 0 for word in plaintext.split(' '): if word and english_dictionnary.check(word): number_of_english_words += 1 if number_of_english_words > best_number_of_english_words: best_number_of_english_words = number_of_english_words best_plaintext = plaintext best_key = key click.echo(f'The most likely encryption key is {best_key}. It gives the following plaintext:nn{best_plaintext[:1000]}...') output_file.write(best_plaintext) if __name__ == '__main__': caesar_breaker()
这里使用了tqdm库,tqdm.tqdm类可以将任何可迭代对象转化为一个进度条。click也提供了类似的接口来创建进度条(click.progress_bar),但我觉得它不如tqdm好用。
The above is the detailed content of Use click to create the perfect Python command line program. For more information, please follow other related articles on the PHP Chinese website!