在开始构建命令行应用程序之前,让我们来看一下什么是命令行。
自计算机程序创建以来,命令行程序就无处不在了,很多程序都是由命令构建的。命令行程序是一种通过命令行或者是 shell 进行操作的软件。
命令行界面(Command Line Interface,CLI)是一种可通过终端、shell 或控制台上键入命令来控制(而非使用鼠标)的用户界面。控制台是一种显示模式,其整个屏幕仅显示文本,没有图像和 GUI 控件。
根据维基百科:
CLI 是 1960 年代中期与计算机终端上的大多数计算机系统进行交互的主要方式,并且在整个 1970 年代和 1980 年代继续在 OpenVMS 、 Unix 系统和包括 MS-DOS ,CP/M 和 Apple DOS 在内的个人计算机系统上使用。 该界面通常使用命令行 shell 实现,该命令行 shell 是一个接受命令作为文本输入并将命令转换为对应的操作系统功能。
1. 为什么选择 Python ?
Python 经常因为其灵活性和与其他语言很好的结合使用的特性而被视作胶水语言。大部分 Python 代码都是用于脚本和命令行界面。
构建这些命令行界面和工具将变得非常强大,因为它们能够让你将想到的任何事情自动化执行成为可能。
我们处于一个漂亮且交互式界面的时代,所以 UI (User Interactive,用户交互) 和 UX (User Experience ,用户体验) 至关重要。我们需要将这些内容添加到命令行中,以让人们能够实现它们。而这些已被 Heroku 等流行公司正式使用。
大量的 Python 库和模块可以帮助构建命令行应用程序,包括从参数解析和选项标记到完整的 CLI 框架。这些工具可以执行彩色输出,进度条,发送邮件等操作。
使用这些模块,你可以创建像 Heroku 以及 Node 中 Vue-init 和 NPM-init 这些漂亮的交互式命令行界面程序了。
为了方便构建漂亮的 vue init
命令行程序,我建议使用 Python-inquirer ,这是 Inquirer.js 在 Python 的移植版本。
不幸的是,由于使用了 blessings 模块(一个引入了只有类 UNIX 系统才可以使用的 _curses
和 fcntl
模块的包),Python-inquirer 无法在 Windows 上运行。然而,有一些出色的开发人员能够将 _curses
移植到 Windows 上,但无法移植 fcntl
. Windows 中的 fctnl
可替代品是 win32api
.
但是,经过了在 Google 上大量的搜索之后,我碰到了一个 Python 模块,我对其进行全面修复,并将其称为 PyInquirer ,它是 python-inquirer 的替代品。而它有一个很大的好处就是:它可以在包括 Windows 在内的所有操作系统平台上使用。赞!
2. Python 命令行界面的基础知识
现在让我们来看看命令行界面,并在 Python 中创建一个。
命令行界面(CLI)通常以可执行文件的名称开头。你只需要在控制台中输入名称,这就是访问脚本的的主要入口点,例如 pip
.
你需要根据脚本的开发方式将参数传递给脚本,它们可以是:
- 参数**:** 这是传递给脚本的必需参数。如果不提供,程序就会出错。例如在:
pip install django
这个命令中,django
就是命令的参数。 - 选项**:** 顾名思义,它是一个可选参数,通常是带有名称和值组成的对,例如:
pip install django --cache-dir ./my-cache-dir
中-cache-dir
是一个选项参数的名称,./my-cache-dir
则是值,这组成了一对,意义表示缓存目录指定为./my-cache-dir
这个目录。 - 标志**:** 这是一个特殊的选项参数,它是用来告诉脚本启用或禁用某些行为,最常见的一种可能就是
-help
了。
使用 Heroku Toolbelt 等复杂的命令行程序,其实是将访问的所有命令全部归入到主入口点了。它们通常被视为命令或者子命令。
现在,让我们看以下如何使用不同的 Python 包构建智能且美观的命令行程序。
3. Argparse
Argparse 是 Python 中用于构建命令行程序自带的模块。它提供了构建简单的命令行程序的所需的所有功能。
import argparseparser = argparse.ArgumentParser(description='Add some integers.')
parser.add_argument('integers', metavar='N', type=int, nargs='+',
help='interger list')parser.add_argument('--sum', action='store_const',
const=sum, default=max,
help='sum the integers (default: find the max)')args = parser.parse_args()print(args.sum(args.integers))
这是一个简单的加法操作。argparse.ArgumentParser
让你能够添加程序描述到其中,而 parser.add_argument
让你添加一个命令。parser.parse_args()
则会返回一组参数,它们通常以 名称-值 对的形式出现。
例如:你可以通过 args.integers
访问 integers
的参数值。在上述脚本中,-sum
是一个可选参数, N
是位置参数。
4. Click
与 Argparse 相比,使用 Click 能够更简单的创建命令行程序。argparse 能做的,Click 同样能做。但是使用了稍微不一样的方法。它使用了 decorators 的概念,这使命令成为可以使用装饰器包装的函数。
# cli.py
import click
@click.command()
def main():
click.echo("This is a CLI built with Click ✨")
if __name__ == "__main__":
main()
你可以按下面的方式添加参数和选项:
# cli.py
import click
@click.command()
@click.argument('name')
@click.option('--greeting', '-g')
def main(name, greeting):
click.echo("{}, {}".format(greeting, name))
if __name__ == "__main__":
main()
运行上述脚本,则应有:
$ python cli.py --greeting <greeting> Oyetoke
Hey, Oyetoke
将以上所有的整合在一起,我能够构建一个简单的命令行(CLI)程序来查询 Google Books 中的图书。
import click
import requests
__author__ = "Oyetoke Toby"
@click.group()
def main():
"""
Simple CLI for querying books on Google Books by Oyetoke Toby
"""
pass
@main.command()
@click.argument('query')
def search(query):
"""This search and return results corresponding to the given query from Google Books"""
url_format = '<https://www.googleapis.com/books/v1/volumes>'
query = "+".join(query.split())
query_params = {
'q': query
}
response = requests.get(url_format, params=query_params)
click.echo(response.json()['items'])
@main.command()
@click.argument('id')
def get(id):
"""This return a particular book from the given id on Google Books"""
url_format = '<https://www.googleapis.com/books/v1/volumes/{}>'
click.echo(id)
response = requests.get(url_format.format(id))
click.echo(response.json())
if __name__ == "__main__":
main()
更多有关信息,您可以从官方文档上深入了解 Click.
5. Docopt
Docopt 是一个轻量级的 Python 软件包,可以通过解析 POSIC-样式 或 Markdown 使用说明轻松地创建命令行界面。Docopt 使用多年来用于格式化帮助信息和手册页来描述命令行界面的约定。 *docopt**
中的界面描述就是这样的帮助信息,但是形式化。
Docopt 非常关心文件顶部格式化所需的文档字符串方式。在工具名称的下一行开始,文档第一部分顶部必需是 “Usage”,并且应列出你期望命令被调用的方式。
文档的第二部分开始必需是 “Options” ,这应该提供有关在 “Usage” 中标识的选项和参数的更多信息。文档字符串的内容将程序帮助文档的内容。
"""HELLO CLI
Usage:
hello.py
hello.py <name>
hello.py -h|--help
hello.py -v|--version
Options:
<name> Optional name argument.
-h --help Show this screen.
-v --version Show version.
"""
from docopt import docopt
def say_hello(name):
return("Hello {}!".format(name))
if __name__ == '__main__':
arguments = docopt(__doc__, version='DEMO 1.0')
if arguments['<name>']:
print(say_hello(arguments['<name>']))
else:
print(arguments)
6. PyInquirer
PyInquirer 是一个用于交互式命令行用户界面的模块。我们在上面看到的程序包并没有实现我们想要的 “漂亮界面” 的目标。因此,让我们来看以下如何使用 PyInquirer.
像 Inquirer.js 一样,PyInquirer 分为两个简单的步骤:
- 您定义问题列表并将其传递给命令行。
- 命令行返回答案列表。
from __future__ import print_function, unicode_literals
from PyInquirer import prompt
from pprint import pprint
questions = [
{
'type': 'input',
'name': 'first_name',
'message': 'What\'s your first name',
}
]
answers = prompt(questions)
pprint(answers)
一个互动的例子
from __future__ import print_function, unicode_literals
from PyInquirer import style_from_dict, Token, prompt, Separator
from pprint import pprint
style = style_from_dict({
Token.Separator: '#cc5454',
Token.QuestionMark: '#673ab7 bold',
Token.Selected: '#cc5454', # default
Token.Pointer: '#673ab7 bold',
Token.Instruction: '', # default
Token.Answer: '#f44336 bold',
Token.Question: '',
})
questions = [
{
'type': 'checkbox',
'message': 'Select toppings',
'name': 'toppings',
'choices': [
Separator('= The Meats ='),
{
'name': 'Ham'
},
{
'name': 'Ground Meat'
},
{
'name': 'Bacon'
},
Separator('= The Cheeses ='),
{
'name': 'Mozzarella',
'checked': True
},
{
'name': 'Cheddar'
},
{
'name': 'Parmesan'
},
Separator('= The usual ='),
{
'name': 'Mushroom'
},
{
'name': 'Tomato'
},
{
'name': 'Pepperoni'
},
Separator('= The extras ='),
{
'name': 'Pineapple'
},
{
'name': 'Olives',
'disabled': 'out of stock'
},
{
'name': 'Extra cheese'
}
],
'validate': lambda answer: 'You must choose at least one topping.' \\
if len(answer) == 0 else True
}
]
answers = prompt(questions, style=style)
pprint(answers)
结果:
让我们来看看脚本的部分内容:
style = style_from_dict({
Token.Separator: '#cc5454',
Token.QuestionMark: '#673ab7 bold',
Token.Selected: '#cc5454', # default
Token.Pointer: '#673ab7 bold',
Token.Instruction: '', # default
Token.Answer: '#f44336 bold',
Token.Question: '',
})
style_from_dict
被用来定义让界面自定义样式。 Token
就像一个组件,它下面有其他的组件。
我们在前面的示例中看到了 questions
列表。并将其传递到prompt
中进行处理。
你可以按照下面的示例来创建一个交互式命令行程序:
# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
import regex
from pprint import pprint
from PyInquirer import style_from_dict, Token, prompt
from PyInquirer import Validator, ValidationError
style = style_from_dict({
Token.QuestionMark: '#E91E63 bold',
Token.Selected: '#673AB7 bold',
Token.Instruction: '', # default
Token.Answer: '#2196f3 bold',
Token.Question: '',
})
class PhoneNumberValidator(Validator):
def validate(self, document):
ok = regex.match('^([01]{1})?[-.\\s]?\\(?(\\d{3})\\)?[-.\\s]?(\\d{3})[-.\\s]?(\\d{4})\\s?((?:#|ext\\.?\\s?|x\\.?\\s?){1}(?:\\d+)?)?$', document.text)
if not ok:
raise ValidationError(
message='Please enter a valid phone number',
cursor_position=len(document.text)) # Move cursor to end
class NumberValidator(Validator):
def validate(self, document):
try:
int(document.text)
except ValueError:
raise ValidationError(
message='Please enter a number',
cursor_position=len(document.text)) # Move cursor to end
print('Hi, welcome to Python Pizza')
questions = [
{
'type': 'confirm',
'name': 'toBeDelivered',
'message': 'Is this for delivery?',
'default': False
},
{
'type': 'input',
'name': 'phone',
'message': 'What\'s your phone number?',
'validate': PhoneNumberValidator
},
{
'type': 'list',
'name': 'size',
'message': 'What size do you need?',
'choices': ['Large', 'Medium', 'Small'],
'filter': lambda val: val.lower()
},
{
'type': 'input',
'name': 'quantity',
'message': 'How many do you need?',
'validate': NumberValidator,
'filter': lambda val: int(val)
},
{
'type': 'expand',
'name': 'toppings',
'message': 'What about the toppings?',
'choices': [
{
'key': 'p',
'name': 'Pepperoni and cheese',
'value': 'PepperoniCheese'
},
{
'key': 'a',
'name': 'All dressed',
'value': 'alldressed'
},
{
'key': 'w',
'name': 'Hawaiian',
'value': 'hawaiian'
}
]
},
{
'type': 'rawlist',
'name': 'beverage',
'message': 'You also get a free 2L beverage',
'choices': ['Pepsi', '7up', 'Coke']
},
{
'type': 'input',
'name': 'comments',
'message': 'Any comments on your purchase experience?',
'default': 'Nope, all good!'
},
{
'type': 'list',
'name': 'prize',
'message': 'For leaving a comment, you get a freebie',
'choices': ['cake', 'fries'],
'when': lambda answers: answers['comments'] != 'Nope, all good!'
}
]
answers = prompt(questions, style=style)
print('Order receipt:')
pprint(answers)
结果:
7. PyFiglet
Pyfiglet 是一个用于将字符串转换为带有艺术字体的 ASCII 文本的 Python 模块。Pyfiglet 是 FIGlet (http://www.figlet.org/) 在纯 Python 中的完整移植。
from pyfiglet import Figlet
f = Figlet(font='slant')
print f.renderText('text to render')
运行结果:
8. Clint
Clint 包含了创建命令行程序所需的一切。它支持颜色,超强的可嵌套缩紧上下文管理器,支持自定义邮件样式的引号,超强列打印以及可选的自动扩展列,等等。
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import sys
import os
sys.path.insert(0, os.path.abspath('..'))
from clint.arguments import Args
from clint.textui import puts, colored, indent
args = Args()
with indent(4, quote='>>>'):
puts(colored.blue('Aruments passed in: ') + str(args.all))
puts(colored.blue('Flags detected: ') + str(args.flags))
puts(colored.blue('Files detected: ') + str(args.files))
puts(colored.blue('NOT Files detected: ') + str(args.not_files))
puts(colored.blue('Grouped Arguments: ') + str(dict(args.grouped)))
print()
酷吧?我知道。
9. 其他 Python CLI 工具
Cement: 它是完整的命令行界面框架。Cement 提供了轻量且功能齐全的基础,可以构建从单个文件脚本到复杂且设计精巧的应用程序。
Cliff: Cliff 是用于构建命令行程序的框架。它使用 setuptools 入口点提供子命令,输出格式化程序和其他扩展。
Plac: Plac 是 Python 标准库 argparse 的简单封装。该库通过声明性接口隐藏了大多数复杂的东西:推断参数解析器,而不是强制性地写下来。
10. EmailCLI
将所有内容加在一起,我编写了一个简单的命令行程序,用于通过 SendGrid 发送邮件。因此,要使用以下脚本,请从 SendGrid 获取你的 API 密钥。
10.1 安装
pip install sendgrid click PyInquirer pyfiglet pyconfigstore colorama termcolor six
import os
import re
import click
import sendgrid
import six
from pyconfigstore import ConfigStore
from PyInquirer import (Token, ValidationError, Validator, print_json, prompt,
style_from_dict)
from sendgrid.helpers.mail import *
from pyfiglet import figlet_format
try:
import colorama
colorama.init()
except ImportError:
colorama = None
try:
from termcolor import colored
except ImportError:
colored = None
conf = ConfigStore("EmailCLI")
style = style_from_dict({
Token.QuestionMark: '#fac731 bold',
Token.Answer: '#4688f1 bold',
Token.Instruction: '', # default
Token.Separator: '#cc5454',
Token.Selected: '#0abf5b', # default
Token.Pointer: '#673ab7 bold',
Token.Question: '',
})
def getDefaultEmail(answer):
try:
from_email = conf.get("from_email")
except KeyError, Exception:
from_email = u""
return from_email
def getContentType(answer, conttype):
return answer.get("content_type").lower() == conttype.lower()
def sendMail(mailinfo):
sg = sendgrid.SendGridAPIClient(api_key=conf.get("api_key"))
from_email = Email(mailinfo.get("from_email"))
to_email = Email(mailinfo.get("to_email"))
subject = mailinfo.get("subject").title()
content_type = "text/plain" if mailinfo.get("content_type") == "text" else "text/html"
content = Content(content_type, mailinfo.get("content"))
mail = Mail(from_email, subject, to_email, content)
response = sg.client.mail.send.post(request_body=mail.get())
return response
def log(string, color, font="slant", figlet=False):
if colored:
if not figlet:
six.print_(colored(string, color))
else:
six.print_(colored(figlet_format(
string, font=font), color))
else:
six.print_(string)
class EmailValidator(Validator):
pattern = r"\\"?([-a-zA-Z0-9.`?{}]+@\\w+\\.\\w+)\\"?"
def validate(self, email):
if len(email.text):
if re.match(self.pattern, email.text):
return True
else:
raise ValidationError(
message="Invalid email",
cursor_position=len(email.text))
else:
raise ValidationError(
message="You can't leave this blank",
cursor_position=len(email.text))
class EmptyValidator(Validator):
def validate(self, value):
if len(value.text):
return True
else:
raise ValidationError(
message="You can't leave this blank",
cursor_position=len(value.text))
class FilePathValidator(Validator):
def validate(self, value):
if len(value.text):
if os.path.isfile(value.text):
return True
else:
raise ValidationError(
message="File not found",
cursor_position=len(value.text))
else:
raise ValidationError(
message="You can't leave this blank",
cursor_position=len(value.text))
class APIKEYValidator(Validator):
def validate(self, value):
if len(value.text):
sg = sendgrid.SendGridAPIClient(
api_key=value.text)
try:
response = sg.client.api_keys._(value.text).get()
if response.status_code == 200:
return True
except:
raise ValidationError(
message="There is an error with the API Key!",
cursor_position=len(value.text))
else:
raise ValidationError(
message="You can't leave this blank",
cursor_position=len(value.text))
def askAPIKEY():
questions = [
{
'type': 'input',
'name': 'api_key',
'message': 'Enter SendGrid API Key (Only needed to provide once)',
'validate': APIKEYValidator,
},
]
answers = prompt(questions, style=style)
return answers
def askEmailInformation():
questions = [
{
'type': 'input',
'name': 'from_email',
'message': 'From Email',
'default': getDefaultEmail,
'validate': EmailValidator
},
{
'type': 'input',
'name': 'to_email',
'message': 'To Email',
'validate': EmailValidator
},
{
'type': 'input',
'name': 'subject',
'message': 'Subject',
'validate': EmptyValidator
},
{
'type': 'list',
'name': 'content_type',
'message': 'Content Type:',
'choices': ['Text', 'HTML'],
'filter': lambda val: val.lower()
},
{
'type': 'input',
'name': 'content',
'message': 'Enter plain text:',
'when': lambda answers: getContentType(answers, "text"),
'validate': EmptyValidator
},
{
'type': 'confirm',
'name': 'confirm_content',
'message': 'Do you want to send an html file',
'when': lambda answers: getContentType(answers, "html")
},
{
'type': 'input',
'name': 'content',
'message': 'Enter html:',
'when': lambda answers: not answers.get("confirm_content", True),
'validate': EmptyValidator
},
{
'type': 'input',
'name': 'content',
'message': 'Enter html path:',
'validate': FilePathValidator,
'filter': lambda val: open(val).read(),
'when': lambda answers: answers.get("confirm_content", False)
},
{
'type': 'confirm',
'name': 'send',
'message': 'Do you want to send now'
}
]
answers = prompt(questions, style=style)
return answers
@click.command()
def main():
"""
Simple CLI for sending emails using SendGrid
"""
log("Email CLI", color="blue", figlet=True)
log("Welcome to Email CLI", "green")
try:
api_key = conf.get("api_key")
except KeyError:
api_key = askAPIKEY()
conf.set(api_key)
mailinfo = askEmailInformation()
if mailinfo.get("send", False):
conf.set("from_email", mailinfo.get("from_email"))
try:
response = sendMail(mailinfo)
except Exception as e:
raise Exception("An error occured: %s" % (e))
if response.status_code == 202:
log("Mail sent successfully", "blue")
else:
log("An error while trying to send", "red")
if __name__ == '__main__':
main()
如此而已。
可以看看这篇文章 https://www.davidfischer.name/2017/01/python-command-line-apps/
翻译自:https://codeburst.io/building-beautiful-command-line-interfaces-with-python-26c7e1bb54df