第6章 变量、判断、重复动作

6.1 变量与算术

6.1.1 变量赋值与环境

  1. 普通赋值:变量=值,等号两边不能有空格
  2. readonly:设置只读变量,不能修改、不能重新赋值,用来做常量
1
2
3
4
hours_per_day=24
readonly hours_per_day
# POSIX简写
readonly hours_per_day=24
  1. export:把变量导出到环境,子进程/子脚本可以继承这个变量
1
2
3
4
5
6
PATH=$PATH:/usr/local/bin
export PATH
# POSIX简写
export PATH=$PATH:/usr/local/bin
# 查看所有导出环境变量
export -p

小技巧:只对这一条命令临时修改变量,不改变全局环境

1
PATH=/bin:/usr/bin awk '...' file1 file2 # 这是一条命令,shell 语法:前置环境变量赋值,为awk设置path
  1. env 命令
  • env:打印全部环境变量
  • env -i:清空继承的环境,只使用命令行给出的变量
1
env -i PATH=$PATH HOME=$HOME awk '...' file1 file2
  1. unset:删除变量 / 删除函数
  • unset 变量名 等价 unset -v 变量名:删除变量
  • unset -f 函数名:删除函数

区分

  • myvar=:变量还存在,只是赋值为空字符串,没有删除
  • unset myvar:彻底把变量删掉

6.1.2 参数展开

参数展开:Shell把$变量替换成变量实际内容的过程。

  1. 基础形式
  • 简写:$reminder
  • 完整写法:${reminder} 用大括号划定变量名边界

什么时候必须写 ${var}?变量后面紧跟字母、数字、下划线。

1
2
3
reminder="hi"
echo ${reminder}_   # 正确,输出 hi_
echo $reminder_     # 错误,shell把 reminder_ 当成完整变量,得到空

⚠警告:未定义的变量,参数展开会变成空字符串,会造成灾难!

1
2
rm -fr $MYPROGRAM
# MYPROGRAM没定义时等价执行 rm -fr ,危险!

6.1.2.1 展开运算符

语法:${变量 符号 字串},冒号:可选
带冒号:条件:变量存在 并且 不为空
去掉冒号:只判断变量是否被定义,就算值是空字符串也视为成立

语法 作用 是否修改变量
${var:-word} 变量不存在/为空,返回word;否则返回变量原值
${var:=word} 变量不存在/为空,把var赋值为word,返回word
${var:?message} 变量不存在/为空,打印报错信息,脚本直接退出
${var:+word} 变量存在且非空,返回word;否则返回空

示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
unset count
echo ${count:-0}   # 返回0,count仍然不存在

unset count
echo ${count:=0}   # count被赋值0,返回0

unset count
echo ${count:?"count必须设置!"} # 报错退出

count=99
echo ${count:+1}   # 返回1,不是返回99

去掉冒号对比演示

1
2
3
count=""
echo ${count:-0}   # 带冒号:变量为空,输出0
echo ${count-0}    # 去掉冒号:只要定义过就生效,输出空

6.1.2.2 模式匹配运算符

# 对应字符串开头(左边)%对应字符串末尾(右边)
单个符号=最短匹配;双符号=最长匹配
使用shell通配符* ?不是正则表达式;不会修改原变量,只返回新字符串。

示例变量:path="/home/tolstoy/mem/long.file.name"

