基础语法
本章介绍 Shell 脚本的基础语法,包括 Shebang、注释、命令分隔符、引号使用等核心概念。掌握这些基础知识是编写正确、可维护脚本的前提。
Shebang(脚本解释器声明)
Shebang 是脚本文件第一行的特殊标记,用于指定脚本的解释器。它的格式是 #! 后跟解释器的绝对路径。
基本用法
#!/bin/bash
这行代码告诉操作系统:使用 /bin/bash 来执行这个脚本。
使用 env 查找解释器
#!/usr/bin/env bash
使用 env 的好处是它会在 PATH 环境变量中查找 bash,这样脚本在不同系统上更具可移植性。因为不同系统上 bash 的安装路径可能不同,有的在 /bin/bash,有的在 /usr/local/bin/bash。
指定其他解释器
#!/bin/sh # 使用 POSIX 标准的 sh
#!/usr/bin/env python3 # Python 脚本
#!/usr/bin/env node # Node.js 脚本
Shebang 的工作原理
当你在命令行执行 ./script.sh 时,操作系统会读取文件的前两个字节。如果是 #!,它会读取该行剩余部分作为解释器路径,然后用该解释器来执行脚本文件。
实际上,./script.sh 等价于 /bin/bash ./script.sh(假设 shebang 是 #!/bin/bash)。
没有 Shebang 会怎样
如果脚本没有 shebang 行,直接执行 ./script.sh 会使用当前 Shell 来解释脚本。这可能导致不同用户执行同一脚本时使用不同的解释器,产生不可预期的行为。因此,始终建议在脚本开头添加 shebang。
注释
注释是脚本中不会被解释器执行的部分,用于解释代码的功能和逻辑。良好的注释习惯对于脚本的可维护性至关重要。
单行注释
Shell 只支持单行注释,以 # 开头,从 # 到行末的内容都是注释。
#!/bin/bash
# 这是一个单行注释
echo "Hello" # 这也是注释,从 # 开始到行末
# 注释可以独占一行
# 也可以跟在命令后面
# 作者:Your Name
# 日期:2024-01-01
# 功能:演示注释的使用
多行注释
Shell 没有专门的多行注释语法,但可以通过以下方式实现:
方式一:每行都加 #
# 这是第一行注释
# 这是第二行注释
# 这是第三行注释
方式二:使用 Here Document
: <<'EOF'
这是多行注释的第一行
这是多行注释的第二行
这是多行注释的第三行
EOF
这里使用了 : 命令(一个不做任何操作的命令)和 Here Document。<<'EOF' 到 EOF 之间的内容作为 : 命令的输入,但 : 命令会忽略这些输入。
注意使用单引号 'EOF' 而不是 EOF,这样可以防止变量替换和命令替换。
方式三:使用函数
comment() {
这是多行注释的第一行
这是多行注释的第二行
这是多行注释的第三行
}
这种方式利用了函数体不会被立即执行的特性。但这种方式不推荐使用,因为容易造成混淆。
注释的最佳实践
注释应该解释"为什么"而不是"是什么"。代码本身应该足够清晰,让人能够理解它在做什么。注释应该解释代码背后的意图、决策原因、注意事项等。
# 好的注释:解释为什么
# 使用 --time-style=long-iso 以确保跨平台时间格式一致
ls -la --time-style=long-iso
# 不好的注释:只是重复代码
# 列出文件
ls -la
命令分隔符
Shell 脚本中,多个命令可以通过不同的分隔符连接,每种分隔符有不同的语义。
分号 ;
分号用于顺序执行多个命令,无论前一个命令是否成功。
echo "First"; echo "Second"; echo "Third"
这三个命令会依次执行,即使前面的命令失败了,后面的命令仍然会执行。
分号常用于将多个相关命令写在同一行,使代码更紧凑:
cd /tmp; ls -la; cd -
换行符
在 Shell 中,换行符本身就是命令分隔符。每个命令独占一行是最常见的写法:
echo "First"
echo "Second"
echo "Third"
这与使用分号的效果相同,但可读性更好。
逻辑与 &&
&& 表示"与"操作:只有前一个命令执行成功(退出状态码为 0),才会执行后一个命令。
mkdir /tmp/mydir && cd /tmp/mydir && echo "Directory created and entered"
这个命令链的含义是:创建目录,如果成功则进入目录,如果进入成功则输出消息。任何一步失败,后续命令都不会执行。
&& 常用于条件执行:
test -f /etc/passwd && echo "File exists"
逻辑或 ||
|| 表示"或"操作:只有前一个命令执行失败(退出状态码非 0),才会执行后一个命令。
mkdir /tmp/mydir || echo "Failed to create directory"
如果创建目录失败,则输出错误消息。
|| 常用于错误处理:
cd /nonexistent || exit 1
如果切换目录失败,则退出脚本。
组合使用 && 和 ||
&& 和 || 可以组合使用,实现类似 if-else 的逻辑:
test -f /etc/passwd && echo "File exists" || echo "File not found"
这等价于:
if test -f /etc/passwd; then
echo "File exists"
else
echo "File not found"
fi
注意:这种写法有陷阱。如果 && 后面的命令失败了,|| 后面的命令也会执行。所以这种写法只适用于简单的条件判断。
管道 |
管道将前一个命令的标准输出连接到后一个命令的标准输入。
ls -la | grep ".txt" | wc -l
这个命令链的含义是:列出当前目录的文件,筛选出包含 .txt 的行,统计行数。
管道是 Shell 最强大的特性之一,它允许你将多个简单命令组合成复杂的数据处理流程。
后台执行 &
& 将命令放到后台执行,Shell 不会等待命令完成就继续执行下一条命令。
sleep 10 &
echo "This prints immediately"
sleep 10 & 会在后台运行,脚本不会等待它完成就直接执行 echo 命令。
命令组合 () 和 {}
() 和 {} 可以将多个命令组合成一个整体。
(): 子 Shell 中执行
(cd /tmp; ls -la)
括号内的命令在子 Shell 中执行。子 Shell 是当前 Shell 的子进程,它有自己的环境。子 Shell 中的变量修改、目录切换等不会影响父 Shell。
x=1
(x=2; echo "In subshell: $x")
echo "In parent: $x"
# 输出:
# In subshell: 2
# In parent: 1
{}: 当前 Shell 中执行
{ cd /tmp; ls -la; }
花括号内的命令在当前 Shell 中执行。注意花括号语法要求:左花括号后必须有空格,右花括号前必须有分号或换行。
x=1
{x=2; echo "In block: $x";}
echo "Outside: $x"
# 输出:
# In block: 2
# Outside: 2
引号
引号在 Shell 中有特殊的作用,它们影响 Shell 对字符串的解释方式。正确使用引号是编写健壮脚本的关键。
单引号 '...'
单引号保留字符串的字面值,其内部的所有字符都被视为普通字符,包括 $、`、\ 等特殊字符。
name="World"
echo 'Hello, $name!'
# 输出:Hello, $name!
echo 'Current directory: $(pwd)'
# 输出:Current directory: $(pwd)
echo 'Line1
Line2'
# 输出:
# Line1
# Line2
单引号内不能包含单引号本身,即使使用转义也不行:
echo 'It'\''s a test'
# 输出:It's a test
这里使用了单引号拼接技巧:'It' + \' + 's a test'。
双引号 "..."
双引号允许变量扩展和命令替换,但保留其他字符的字面值。
name="World"
echo "Hello, $name!"
# 输出:Hello, World!
echo "Current directory: $(pwd)"
# 输出:Current directory: /home/user
echo "User's home: $HOME"
# 输出:User's home: /home/user
在双引号中,以下字符仍然有特殊含义:
$:变量扩展`:命令替换(旧语法)\:转义字符!:历史扩展(在交互式 Shell 中)
双引号内的转义:
echo "Price: \$100" # 转义 $
# 输出:Price: $100
echo "Tab:\there" # \t 不是特殊转义
# 输出:Tab:there
echo "Quote: \"text\"" # 转义双引号
# 输出:Quote: "text"
反引号 `...`
反引号是命令替换的旧语法,它会执行其中的命令并将输出替换到当前位置。
echo "Current time: `date`"
反引号语法已被 $() 取代,因为 $() 更清晰、更易嵌套:
# 反引号嵌套需要转义
echo `echo \`echo hello\``
# $() 嵌套更清晰
echo $(echo $(echo hello))
为什么总是使用双引号
在 Shell 脚本中,包含变量的字符串应该总是用双引号包围,除非你有特殊原因不这样做。
原因一:防止词分割
file="my document.txt"
ls $file # 错误:会被分割成 ls my document.txt
ls "$file" # 正确:ls "my document.txt"
原因二:防止通配符扩展
pattern="*.txt"
echo $pattern # 可能输出:file1.txt file2.txt file3.txt
echo "$pattern" # 输出:*.txt
原因三:处理空值
name=""
echo $name # 输出空行
echo "$name" # 输出空行,但更安全
引号使用规则总结
- 字符串包含变量或命令替换时,使用双引号
- 需要保留字面值时,使用单引号
- 变量扩展时总是用双引号包围:
"$var" - 命令替换推荐使用
$()而不是反引号
转义字符
反斜杠 \ 是 Shell 的转义字符,它可以消除其后字符的特殊含义。
转义特殊字符
echo "Price: \$100" # $ 被转义,不再触发变量扩展
echo "File: \"test.txt\"" # " 被转义,成为字符串的一部分
echo 'It'\''s a test' # 单引号内嵌套单引号
转义空格
ls my\ file.txt # 访问名为 "my file.txt" 的文件
转义换行
反斜杠后跟换行符可以续行:
echo "This is a very long \
line that continues"
# 输出:This is a very long line that continues
特殊转义序列
在 $'...' 语法中,支持类似 C 语言的转义序列:
echo $'Hello\tWorld' # Tab 字符
echo $'Line1\nLine2' # 换行符
echo $'\x41' # 十六进制 ASCII,输出 A
echo $'\u4e2d\u6587' # Unicode,输出 中文
命令替换
命令替换允许你将命令的输出作为另一个命令的参数或赋值给变量。
$() 语法(推荐)
current_dir=$(pwd)
echo "Current directory: $current_dir"
files=$(ls | wc -l)
echo "Number of files: $files"
# 嵌套使用
inner=$(basename $(dirname /path/to/file.txt))
反引号语法(旧式)
current_dir=`pwd`
反引号语法是历史遗留,新脚本应使用 $() 语法。
命令替换的注意事项
命令替换会移除末尾的换行符:
output=$(echo -e "line1\nline2\n")
echo "$output"
# 输出:
# line1
# line2
# 注意末尾没有空行
命令替换中的双引号:
# 错误:内部双引号会提前结束外部双引号
echo "Result: "$(grep "pattern" file)""
# 正确:使用转义或避免嵌套双引号
echo "Result: $(grep 'pattern' file)"
算术扩展
Shell 支持整数算术运算,使用 $(( )) 语法。
基本运算
a=10
b=3
echo $((a + b)) # 加法:13
echo $((a - b)) # 减法:7
echo $((a * b)) # 乘法:30
echo $((a / b)) # 除法:3(整数除法,向下取整)
echo $((a % b)) # 取模:1
echo $((a ** b)) # 幂运算:1000
自增和自减
i=5
echo $((i++)) # 输出 5,然后 i 变为 6
echo $((++i)) # i 先变为 7,然后输出 7
echo $((i--)) # 输出 7,然后 i 变为 6
echo $((--i)) # i 先变为 5,然后输出 5
复合赋值
n=10
((n += 5)) # n = n + 5,结果 15
((n -= 3)) # n = n - 3,结果 12
((n *= 2)) # n = n * 2,结果 24
((n /= 4)) # n = n / 4,结果 6
位运算
a=5 # 二进制:101
b=3 # 二进制:011
echo $((a & b)) # 按位与:1(001)
echo $((a | b)) # 按位或:7(111)
echo $((a ^ b)) # 按位异或:6(110)
echo $((~a)) # 按位取反:-6
echo $((a << 1)) # 左移:10(1010)
echo $((a >> 1)) # 右移:2(10)
三元运算符
a=10
b=20
echo $((a > b ? a : b)) # 输出较大值:20
使用 expr 命令(旧式)
expr 是外部命令,效率低于 $(( )),且需要注意转义:
expr 5 + 3 # 输出 8
expr 5 \* 3 # * 需要转义
result=$(expr $a + $b)
使用 let 命令
let "x = 5 + 3"
echo $x # 输出 8
let x++ # 自增
花括号扩展
花括号扩展是一种生成任意字符串的机制。
枚举扩展
echo {a,b,c} # 输出:a b c
echo file.{txt,md,sh} # 输出:file.txt file.md file.sh
范围扩展
echo {1..5} # 输出:1 2 3 4 5
echo {a..e} # 输出:a b c d e
echo {1..10..2} # 步长为 2,输出:1 3 5 7 9
echo {10..1} # 倒序,输出:10 9 8 7 6 5 4 3 2 1
嵌套扩展
echo {a,b}{1,2} # 输出:a1 a2 b1 b2
echo {a,b,c}{1..3} # 输出:a1 a2 a3 b1 b2 b3 c1 c2 c3
实际应用
# 创建多个目录
mkdir dir_{1..5}
# 批量重命名
mv file.{txt,txt.bak}
# 创建备份
cp script.sh{,.bak} # 复制为 script.sh.bak
波浪号扩展
波浪号 ~ 在 Shell 中有特殊含义。
主目录扩展
echo ~ # 当前用户的主目录,如 /home/user
echo ~root # root 用户的主目录,如 /root
当前工作目录
echo ~+ # 当前工作目录,等同于 $PWD
上一个工作目录
cd /tmp
cd ~-
echo ~- # 上一个工作目录
小结
本章介绍了 Shell 脚本的基础语法,这些是编写脚本的基石:
- Shebang 指定脚本解释器,确保脚本正确执行
- 注释 提高代码可读性和可维护性
- 命令分隔符 控制命令的执行顺序和条件
- 引号 控制字符串的解释方式,正确使用引号是编写健壮脚本的关键
- 转义字符 消除特殊字符的特殊含义
- 命令替换 将命令输出嵌入到脚本中
- 算术扩展 进行整数运算
- 花括号扩展 生成字符串序列
掌握这些基础知识后,你就可以开始学习变量、流程控制等更高级的主题了。