Linux命令行与脚本-03-Shell脚本编程基础:从零开始编写自动化脚本
全文摘要
本文将带你从零开始掌握Shell脚本编程,帮助你理解脚本的基本结构和编程范式。你将学到脚本的创建与执行方式、变量的定义与使用、条件判断与循环控制、函数的定义与调用、以及如何处理用户输入。通过阅读本文,你将能够编写实用的Shell脚本来自动化日常系统管理任务。
全书总结
Shell脚本编程是Linux系统自动化的核心技能,它将命令行工具组合成可重复使用的程序。本文系统梳理了Shebang与脚本执行、变量与环境变量、条件测试与分支控制、循环结构与函数定义、输入输出处理、以及脚本调试的最佳实践。从简单的命令序列到完整的程序结构,涵盖了Bash脚本编程的核心概念。适合运维工程师、DevOps工程师、系统管理员、以及对Linux自动化感兴趣的技术人员阅读。
一、第一个Shell脚本
理解脚本的最佳方式是动手编写一个。
#!/bin/bash
# 这是一个简单的Shell脚本示例
# 文件名: hello.sh
echo "Hello, World!"
echo "Today is $(date)"
echo "Current user is $USER"
echo "Current directory is $(pwd)"flowchart TB subgraph Exec[脚本执行流程] direction TB Create[创建脚本文件<br/>hello.sh] --> Perm[添加执行权限<br/>chmod +x hello.sh] Perm --> Run[执行脚本<br/>./hello.sh 或<br/>bash hello.sh] Run --> Output[输出结果] end subgraph Shebang[Shebang的作用] S1[#!/bin/bash<br/>指定解释器] S2[系统查找bash<br/>并执行脚本] end Exec --> Shebang style Create fill:#e3f2fd style Perm fill:#fff9c4 style Run fill:#c8e6c9 style Shebang fill:#ba68c8
图表讲解:这张图展示了Shell脚本的创建和执行流程——理解这个流程是编写可执行脚本的第一步。
Shebang(#!)是脚本的第一行,告诉系统用哪个解释器执行该脚本。#!/bin/bash表示用/bin/bash解释器。如果没有shebang,执行脚本时会使用当前Shell(可能是zsh、sh等),可能导致兼容性问题。Shebang必须是第一行,前面不能有空格。
执行脚本的几种方式:
bash hello.sh:明确指定用bash解释器执行,不需要执行权限。./hello.sh:需要脚本有执行权限(chmod +x hello.sh),会使用shebang指定的解释器。source hello.sh或. hello.sh:在当前Shell中执行脚本,脚本中的变量会保留在当前Shell环境中(source方式)。
最佳实践:
- 脚本文件名以
.sh结尾,便于识别 - 脚本开头添加注释说明用途、作者、创建日期
- 使用
set -e让脚本在命令失败时退出(错误检查) - 使用
set -u让脚本在使用未定义变量时退出
二、变量与字符串操作
变量是存储数据的容器,Bash中的变量不需要声明类型。
flowchart TB subgraph VarTypes[变量类型] direction TB Local[局部变量<br/>name=value] Env[环境变量<br/>export KEY=value] ReadOnly[只读变量<br/>readonly var=value] Array[数组<br/>arr=(a b c)] end subgraph VarUsage[变量使用] direction TB Ref[引用变量<br/>$name 或 ${name}] Default[默认值<br/>${var:-default}] Indirect[间接引用<br/>${!varname}] end VarTypes --> Exp[表达式求值] VarUsage --> Exp style Local fill:#e3f2fd style Env fill:#fff9c4 style Array fill:#c8e6c9
图表讲解:这张图展示了变量的类型和使用方式——变量是脚本编程的基础。
定义和使用变量:
name="Alice"
echo $name # Alice
echo ${name} # Alice(花括号避免歧义)
echo "Hello $name" # Hello Alice
echo "Hello ${name}!" # Hello Alice!(!需要花括号)环境变量(export)会被子进程继承:
export DB_USER="admin"
export DB_PASS="secret"
# 现在子Shell和脚本都能访问这些变量命令替换:将命令的输出赋值给变量:
current_dir=$(pwd) # 或 current_dir=`pwd`
files_count=$(ls | wc -l)
today=$(date +%Y-%m-%d)只读变量:
readonly PI=3.14
# PI=3.14159 # 会报错:只读变量不能修改数组:
fruits=(apple banana orange)
echo ${fruits[0]} # apple(第一个元素)
echo ${fruits[@]} # 所有元素
echo ${fruits[@]} # 数组长度
echo ${fruits[@]: -1} # 最后一个元素三、条件判断与分支控制
让脚本根据不同情况执行不同操作,需要条件判断。
flowchart TB Start[开始] --> Condition{条件测试} Condition -->|真| TrueBranch[执行then分支] Condition -->|假| FalseBranch[执行else分支] TrueBranch --> End[继续] FalseBranch --> End subgraph Tests[常用测试] Eq[字符串相等<br/>[ "$a" = "$b" ]] Ne[字符串不等<br/>[ "$a" != "$b" ]] Lt[小于<br/>[ $a -lt $b ]] Gt[大于<br/>[ $a -gt $b ]] File[文件存在<br/>[ -f file ]] end End --> Tests style Condition fill:#e3f2fd style TrueBranch fill:#c8e6c9 style Tests fill:#fff9c4
图表讲解:这张图展示了条件判断的逻辑和常用的测试条件——条件控制让脚本有了决策能力。
if语句:
#!/bin/bash
count=$(ls | wc -l)
if [ $count -gt 10 ]; then
echo "There are more than 10 files."
elif [ $count -eq 10 ]; then
echo "Exactly 10 files."
else
echo "Less than 10 files."
fi条件测试:
- 字符串比较:
[ "$str1" = "$str2" ](相等)、[ "$str1" != "$str2" ](不等) - 数值比较:
-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)、-ge(大于等于)、-le(小于等于) - 文件测试:
-f file(文件存在)、-d dir(目录存在)、-r file(可读)、-w file(可写)、-x file(可执行) - 逻辑操作:
-a(与)、-o(或)、!(非)
case语句(多分支选择):
#!/bin/bash
echo "Enter a number:"
read num
case $num in
1)
echo "You entered one."
;;
2|3)
echo "You entered two or three."
;;
*)
echo "You entered a different number."
;;
esac四、循环结构
循环让脚本能够重复执行任务。
flowchart TB subgraph Loops[循环类型] direction TB ForIn[for...in<br/>遍历列表] ForC[for ((;;))<br/>计数循环] While[while<br/>条件循环] Until[until<br/>直到满足条件] end subgraph Control[循环控制] direction TB Break[break<br/>跳出循环] Continue[continue<br/>跳过本次迭代] end Loops --> Use[使用场景] Control --> Use style ForIn fill:#e3f2fd style ForC fill:#fff9c4 style While fill:#c8e6c9 style Break fill:#ef5350
图表讲解:这张图展示了四种循环类型和控制语句——循环是自动化的基础。
for循环(遍历列表):
#!/bin/bash
# 遍历文件
for file in *.txt; do
echo "Processing: $file"
# 对文件进行处理
done
# 遍历数字序列
for i in {1..10}; do
echo "Number: $i"
donewhile循环(条件循环):
#!/bin/bash
counter=1
while [ $counter -le 5 ]; do
echo "Counter: $counter"
((counter++))
doneuntil循环(直到条件为真):
#!/bin/bash
counter=1
until [ $counter -gt 5 ]; do
echo "Counter: $counter"
((counter++))
doneC风格的for循环:
#!/bin/bash
for ((i=0; i<10; i++)); do
echo "Number: $i"
done循环控制:
break:跳出整个循环continue:跳过本次迭代,继续下一次
for i in {1..10}; do
if [ $i -eq 5 ]; then
continue # 跳过5
fi
if [ $i -eq 8 ]; then
break # 8时跳出
fi
echo "Number: $i"
done五、函数与模块化编程
函数让代码可以重用,是模块化编程的基础。
flowchart TB subgraph FuncDef[函数定义] direction TB Name[函数名] Param[参数<br/>$1, $2, ...] Body[函数体<br/>命令序列] Return[返回值<br/>return数字] end subgraph FuncCall[函数调用] Call1[调用函数<br/>func arg1 arg2] Result[获取返回值<br/>return_code=$?] end subgraph Scope[作用域] Local[局部变量<br/>local var=value] Global[全局变量<br/>函数内可访问] end FuncDef --> Use[代码重用] FuncCall --> Use Scope --> Use style Name fill:#e3f2fd style Return fill:#fff9c4 style Local fill:#c8e6c9
图表讲解:这张图展示了函数的定义、调用和作用域——函数是代码组织的重要工具。
定义函数:
#!/bin/bash
# 函数定义
greet() {
local name=$1 # local声明局部变量
echo "Hello, $name!"
return 0 # 返回值:0表示成功,非0表示失败
}
# 调用函数
greet "Alice"带参数的函数:
#!/bin/bash
backup_file() {
local src=$1
local dst=$2
if [ -f "$src" ]; then
cp "$src" "$dst"
echo "Backed up $src to $dst"
return 0
else
echo "Error: $src does not exist."
return 1
fi
}
backup_file /etc/hosts /tmp/hosts.backup获取返回值:
#!/bin/bash
check_file() {
if [ -f "$1" ]; then
return 0
else
return 1
fi
}
check_file "/etc/passwd"
if [ $? -eq 0 ]; then
echo "File exists."
else
echo "File does not exist."
fi函数库:将常用函数放在单独的文件中,然后在脚本中source:
# functions.sh
log_message() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" >> /var/log/myscript.log
}
# main.sh
source ./functions.sh
log_message "Script started"六、用户输入与输出处理
与用户交互是脚本的重要功能。
flowchart TB subgraph Input[输入方式] direction TB Read[read<br/>读取单行输入] Arg[位置参数<br/>$1, $2, ...] Opt[选项解析<br/>getopts] end subgraph Output[输出方式] direction TB Echo[echo<br/>简单输出] Printf[printf<br/>格式化输出] Redir[重定向<br/>> >>file.log] Err[错误输出<br/>>&2] end subgraph Dialog[交互式输入] direction TB Prompt[提示符<br/>read -p] Silent[静默输入<br/>read -s] Select[菜单选择<br/>select] end Input --> Script[脚本交互] Output --> Script Dialog --> Script style Read fill:#e3f2fd style Opt fill:#fff9c4 style Printf fill:#c8e6c9 style Select fill:#ba68c8
图表讲解:这张图展示了脚本的输入输出方式——交互是脚本实用性的关键。
读取用户输入:
#!/bin/bash
echo "What is your name?"
read name
echo "Hello, $name!"
# 带提示符的读取
read -p "Enter your age: " age
echo "You are $age years old."
# 静默输入(不显示输入,适合密码)
read -s -p "Enter password: " password
echo # 换行位置参数:
#!/bin/bash
# ./script.sh arg1 arg2 arg3
echo "First argument: $1" # arg1
echo "Second argument: $2" # arg2
echo "All arguments: $@" # arg1 arg2 arg3
echo "Number of arguments: $#" # 3格式化输出:
#!/bin/bash
name="Alice"
age=25
# printf比echo更灵活(类似C语言)
printf "Name: %-10s Age: %3d\n" "$name" "$age"
# %-10s 左对齐10个字符,%3d 右对齐3位数字菜单选择:
#!/bin/bash
echo "Choose an option:"
select option in "Create File" "Delete File" "Exit"; do
case $option in
"Create File")
echo "Creating file..."
break
;;
"Delete File")
echo "Deleting file..."
break
;;
"Exit")
echo "Goodbye!"
exit 0
;;
esac
done结语
Shell脚本编程是Linux自动化的核心技能。通过将命令行工具组合成脚本,你可以:
- 自动化重复性任务:如日志分析、数据备份、批量处理
- 简化复杂操作:将多步骤操作封装成单个命令
- 提高工作效率:一次编写,多次使用
- 减少人为错误:脚本按固定逻辑执行,避免手动操作失误
脚本编写的最佳实践:
- 从简单开始:先写简单的脚本,逐步增加功能
- 添加注释:解释脚本的用途和关键逻辑
- 错误处理:使用
set -e让脚本在错误时退出 - 测试脚本:在不同环境中测试,确保健壮性
- 使用函数:模块化代码,提高可重用性
接下来的文章将深入更高级的脚本主题:文本处理、高级数组、信号处理等。掌握这些技能后,你将能够编写复杂而强大的自动化脚本。
常见问题解答
Q1:[ ] 和 有什么区别,应该用哪个?
答:[ ]是shell内置命令,[[ ]]是bash关键字(更强大的测试命令)。推荐使用[[ ]],因为它:(1)不需要对变量加引号([ "$var" == "val" ] vs [[ $var == "val" ]]);(2)支持逻辑运算符(&&、||、>、<)而不需要转义;(3)支持模式匹配(=~正则表达式)。
但[[ ]]是bash特性,不是POSIX标准,如果需要移植到sh(如#!/bin/sh),应该使用[ ]。新脚本推荐[[ ]],需要兼容性时用[ ]。
Q2:$(…) 和 ... 和 ’…’ 有什么区别?
答:这三种引号的作用不同。$(...)或...是命令替换,执行命令并返回输出,例如echo "Current time: $(date)"。
单引号'...'完全引用,所有字符按字面意思处理,不进行变量替换、命令替换、转义。双引号"..."部分引用,变量替换、命令替换会执行,但转义字符(如$、\、```)仍然有特殊含义。
例子:echo '$USER'输出$USER(字面),echo "$USER"输出alice(变量值),echo "$(whoami)"输出root(命令执行结果),echo '$(whoami)'输出$(whoami)(字面)。
Q3:如何在脚本中处理命令行参数的选项?
答:使用getopts内置命令解析选项。getopts optstring varname:optstring是要识别的选项(如a:b:表示-a是一个标志,-b和:都需要参数),varname是存储当前选项的变量名。选项值存储在OPTARG变量中。
例子:
while getopts ":u:p:" opt; do
case $opt in
u) user=$OPTARG ;;
p) pass=$OPTARG ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
esac
done:开头的选项不报错(用于错误处理)。:后跟字母表示该选项需要参数。
Q4:如何让脚本在后台运行且不因终端关闭而终止?
答:使用nohup命令让脚本忽略SIGHUP信号(挂断信号),重定向输出到文件:
nohup ./script.sh > output.log 2>&1 &nohup让脚本忽略SIGHUP(终端关闭时发送的信号),&让脚本在后台运行。> output.log 2>&1将标准输出和错误输出重定向到output.log。
如果不关心输出,可以重定向到/dev/null:nohup ./script.sh > /dev/null 2>&1 &。要检查后台脚本的PID,可以用echo $!(最近的后台进程PID)或在脚本中保存echo $$ > script.pid。
Q5:如何调试Shell脚本?
答:Shell脚本调试的几种方法:
bash -x script.sh:执行脚本并显示每一条命令(展开后),可以看到变量的值、条件判断的结果。- 在脚本中添加
set -x:开启调试模式,set +x关闭调试模式。可以只调试特定部分:
set -x # 开启调试
for file in *.txt; do
# 调试这部分代码
echo "Processing $file"
done
set +x # 关闭调试bash -v script.sh:verbose模式,显示脚本读取的每一行。strace -f bash -x script.sh:跟踪系统调用,看到脚本调用的每个系统调用(打开文件、读写等)。- 使用
echo或logger输出调试信息到日志文件。 - ShellCheck(
shellcheck script.sh):静态分析工具,检查语法错误、常见问题。