写法 作用 示例 输出结果
${var#pattern} 开头,删最短 ${path#/*/} tolstoy/mem/long.file.name
${var##pattern} 开头,删最长 ${path##*/} long.file.name
${var%pattern} 末尾,删最短 ${path%.*} /home/tolstoy/mem/long.file
${var%%pattern} 末尾,删最长 ${path%%.*} /home/tolstoy/mem/long

额外:${#var} 获取字符串字符个数

1
2
x=hello
echo ${#x} #输出5

6.1.2.2 位置参数

脚本/函数的命令行参数,$1 $2 … ${10},大于9必须加大括号。

特殊参数:

  • $#:参数总个数
  • $0:脚本自身名字
  • $* / $@:全部参数
写法 行为
$* 无引号:按IFS做单词切割,破坏带空格参数,不推荐
"$*" 全部参数合并成一个字符串,用IFS第一个字符分隔
$@ 无引号:同样会切割,不推荐
"$@" 标准写法,保留每个参数原始边界,空格不会被拆开

set -- 参数1 参数2:手动设置位置参数;--防止参数以-开头被识别为选项。

1
2
set -- hello "hi there" greetings
echo $#   #输出3

shift:位置参数整体向左移动一位;shift n移动n个。

6.1.2.3 Shell特殊内置变量

变量 说明
$? 上一条命令退出状态码,0成功,非0失败
$$ 当前shell进程PID,常用来生成临时文件名
$! 最近后台命令的PID
$0 脚本名称
ENV 仅交互式shell读取,指定启动执行的脚本文件;脚本(非交互式)完全忽略
IFS 内部字段分隔符,默认:空格、tab、换行,用来切割字符串
HOME 家目录
PATH 命令搜索路径
PPID 父进程PID
PS1 主提示符;PS2续行提示符;PS4调试set‑x提示符
PWD 当前工作目录

6.1.3 算术展开 $(( ... ))

shell本身是字符串语言,$(( ))专门做整数算术运算;不支持小数。
内部运算符和C语言几乎一样,* < > & |等特殊符号不需要反斜杠转义

规则:

  1. $((表达式)):做计算,把计算结果替换出来(参数展开)
  2. 括号内部变量可以不写$位置参数必须写$,如 $(( $1 + $2 ))
  3. 只做计算不会自动保存结果,需要赋值接收
1
2
3
i=1
echo $((i + 1)) #输出2,i的值不变
i=$((i + 1))    #赋值,i真正变成2

自增自减:

1
2
3
i=0
echo $((i++)) #先返回旧值,再加1
echo $((++i)) #先加1,再返回新值

区分:

  • $(( ))带美元符号,获取计算结果
  • (( )) bash特有,只做判断,不返回数值。

6.2 退出状态

每一条命令结束都会返回一个整数退出状态
约定:0 =执行成功;非0=失败。

退出码 含义
0 成功
1‑125 命令自定义错误码
126 找到命令,但无法执行
127 命令找不到
>128 进程被信号杀死

exit命令

exit [数字]:脚本退出,向外返回退出码。

  • exit不带数字,等价 exit $?,继承上一条命令的退出状态。

坑点:$?会被每一条新命令覆盖;如果要保存,先存到自定义变量。

6.2.2 if‑elif‑else‑fi

语法

1
2
3
4
5
6
7
if 命令; then
    #成功执行
elif 命令; then
    #成功执行
else
    #全部失败执行
fi

if判断的是命令的退出状态,不是布尔值。

1
2
3
4
if grep "abc" file >/dev/null
then
    echo "找到字符串"
fi

6.2.3 ! && || 逻辑运算符

  1. ! 命令:取反,命令失败则整体为真
1
2
3
4
if ! grep "abc" file >/dev/null
then
    echo "没有找到"
fi
  1. cmd1 && cmd2:短路与;cmd1成功才执行cmd2
  2. cmd1 || cmd2:短路或;cmd1失败才执行cmd2

不要滥用&& ||代替if,可读性变差。

6.2.4 test 命令([ ]

test 表达式[ 表达式 ] 完全等价。

⚠注意:[是命令,后面、前面、结尾]前后必须要有空格

常用测试表达式(节选)

表达式 条件为真
-b file file是块设备文件
-c file file是字符设备文件
-d file file是目录
-e file file存在(任意类型文件)
-f file file为普通文件
-g file file设置了setgid位
-h file file是符号链接
-L file file是符号链接(等同于 -h
-n string string是非null(字符串不为空)
-p file file是命名管道(FIFO文件)
-r file file是可读的
-S file file是socket套接字文件
-s file file不是空的(文件大小>0)
-t n 文件描述符n指向终端
-u file file设置了setuid位
-w file file是可写入的
-x file file是可执行的;或者目录可被查找访问
-z string string为null(字符串为空)
s1 = s2 字符串s1与s2相同
s1 != s2 字符串s1与s2不相同
n1 -eq n2 整数n1等于n2
n1 -ne n2 整数n1不等于n2
n1 -lt n2 整数n1小于n2
n1 -gt n2 整数n1大于n2
n1 -le n2 整数n1小于或等于n2
n1 -ge n2 整数n1大于n2活等于n2

历史老技巧:if [ "X$answer" = "Xyes" ] 古老shell兼容手段,防止变量为空造成test语法报错;现代shell只要给变量加双引号"$answer"就不再需要X前缀。

可移植性建议:不要用test内部的‑a‑o做与或;改用多个[ ]搭配shell的&& ||

1
2
3
4
5
6
#推荐写法
if [ -f "$file" ] && ! [ -w "$file" ]
then
    echo "$0: $file is not writable" >&2
    exit 1
fi

6.3 case 语句

多重模式匹配,相当于其他语言的switch。 语法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
case $变量 in
模式1)
    语句
    ;;
模式2 | 模式3)   # |代表或
    语句
    ;;
*) #默认分支
    语句
    ;;
esac

模式支持shell通配符* ? [];每个分支以;;结束。

示例

1
2
3
4
5
case $1 in
-f) echo "收到‑f选项" ;;
-d|--dir) echo "收到目录选项" ;;
*) echo "$1 未知参数" >&2; exit 1 ;;
esac

6.4 循环

6.4.1 for循环

  1. for 变量 in 列表; do … done
1
2
3
4
for f in *.txt
do
    echo "$f"
done
  1. 省略in 列表:等价 for 变量 in "$@",遍历全部位置参数。

