Shell编程入门:变量、条件判断与流程控制实战指南

📅 2026/8/12 16:56:28
Shell编程入门:变量、条件判断与流程控制实战指南
1. 项目概述为什么每个Linux用户都绕不开Shell编程如果你在Linux世界里待过一段时间不管是运维、开发还是数据分析迟早会碰到一个场景面对一堆重复性的命令操作你开始琢磨能不能把这些敲敲打打的活交给电脑自己干这个“让电脑自己干活”的钥匙就是Shell编程。很多人一听到“编程”就觉得头大觉得那是开发者的专属领域。但我想说Shell脚本可能是你接触过的最实用、门槛最低的“编程”。它不是什么高深的魔法本质上就是你平时在终端里输入的那些命令的“批处理”和“自动化”。想想这些场景每天上班第一件事登录服务器检查日志、备份关键数据库、清理临时文件或者你需要处理几百个数据文件重命名、格式转换、内容提取又或者部署一个应用需要按顺序安装依赖、配置环境、启动服务。这些工作手动做费时费力还容易出错。写成一个Shell脚本下次一键运行省下来的时间喝杯咖啡不香吗这就是Shell编程的核心价值——将重复、繁琐的命令操作流程化、自动化极大地提升在Linux环境下的工作效率和可靠性。它不要求你精通数据结构或设计模式只要你熟悉基本的Linux命令就能开始创造。接下来我会带你从最基础的变量和运算符开始一步步构建出能处理复杂逻辑的脚本把那些令人生畏的“流程控制”变成你得心应手的工具。2. Shell编程核心变量、运算符与条件判断2.1 变量脚本里的“记忆单元”变量是Shell脚本的基石它就像一个贴了标签的盒子用来存放数据。Shell中的变量定义和使用极其简单但也因此有一些独特的“坑”。定义与使用定义一个变量直接变量名值即可等号两边不能有空格这是新手最容易踩的坑之一。my_nameShell Learner count100使用变量时需要在变量名前加上美元符号$。为了明确变量边界避免歧义通常建议用花括号{}将变量名括起来。echo $my_name # 输出Shell Learner echo ${count}th # 输出100th 这里花括号明确了变量名是count而不是countth变量类型与引号哲学Shell变量默认都是字符串。引号的使用大有讲究双引号弱引用。引号内的变量$var、命令替换command或$(command)和转义字符\会被解析。greetingHello, $my_name echo $greeting # 输出Hello, Shell Learner单引号强引用。引号内所有字符都原样输出不做任何解析。echo $greeting # 输出$greeting无引号不推荐。字符串中如果包含空格、通配符*,?等会被Shell特殊处理极易出错。file_list*.txt # 这里*.txt会被展开为当前目录所有.txt文件列表实操心得除非有特殊需要如明确希望展开通配符否则给变量赋值和引用时总是使用双引号。这能避免绝大多数因空格或特殊字符引发的诡异错误。例如cp $source_file $dest_dir就比cp $source_file $dest_dir安全得多。特殊变量与参数传递Shell提供了一系列特殊的只读变量用于获取脚本运行时的上下文信息$0当前脚本的文件名。$1,$2,$3...脚本执行时传入的第1、2、3...个参数。$#传递给脚本的参数个数。$*和$都表示所有参数。区别在于当被双引号包裹时“$*”将所有参数视为一个整体字符串“$1 $2 ...”而“$”将每个参数视为独立的字符串“$1” “$2” ...。在需要遍历参数时总是使用“$”。$?上一个命令的退出状态码。0表示成功非0表示失败。这是进行条件判断的关键。$$当前Shell进程的PID。2.2 运算符脚本里的“计算器”Shell本身并不擅长复杂的数学运算但基础的算术和比较是必备技能。算术运算符在$(( ))或let命令中进行算术运算。a10 b3 echo $((a b)) # 加法输出13 echo $((a - b)) # 减法输出7 echo $((a * b)) # 乘法输出30 echo $((a / b)) # 除法整除输出3 echo $((a % b)) # 取模输出1 echo $((a)) # 后置自增输出10先返回值再自增 echo $((a)) # 前置自增输出12先自增再返回值 let “c a * 2” echo $c # 输出24关系运算符用于数值比较关系运算符通常用在test命令或[ ]、[[ ]]结构中用来比较数字。-eq等于equal-ne不等于not equal-gt大于greater than-lt小于less than-ge大于等于greater or equal-le小于等于less or equal字符串运算符或字符串相等。!字符串不相等。-z字符串长度为零Zero。-n字符串长度非零Non-zero。str1 str2按字典序比较str1大于str2。str1 str2按字典序比较str1小于str2。文件测试运算符这是Shell脚本非常强大的功能用于判断文件属性。-e file文件/目录是否存在Exist。-f file是否是普通文件File。-d file是否是目录Directory。-r file是否可读Readable。-w file是否可写Writable。-x file是否可执行eXecutable。-s file文件大小是否大于0Size 0。file1 -nt file2file1是否比file2新Newer Than。file1 -ot file2file1是否比file2旧Older Than。2.3 条件判断脚本的“决策大脑”条件判断让脚本有了“智能”可以根据不同情况执行不同的分支。Shell中主要通过test命令、[ ]和[[ ]]来实现。test命令与[ ]test命令用于检查某个条件是否成立。[ ]是test命令的另一种写法方括号内部两端必须有空格。num10 if test $num -gt 5; then echo “数字大于5” fi # 等价写法更常见 if [ $num -gt 5 ]; then echo “数字大于5” fi[[ ]]更强大的条件判断[[ ]]是Bash等现代Shell的扩展功能更强大语法更自然且内部变量引用可以不加引号但仍建议加能避免很多意外。支持逻辑运算符(与)、||(或)更直观。支持模式匹配[[ $str pattern ]]和正则匹配[[ $str ~ regex ]]。filename“my_script.sh” if [[ -f “$filename” -x “$filename” ]]; then echo “文件存在且可执行” fi if [[ “$filename” *.sh ]]; then echo “这是一个Shell脚本文件” fi email“userexample.com” if [[ “$email” ~ ^[a-zA-Z0-9._%-][a-zA-Z0-9.-]\.[a-zA-Z]{2,}$ ]]; then echo “邮箱格式看起来正确” fi注意事项在编写需要跨平台如/bin/sh运行的脚本时为了兼容性应使用[ ]和-a、-o进行逻辑组合。如果是Bash脚本强烈推荐使用功能更强大、更安全的[[ ]]。组合条件与逻辑运算在[ ]中使用-a表示AND-o表示OR。在[[ ]]中直接使用和||可读性更好。age25 gender“male” # 使用 [ ] 的写法 if [ “$age” -gt 18 -a “$gender” “male” ]; then echo “成年男性” fi # 使用 [[ ]] 的写法推荐 if [[ $age -gt 18 $gender “male” ]]; then echo “成年男性” fi3. 流程控制构建脚本的逻辑骨架掌握了变量和判断就可以用流程控制语句将它们组织起来形成完整的逻辑。Shell的流程控制主要包括分支选择和循环。3.1 分支结构if-else与caseif-elif-else 语句这是最常用的分支结构根据条件真假执行不同代码块。#!/bin/bash score85 if [[ $score -ge 90 ]]; then echo “成绩优秀” elif [[ $score -ge 80 ]]; then echo “成绩良好” elif [[ $score -ge 60 ]]; then echo “成绩及格” else echo “成绩不及格” fiif语句必须以fiif倒过来写结束。elif和else都是可选的。case-esac 语句当需要对一个变量的多种可能值进行匹配时case语句比多层if-elif更清晰、更易维护。#!/bin/bash echo “请选择操作” echo “1. 启动服务” echo “2. 停止服务” echo “3. 重启服务” read -p “请输入数字 (1-3): ” choice case $choice in 1) echo “正在启动服务...” # systemctl start myservice ;; 2) echo “正在停止服务...” # systemctl stop myservice ;; 3) echo “正在重启服务...” # systemctl restart myservice ;; *) # 匹配所有其他情况 echo “输入错误请输入1, 2 或 3。” exit 1 # 以非0状态退出表示错误 ;; esac每个匹配模式以)结束对应的代码块以;;双分号结束。*是通配符匹配任何值。case语句以esaccase倒过来写结束。3.2 循环结构for、while与untilfor 循环用于遍历一个列表中的每一项。# 遍历一个显式列表 for fruit in apple banana orange; do echo “我喜欢吃 $fruit” done # 遍历命令执行结果例如当前目录下的.txt文件 for file in *.txt; do echo “处理文件$file” # 可以对$file进行操作如 wc -l “$file” done # C语言风格的for循环Bash支持 for ((i1; i5; i)); do echo “这是第 $i 次循环” donewhile 循环只要条件为真就不断执行循环体。务必确保循环条件有变为假的可能否则会陷入死循环。# 条件循环 count1 while [[ $count -le 5 ]]; do echo “Count: $count” ((count)) # 使用双括号进行算术运算使count自增 done # 读取文件内容经典用法 while IFS read -r line; do echo “行内容$line” done “/path/to/file.txt” # IFS 防止行首尾空格被修剪-r 防止反斜杠转义until 循环与while相反只要条件为假就继续循环。可以理解为“直到...为止”。num10 until [[ $num -le 0 ]]; do echo “倒计时$num” ((num--)) sleep 1 # 等待1秒 done echo “时间到”循环控制break 与 continuebreak立即跳出当前循环。continue跳过本次循环剩余的代码直接开始下一次循环。for i in {1..10}; do if [[ $i -eq 3 ]]; then continue # 跳过数字3 fi if [[ $i -eq 7 ]]; then break # 当i等于7时跳出整个循环 fi echo “当前数字$i” done # 输出1, 2, 4, 5, 63.3 实战一个综合性的日志分析脚本让我们把上面所有的知识点串联起来写一个有点实用价值的脚本。假设我们有一个需求监控一个应用日志文件/var/log/myapp/app.log实时检查其中是否出现“ERROR”关键字如果出现则提取包含错误的那一行及其前后各两行上下文并发送邮件通知这里用打印到屏幕模拟。#!/bin/bash # 文件名log_error_monitor.sh # 功能监控日志文件中的ERROR并报告上下文 LOG_FILE“/var/log/myapp/app.log” TEMP_FILE“/tmp/error_context.tmp” # 假设我们记录上次检查到的行号 LAST_LINE_FILE“/tmp/last_line_number.txt” # 初始化如果上次行号文件不存在则从文件末尾开始假设只监控新日志 if [[ ! -f “$LAST_LINE_FILE” ]]; then # 获取文件总行数作为起始行 if [[ -f “$LOG_FILE” ]]; then wc -l “$LOG_FILE” “$LAST_LINE_FILE” else echo “0” “$LAST_LINE_FILE” fi fi # 读取上次检查到的行号 last_line$(cat “$LAST_LINE_FILE”) current_total_lines$(wc -l “$LOG_FILE” 2/dev/null || echo 0) # 如果当前行数小于上次记录日志被轮转或清空了则重置为从0开始 if [[ $current_total_lines -lt $last_line ]]; then last_line0 fi # 检查是否有新行 if [[ $current_total_lines -gt $last_line ]]; then echo “发现新日志从 $last_line 行到 $current_total_lines 行开始检查ERROR...” # 使用sed提取新增加的行进行检查 # -n ‘p’ 抑制默认输出只打印处理的行 # 将新行保存到临时变量中处理 new_content$(sed -n “${last_line},${current_total_lines}p” “$LOG_FILE”) # 一个更健壮的方法逐行处理新内容 line_count0 while IFS read -r line; do ((line_count)) absolute_line_num$((last_line line_count)) # 检查是否包含ERROR忽略大小写 if [[ “${line^^}” *“ERROR”* ]]; then echo “[警报] 在 $LOG_FILE 第 ${absolute_line_num} 行发现ERROR” echo “错误行内容$line” echo “--- 上下文前后各2行 ---” # 计算上下文行范围确保不超出文件边界 context_start$((absolute_line_num - 2)) context_start$((context_start 1 ? 1 : context_start)) context_end$((absolute_line_num 2)) # 使用sed打印上下文到临时文件并显示 sed -n “${context_start},${context_end}p” “$LOG_FILE” “$TEMP_FILE” cat “$TEMP_FILE” echo “--------------------------------” # 在实际应用中这里可以添加发送邮件或告警的逻辑 # mail -s “应用错误警报” adminexample.com “$TEMP_FILE” fi done “$new_content” # 这里使用“Here String”将变量内容作为输入 # 更新最后检查到的行号 echo “$current_total_lines” “$LAST_LINE_FILE” echo “检查完成。当前已处理至第 ${current_total_lines} 行。” else echo “没有新的日志内容。” fi # 清理临时文件如果存在 [[ -f “$TEMP_FILE” ]] rm -f “$TEMP_FILE”这个脚本虽然不长但用到了我们讨论的多个核心概念变量LOG_FILE,last_line等。条件判断if [[ ! -f ... ]]检查文件是否存在if [[ $current_total_lines -gt $last_line ]]比较数值。文件测试运算符-f检查文件。算术运算$(( ... ))计算行号。流程控制if-else分支while循环读取每一行新日志。命令替换$(wc -l ...)获取行数。输入重定向while ... done “$new_content”将变量内容作为循环输入。你可以通过crontab设置这个脚本每分钟运行一次就能实现一个简单的实时错误日志监控。当然生产环境有更专业的工具如logwatch,ELK等但理解这个脚本的每一行意味着你已经掌握了Shell自动化编程的精髓。4. Shell脚本调试与最佳实践避坑指南写出来的脚本能跑只是第一步写得健壮、易维护、少踩坑才是高手和菜鸟的区别。这部分分享的都是血泪教训换来的经验。4.1 脚本调试技巧启用调试模式在脚本开头或运行脚本时可以开启调试让Shell打印出执行的每一行命令。在脚本内启用在脚本第一行#!/bin/bash之后添加set -x # 开启命令追踪 # 或者更详细的 set -xe # -x: 追踪命令 -e: 任何命令失败返回非0立即退出脚本在运行命令时启用bash -x your_script.sh-x会让Shell在执行每个命令前先打印出扩展后的命令变量已被替换这对于理解脚本执行流程和定位变量展开错误极其有用。使用-e选项出错即退出在脚本开头加上set -e可以让脚本在任何命令执行失败返回非零状态码时立即退出而不是继续执行可能产生更严重后果的命令。这对于确保脚本的健壮性至关重要。#!/bin/bash set -e rm -rf /some/important/directory/* # 如果这步失败脚本会停止不会执行后面的操作 cp new_files/* /some/important/directory/使用-u选项未定义变量报错set -u会让脚本在尝试使用一个未定义的变量时报错并退出。这能有效防止因为拼写错误导致变量为空从而引发意料之外的行为。#!/bin/bash set -u name“Alice” echo “Hello, $naem” # 这里拼写错误应该是$name。执行时会报错naem: unbound variable实操心得我个人的习惯是在所有严肃的脚本开头都写上set -euo pipefail。pipefail是另一个有用的选项它使得管道命令中任何一个命令失败整个管道的返回值就是失败命令的返回值而不是最后一个命令的返回值。这四条组合-e出错退出、-u未定义变量报错、-o pipefail管道失败检测、-x调试追踪是写出可靠Shell脚本的护身符。手动输出调试信息在关键位置使用echo打印变量状态或执行进度。echo “[DEBUG] 当前处理的文件是$file” echo “[DEBUG] 用户输入的值是$input” # 执行某个可能失败的操作 if some_command; then echo “[INFO] 命令执行成功” else echo “[ERROR] 命令执行失败退出码$?” 2 # 2 表示输出到标准错误流 exit 1 fi4.2 常见“坑”与最佳实践坑1空格引发的血案这是Shell脚本第一杀手。赋值时等号两边不能有空格varvalue正确var value错误会被解析为命令var带参数和value。[ ]测试语句内部括号与参数间必须有空格[ “$var” -eq 10 ]正确[“$var” -eq 10]错误。字符串比较时变量一定要用双引号括起来if [ “$var” “value” ]; then。如果$var为空不加引号会变成if [ “value” ]; then语法错误。坑2反引号与 $() 的选择** 两者都用于命令替换将命令的输出赋值给变量。**强烈推荐使用$()。$()更清晰尤其支持嵌套。$(command1 $(command2))。反引号在视觉上容易和单引号混淆且不支持嵌套需要转义非常丑陋。坑3脚本执行方式与权限./script.sh这种方式执行需要脚本有可执行权限chmod x script.sh并且第一行必须有正确的shebang如#!/bin/bash来指定解释器。bash script.sh或sh script.sh直接指定解释器执行脚本文件本身不需要可执行权限。但要注意sh可能是bash的兼容模式或更简单的dash某些Bash特性可能不可用。source script.sh或. script.sh在当前Shell环境中执行脚本而不是启动一个子Shell。这意味着脚本中定义的变量、函数在当前终端会话中依然有效。常用于加载环境配置如. ~/.bashrc。最佳实践清单指定解释器脚本第一行总是#!/bin/bash或#!/usr/bin/env bash以增强可移植性。启用严格模式在脚本开头使用set -euo pipefail。变量引用加双引号几乎总是使用“$var”除非你明确知道不需要。使用$()进行命令替换放弃反引号。使用[[ ]]进行条件测试如果脚本明确用于Bash它更安全、功能更强。对路径变量使用绝对路径或进行校验特别是涉及rm、mv等危险操作时。函数返回状态码在函数中使用return 0表示成功return 1或其他非零值表示失败。调用者通过$?检查。添加注释和日志说明脚本目的、参数、关键步骤。使用echo或专业的日志函数输出运行信息。处理信号对于长时间运行的脚本使用trap命令捕获CtrlCSIGINT等信号进行必要的清理工作后再退出。cleanup() { echo “正在清理临时文件...” rm -f /tmp/temp_*.$$ echo “脚本退出。” } trap cleanup EXIT INT TERM # 在脚本退出、被中断、被终止时执行cleanup函数代码风格统一缩进使用4个空格或2个但保持一致函数名、变量名使用小写加下划线my_function,log_file常量使用大写MAX_RETRY。Shell编程的魅力在于它直接连接了你和操作系统。每一个高效的脚本都是你对工作流程思考的结晶。从今天开始试着把那些重复三次以上的命令行操作封装成一个脚本吧。你会惊讶于它带来的效率提升和那种“一切尽在掌控”的感觉。记住最好的学习就是动手从修改一个现有的脚本开始逐步加入自己的逻辑你很快就能驾驭这个强大的自动化工具。