Linux bash从入门到脚本:命令、变量、条件循环与调试手册

发布时间:2026/10/20 10:07:44
Linux bash从入门到脚本:命令、变量、条件循环与调试手册
我到现在还记得第一次打开Linux虚拟机时屏幕上只有一个孤零零的提示符等着我输入点什么。周围坐着的同学已经在鼠标点来点去我却面对着那个黑乎乎的窗口发呆我该敲什么它能听懂我说话吗后来才知道那个默默等我的程序就是Shell——一个把人类语言翻译成系统指令的中间层。而bash正是这个翻译员里最常见、最通用的一位。这篇文章不打算讲什么高深技巧就像跟朋友面对面聊一样把bash的基础知识、核心操作和常见坑位都过一遍。适合刚接触Linux、被命令行吓到的同学也适合写过几行脚本但一直没弄懂变量引用和条件判断细节的人。看完你至少会明白命令是怎么被执行的变量和引号为什么那么绕怎么写一个能复用的脚本以及脚本报错时怎么定位问题。1. 从“点按钮”到“敲命令”Shell到底是个什么玩意1.1 一句话先理清概念很多人会把“终端”“命令行”“Shell”这几个词混着用。严格来说终端是硬件概念现在一般指那个窗口程序命令行是你在窗口里看到的输入环境而Shell是真正执行命令、解释语法的那个程序。你在终端里敲了ls -l被bash接收bash去调用系统里的ls命令再把输出结果返回给你。这个过程跟你在图形界面双击一个文件夹图标本质是一样的——都是为了系统干活只是一个用鼠标一个用字符串。bash的全称是Bourne Again Shell看名字也知道它是Bourne Shellsh的后继者。Linux发行版里绝大多数默认shell就是bashmacOS早期也是后来才改成zsh。所以说bash是入门的第一选择理由不只是它用得广还包括资料多、脚本兼容性好。你在网上搜到的shell教程十篇里有八篇默认是讲bash学会了它再切到zsh、fish这类新兴shell核心思路完全通用。1.2 为什么入门的首选是bash市面上的shell并不少bash、zsh、fish、dash、csh、ksh……各有各的拥趸。zsh以丰富的自动补全和主题出名fish则更激进直接把语法改得像一门现代语言。但bash的位置非常特殊它不是功能最炫的却是兼容性最强的。绝大多数Linux服务器上你写一个bash脚本拷到另一台机器上照样能跑但如果你写了fish专属语法到目标机器上可能直接报错。对于要登录服务器做运维、部署服务的人来说bash就是那个“到哪都能用”的通用语言。bash还内置了很多实用的编程特性变量、数组、条件判断、循环、函数、算术运算、字符串处理这些叠加起来让bash不仅能敲命令还能像一门真正的编程语言一样写逻辑。说得直白点命令行是bash的日常用法脚本才是它真正发挥力量的场景。掌握bash脚本等于给Linux操作加了一个自动化齿轮批处理文件、定时备份、日志分析、环境搭建都能用几十行脚本搞定。1.3 我建议的学习路径我见过很多新手一上来就背命令今天学find明天学grep结果一周后全忘了。我的建议是反过来先理解bash的语法骨架再在写脚本的过程中把那些命令自然带出来。你可以按这个顺序推进先熟悉基本的文件操作命令cd、ls、cp、mv、rm、mkdir、cat、less。理解命令、选项、参数三者的关系学会看man帮助文档。掌握变量、引号、通配符、管道、重定向这五个基础概念。学会写条件判断和循环能够拼出有逻辑的脚本。处理参数、写函数让脚本具备复用和容错能力。这篇文章会顺着这个路径展开每个部分都尽量说清楚“为什么这么做”而不只是贴命令。2. 先搞懂bash的日常操作命令、变量与引用2.1 命令的本质你说的话系统怎么听bash每收到一行输入会先做分词、展开、再执行。举个例子你敲了ls -l /home/userbash会把字符串按空白拆成ls、-l、/home/user三部分然后去PATH环境变量指定的目录列表里查找有没有叫ls的可执行文件。PATH的定义通常是/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin这些目录的合集。找到了就执行找不到就报command not found。这也是为什么你要带路径执行当前目录的程序时需要敲./script.sh因为当前目录.默认不在PATH里。bash并不会默认在当前目录找命令这是刻意的安全设计——防止你在某个目录里被一个恶意的同名程序劫持。命令还能分成几类内建命令由bash自己实现如cd、echo、export、alias不依赖外部文件。外部命令独立的可执行文件如ls、grep、awk。函数你或系统定义在shell会话里的逻辑。别名给命令设置的快捷方式如alias llls -l。用type -a 命令名可以查看一个命令属于哪一类、位置在哪。比如type -a cd会显示cd是shell内建命令而type -a grep会显示它的路径。这对后面调试脚本时判断“命令为什么找不到”非常有用。2.2 变量与引号bash里最容易栽跟头的地方变量是bash里最小的存储单位。定义一个变量很简单namezhangsan age25注意赋值号两边不能有空格。name zhangsan会被解析成命令name和参数、zhangsan直接报错。引用变量时前面加美元符号echo $name。我建议写脚本时总是用大括号包裹变量名写成${name}。看起来多打了两个字符但能避免很多边界问题比如变量名后面紧跟着其他字符时容易解析错。bash里有三类引号各自分工不同双引号可以保留空格里面的变量和命令会展开比如echo $name is $age years old。单引号完全字面量里面所有字符原样输出echo $name会打印$name。反引号命令替换已不建议使用。现在更推荐$(command)形式。这个区别极度重要。网上能看到无数个新手栽在这里日志路径没有加引号路径里有空格脚本直接炸了。比如file_path/home/user/my files/note.txt cat $file_pathbash会把$file_path展开成两个词“/home/user/my”和“files/note.txt”然后当作两个文件处理结果自然是No such file or directory。正确的写法是cat $file_path让整个字符串作为一个整体传递。这个习惯建议从第一天就养成凡是可能包含空格的变量引用时默认加双引号。bash还支持一些常用的变量默认值操作。${var:-default}表示如果var没定义或为空使用default${var:default}更激进还会把default赋值给var。再配合${#var}获取字符串长度、${var%.mp4}去掉后缀名、${var// /_}把空格替换成下划线日常的字符串处理基本不用去翻awk和sed了。2.3 从实操场景看管道与重定向管道和重定向是shell最强大的两个武器很多人一开始接触时不明所以我用一个类比解释每个命令其实有三个默认数据通道——标准输入stdin、标准输出stdout、标准错误stderr。默认情况下标准输入来自键盘标准输出和标准错误都打印到屏幕。重定向做的事情就是改变这些通道的流向。把标准输出写到文件并覆盖原内容则是追加。2把标准错误重定向。最常见的组合是21把标准错误合并到标准输出里比如你执行一个编译命令想同时把正常日志和报错都存到一个文件就写make build.log 21这个写法的顺序有讲究21必须跟在 build.log后面意思是“把标准错误指向标准输出当前指向的地方”。如果反过来写标准错误会被指到原本的终端上效果就不同了。管道则是把一个命令的标准输出接到另一个命令的标准输入。比如ps aux | grep nginx | grep -v grep当前命令的输出顺着管道一直传给后面的命令处理。这里能看出shell设计的巧思每个命令都是一个个小工具通过管道拼装成流水线。跟编程语言里把大函数拆成小函数再组合是同一个思路。有几个容易混淆的符号再强调一下$(command)执行命令并把输出作为字符串插入。$((expression))算术运算如echo $((3 4))输出7。command 让命令在后台运行。command command2前一个成功才执行后一个。command ; command2不管结果如何顺序执行。配合通配符一起用更顺手。*匹配任意字符串?匹配单个字符[abc]匹配方括号内任一字符。通配符是依赖文件名匹配的跟正则表达式不同别混了。3. 让简单的语句组成逻辑条件判断与循环3.1 if判断的“格子”问题bash的条件判断有个让新手抓狂的地方空格和方括号必须严格对位。一个最基本的if写法if [ -f $file ]; then echo 文件存在 else echo 文件不存在 fi方括号后面、前面都必须有空格]前面也要有空格。[其实是一个命令全名叫test所以[ $a $b ]的意思就是调用test命令测试条件是否成立。既然是一个命令单词之间就必须用空格分隔这就是为什么[ -f $file ]不能写成[-f $file]。除了[bash还提供了[[关键字功能更强大支持模式匹配、和||、正则匹配等而且即使变量为空也不会出错[在这种情况下经常报错。条件测试优先级上我推荐新手直接用[[写法更现代逻辑更不易踩坑if [[ -f $file -r $file ]]; then echo 文件存在且可读 fi判断条件里有两个特别容易混淆的点数字比较用-eq、-ne、-gt、-lt字符串比较用、!、\、\。[ 10 -gt 2 ]正确[ 10 \ 2 ]也正确但看起来费劲。如果你看到条件结果总是不对先检查是不是把数字比较和字符串比较混了。文件测试常用参数也列一下-f普通文件、-d目录、-e路径存在、-s文件非空、-r可读、-w可写、-x可执行。这些配合条件判断能组合出很多实用场景比如判断参数是否传入、配置文件是否存在、备份目录是否可写。3.2 for循环的多种写法for循环是最常碰到的循环结构bash里它跟一般编程语言的“遍历数组”更接近而不是写一个数字范围然后自增。最经典的写法是遍历一个列表for ip in 192.168.1.1 192.168.1.2 192.168.1.3; do echo Pinging $ip ping -c 1 $ip done但这种写法有个潜在问题默认情况下for会按空格切割列表所以如果你遍历的是一个包含空格的文件名就会出错。针对这种情况可以用while read配合输入重定向while IFS read -r line; do echo $line done file_list.txtIFS和-r都是为了安全处理特殊字符前者防止行首尾空白被吃掉后者防止反斜杠被当作转义符号。处理含换行符的文件列表时这个方法比for更可靠。如果你需要写一个类似C语言的数字循环bash也有办法for i in $(seq 1 10); do echo $i done # 或 for i in {1..10}; do echo $i done{1..10}这种花括号展开在bash 4.0以上版本都能支持简单直观。循环里用break可以提前退出continue跳过本次这跟其他语言一样。需要提醒的是for i in $(seq 1 10)里用到了命令替换如果seq命令不存在这个循环就直接废了。{1..10}是纯bash特性更可靠。3.3 函数把重复的活收起来当一个脚本里重复出现好几段相似操作时就该封装成函数了。bash里定义一个函数很简单log() { echo $(date %Y-%m-%d %H:%M:%S) - $1 }位置参数在函数内部同样有效$1表示第一个参数$2第二个$表示所有参数。注意函数里的$1是函数自己的参数跟脚本全局的$1不冲突。调用时直接写函数名log 开始部署函数还能设置返回值但bash里返回的是0~255的整数0表示成功非0表示失败或状态码。用return 1这类写法和脚本退出码exit逻辑基本一致只是作用域不同。如果你希望函数打印出处理结果而不只是状态最简单的方式是用echo打印然后在调用处用$(函数名)捕获输出。get_size() { local file$1 local size$(stat -c %s $file 2/dev/null || echo 0) echo $size } current_size$(get_size /etc/passwd)这里多用一个local关键字非常关键。bash里函数默认共享全局变量一个函数里随手改的变量到了另一个函数可能还在容易产生莫名其妙的状态污染。local把变量限制在函数内部写脚本时尽量给内部变量都加上local。4. 从命令行到脚本文件写出第一个可复用的脚本4.1 脚本文件的三件套当你发现一组命令需要反复敲、或者要做一些多层判断时就该把命令装进脚本文件了。一个bash脚本的基本构成是三件事shebang行、可执行权限、正确的执行方式。第一行通常是#!/bin/bash这行以#!开头用来告诉系统这个文件应该用哪个解释器执行。虽然在终端里直接执行时这行未必是必需的——比如你用bash script.sh时系统已经知道用bash——但加上它可以让你直接./script.sh就能运行并且能减少误判。第二件事是赋可执行权限chmod x script.shx表示给文件增加执行权限。很多新手写完脚本直接./script.sh得到Permission denied就是忘了这一步。但如果你不想动权限也可以用bash script.sh执行。不过我强烈建议你养成加执行权限的习惯因为这才是“脚本”被系统当作独立程序处理的方式也方便进一步加入定时任务。第三件事是记住脚本文件内部末尾最好有一个换行。别小看这个细节某些工具会提示“no newline at end of file”。虽然bash通常不报错但单行命令、其他编辑器可能受到影响。规范的脚本后面跟一个换行省心很多。一个可运行的脚本大概长这样#!/bin/bash # 项目部署前检查脚本 set -euo pipefail echo 正在检查依赖... if ! command -v curl /dev/null 21; then echo curl不存在请先安装 2 exit 1 fi if curl -s -o /dev/null -w %{http_code} http://localhost:8080 | grep -q 200; then echo 服务状态正常 else echo 服务未响应 2 exit 1 fi这里set -euo pipefail是我写脚本时几乎必加的一行。其中-e表示任何命令返回非零就立即退出-u表示变量未定义就报错避免变量打错字造成静默错误-o pipefail表示管道中任何一个命令失败整个管道返回失败。这三件套能帮你第一时间发现脚本里的问题而不是带着错误继续跑下去。我踩过最典型的坑是草稿里少写一个字符变量名拼错结果脚本里那个变量当作空值处理后面的逻辑整个偏离预期。加了-u之后这种错误会立刻暴露出来报错信息直接指出哪一行用了未定义变量。4.2 给脚本加点“参数处理”脚本写出来给别人用就得处理外部传入的参数。bash里几个位置参数约定$0脚本自身的名字。$1、$2依次是第一个、第二个位置参数。$#参数个数。$所有参数常用于遍历。$*所有参数当作一个整体字符串。参数少的时候直接判断即可if [ $# -lt 1 ]; then echo 用法: $0 目标目录 exit 1 fi target_dir$1参数稍微多起来或者带选项比如-s、-v就该用getopts了。getopts是bash内建命令专门处理短选项while getopts s:v opt; do case $opt in s) source_dir$OPTARG ;; v) verbose1 ;; *) echo 未知选项: -$OPTARG exit 1 ;; esac done有两点需要指出来第一个是后面的:表示s选项需要带参数取到的参数放在$OPTARG里第二个是v选项不需要参数所以没有冒号。getopts会自动把选项和位置参数分离你可以在最后用shift $((OPTIND - 1))把剩余的裸参数拿到手。当然实际写脚本时完全可以用更现代的argparse系列库比如bash第三方库或干脆嵌入一段手动解析。但对绝大多数脚本getopts够用且可靠。至少它不用你去处理那些相同选项重复出现、选项和参数粘连的边界问题。4.3 实例一个简单的备份脚本纸上谈兵够多了直接写一个能用的备份脚本把前面提到的知识点串起来。这个脚本模拟的场景是备份一个指定目录下的所有.conf文件打包成带时间戳的压缩包只保留最近7天的备份。代码本身不算复杂但结构完整。#!/bin/bash set -euo pipefail config_dir/etc/myapp backup_root/data/backups today$(date %Y%m%d_%H%M%S) backup_file$backup_root/config_backup_$today.tar.gz if [[ ! -d $config_dir ]]; then echo 配置目录 $config_dir 不存在推出备份 2 exit 1 fi mkdir -p $backup_root find $config_dir -name *.conf -print0 | tar --null -czf $backup_file -T - 2/dev/null if [[ -s $backup_file ]]; then echo 备份完成: $backup_file else echo 备份失败: 生成的文件为空 2 exit 1 fi find $backup_root -name config_backup_*.tar.gz -mtime 7 -delete echo 已清理7天前的备份有几个细节说一下。find的输出不是普通换行文本而是-print0配合tar --null核心是为了应对文件名里带换行或空格的情况。如果文件名有空格用普通find ... | tar会直接拆错-print0用空字符分隔才安全。这段逻辑是我后来从生产环境实战中总结出来的新手写脚本时忽略了它一旦遇到特殊文件名就会出问题。-s $backup_file判断文件存在且非空。备份程序可能没报错就退出了但生成的文件是0字节这种异常要靠这个判断兜底。删除7天前备份的这一行用了-mtime 7。注意不是“超过7天前的文件”就删除而是修改时间距今超过7天的文件。如果机器本身时间有问题这一行的效果也会受影响。5. 排错与经验我踩过的坑和检查技巧5.1 高频问题速查bash脚本报错信息有时候相当直白有时候又很隐晦。整理了一张表列出我见过的高频错误和对应的处理思路方便你对号入座错误现象常见原因处理办法command not foundPATH有问题命令未安装或脚本没有shebang用type -a查命令位置echo $PATH查环境变量Permission denied文件没有执行权限chmod x 文件名[: 缺少][和]周围空格不对或者变量为空改成[[写法检查方括号两边空格脚本执行一半就停了某个命令返回非零配合set -e触发了退出定位退出点检查该命令的退出码文件内容带空格被拆开引用变量时没有加双引号给变量引用统一加双引号\r字符导致命令执行失败脚本在Windows下编辑过带有CRLF换行dos2unix 文件名或换行符转成LF变量名拼写没问题但结果是空变量未定义或者写成了var而不是$var加set -u检查是否缺少$符号很多时候问题不在于语法多复杂而在于“你以为你写的”和“bash以为你写的”不是一回事。比如在CRLF换行的情况下脚本第一行的#!/bin/bash\r会被系统当成一个带特殊字符的解释器路径直接报错。这种问题不做排查根本想不到是换行符的锅。5.2 排错工具链bash -x和echo调试bash自带一个调试模式运行脚本时加-x参数会打印每一条实际执行的命令及展开结果。这是我最常用的排错手段。bash -x script.sh-x会把命令展开后的结果打印出来。比如脚本里有$filename它会显示成cat /data/my file.txt这种展开后的样子你一眼就能看出变量引用、空格处理有没有问题。配合-n可以做语法检查bash -n script.sh只检查语法错误但不执行当脚本很长时先跑一遍能快速发现括号不匹配、语法错漏。也有时候不想整个脚本都开调试那就只是在重点位置加echo插桩。最常见的方法是打印变量值echo DEBUG: config_dir$config_dir打印时建议加上标记前缀比如DEBUG:方便之后批量删除。如果脚本里有连续多步操作还可以打印“进入第X步”这类信息确认执行顺序是否符合预期。我对当前这个备份脚本实际调试的过程印象挺深。第一次写完测试时发现备份文件总是空的。加了-x运行发现find $config_dir -name *.conf没有输出任何内容再检查才发现目录里的配置文件后缀是.cnf而不是.conf。这种低级错误靠读代码很难发现但靠bash -x简直是一目了然。5.3 维护bash脚本的几个经验习惯最后整理一些在实操中验证过的小习惯虽然琐碎但能极大提升脚本编写的舒适度。第一个习惯是给每个脚本加上“公共头”。我通常在文件开头写这样几行#!/bin/bash set -euo pipefail IFS$\n\tset那行前面已经说过。IFS改成换行和Tab而不是默认的包含空格这样for循环遍历文件列表时含有空格的文件名不会被拆散。这个习惯是从某次批量处理带空格文件名被坑之后养成的之后写任何涉及文件名的脚本都默认带上。第二个习惯是日志输出明确区分正常和错误。正常输出走标准输出错误输出走标准错误echo 处理完成 echo 处理失败: 目录不存在 2这样在使用脚本的时候如果加了2error.log重定向只有错误信息留在日志文件里排查起来一目了然。第三个习惯是脚本命名用小写字母加下划线后缀用.sh。虽然不是硬性规定但业界通识就是小写加下划线方便辨认。文件名能用backup_config.sh就不必写成backupConfigScriptV2.sh有些系统对文件名大小写敏感别给自己添麻烦。第四个习惯是给脚本写注释。这个可能听起来像废话但bash脚本有一个特点写的时候大脑还清晰三个月后回来看基本就等同于天书。至少在关键的判断逻辑、参数处理、磁盘操作这几个位置各写一行注释说明意图不然后面维护的成本远高于写的时候多敲几行字。最后的经验体会写了这么多年命令行脚本最大的体会是bash是一门“故事语言”它不追求严密的类型系统和抽象能力追求的是让系统里的工具能灵活地拼装起来。正因为这个特性学bash不要试图一次性把所有语法都学会而是随手记下自己在真实操作中需要用到的那些片段。今天部署服务时写一个判断进程是否在运行的脚本明天日志清理时写一个定时备份任务用到的知识点就这么一点一点积累起来了。这篇介绍覆盖的也就是最基础的那一层——变量怎么存、引号怎么用、判断循环怎么写、脚本怎么跑起来。把这些搞利索往后去看那些复杂的bash开源脚本至少能读懂它们在干什么而不是望而生畏。