Shell Utils: 简易的 Shell 脚本工具集

不知道为什么,最近高强度写 Bash 脚本……因此整理了一份小小的 bash 工具集,包含了一些莫名其妙的功能。

仓库地址:lingsamuel/shell-utils

Shell 工具集

持续进化中……

  • 日志相关函数:着色用。没有颜色的 log 是没有灵魂的。
  • 数组相关函数
  • 杂七杂八的函数

引入方法

source ./shell-utils.sh

栈的用法:

new_stack stack 200 # 新建一个名为 stack 的栈,大小为 200(默认为 100)
push stack 1
push stack 2
pop stack
echo $stack_POP # 返回值储存在 <stack_name>_POP 变量里
push stack 3
pop stack
echo $stack_POP
pop stack
echo $stack_POP
pop stack
echo $stack_POP # 空栈的返回值也是空(<stack_name>_POP 被 unset 了)
pop stack
echo $stack_POP

之所以 pop 的用法这么奇怪(更符合直觉的用法应该是 VAL=$(pop stack)),是因为这个栈实现是依赖环境变量的:它将 StackPointer 的信息存储在名为 <stack_name>_SP 的环境变量里。而 Subshell 的环境变量修改不影响父 Shell,所以不能通过 $() 语法来获取返回值。最简单的做法就是新建一个变量储存返回值了(实话说,我没想到别的解决方法)。

另外,此实现访问数据只依赖 StackPointer,因此实际上 stack 这个数组里的数据在 pop 后是不会被删除的,最多会在 push 时被覆盖。

使用 print_stack <stack_name> 来打印栈信息,输出格式为:
<stack_name> <stack_pointer>/<max_size> <values>

运行结果(在每次 pushpop 后添加了 print_stack):

可以看到 pop 并不会删除数据,只会修改 StackPointer。只有再次 push,才会将数据覆盖。

实现原理

由于需要支持动态的栈变量名,所以主要依靠 Variable Indirection Expansion 来动态展开变量。

其中,new_stack 依靠 declare -a 实现,push 则是将表达式拼成字符串然后 evalpop 比较简单,利用 Indirection Expansion 直接取的值。

保存 ShellOpts

实现栈主要就是为了这个功能。

有时候,脚本需要 set -eo pipefail 来保证某个命令失败的时候,脚本能退出,而不会在错误的状态下继续执行。

但是,有的命令例如 grepexitcode!=0 时也不一定是异常,这种行为可能是符合预期的。因此要么临时 set +eo pipefail,要么在命令后面加一个 || true。 这种方案临时用用还行,如果是一大段,或者干脆是一个调用,就不那么管用了。

总之,因为某种临时需求关闭了 errexitpipefail,那么,后续是否要再次启用?这是不一定的。因为外部可能根本没打开这个选项,如果盲目启用,反而可能会导致外部脚本执行错误。

store_shell_optsrestore_shell_opts 就是为了这个场景实现的。

范例:

#!/bin/bash
store_shell_opts # 保存最初始的 ShellOpts
set_must_success # set -eo pipefail
# do something must success...

func() {
  store_shell_opts # 保存上级 ShellOpts
  set_could_fail # set +eo pipefail
  # do something could fail, such as grep...
  restore_shell_opts # 恢复上级 ShellOpts
}
func

# do something must success...
restore_shell_opts

原本这个函数的实现是 export 一个固定名称的环境变量,不支持嵌套。栈使得嵌套成为可能。

(有颜色的)日志

人生已经如此艰难,写个 Shell 脚本的 log 还不带颜色,debug 起来眼睛不会瞎吗?

基本颜色

gray "gray"
red "red"
green "green"
yellow "yellow"
blue "blue"
magenta "magenta"
cyan "cyan"
light_gray "light gray"
black "black"
dark_red "dark red"
dark_green "dark green"
dark_yellow "dark yellow"
dark_blue "dark blue"
dark_magenta "dark magenta"
dark_cyan "dark cyan"
white "white"
light_purple "light purple"
light_blue "light blue"

实际显示效果与 shell 配置有关。

脚本执行进程日志

e_header "System Installation"
e_success "Install success"
e_error "Install failed"
e_warning "Missing dependencies"
e_underline "Underline text"
e_bold "Bold"
e_note "Start with Note:"
e_step "1. Auto increment step note"
e_step "2. Auto increment step note"
e_reset_step
e_step "1. Resetted"

习惯了有颜色的脚本日志之后,根本看不懂没格式的日志了……

数组相关

8 月 25 日,学会了更多 zsh 的语法,一些脚本可以写出 zsh 兼容版的了。

  • join_array: bash/zsh,不使用特殊语法
  • append/prepend: zsh 使用了特有的语法,bash 使用 eval
  • split_to_array: zsh 使用 typeset,bash 使用 name ref

只能说动态操作变量的事情到最后都变成了 eval。

eval 和 typeset 有一个很大的缺陷,它们实际上不能像一个指针一样工作。如果变量名与函数内部的 local 变量冲突,那就坏菜了。

join_array

arr=(a b c)
join_array ',' ${arr[@]}
join_array ' ' ${arr[@]}
join_array '-' ${arr[@]}

split_to_array

ARR=(a b c)
split_to_array ARR ',' 'd,e,,f'
declare -p ARR
split_to_array NEW_ARR ',' 'd,e,,f'
declare -p NEW_ARR

append/prepend

arr=(a b c)
append arr d e 'f g'
prepend arr 0 1 '2 3'
declare -p arr

append new_arr 1 2 3
declare -p new_arr