计算机中 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 这里的 : 是空命令,相当于丢弃了后面的输入,如果换成 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.htmlshell数组详解:https://blog.csdn.net/anqixiang/article/details/114415491UNIX考古记:一个“遗失”的SHELL: https://coolshell.cn/articles/9410.htmlEvolution of shells in Linux: https://developer.ibm.com/tutorials/l-linux-shells/ 相关链接
发表评论