6.4.2 while / until循环

  • while 条件命令; do ... done条件成功就继续循环
  • until 条件命令; do ... done条件失败就继续循环,条件成功退出

示例:不断从尾部截短字符串直到为空

1
2
3
4
5
6
7
pattern=";*"
string="a;b;c;d"
while [ -n "$string" ]
do
    echo "$string"
    string=${string%$pattern}
done

6.4.3 break continue

  • break:跳出循环;break N跳出N层嵌套循环
  • continue:直接进入下一轮循环;continue N作用多层循环

6.4.4 shift 与选项解析

shift:位置参数整体左移,$#自减。

手写解析选项模板:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
while [ $# -gt 0 ]
do
    case $1 in
    -f) file=$2; shift ;;
    --) shift; break ;;
    -*) echo "错误参数" >&2; exit 1 ;;
    *) break ;;
    esac
    shift
done

getopts 工具

变量说明:

  • OPTARG:保存带参数选项的参数值
  • OPTIND:下一个待处理参数下标

模板,开头冒号代表关闭内置报错,自己处理错误

getopts :f:vql opt 逐个拆解,语法:getopts 选项串 变量名

  • 第一个字符 ::开启静默错误模式(关闭getopts自带的报错,我们自己在脚本里处理错误,推荐写法)
  • f:f是选项,后面带冒号 = 这个选项必须跟参数。也就是 -f 文件名-f后面必须写值。
  • v q l:选项后面无冒号,代表只是开关选项,不需要跟参数,直接 -v-q-l 即可。
  • opt:每次循环,getopts 会把当前解析到的选项字母存入变量$opt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
while getopts :f:vql opt
do
    case $opt in
    f) file="$OPTARG" ;;                # 读到 -f,把-f后面的参数存入file变量
    v) verbose=1 ;;                     # 读到 -v,打开verbose详细输出标记
    q) quiet=1 ;;                       # 读到 -q,打开静默模式标记
    l) long=1 ;;                        # 读到 -l,打开长格式标记 # 读到 -l,打开长选项模式标记
    ?) echo "非法选项" >&2; exit 1 ;;    # 遇到不认识的选项,进入这里,报错退出
    esac
done
shift $((OPTIND - 1)) #移除已经处理完毕的选项,剩下普通参数在$@

假设脚本名叫 demo.sh,调用:

1
./demo.sh -f data.txt -v -q inputfile output.txt

参数列表: $1=-f$2=data.txt$3=-v$4=-q$5=inputfile$6=output.txt

  1. getopts依次解析 -f-v-q
    • -fopt=fOPTARG=data.txtfile=data.txt
    • -vopt=vverbose=1
    • -qopt=qquiet=1
  2. 选项解析完毕,退出while循环;此时OPTIND=5
  3. shift $((5 -1))shift 4,删掉前4个参数
  4. shift之后:$1=inputfile$2=output.txt,这两个就是剩余普通参数,保存在$@

6.5 Shell函数

定义格式(POSIX标准,推荐)

1
2
3
myfunc () {
    #函数代码
}

函数必须先定义,后调用

函数内拥有自己的位置参数 $1 $2 $@ $#

⚠重点:

  1. 函数内部的exit终止整个脚本;函数退出返回状态请用return
  2. return n:给调用者返回退出状态;return等价return $?
  3. POSIX shell没有局部变量,全部是全局变量,注意变量互相污染。

获取函数输出:用命令替换捕获$(函数名 "$@")

1
2
3
4
5
demo () {
    echo "hello world"
}
res=$(demo)
echo "$res"

函数的标准输出被$()截获,不再输出屏幕,保存进变量。

  1. 执行 res=$(demo)
  2. Shell 看见 $( ... ),立刻执行括号内的 demo 函数。
  3. demo 内部运行 echo "hello world",屏幕输出文字 hello world
    ⚠重点:因为包在$()里面,不会直接打印到屏幕!输出被拦截捕获
  4. Shell 把捕获到的输出文本:hello world,替换掉 $(demo) 这整块内容。
  5. 于是语句实际变成:res="hello world"
  6. 最后 echo "$res",打印变量:hello world

6.6 本章总览小结

  1. 变量:赋值、readonly只读、export导出环境、unset删除变量/函数。
  2. 参数展开 ${...}
    • 基础取值;4套默认值运算符;字符串裁剪# ## % %%;求长度 ${#var}
  3. 位置参数与特殊变量:$1 $# $0 $* $@ $? $$ IFS ENV
  4. 算术展开:$(( )),整数运算。
  5. 退出状态:0成功非0失败;exit脚本退出;return函数返回状态。
  6. 判断:ifcasetest / [ ]! && ||逻辑。
  7. 循环:for while untilbreak continueshiftgetopts解析命令行选项。
  8. 函数:封装代码块;拥有独立位置参数;无原生局部变量。