设计 Command line Interface 的三个 Pattern

VT100 serial terminal

在我们讨论 User Interface 的时候,通常讨论的是 Graph User Interface (GUI)。在我们创建 Command line 工具的时候,好的 User Interface 可以让工具具有很好的用户体验。这种交互方式称为 Command line Interface。比如 Git 就是一个非常优秀的 Command line Interface 案例。

本文将介绍设计 Command line Interface 的三个 Pattern。

1. Command 名称即功能

这是最简单的一种 Command line,这种 Command line 没有参数,通常名称即 Command 功能。比如 install.sh。通常我会用这种方式简化重复的,或者复杂参数的指令。

比如,使用 Docker 启动 mysql 数据库,我会在项目目录下创建一个脚本 run-mysql,它需要包含如下内容:

  • Command 文件
  • 具有可执行权限

run_mysql.sh:

#!/usr/bin/env sh

docker-machine start dev
eval "$(docker-machine env dev)"

docker run --rm -p 3306:3306 --name mysql-5.6 -e MYSQL_ROOT_PASSWORD=root mysql:5.6 --lower-case-table-names=1

赋予执行权限:

➜  shell ✗ chmod +x run-mysql.sh
➜  shell ✗ mv run-mysql.sh run-mysql #去掉 .sh 后缀

使用:

➜  shell ✗ ./run-mysql
Starting "dev"...
Machine "dev" is already running.
Initializing database

在我们创建 run-mysql.sh 时,默认情况下它是没有执行权限的,需要使用 chmod +x run-mysql.sh 来赋予执行权限。

2. 带参数的 Command

通常 Command 需要接收参数,比如: curl http://www.google.com/。这类 Command 会有点复杂度,需要包含如下部分内容:

  • 使用说明,说明参数的意义
  • 对参数做校验
  • 处理参数

例如,我们把上面的脚本做下修改,让他支持 version 这个参数。

#!/usr/bin/env sh

function usage() {
cat <<EOF
Usage:
    run-mysql <version>

Description:
    Run mysql in docker container with specific version.

EOF
  exit 0
}

if [ $# == 0 ]; then
  usage
fi

MYSQL_VERSION=$1
echo "Run mysql:$MYSQL_VERSION via docker ..."

docker-machine start dev
eval "$(docker-machine env dev)"

docker run --rm -p 3306:3306 --name mysql-$MYSQL_VERSION -e MYSQL_ROOT_PASSWORD=root mysql:$MYSQL_VERSION --lower-case-table-names=1

默认输出 Usage:

➜  shell ✗ ./run-mysql
Usage:
    run-mysql <version>

Description:
    Run mysql in docker container with specific version.

提供 version 参数:

➜  shell ✗ ./run-mysql 5.6
Run mysql:5.6 via docker ...
Starting "dev"...
Machine "dev" is already running.
Initializing database

3. Git-like Command line

用过 Git 的小伙伴应该都比较熟悉这种方式,它采用通常 Command + <Sub-Command> + [ARGS] 的方式来组织 Interface。它更适合更多的指令。现代的,大多数流行的 Command line 都采用这种方式,比如 Gem, Rails, Gradle 等。 此时 Command line 需要包含如下内容:

  • 使用说明,说明 Sub-Command 和参数的意义
  • 对参数做校验
  • 处理 Sub-Command
  • 在 Sub-Command 中处理参数

我们对上面的 run-mysql 再进行改造,让它支持 runkill 两个 Sub-Command

例如:

我们将脚本改为 mysql-docker:

➜  shell ✗ ./mysql-docker
Usage:
    mysql-docker <command> <version>

Commands:
    run <version>         Run mysql in docker container with specific version.
    kill <version>        Kill mysql:<version>

启动 mysql 5.6:

➜  shell ✗ ./mysql-docker run 5.6
Starting "dev"...
Machine "dev" is already running.
Run mysql:5.6 via docker ...
Initializing database

Kill mysql 5.6:

➜  shell ✗ ./mysql-docker kill 5.6
Starting "dev"...
Machine "dev" is already running.
Kill mysql 5.6 ...
mysql-5.6

脚本:

#!/usr/bin/env sh

function usage() {
cat <<EOF
Usage:
    mysql-docker <command> <version>

Commands:
    run <version>         Run mysql in docker container with specific version.
    kill <version>        Kill mysql:<version>

EOF
  exit 0
}

if [ $# != 2 ]; then
  usage
fi

docker-machine start dev
eval "$(docker-machine env dev)"

function run() {
  local version=$1
  echo "Run mysql:$version via docker ..."
  docker run --rm -p 3306:3306 --name mysql-$version -e MYSQL_ROOT_PASSWORD=root mysql:$version --lower-case-table-names=1
}

function kill() {
  local version=$1
  echo "Kill mysql $version ..."
  docker kill mysql-$version
}

$@

此时 $@ 用来处理 Sub-command。它用来获取 Command 所有参数。当我们执行 ./mysql-docker run 5.6 时, $@run 5.6。Shell script 按照指令展开来解析 run 5.6,此时会调用 run 方法,并且将 5.6 作为参数。

实践应用

DRY 一直是我们追求的目标,对于像我这么懒惰的 Developer,如果能自动化的事情,我一定不会用手动的方式重复去做。平时在项目中我会创建一些方便的脚本自动化重复的工作。这些脚本会遵循本文提到的 Pattern。

1. Command 名称即功能

./bin/run.sh,这个脚本在所有的 api service 代码库中都有。Dockerfile 中调用它来启动 service:

Dockerfile:

FROM ubuntu-ruby2.3:latest
# setup environment
CMD ["bin/run"]

此时 Dockerfile 并不需要关心如何启动 service,./bin/run.sh 解耦了 service 运行步骤。

2. 带参数的 Command

ssh_ec2 用来链接 AWS EC2 Instance:

➜  shell ✗ ssh_ec2

  Usage:
      ssh_ec2 [INSTANCE_NAME ...] -- ssh to ec2 instances

  Samples:
      ssh_ec2 service-name        -- ssh to service-name ec2 instance

3. Git-like Command line

这类 Command line Interface,非常具有描述性,也是我用的最多的一种。比如:

通过 AWS Auto Scaling 来手动控制 ETL service:

➜  etl-service git:(master) ./bin/etl
Usage:
    ./etl <start|stop> <test|prod>

Commands:
    start         Run etl service
    stop          Terminate etl service ec2 instance

通过 AWS Cloudformation 来创建 SNSSQS 基础设施:

➜  aws-queues git:(master) ./stack
Usage:
    ./stack <command> [ARG]

Commands:
    create [test|prod] [STACK-NAME ...]     Create SNS and SQS stack
    update [test|prod] [STACK-NAME ...]     Update SNS and SQS stack, only avaiable for updating policy
    delete [test|prod] [STACK-NAME ...]     Delete SNS and SQS stack

总结

一个好的 Command line 应该易于理解和使用。按照本文提到的三个 Pattern 可以帮助你设计一个易用的 Command line Interface。

引用一段来自程序员的搞笑注释以供自勉:

// 写这段代码的时候,只有上帝和我知道它是干嘛的
// 现在只有上帝知道

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容