Shell 脚本常用语法总结
计算机中 Shell 术语最早是在 1964 年 Multics 操作系统中定义的,作用是提供人机交互的操作界面,它会解释执行用户输入命令并输出结果。对 Shell 通常的理解是 类Unix系统 上的命令行和批处理程序,可以看作是早期操作系统的桌面,像如今的图形化界面(文件资源管理器、任务管理器、控制面板)一样,可以操作文件、查看进程、配置系统、管理硬件等功能。图形化界面虽然能更直观的使用计算机,但无法使用没有界面的程序,且没有Shell脚本灵活的编程复用能力。Shell 通常基于内核API实现,演变出了很多版本,下列介绍目前最常用的版本:
首版 | 名称 | 开发者 | 历史 |
---|---|---|---|
1971年 | Thompson shell(sh) | Ken Thompson | 为 Unix V1 开发,简化了早期的 Multics shell,新增了如 输入(<),输出(>) 等特性 |
1977年 | Bourne shell(sh) | Stephen Bourne | 为 Unix V7 开发,给脚本添加了变量、循环等特性,是当今 shell 的基础,衍生了 csh/ksh/zsh/bash 等 |
1989年 | Bourne-Again Shell(Bash) | Brian Fox | 为 GNU计划 开发的免费软件,功能上是 Bourne shell 的超集,名字源自 born again 双关语,被 Linux、macOS 系统很多版本广泛使用 |
1990年 | Z shell(Zsh) | Paul Falstad | 是 csh 的子集,包含了 bash/ksh 部分特性,有热度很高的 Oh My Zsh 插件和社区,并从 2019年 起作为 macOS 默认 shell |
Shell 本身是指一种应用程序,现在通常也指 Shell 脚本(shell script)。这篇文章主要介绍 Shell 脚本的语法,由于不同版本会有差异,本文采用的案例以覆盖最广泛的 Bash 为准,测试环境为 macOS 中的 Terminal。
一、类型定义
1,Shebang
Shebang 通常是类Unix系统上脚本的第一行,作用是在执行脚本文件时指明用哪个解释器,比如用 Bash(#!/bin/bash
)、Python(#!/usr/bin/python
),在 #!
标识后面跟的是解释器的绝对路径。如果脚本中不写这一行,会采用默认的 $SHELL
解释器执行。
#!/bin/bash
2,变量
变量定义时不需要声明类型,使用 name=value
格式,值属于弱类型等同于字符串。变量命名规则跟主流编程语言差不多,需注意等号 (=) 间不要有空格!!!
。
自定义变量使用 unset xxx
清除,另外查看环境变量用 env
命令,查看所有变量用 declare -p
命令。
定义变量
var1=hello # 定义变量 var1(可以不加引号)
var1="hello world" # 修改变量 var1(包含空格时需添加引号)
readonly var2="hello world" # 定义只读变量 var2
引用变量
echo ${var1}
echo $var1 # 打印 var1(在引用无歧义时可以不加{})
通过变量引用变量
使用变量值作为要引用变量的名称,类似于通过变量名称反射它的值,如:
var1="hello"
var2="var1"
echo ${!var2} # 注意添加了感叹号,输出:hello
3,字符串
单引号中任何字符都会原样输出且不能有变量:
单引号与双引号
var1="world"
echo 'hello ${var1}' # 输出:hello ${var1}
echo "hello ${var1}" # 输出:hello world
截取、删除和替换
filepath="app/src/main.c"
echo ${#filepath} # 打印字符串长度,输出:14
echo ${filepath:3} # 从第3位开始(Index从0开始),截取到最后,输出:/src/main.c
echo ${filepath:3:4} # 从第3位开始,截取4个字符,输出:/src
echo ${filepath:0:${#filepath}-6} # 删除字符串末尾的6个字符,输出:app/src/
echo ${filepath#*/} # 删除左侧匹配(*/)到的字符串(app/),输出:src/main.c
echo ${filepath##*/} # 删除左侧贪婪匹配(*/)到的字符串(app/src/),输出:main.c
echo ${filepath%/*} # 删除右侧匹配(/*)到的字符串(/main.c),输出:app/src
echo ${filepath%%/*} # 删除右侧贪婪匹配(/*)到的字符串(/src/main.c),输出:app
echo ${filepath/app/sdk} # 将第一个 app 字符串替换为 sdk,输出:sdk/src/main.c
echo ${filepath//c/cpp} # 将所有 c 字符串替换为 cpp,输出:app/srcpp/main.cpp
4,数组
数组用括号 ()
表达,元素间用空格 (
) 分隔,下面只介绍 索引数组
。此外还有用字符串作为下标的 关联数组
,删除数组或数组元素都可以用 unset。
定义数组
array=("aa" "bb" "cc") # 创建三个数组元素
或:使用字符串的处理结果创建数组
text="aa:bb:cc"
array=(${text//:/ })
修改数组
array[3]="dd" # 设置指定下标的值(超过数组长度的下标会新增)
array[${#array[@]}]="ee" # 添加单个元素
array=("${array[@]}" "ff" "gg") # 添加两个元素
查看数组
echo ${array[0]} # 输出第一个元素,结果:aa
echo ${array[${#array[@]}-1]} # 输出最后一个元素,结果:gg
echo ${array[@]} # 输出全部元素,结果:aa bb cc dd ee ff gg
echo ${#array[@]} # 输出数组长度,结果:7
echo ${!array[@]} # 输出数组下标,结果:0 1 2 3 4 5 6
5,函数
函数定义
function name() {
return 0
}
- function:函数修饰符(可选)
- name():函数名称,括号内不支持写参数名
- return 0:返回值,仅限数值0-255,0表示成功(可选)
函数参数
函数参数是数组形式,在函数内引用参数从 $1
开始,查看全部参数用 $@
,获取参数跟查看数组的语法基本一致。示例如下:
function fun1() {
echo "第一个参数:$1,全部参数:[$@],参数个数:$#"
return 66
}
fun1 "aa" "bb"
echo "函数返回值:$?"
输出结果:
第一个参数:aa,全部参数:[aa bb],参数个数:2
函数返回值:66
6,注释
语法中提供了特殊字符 #
作为单行注释标识符,但多行注释并未提供,下面贴出其他同学的解法。
单行注释
# echo "hello world"
多行注释
-
转化为
<<EOF ... EOF
多行文本的形式,然后丢弃掉输入内容:<<EOF ... EOF
- 这里的
:
是空命令,相当于丢弃了后面的输入,如果换成cat
是打印注释的内容。 - 问题不足:如果内容包含反引号
`
,反引号中的内容会被执行,如`touch ~/Downloads/test.log`
,依旧会在下载目录创建文件。
- 这里的
-
转成函数定义,不调用就不会执行
annotation() { ... }
二、流程控制
1,逻辑分支
if/else 语句
if [ $num1 -gt 100 ]; then
echo "number > 100"
elif [ $num1 -lt 50 ]; then
echo "number < 50"
else
echo "50 <= number <= 100"
fi
判断变量 num1
的数字范围,-gt
是大于,-lt
是小于。
for 循环语句
# array=("aa" "bb" "cc")
# for item in ${array[@]}; do
for item in "aa" "bb" "cc"; do
echo "item = $item"
done
for 循环数组,可以先定义数组再引用,或者直接在循环语句中定义。
while 循环语句
while [ $num1 -lt 10 ]; do
echo "number = $num1"
let "num1++"
done
while 当条件为 true 时一直循环,若 num1=1
输出结果是 1 到 9,let 命令用于执行表达式。
switch 选择语句
case $num1 in
1|2)
echo 'number = 1 or 2'
;;
3)
echo 'number = 3'
;;
*)
echo 'number other'
;;
esac
在 shell 中选择语句的定义是 case ... esac
,多个可选值用 |
分割,默认值用 *
表示。
2,条件判断
比较数值关系或检测文件状态,用于 if
语句后面的条件时,需要使用关键字修饰,常有这几种写法:
test
关键字,判断语句后的表达式为 true/false;[ ]
单个中括号,基本等同 test 关键字(注意括号内要有空格);[[ ]]
双中括号,是 [] 写法的升级版,是在 bash 中引入,支持了&&
、||
和!
逻辑运算符,和字符串的正则表达式;
以下三种写法是等同的:
if test $num -ge 50 -a $num -le 100; then
# if [ $num -ge 50 -a $num -le 100 ]; then
# if [[ $num -ge 50 && $num -le 100 ]]; then
echo "50 <= number <= 100"
fi
比较符号
布尔运算符:用于连接多条表达式
符号 | 含义 | 符号 | 含义 | 符号 | 含义 | ||
---|---|---|---|---|---|---|---|
! | 非运算 | -o (or) | 或运算(|) | -a (and) | 与运算(&) |
数值比较
符号 | 含义 | 符号 | 含义 | |
---|---|---|---|---|
-eq (equal) | 等于(=) | -ne (not equal) | 不等于(!=) | |
-gt (greater than) | 大于(>) | -ge (greater than or equal) | 大于等于(>=) | |
-lt (less than) | 小于(<) | -le (less than or equal) | 小于等于(<=) |
字符串比较
符号 | 含义 | 符号 | 含义 | |
---|---|---|---|---|
== / = | 字符串相同 | != | 字符串不相同 | |
-z | 字符串为空(长度为0) | -n / $xxx | 字符串非空(长度大于0) | |
> | 字符串的字典顺序先后 | =~ | 字符串包含 |
if [ $string1 == 'xxx' ]; // 判断字符串相同(也可以用单个等号 (=))
if [ -z "$string1" ]; // 判断字符串为空(建议变量用双引号 ("") 包裹,避免变量未定义时出错)
if [ -n "$string1" ]; // 判断字符串不为空(也可以直接写变量,如 if [ $string1 ];)
if [[ "$string1" > "$string2" ]]; // 判断字符串 string1 排序在 string2 之后(注:要用双中括号,单中括号是ASCII排序)
if [[ "$string1" =~ "xxx" ]]; // 判断字符串 string1 中是否包含某个字符 (注:要用双中括号,单括号会报错)
文件状态判断
符号 | 含义 | 符号 | 含义 | |
---|---|---|---|---|
-e (exist) / -a | 文件存在 | -s | 文件存在且文件尺寸大于零 | |
-f (file) | 文件存在且为普通文件 | -d (directory) | 文件存在且为目录 | |
-L (link) | 文件存在且为链接文件 | -r (read) | 文件存在且可读 | |
-w (write) | 文件存在且可写 | -x (eXecute) | 文件存在且可执行 |
if [ -s $file ]; // 判断文件存在且文件尺寸大于零
if [ ! -e $file ]; // 判断文件不存在
三、脚本交互参数
在调用脚本前需要先添加执行权限(chmod +x file
),相对路径时要以点开头(./),最终会使用脚本中 Shebang 定义的解释器执行。我通常使用解释器加文件的方式运行(bash xxx.sh
),这样可以省去添加执行权限。
默认传参
在执行脚本后面追加的字符串,会被当作参数全部传入,如执行:
bash xxx.sh "aa" "bb" "cc"
其中 xxx.sh 的内容为:
echo "第一个参数:${1}" // 输出:aa
echo "参数个数:${#}" // 输出:3
echo "全部参数:${@}" // 输出:aa bb cc
getopts 解析传参
当要传多个参数时,更友好的做法是用(-key value)形式,系统内置了 getopts 命令来解析这种参数,不过它只支持单字符选项。要多字符选项请参照 getopt
命令,或自行实现。下列命令执行后,会输出a、b的值:
bash xxx.sh -a "111" -b "222"
其中 xxx.sh 的内容为:
while getopts a:b:h ops; do
case ${ops} in
a)
echo "a value = ${OPTARG}"
;;
b)
echo "b value = ${OPTARG}"
;;
h)
echo "help : "
echo "-a xxx"
echo "-b xxx"
echo "-h help"
exit
;;
*)
echo "unknow params"
exit
;;
esac
done
- 通过 while 循环逐个解析参数;
- 代码(
a:b:h
):a、b字符后有冒号,意思是要携带值,h字符后无冒号表示不用额外值;
运行时传参
在脚本运行中与用户交互,读取用户输入信息通过 read 命令,示例如下:
read -p "请输入 ok 继续执行: " inputCmd
if [ 'ok' != "$inputCmd" ]; then
echo "已取消,退出程序"
exit
fi
参考资料
- Bash手册: http://www.gnu.org/software/bash/manual/bash.html
- 菜鸟联盟: https://www.runoob.com/linux/linux-shell.html
- shell数组详解:https://blog.csdn.net/anqixiang/article/details/114415491
- UNIX考古记:一个“遗失”的SHELL: https://coolshell.cn/articles/9410.html
- Evolution of shells in Linux: https://developer.ibm.com/tutorials/l-linux-shells/