Shell (Bash) 编程(篇一):语法基础

之前零零碎碎学了几遍 shell(主要是 bash)编程的语法,用的不多很容易忘,归纳一下常用语法备查。

详细的教程与示例,请参考官方的文档或其他优秀的教程:

  1. www.gnu.org GNU 的 Bash 手册(英文)
  2. 阮一峰:Bash 脚本教程
  3. runoob.com 菜鸟教程:Shell

内容规划

  • 变量与语句:声明和使用,特殊变量
  • 类型:字符串,数组
  • 运算与运算符:算数运算,关系运算,逻辑运算
  • 流程控制:循环,分支,函数

变量与语句

声明和使用

基础用法

1
2
3
4
5
6
7
8
9
10
# 声明
myname="zhaopeng"
arr=(11 22 33) # 声明一个数组
readonly myname # 只读变量

# 使用
arr[3]=44 # 给第四个元素赋值
echo arr[@] # 打印数组所有的元素
echo ${myname} # 打印
unset myname # 删除变量

变量的条件引用

变量替换时根据变量的具体的状态 (值) 来改变条件引用的值

1
2
3
4
5
6
7
8
9
10
11
# 条件A:name未定义; 条件B:name为空
# 成立条件: 执行结果:
${name} # - 返回 name 的值
${name-"default"} # A 返回 default
${name:-"default"} # A or B 返回 default
${name="default"} # A 返回 default, 并赋值给name
${name:="default"} # A or B 返回 default, 并赋值给name
${name?"default"} # A 返回 default, 并终止脚本
${name:?"default"} # A or B 返回 default, 并终止脚本
${name+"default"} # !A 返回 default
${name:+"default"} # !A and !B 返回 default

特殊变量

一些以 $ 开头的特殊变量,在 shell 中有特殊的含义

1
2
3
4
5
6
7
$$ # 当前shell进程的ID
$0 # 当前脚本的文件名
$1 # 传递给脚本(或函数)的第一个参数
$# # 传递给脚本或函数的参数个数
$* # 函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认空格,可自定义
$@ # 传递给脚本或函数的所有参数, 空格分隔
$? # 上个命令的退出状态,或函数的返回值

举例:

1
2
3
4
5
6
7
print each param from "$*"
// 111 222 333

print each param from "$@"
// 111
// 222
// 333

类型

字符串

截取字符串

