编码 · 编程语言
Bash 脚本入门教程
简短回答
Bash 是 Unix 命令行 Shell 和脚本语言,广泛用于 Linux 服务器、macOS 终端和 CI 流水线。Bash 脚本是由 Shell 命令组成的文本文件,可结合变量、条件、循环、函数和管道。本页介绍如何编写和运行脚本、安全地给变量加引号、流程控制、使用数组、重定向输出,以及用 set -euo pipefail 处理错误。
打开chmod 权限计算Bash 是什么,用来做什么
Bash(Bourne Again SHell)是 GNU 项目的命令行 Shell 和脚本语言。它读取你输入的命令,运行程序,并用管道和重定向把它们连接起来。Bash 脚本就是由这些同样的命令组成的文本文件,所以你在终端里手动做的任何事情都可以自动化。
Bash 是大多数 Linux 发行版的默认交互式 Shell;macOS 现在默认使用 zsh,但仍自带 Bash;Windows 上可通过 WSL 或 Git Bash 使用。开发者、系统管理员和 DevOps 工程师用它编写构建脚本、部署、CI 任务、服务器维护,以及配合 grep、sed、awk、sort 等工具快速处理数据。
编写并运行 Hello, world
脚本以 shebang 行开头,告诉系统使用哪个解释器。用 chmod 让文件可执行,然后运行它。
#!/usr/bin/env bash
# hello.sh
echo "Hello, world!"chmod +x hello.sh
./hello.sh
# Hello, world!
bash hello.sh # also works without chmod
bash --version # check which version you have#!/usr/bin/env bash 会在 PATH 中查找 Bash,比写死 /bin/bash 更具可移植性。如果脚本只使用 POSIX 特性,可以改用 #!/bin/sh,但此时不允许使用 [[ ]] 和数组等 Bash 专有语法。
变量、引号与展开
用 name=value 赋值,用 $name 或 ${name} 读取。每个变量都是字符串;Bash 只在 $(( )) 内做整数运算。请在展开处使用双引号,以保留空格和特殊字符。
name="Ada" # no spaces around =
count=3
echo "Hello, $name" # Hello, Ada
echo "Next: $((count + 1))" # Next: 4 (integer arithmetic)
today=$(date +%Y-%m-%d) # command substitution
echo "Today is $today"
file="my report.txt"
echo "${file%.txt}.pdf" # my report.pdf (strip suffix)
echo "${#name}" # 3 (string length)
echo "${missing:-default}" # default (fallback value)| 语法 | 含义 |
|---|---|
"$var" | 变量的值,作为一个整体保留 |
'text' | 字面文本,完全不做展开 |
$(command) | 命令的输出 |
$((a + b)) | 整数运算 |
$1, $2, $@, $# | 脚本或函数的参数、全部参数、参数个数 |
$? | 上一条命令的退出状态(0 表示成功) |
条件判断与 case
if 运行一条命令并检查其退出状态。[[ ]] 测试命令用于比较字符串和检查文件;(( )) 用于比较数字。case 将值与 glob 模式进行匹配。
#!/usr/bin/env bash
file="${1:-notes.txt}"
if [[ -f "$file" ]]; then
echo "$file exists"
elif [[ -d "$file" ]]; then
echo "$file is a directory"
else
echo "$file not found"
fi
n=7
if (( n % 2 == 1 )); then echo "$n is odd"; fi
case "$file" in
*.txt) echo "text file" ;;
*.sh) echo "shell script" ;;
*) echo "something else" ;;
esac| 测试 | 何时为真 |
|---|---|
[[ -f path ]] | 路径是普通文件 |
[[ -d path ]] | 路径是目录 |
[[ -z "$s" ]] | 字符串为空 |
[[ "$a" == "$b" ]] | 字符串相等 |
(( a > b )) | 数字 a 大于 b |
[[ $a -lt $b ]] | 数字 a 小于 b(旧式写法) |
循环
Bash 有遍历单词列表的 for、C 风格的 for (( ))、while 和 until。逐行读取文件使用 while read。
for fruit in apple banana cherry; do
echo "fruit: $fruit"
done
for (( i = 1; i <= 3; i++ )); do
echo "i = $i"
done
for f in *.log; do
[[ -e "$f" ]] || continue # skip if no .log files match
echo "log file: $f"
done
while IFS= read -r line; do
echo "line: $line"
done < input.txtIFS= read -r 会原样读取每一行:IFS= 保留行首空格,-r 保留反斜杠。
函数
用 name() { ... } 定义函数,并像命令一样调用。参数以 $1、$2 等形式传入。函数用 return 返回退出状态(0 到 255);若要返回数据,请把它打印出来,再用 $( ) 捕获输出。
greet() {
local name="${1:-world}" # local variable, default value
echo "Hello, $name!"
}
is_even() {
(( $1 % 2 == 0 )) # exit status 0 means true
}
greet # Hello, world!
greet "Ann" # Hello, Ann!
msg=$(greet "Bo") # capture output
echo "Got: $msg" # Got: Hello, Bo!
if is_even 4; then echo "4 is even"; fi数组与关联数组
索引数组保存字符串列表。关联数组把字符串键映射到值,需要 Bash 4 或更高版本,因此 macOS 自带的旧版 Bash 3.2 不支持。
langs=("bash" "python" "go")
langs+=("rust")
echo "${langs[0]}" # bash
echo "${#langs[@]}" # 4
for l in "${langs[@]}"; do echo "- $l"; done
# associative array (Bash 4 or later)
declare -A port
port[http]=80
port[https]=443
echo "${port[https]}" # 443
for key in "${!port[@]}"; do
echo "$key -> ${port[$key]}"
done遍历元素时始终写成带引号的 "${array[@]}";这样即使元素含有空格,也会被当作一个整体。
管道与重定向
Bash 的标志性特性是把小程序组合起来。管道 | 把一条命令的输出送给下一条命令作为输入。重定向把输出写入文件,或从文件读取输入。文件描述符 1 是标准输出,2 是标准错误。
ls -l > files.txt # write stdout to a file (overwrite)
echo "more" >> files.txt # append
grep error app.log 2> /dev/null # discard error messages
make > build.log 2>&1 # stdout and stderr to the same file
# count the 3 most common words in a file
tr -cs '[:alpha:]' '\n' < book.txt | tr '[:upper:]' '[:lower:]' |
sort | uniq -c | sort -rn | head -n 3错误处理与安全脚本
默认情况下,Bash 在命令失败后会继续执行。许多脚本以 set -euo pipefail 开头:-e 在出现未处理的错误时退出,-u 把未设置的变量视为错误,-o pipefail 让管道中任何一条命令失败时整个管道都失败。trap 在脚本退出时运行清理代码。
#!/usr/bin/env bash
set -euo pipefail # stop on errors, unset vars, pipe failures
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT # clean up when the script exits
if ! grep -q "ready" status.txt; then
echo "status is not ready" >&2 # errors go to stderr
exit 1
fi
cp config.txt "$tmp" || { echo "copy failed" >&2; exit 2; }
echo "ok"用 || 在一行内处理失败,用 && 仅在上一条命令成功时才运行下一条。set -e 有例外(例如 if 条件中的命令不会触发它),所以重要的命令要显式检查。
一个完整的小脚本:带日期的备份
这个脚本把目录归档为带时间戳的 .tar.gz 文件,并只保留最新的五个归档。它用到了参数、默认值、测试、命令替换和管道。
#!/usr/bin/env bash
# backup.sh: copy a directory into a dated .tar.gz archive
set -euo pipefail
usage() { echo "usage: $0 SOURCE_DIR [DEST_DIR]" >&2; exit 1; }
[[ $# -ge 1 ]] || usage
src="$1"
dest="${2:-$HOME/backups}"
[[ -d "$src" ]] || { echo "not a directory: $src" >&2; exit 1; }
mkdir -p "$dest"
stamp=$(date +%Y%m%d-%H%M%S)
archive="$dest/$(basename "$src")-$stamp.tar.gz"
tar -czf "$archive" -C "$(dirname "$src")" "$(basename "$src")"
size=$(du -h "$archive" | cut -f1)
echo "Created $archive ($size)"
# keep only the 5 newest archives for this source
ls -1t "$dest"/"$(basename "$src")"-*.tar.gz | tail -n +6 | while read -r old; do
rm -f -- "$old"
echo "Removed $old"
done./backup.sh ~/projects/site
# Created /home/ann/backups/site-20261031-093000.tar.gz (2.1M)接下来学什么
- GNU Bash Reference Manual,官方文档。
- 终端中的
man bash和help(例如help read)。 - ShellCheck:一款静态分析工具,会解释常见的脚本缺陷。
- 如果需要脚本同时能在
sh、dash或 BusyBox 中运行,请阅读 POSIX Shell 规范。
常见问题
Bash 和 sh 有什么区别?
sh 是 POSIX Shell 标准,由 dash 等程序实现。Bash 兼容 POSIX sh,但增加了 [[ ]]、数组、(( )) 和花括号展开等特性。
Bash 和 zsh 有什么区别?
对大多数日常命令来说,两者是兼容的。zsh 增加了交互特性,是 macOS 的默认 Shell;用 bash shebang 编写的脚本仍然由 Bash 运行。
为什么我的脚本提示 permission denied?
文件没有可执行权限。运行 chmod +x script.sh,或者用 bash script.sh 启动它。
如何向 Bash 脚本传递参数?
把参数写在脚本名之后,如 ./script.sh one two。在脚本内部它们是 $1 和 $2,$@ 包含全部参数,$# 是参数个数。
应该用 Bash 写大型程序吗?
Bash 最适合把命令粘合在一起。当脚本变得复杂,需要数据结构或细致的错误处理时,Python 等语言通常更易于维护。