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(BashBrian Fox为 GNU计划 开发的免费软件,功能上是 Bourne shell 的超集,名字源自 born again 双关语,被 Linux、macOS 系统很多版本广泛使用
1990年Z shell(ZshPaul 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"
多行注释
  1. 转化为 <<EOF ... EOF 多行文本的形式,然后丢弃掉输入内容

    :<<EOF
    ...
    EOF
    
    • 这里的 : 是空命令,相当于丢弃了后面的输入,如果换成 cat 是打印注释的内容。
    • 问题不足:如果内容包含反引号 `,反引号中的内容会被执行,如 `touch ~/Downloads/test.log`,依旧会在下载目录创建文件。
  2. 转成函数定义,不调用就不会执行

    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

参考资料

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值