Linux基础系列文章大纲 Shell系列文章大纲
使用bashly构建bash脚本命令行 众所周知,使用比较古老的shell(比如bash)去编写带命令行选项的脚本是非常让人头疼的事情,比较现代的shell,在设计命令行选项方面会更简单一些,比如现在已经烧出了一点火势的更现代化的nushell,它对编写命令行选项的支持力度就非常不错。
Bash设计命令行选项参数的苦恼 以Bash为例,如果脚本的选项参数比较简单,倒也能接受,通常会采用while + case
来处理选项和参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 while [ $# -gt 0 ];do case "$1 " in -a | --aaa) arg_a = $2 ; shift shift ;; -b | --bbb) arg_b = 1; shift ;; ...... *) usage exit 1 esac done
如果选项和参数的逻辑稍微再复杂一点,一般会结合getopt
命令或getopts
命令先收集并整理参数,再结合while + case
来处理整理后的选项和参数。
如果逻辑再复杂一些,我一般不会再有心思去设计一个健壮的、完整的shell脚本的命令行选项,这会花费大量时间,并且很难让它健壮,因此会让我在心态上就产生退却感:只是一个shell脚本,将就用用吧。
我个人比较常写shell脚本,也经常需要设计脚本的命令行选项,也常常因此而有些烦恼:设计shell脚本的命令行太耗时太啰嗦了。
谁能拯救我,唯有bashly bashly是一个用来构建bash脚本命令行选项的框架(算是脚手架吧,没想到吧,bash竟然都有脚手架)。通过在yaml格式的配置文件中编写命令行选项和参数以及其它和命令行有关的信息,就可以生成一个功能完整的命令行脚本框架,在生成的框架结构下,用户只需填补命令行的运行逻辑即可。
从我看它的文档开始,我就迷上它了。我可以肯定(而且现在已经肯定),它丝毫不逊色于通用编程语言的那些优秀的用来设计命令行选项的第三方库,甚至我觉得学习时比它们要更简单更统一。尽管刚开始看到它的时候我很惊讶,因为它颠覆了我一贯的看法:bash这个丑陋的老祖宗只能用非常古老和繁琐的手法来供着。
注意,bashly只适配bash,且要求bash版本高于4.0
下面给一个非常简单的示例过过眼,之后再详细介绍bashly的用法和功能。
假设现在已经安装好了bashly(后文介绍安装方式),执行如下命令初始化脚本项目(项目目录bashly_test
):
1 2 3 4 5 $ mkdir bashly_test && cd bashly_test $ bashly init --minimal
bashly init
将会在项目目录下生成src/bashly.yml
(目前仅生成了这一个文件),要设计脚本的选项和参数,编辑这个bashly.yml配置文件即可。目前这个自动生成的文件内容为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 name: download help: Sample minimal application without commands version: 0.1 .0 args: - name: source required: true help: URL to download from - name: target help: "Target filename (default: same as source)" flags: - long: --force short: -f help: Overwrite existing files examples: - download example.com - download example.com ./output -f
这个文件的内容表示,脚本命令的名称是download
,有一个选项和两个参数,用法在examples
字段已经非常明显。
当然,现在只生成了这一个文件,并没有生成download命令。
当编写好或修改好bashly.yml之后,就可以通过bashly的generate
子命令来生成download命令:
这将会生成如下目录结构:
1 2 3 4 5 6 7 $ tree . . ├── download └── src ├── bashly.yml ├── initialize.sh └── root_command.sh
download命令已经生成好了,在项目根目录之下,且具备执行权限。它是一个完整的bash编写的脚本,可以单独拷贝到任何地方去执行。
执行download命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 $ ./download --help download - Sample minimal application without commands Usage: download SOURCE [TARGET] [options] download --help | -h download --version | -v Options: --help , -h Show this help --version, -v Show version number --force, -f Overwrite existing files Arguments: SOURCE URL to download from TARGET Target filename (default: same as source ) Examples: download example.com download example.com ./output -f $ ./download missing required argument: SOURCE usage: download SOURCE [TARGET] [options] $ ./download income outcome -f args: - ${args[--force]} = 1 - ${args[source]} = income - ${args[target]} = outcome
最后执行./download income outcome -f
的输出结果,是未做任何修改的初始模板运行逻辑,逻辑部分是可以修改的。后文再详细介绍如何修改逻辑。
从运行download命令的结果可以看到,bashly根据yml配置文件生成的download脚本,其命令行选项的功能很不错,确实是那个我们熟悉的味道。
安装bashly 最简单的方式是直接使用已经配置好bashly环境的docker镜像:
1 alias bashly='docker run --rm -it --user $(id -u):$(id -g) --volume "$PWD:/app" dannyben/bashly'
如果要在本机使用bashly,按照下面的步骤操作。
确保已经安装bash 4.0以上的版本,如果系统里没有bash或者版本低于4.0,自行搜索安装方法。我更建议直接换个自带bash 4.0以上版本的系统,之后再将bashly生成好的脚本命令拷贝到没有bash的环境。
bashly是Ruby编写的,bashly运行时也需要Ruby,因此先安装Ruby(要求版本号大于2.7),再通过Ruby的包管理器gem(安装Ruby的同时会安装好gem)来安装bashly。
可以先查看包管理器提供的官方Ruby版本是否大于2.7,如果版本高于2.7,可直接通过包管理器快速安装:
1 2 3 4 5 6 7 8 $ apt show ruby Package: ruby Version: 1:2.7+1 Priority: optional Section: interpreters Source: ruby-defaults ......
如果包管理器提供的Ruby版本不够,考虑使用rbenv来管理安装Ruby。关于安装rbenv和安装Ruby的方法,参考我那古老的文章:https://www.junmajinlong.com/ruby/ruby_rails_install/ 。
假如已经安装好了Ruby和gem,如下方式安装bashly:
简单分析bashly生成的文件内容 bashly根据配置文件生成了模板命令之后,需要手动去填充命令的运行逻辑。
以前文生成的download命令为例,生成download后的目录结构如下:
1 2 3 4 5 6 7 $ tree . . ├── download └── src ├── bashly.yml ├── initialize.sh └── root_command.sh
除了download外,还生成了root_command.sh和initialize.sh两个文件。
initialize.sh是用来初始化的,在download中调用任何函数之前,会先执行这个文件中的命令。看bashly生成的download文件中执行逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 initialize () { version="0.1.0" long_usage='' set -e } run () { ...... } initialize run "$@ "
initialize.sh文件初始时是空的(只有一些注释行)。
需注意,所有##
开头的被认为是bashly的注释信息,是完全被忽略的,而那些只有一个#
的行,会被写入download文件。
比如可以在initialize.sh中来设置脚本内部运行的环境变量,因为环境变量的设置一般要优先于逻辑的运行。注意,initialize.sh文件不能被删除。
再看另一个root_command.sh文件。root_command.sh文件就是填充命令行运行逻辑的地方。该文件的初始内容:
1 2 3 echo "# this file is located in 'src/root_command.sh'" echo "# you can edit it freely and regenerate (it will not be overwritten)" inspect_args
两个echo
命令和一个inspect_args
命令,inspect_args
命令是bashly自动生成的用来查看选项和值信息的函数,它被定义在download
文件中。
root_command.sh文件的内容在每次执行bashly generate
的时候都会自动填充到download
文件中。因此,在没有对root_command.sh文件做出任何修改的时候,执行download命令的输出结果正是这里初始化的内容:
1 2 3 4 5 6 7 $ ./download income outcome -f args: - ${args[--force]} = 1 - ${args[source]} = income - ${args[target]} = outcome
最后,看一下bashly生成的download文件的内容(比较长,因此删除一部分):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #!/usr/bin/env bash root_command () { } download_usage () { ...... } normalize_input () { local arg flags while [[ $# -gt 0 ]]; do ...... done } inspect_args () { ...... } parse_requirements () { ...... action="root" while [[ $# -gt 0 ]]; do key="$1 " case "$key " in ...... esac done } initialize () { } run () { declare -A args=() declare -a other_args=() declare -a input=() normalize_input "$@ " parse_requirements "${input[@]} " if [[ $action == "root" ]]; then root_command fi } initialize run "$@ "
因此,bashly生成的脚本命令的逻辑很简单:
(initialize函数)执行初始化操作(操作内容来自src/initialize.sh文件)
(normalize_input函数)收集所有命令行中的选项和参数,放入input
数组
(parse_requirements函数)处理input数组,将选项和选项的值放进args
数组,将剩余的参数放进other_args
数组
(root_command函数)执行代码逻辑(代码逻辑来自src/root_command.sh文件)
注意,在本示例中生成的download文件中没有填充other_args
数组的代码,因为本示例的download命令不允许额外参数。如果要允许额外参数,需在配置文件中使用catch_all
指令。后文会介绍该指令。
当需要编写自己的命令行选项逻辑时,应该将相关的逻辑填充到src/root_command.sh
文件中,在这过程中,最重要的就是从args
数组获取选项和参数的值。
bashly如何定义Command 定义命令行时的常用指令 在bashly.yml文件中,大概以这种方式定义一个命令行选项和参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 name: cli help: 这是一个命令行 version: 0.0 .1 flags: - long: --flag1 short: -f help: 这是flag1 - long: --flag2 short: -F arg: FLAG2 help: 这是flag2, 该选项需要一个参数 - long: --flag3 required: true help: 这是flag3, 该选项没有对应的短选项,且该选项不可省略 args: - name: arg1 required: true help: 这是参数1,不可省略 - name: arg2 help: 这是参数2,可以省略 examples: - cli -f -F F_arg --flag3 arg1 arg2 - cli --flag3 arg1 footer: | 这是footer信息,在help信息的下方显示, 可以使用多行
根据这个配置文件,生成命令行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 $ bashly generate $ ./cli --help cli - 这是一个命令行 Usage: cli ARG1 [ARG2] [options] cli --help | -h cli --version | -v Options: --help , -h Show this help --version, -v Show version number --flag1, -f 这是flag1 --flag2, -F FLAG2 这是flag2, 该选项需要一个参数 --flag3 (required) 这是flag3, 该选项没有对应的短选项,且该选项不可省略 Arguments: ARG1 这是参数1,不可省略 ARG2 这是参数2,可以省略 Examples: cli -f -F F_arg --flag3 arg1 arg2 cli --flag3 arg1 这是footer信息,在help 信息的下方显示, 可以使用多行
bashly定义子命令(sub command) bashly定义子命令的方式出乎意料的简单,直接在command下使用commands
指令就可以。
需要注意,在使用bashly定义子命令时,是不支持父命令带有其它选项的。也就是说,bashly.yml文件中commands
指令的同一层次,不允许和args
指令共存,如果共存将报错,但是可以和flags
指令共存,此时flags指定的是这些子命令的全局选项,全局选项需在命令行的子命令之前指定,例如cli -w a subcmd
,此处-w VALUE
是子命令的全局选项。
另外,子命令可以嵌套,即子命令中定义子命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 name: cli help: 这是一个命令行 version: 0.0 .1 commands: - name: sub1 help: 这是子命令1 flags: - short: -f help: 子命令1的选项1,该选项只有短选项 - long: --FFF short: -F help: 子命令1的选项2 args: - name: arg1 help: 子命令1的参数 - name: sub2 help: 这是子命令2 flags: - short: -f help: 子命令2的选项1,该选项只有短选项 - long: --FFF short: -F help: 子命令2的选项2 args: - name: arg1 help: 子命令2的参数
生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 $ bashly generate $ ./cli --help cli - 这是一个命令行 Usage: cli [command ] cli [command ] --help | -h cli --version | -v Commands: sub1 这是子命令1 sub2 这是子命令2 Options: --help , -h Show this help --version, -v Show version number $ ./cli sub1 --help cli sub1 - 这是子命令1 Usage: cli sub1 [ARG1] [options] cli sub1 --help | -h Options: --help , -h Show this help -f 子命令1的选项1,该选项只有短选项 --FFF, -F 子命令1的选项2 Arguments: ARG1 子命令1的参数
需注意,使用子命令时,在bashly generate
的时候会为每一个子命令都生成一个与子命令对应的shell脚本文件,这些文件专门用来编写只属于该子命令的逻辑代码。
1 2 3 4 5 6 7 8 $ tree . . ├── cli └── src ├── bashly.yml ├── initialize.sh ├── sub1_command.sh └── sub2_command.sh
alias指令:子命令别名 alias指令定义子命令的别名,只能用于子命令中。
有以下几种别名定义方式:
1 2 3 4 5 6 7 8 name: index alias: i name: download alias: d* name: upload alias: [u , push ]
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 name: cli help: Sample application version: 0.1 .0 commands: - name: download alias: d help: Download a file args: - name: source required: true help: URL to download from - name: target help: "Target filename (default: same as source)" - name: upload alias: [u , push ] help: Upload a file args: - name: source required: true help: File to upload
对于上面配置文件生成的脚本文件,它:
1 2 3 4 5 6 7 8 cli download example.com ./output cli d example.com ./output cli upload README.md cli push README.md cli u README.md
group指令:子命令分组显示 如果子命令数量较多,在输出的帮助信息中,可以按照类别进行分类显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 name: ftp help: Sample application with command grouping version: 0.1 .0 commands: - name: download help: Download a file group: File args: - name: file required: true help: File to download - name: upload help: Upload a file group: File args: - name: file required: true help: File to upload - name: login help: Write login credentials to the config file group: Login - name: logout help: Delete login credentials to the config file group: Login
输出帮助信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ ./ftp --help ftp - Sample application with command grouping Usage: ftp [command ] ftp [command ] --help | -h ftp --version | -v File Commands: download Download a file upload Upload a file Login Commands: login Write login credentials to the config file logout Delete login credentials to the config file Options: --help , -h Show this help --version, -v Show version number
嵌套子命令 下面是子命令中可以嵌套子命令的示例,用法之一是./get_ks url -w spot cm -f 2022-03-22
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 name: get_ks help: 解析和下载数据, 并保存和转换 version: 0.1 .0 commands: - name: url alias: u help: 分析待下载的URL并输出至标准输出 flags: - long: --which short: -w arg: WHICH allowed: [spot , future , all ] help: spot还是future, 或是两者 commands: - name: history alias: hs help: 返回所有历史数据 - name: check-missed alias: cm help: 检查缺失数据 flags: - long: --from short: -f arg: Date help: 从什么时候开始检查
environment_variables指令:设置环境变量 environment_variables
用来设置命令执行时的环境变量。
1 2 3 4 5 6 7 environment_variables: - name: config_path help: Location of the config file default: ~/config.ini - name: api_key help: Your API key required: true
当将某环境变量设置为required,如果在运行时如果没有提供该环境变量,脚本的执行将报错。
可以在bashly.yml的命令顶层使用该指令指定整个命令的环境变量,也可以在子命令层指定只有运行该子命令时的环境变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 name: ftp help: Sample application with command grouping version: 0.1 .0 environment_variables: - name: config_path help: Location of the config file default: ~/config.ini commands: - name: login help: Write login credentials to the config file environment_variables: - name: api_key help: Your API key required: true - name: logout help: Delete login credentials to the config file
查看帮助信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 $ ./ftp --help ftp - Sample application with command grouping Usage: ftp [command ] ftp [command ] --help | -h ftp --version | -v Commands: login Write login credentials to the config file logout Delete login credentials to the config file Options: --help , -h Show this help --version, -v Show version number Environment Variables: CONFIG_PATH Location of the config file Default: ~/config.ini $ ./ftp login --help ftp login - Write login credentials to the config file Usage: ftp login ftp login --help | -h Options: --help , -h Show this help Environment Variables: API_KEY (required) Your API key
private指令:隐藏某个子命令 private
指令可在输出帮助信息时隐藏某个子命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 name: cli help: Sample application with private commands version: 0.1 .0 commands: - name: connect alias: c help: Connect to the metaverse args: - name: protocol required: true allowed: [ftp , ssh ] help: Protocol to use for connection - name: connect-ftp help: Connect via FTP private: true - name: connect-ssh help: Connect via SSH private: true
输出帮助信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ ./cli --help cli - Sample application with private commands Usage: cli [command ] cli [command ] --help | -h cli --version | -v Commands: connect Connect to the metaverse Options: --help , -h Show this help --version, -v Show version number
catch_all指令:允许定义之外的参数 在没有使用catch_all
的情况下,bashly解析命令行选项时,要求命令行中所给的参数必须完全符合所定义的选项和参数,不允许多提供无法识别的参数。
例如,对于这个配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 name: cli help: 这是一个命令行 version: 0.0 .1 flags: - long: --flag1 short: -f help: 这是flag1 - long: --flag2 short: -F arg: FLAG2 help: 这是flag2, 该选项需要一个参数 args: - name: arg1 required: true help: 这是参数1,不可省略 - name: arg2 help: 这是参数2,可以省略
该命令只能接受最多两个特定的选项,最多两个参数,如果再多传递一个参数,就会报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ ./cli -f -F a a1 a2 args: - ${args[--flag1]} = 1 - ${args[--flag2]} = a - ${args[arg1]} = a1 - ${args[arg2]} = a2 $ ./cli -f -F a a1 a2 a3 invalid argument: a3 $ ./cli -f -F a a1 a2 -a invalid option: -a
如果使用catch_all
指令,则可以提供更多参数而不报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 name: cli help: 这是一个命令行 version: 0.0 .1 catch_all: true flags: - long: --flag1 short: -f help: 这是flag1 - long: --flag2 short: -F arg: FLAG2 help: 这是flag2, 该选项需要一个参数 args: - name: arg1 required: true help: 这是参数1,不可省略 - name: arg2 help: 这是参数2,可以省略
执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 $ ./cli -f -F a a1 a2 a3 args: - ${args[--flag1]} = 1 - ${args[--flag2]} = a - ${args[arg1]} = a1 - ${args[arg2]} = a2 other_args: - ${other_args[*]} = a3 - ${other_args[0]} = a3 $ ./cli -f -F a a1 a2 -a args: - ${args[--flag1]} = 1 - ${args[--flag2]} = a - ${args[arg1]} = a1 - ${args[arg2]} = a2 other_args: - ${other_args[*]} = -a - ${other_args[0]} = -a
从结果看到,额外的参数都会被收集到other_args
数组中。
catch_all
指令有三种设置方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 catch_all: true catch_all: label_name catch_all: label: label_name help: help message required: true
看输出示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 Usage: cli ARG1 [ARG2] [options] [LABEL_NAME...] cli --help | -h cli --version | -v Usage: cli ARG1 [ARG2] [options] [LABEL_NAME...] cli --help | -h cli --version | -v Usage: cli ARG1 [ARG2] [options] [LABEL_NAME...] cli --help | -h cli --version | -v Options: ... Arguments: ... LABEL_NAME... help message Usage: cli ARG1 [ARG2] [options] LABEL_NAME... cli --help | -h cli --version | -v Options: ... Arguments: ... LABEL_NAME... help message
dependencies指令:外部命令依赖 通过dependencies
指令,可以指定该命令或该子命令依赖于某些外部命令,如果外部命令不存在,则执行将会报错。
1 2 3 4 5 6 7 8 9 10 11 12 name: cli help: Sample application that requires dependencies version: 0.1 .0 commands: - name: download help: Download something dependencies: - git - curl - shmurl - name: upload help: Upload something
extensible指令:委托给外部命令 类似于git
,当执行git something
的时候,会转而执行git-something
命令。通过extensible
指令,也可以实现这样的效果。
有两种委托方式:
1 2 3 4 5 6 7 8 9 name: mygit extensible: true name: mygit extensible: git
extensible
不能和default
共存,因为它们的逻辑存在冲突。
bashly如何定义选项 bashly通过flags
指令定义选项,在flags
指令之下,支持下面这些指令:
long
:指定长选项的名称(和short至少提供一个)
short
:指定短选项的名称(和long至少提供一个)
help
:选项说明
arg
:如果选项需要参数,加上该指令,该指令指定参数显示名称。不加该指令,则表示选项不需要参数
default
:为该选项指定默认的参数值,需同时指定arg
指令,不能和required
共存
required
:该选项是否必须存在,设置为false(默认)表示该选项可以省略,不能和default
共存
allowed
:一个数组,该选项的参数的值必须是该数组中的一个,需同时指定arg
指令,且隐含了required: true
,因此使用该功能,意味着该选项必须指定
conflicts
:一个数组,明确指定该选项和哪些选项冲突,即不能和哪些选项共存。使用该指令时,应当在所有互相冲突的选项上都指定该指令
repeatable
:该选项可以重复出现多次,如果使用短选项没有参数,则可以结合,例如-v -v
等价于-vv
,长选项或者带有参数时,必须分开多次指定,例如--f1 --f1
和-v v1 -v v2
。参考https://bashly.dannyb.co/configuration/flag/#repeatable)
validate
:对参数进行验证,需同时指定arg
指令。如何进行验证,参考后文validate:验证参数 。
当选项带有参数时,传递参数时,下面两种方式是等价的:
1 2 -a=arg <=> -a arg --flag=arg <=> --flag arg
bashly如何定义参数 bashly通过args
指令定义参数,在args
指令之下,支持下面这些指令:
filters指令:前置筛选条件 通过filters
指令,可以指定一个或多个命令执行前的筛选函数:
这些筛选函数都是自己定义的
如果任一筛选函数输出了任何内容,都会认为筛选失败,不会执行后续的命令
只有所有筛选函数都不输出任何内容,才会在筛选完成之后继续执行命令的逻辑
例如:
1 2 3 name: viewer filters: - docker_running
这里指定的是docker_running
,将会寻找src/lib/filter_docker_running.sh
文件中的filter_docker_running
函数并执行(在生成最终的脚本命令时会将该函数拷贝到脚本中)。
1 2 3 filter_docker_running () { docker info >/dev/null 2>&1 || echo "Docker must be running" }
需要说明的是,如果是要判断环境变量是否设置,则更建议使用environment_variables
指令,如果是要判断某个命令是否存在,则更建议使用dependencise
指令。
validate指令:验证参数 如果要验证参数的值(可以是选项的参数,也可以是直接定义的参数),可以使用validate
指令。
通过validate
指令,可以指定一个或多个参数验证函数:
这些验证函数有内置的,也可以是自己定义的
如果任一验证函数输出了任何内容,都会认为验证失败从而报错
只有所有验证函数都不输出任何内容,才会继续执行
例如:
1 2 3 4 name: viewer args: - name: path validate: file_exists
这里指定了验证函数file_exists
,这将会寻找src/lib/validate_file_exists.sh
文件中的validate_file_exists
函数。
1 2 3 validate_file_exists () { [[ -f "$1 " ]] || echo "must be an existing file" }
bashly通过执行bashly add validations
命令,可以自动获得一些内置的验证函数:
file_exists
:验证参数指定的是一个文件且存在
dir_exists
:验证参数指定的是一个目录且存在
integer
:验证参数是一个数值
not_empty
:验证参数是非空的
内置库函数和自定义库函数 在src/lib
目录下的所有文件内容,都会合并到最终生成的脚本文件中。
因此,可以在src/lib/xxx.sh
文件中定义一些函数,然后在编写代码逻辑的文件(如root_command.sh
文件)中调用这些函数。
bashly通过执行下面命令也可以获得带颜色输出的内置库函数:
例如:
1 2 echo "before $(red this is red) after" echo "before $(green_bold this is green_bold) after"
colors
默认提供了下面这些输出方式:
1 2 3 4 5 6 7 8 red red_bold red_underlined green green_bold green_underlined yellow yellow_bold yellow_underlined blue blue_bold blue_underlined magenta magenta_bold magenta_underlined cyan cyan_bold cyan_underlined bold underlined
较为复杂的命令行选项解析示例 下面是我个人实际使用的一个命令行选项解析的示例,由多个子命令组成,如果不打散为子命令,逻辑将非常复杂,分散为子命令让每个子命令的逻辑都变得非常简洁。
bashly.yml的文件内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 name: get_ks help: 解析和下载K数据, 并保存和转换 version: 0.1 .0 commands: - name: build alias: b help: 编译并将二进制拷贝到 /tmp/get_ks_apps 目录下 - name: url alias: u help: 分析待下载的URL并输出至标准输出 flags: - long: --url-type short: -u arg: URL-TYPE validate: url_type conflicts: [--his-urls , --his-symbols ] help: 指定url类型, 有效值 zip/api - long: --kline-type short: -k arg: KLINE-TYPE validate: kline_type conflicts: [--his-urls , --his-symbols ] help: 指定K类型, 有效值 [1 |5|15|30 ]m [1 |2|4|6|8|12 ]h 1d - long: --force-from short: -f arg: Date validate: from_date conflicts: [--his-urls , --his-symbols ] help: 强制从何处开始计算要下载的zip, --url-type的值必须是zip, 格式:"2021-12-23" - long: --his-symbols short: -H conflicts: [--his-urls , --force-from , --kline-type , --url-type ] help: 返回所有历史上的USDT交易对(包含曾经下架的交易对) - long: --his-urls short: -h arg: Year conflicts: [--his-symbols , --force-from , --kline-type , --url-type ] validate: from_year help: 从哪一年开始下载历史数据, 有效值:2017及之后 examples: - (1).下载 1m 的 zip 数据 - get_ks url --url-type zip --kline-type 1m - (2).下载 1h 的 api 数据 - get_ks url --url-type api --kline-type 1h - (3).强制从2021-01-01开始下载 1h 的 zip 数据 - get_ks url --url-type zip --kline-type 1h --force-from 2021-01-01 - (4).输出包含已下架的所有USDT交易对 - get_ks url --his-symbols - (5).下载包含已下架的所有USDT交易对从2021年开始的zip数据 - get_ks url --his-urls 2021 - name: download alias: d help: 根据给定的URL下载数据 flags: - long: --url-file short: -u arg: URL-FILE validate: file_exists help: 保存了urls的文件, 将从此文件读取待下载的url链接, 不指定时默认从标准输入中读取 - long: --csv-dir short: -c arg: CSV-DIR default: /tmp/ks/csv help: 保存csv文件的目录路径, 默认 /tmp/ks/csv - long: --delete-csv short: -d help: 开始下载前, 是否先清空csv_dir - long: --zip-file short: -z arg: ZIP-FILE help: | 指定该选项时, 将在下载完成后把所有csv文件归档压缩为zip文件, 该选项指定zip文件的路径(注意,该选项会在归档完成后删除csv_dir) - long: --jobs short: -j arg: NUM validate: check_jobs help: 并发下载的数量(值的范围:1-100), 默认值50 - long: --no-bar short: -n help: 是否禁止输出进度条信息, 默认已开启进度条 - name: store alias: s help: 将csv中的数据存储到数据库 flags: - long: --csv-dir short: -c arg: CSV-DIR default: /tmp/ks/csv validate: dir_exists help: 指定csv文件的存放目录 - long: --delete short: -d help: 存储csv完成后, 是否删除csv文件 examples: - get_ks store --csv-dir /tmp/ks/csv --delete - name: convert alias: c help: 转换数据库中的K类型 flags: - long: --from short: -f arg: SRC-KLINE-TYPE allowed: [1m , 5m , 15m , 30m , 1h , 2h , 4h , 6h , 8h , 12h , 1d ] help: 转换的源类型 - long: --to short: -t arg: DEST-KLINE-TYPE allowed: [1m , 5m , 15m , 30m , 1h , 2h , 4h , 6h , 8h , 12h , 1d ] help: 转换的目标类型 footer: 注意:要求 to 的类型要能够从 from 进行转换, 例如 4h 不能转换为 8h - name: delete alias: D help: 删除或清空数据库中的数据 flags: - long: --symbols short: -s arg: SYMBOLS required: true help: | 指定要删除哪些交易对中的数据, 多个交易对使用逗号分隔, 不区分大小写, 可省略尾部USDT. 特殊值all表示所有交易对, 格式参考: "btcusdt,ETHUSDT,doge" - long: --types short: -t arg: TYPES required: true allowed: [1m , 5m , 15m , 30m , 1h , 2h , 4h , 6h , 8h , 12h , 1d ] help: | 指定要删除的类型, 多个类型使用逗号分隔, 不区分大小写, 特殊值all表示删除所有类型的数据, 格式参考:1m,5m,15m,30m,1h,1d - long: --from short: -f arg: FROM required: true validate: integer help: | 从哪里开始删除数据(秒级Epoch), 特殊值0表示清空数据(truncate), 比删除所有数据更快
最终的文件和目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 . ├── get_ks # 最终生成的命令 └── src ├── bashly.yml ├── build_command.sh # build子命令对应的脚本 ├── convert_command.sh # convert子命令对应的脚本 ├── delete_command.sh # delete子命令对应的脚本 ├── download_command.sh # download子命令对应的脚本 ├── store_command.sh # store子命令对应的脚本 ├── url_command.sh # url子命令对应的脚本 ├── initialize.sh # 用于初始化动作的脚本,其中定义了一些全局变量 └── lib ├── check_apps.sh # 我自己自定义的库函数 ├── colors.sh # bashly add colors添加的库函数 ├── validate_check_jobs.sh # 下面几个都是自定义的参数验证函数 ├── validate_from_date.sh ├── validate_from_year.sh ├── validate_kline_type.sh ├── validate_url_type.sh └── validations # bashly add validations添加的验证库函数 ├── validate_dir_exists.sh ├── validate_file_exists.sh ├── validate_integer.sh └── validate_not_empty.sh