1
2
3
4
5
6
7
${name:start}       # 从start取到末尾
${name:start:len} # 从start开始取len个字符
${name#schema} # 删除左侧匹配到的schema eg:`${address#*四川省}`
${name##schema} # 删除左侧匹配到的schema,贪婪模式 eg:`${address##*四川省}`
${name%schema} # 删除右侧匹配到的schema eg:`${address%*四川省}`
${name%%schema} # 删除右侧匹配到的schema,贪婪模式 eg:`${address%%*四川省}`
$name[start,end] # zsh only,区start到end的字符

其中的 schema 可以是正则表达式,截取字符串的几个例子:

1
2
3
echo ${file%/*}       # 取目录
echo ${file##*/} # 取文件名
echo ${file##*.} # 取扩展名

替换字符串

1
2
3
4
5
6
${name/from/to}     # 将from替换为to,限第一个
${name/from} # 将from删除
${name//from/to} # 将from替换为to,所有
${name//from} # 将from删除
${name/#from/to} # #:从首字符开始匹配
${name/%from/to} # %:从尾字符开始匹配

其中 from 可以是正则表达式,eg:

1
${b//hello[0-9]/world-}

其他几个常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
${#name}                  # name 的长度

expr index $name "substr" # 查找子串
$name[(i)cd] # 查找子串,zsh only; i 从左往右(找不到为0),I 从右往左(找不到返回原长度+1)

${name^} # 首字母大写 bash 4.0
${name,} # 首字母小写 bash 4.0
${name^^} # 全大写 bash 4.0
${name,,} # 全小写 bash 4.0

# 读取文件内容到字符串
content=$(<./aaa.txt)
while read line; do ... done < ./aaa.txt # 遍历每行
IFS=$'\n' && for i in $(<./aaa.txt) ; do ... done # 遍历每行
for line (${(f)"$(<./aaa.txt)"}) { } # 遍历每行,zsh only

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arr=(penn zshc "cc cc" dddd)    # 声明
files=(./*) # 读取 ./* 命令的结果作为数组

# 读取
echo "${#arr}"
echo "${arr[@]}"
echo "${arr[*]}" # 整个数组做为一个参数返回
echo "${arr[0]}"

arr[2]="cc cc" # 单个item赋值
newarr=("${arr[@]}" "cc cc") # 连接数组
newarr=("${arr[@]:1:2}") # 切片

unset arr[2] # 删除元素,已有元素的下标志并不会变化
for i in "${arr[@]}"; do ... done # 遍历
IFS=$'\n' && sorted=($(sort <<<"${arr[*]}")) && unset IFS && echo "${sorted[@]}" # 排序

运算与运算符

直接调用函数:在当前的进程执行
小括号调用函数 $(func): 在子进程中执行
调用脚本 bash aaa.txt:在子进程中执行

[[]] # 字符串运算命令
(()) # 算数运算命令
[] # 执行命令并返回执行结果
{} # zsh:执行命令并返回执行结果 eg if {grep username /etc/passwd}
() # zsh:执行并返回执行结果

算数运算

算数运算符:+、-、*、/、%、=、==、!=

1
$((2 + 3 * 2))      # 可以省略空格:$((2+3*2)) 

关系运算

数值的关系运算

1
2
3
4
5
6
7
8
# 比较运算符: -eq: ==   -ne: !=     -lt: <
# 比较运算符: -gt: > -le: <= -ge: >=

[[ 23 -lt 123 ]] # true
(( 23 > 123)) # false
num1=23
num2=123
((num1 > num2)) # false

字符串的关系运算

1
2
3
4
[[ "a.txt" == a* ]]         # true 模式匹配
[[ "a.txt" =~ .*\.txt ]] # true 正则匹配
[[ "abc" == abc ]] # true 字符串匹配
[[ 11 < 2 ]] # true 字符串匹配

文件 Metadata 判断

1
2
3
4
5
6
7
8
9
10
11
12
# 类型判断符: -b 块设备            -c 字符设备               -d 目录
# 类型判断符: -e 存在的任何文件 -f 普通文件或链接 -g 设置了setgid的文件
# 类型判断符: -h (-L)符号链接 -k 有粘滞位的文件 -p FIFO文件
# 类型判断符: -r 当前进程可读 -s 非空 -u 设置了setuid的文件
# 类型判断符: -x 当前进程可执行 -w 当前进程可写 -O 当前进程用户拥有
# 类型判断符: -G 当前进程用户组拥有 -S socket文件 -N atime、mtime一样的文件
# 类型判断符: -t 当前进程是否打开fd (0:标准输入,1:标准输出,2:错误输出)

[[ -e /bin/zsh ]] # 是否是一个文件

[[ file1 -nt file2 ]] # 比较新旧; -ot: 旧于,-nt: 新于
[[ file1 -ef file2 ]] # 是否是同一个文件;(路径相同或互为硬连接)

逻辑运算

逻辑运算符:&& || !

1
[[ a1 == a2 && b1 == b2 ]] # [[ 、]] 内侧需要空格; == 两侧需要空格;! 在zsh中后面也需要空格

流程控制

循环

  • while:
  • until
  • for…in
  • for
  • select

归纳

1
2
3
4
5
while [[ ]]; do ... done        # 满足条件时运行
until [[ ]]; do ... done # 不满足条件时运行
for i in {1..5} ; do ... done # 括号内可以是一个/多个:数组/字符串/哈希表
for ((;;)); do ... done
select i in list do ... done # 提示用户选择

举例:这里

分支

归纳

1
2
3
4
5
6
7
8
9
10
if condition; then ... [elif condition; then ... ] [else ... ] fi

case expression in
pattern )
... ;;
pattern )
... ;;
*)
...
esac

举例:这里

函数

1
2
3
4
5
6
7
function foo(){}    # 定义函数
unset foo # 删除函数
decleare -f # 查看函数定义
foo ${arr[@]} # 调用函数,参数 arr
local aa=123 # 定义局部变量
return 10 # 函数返回值
$# # 参数的长度

通配符

1
2
3
4
5
*           # 任意字符串
? # 单个字符
[abc] # abc中的任意字符
[^abc] # 非abc字符
[a-c0-9zx] # 字符范围