Shell 脚本是 Linux 运维和日常开发中最常用的自动化工具。本文整理 Shell 最基础的语法知识:变量的定义与引用、字符串操作、条件判断、循环、函数,以及一个脚本究竟有哪几种执行方式、它们之间有什么区别。
变量的定义与引用
变量名由字母、数字和下划线组成,且不能以数字开头。定义时等号两边不能有空格,这是 Shell 新手最容易踩的坑。
name="sungod"
age=18
readonly PI=3.14 # 只读变量,赋值后不可修改
unset name # 删除变量
引用变量时使用 $ 符号,变量名紧跟的字符有歧义时需要加花括号:
echo $name
echo "My name is ${name}_dev" # 花括号明确变量边界,这里不能写成 $name_dev
echo "I am $age years old" # 双引号内变量会被展开
echo 'I am $age years old' # 单引号内不展开,原样输出 $age
除了自定义变量,还有一组常用的特殊变量:
| 变量 | 含义 |
|---|---|
$0 |
脚本名 |
$1 ~ $n |
第 1 到第 n 个位置参数 |
$# |
参数个数 |
$@ / $* |
所有参数列表 |
$? |
上一条命令的退出状态,0 表示成功 |
$$ |
当前 Shell 的 PID |
字符串操作
str="hello world"
# 求长度
echo ${#str} # 输出 11
# 截取子串:从第 6 个字符开始截取 5 个
echo ${str:6:5} # 输出 world
# 删除前缀 / 后缀
file="archive.tar.gz"
echo ${file#*.} # 从头删除最短匹配,输出 tar.gz
echo ${file##*.} # 从头删除最长匹配,输出 gz
echo ${file%.*} # 从尾删除最短匹配,输出 archive.tar
echo ${file%%.*} # 从尾删除最长匹配,输出 archive
# 替换
echo ${str/world/shell} # 输出 hello shell
# 默认值:变量未定义或为空时使用默认值
echo ${UNSET_VAR:-default} # 输出 default
字符串拼接直接写在一起即可:
greeting="Hello, "${name}"!"
greeting2="Hello, ${name}!"
条件判断
条件判断用 if 配合 test(即 [ ])完成,注意 [ 后面和 ] 前面必须有空格:
if [ "$age" -gt 18 ]; then
echo "adult"
elif [ "$age" -gt 12 ]; then
echo "teenager"
else
echo "child"
fi
常用判断条件:
# 数值比较
[ $a -eq $b ] # 等于
[ $a -ne $b ] # 不等于
[ $a -gt $b ] # 大于
[ $a -ge $b ] # 大于等于
[ $a -lt $b ] # 小于
[ $a -le $b ] # 小于等于
# 字符串比较
[ -z "$str" ] # 字符串长度为 0
[ -n "$str" ] # 字符串长度非 0
[ "$str1" = "$str2" ] # 字符串相等
# 文件判断
[ -e /tmp/a.txt ] # 文件存在
[ -f /tmp/a.txt ] # 存在且是普通文件
[ -d /tmp/dir ] # 存在且是目录
[ -x /tmp/a.sh ] # 存在且可执行
# 逻辑组合
[ $a -gt 0 ] && [ $a -lt 10 ] # 与
[ $a -eq 0 ] || [ $a -eq 1 ] # 或
除了 [ ],还有两种更强大的写法:
(( )):用于算术表达式,支持>、<、>=等数学符号,不需要转义;[[ ]]:bash 的增强版条件判断,支持&&、||直接写在括号内,还支持通配符和正则匹配。
if (( a > 10 && a < 20 )); then
echo "a is between 10 and 20"
fi
if [[ $str == hello* ]]; then
echo "str starts with hello"
fi
循环
for 循环
# 列表形式
for i in 1 2 3 4 5; do
echo "number: $i"
done
# 遍历文件
for f in /var/log/*.log; do
echo "$f"
done
# C 风格
for (( i = 0; i < 5; i++ )); do
echo "i = $i"
done
while 循环
# 常见的逐行读文件
while read line; do
echo "$line"
done < /etc/passwd
# 计数循环
count=0
while [ $count -lt 5 ]; do
echo $count
(( count++ ))
done
until 与 while 相反,条件为假时继续循环:
until [ $count -ge 5 ]; do
(( count++ ))
done
函数
函数定义可以带 function 关键字,也可以不带。函数内的 $1、$2 是函数自己的参数,与脚本的参数互不影响。
#!/bin/bash
greet() {
local name=$1 # local 声明局部变量
echo "Hello, $name"
return 0 # return 只能返回 0~255 的整数,表示退出状态
}
greet "world" # 输出 Hello, world
echo $? # 输出 0
# 如果需要"返回值",可以用命令替换捕获 echo 的输出
result=$(greet "shell")
echo "$result" # 输出 Hello, shell
脚本的几种执行方式
同一个脚本,有三种常见执行方式,行为却完全不同:
bash test.sh # 方式一
./test.sh # 方式二(需要有可执行权限)
source test.sh # 方式三,等价写法 . test.sh
bash test.sh:当前 Shell 新建一个子进程,在子进程中用 bash 解释执行脚本。子进程中的变量、cd等操作不影响当前 Shell。./test.sh:脚本第一行的#!/bin/bash(shebang)指明了用哪个解释器来执行,本质上也是新建一个子进程执行,因此环境同样不影响当前 Shell。这种方式要求脚本有可执行权限(chmod +x test.sh)。source test.sh:在当前 Shell 进程中逐行执行脚本,不开子进程。脚本里定义的变量、函数、cd操作都会保留在当前 Shell 中。所以脚本中写exit会直接退出当前 Shell,这一点要格外小心。
验证一下区别:
# test.sh 内容:
cd /tmp
new_var="hello"
bash test.sh
echo $new_var # 输出为空,pwd 也不变
source test.sh
echo $new_var # 输出 hello,当前目录变成了 /tmp
日常写脚本时,一般用 bash xxx.sh 或 ./xxx.sh 执行;而需要修改当前环境(比如加载配置、切换目录)时才用 source。