拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Ubuntu 20.04 LTS Shell脚本入门、避坑与调试实战

1. 先把Shell这件事说清楚它到底解决什么问题很多人第一次打开Ubuntu 20.04 LTS的终端看到那个黑底白字的窗口和末尾闪烁的$第一反应是“这玩意儿是不是上世纪的产物”。但只要你在这台机器上待过一段时间就会发现一个反直觉的事实图形界面里点三分钟才能完成的事一条命令半秒就干完了。Shell就是承载这些命令的那层壳它一头连着你的键盘一头连着操作系统的内核负责把你敲进去的字符串翻译成系统能听懂的系统调用。我把它理解成一个“翻译官加调度员”。你在终端里输入ls /homeShell拿到这串字符先按空格拆成命令名和参数然后去PATH环境变量列出的目录里挨个找有没有叫ls的可执行文件找到后创建一个子进程执行它把结果通过标准输出还给你。这个过程里能出问题的地方很多——命令找不到、权限不够、参数被空格截断、路径里带中文——而这些恰恰是新手最常卡住的地方也是这篇内容想帮你提前避开的。这套东西能做什么说白了凡是你能在Ubuntu上重复做三次以上的操作都值得用Shell固化下来。批量重命名两百张照片、每天定时把日志打包、把一堆CSV合并成一张表、自动备份数据库再上传到远端、在几十台机器上同步执行同一条检查命令这些活儿用Shell脚本几行就能搞定而且写完之后一劳永逸。它不需要编译不需要额外装运行时Ubuntu 20.04 LTS默认就带Bash 5.0开箱即用。适合谁看如果你刚开始接触Linux被各种命令和参数绕得头晕这篇内容会帮你把地基打牢如果你已经会敲几个命令但写脚本总报错第6、第7部分会讲到引号、${}和$()、for循环这些最容易翻车的点如果你是面试准备者中间穿插的那些“为什么这么设计”的追问也基本覆盖了常见的考察角度。我的原则是不堆命令手册每条命令都告诉你什么时候用它、坑在哪、我实际怎么用。2. 环境准备与几个必须先建立的认知2.1 终端、Shell、Bash三个被混用的词新手最容易混淆的就是这三个概念。终端Terminal是一个程序负责显示字符和接收键盘输入它本身不懂任何命令Shell是一个命令行解释器是真正读懂你输入的那层程序Bash是Shell众多实现中的一种全称Bourne Again Shell是Ubuntu 20.04 LTS的默认Shell。打个比方终端是电话机Shell是电话那头接听的人Bash是这个接听的人里最常用的那一位。为什么Ubuntu默认选Bash而不是别的因为它兼容早期的Bourne Shellsh又加上了命令补全、命令历史、数组、关联数组、[[ ]]条件判断等一大堆实用特性同时几乎所有Linux发行版的教程和脚本都按Bash的语法写。你要是换成zsh或者fish体验可能更顺手但看别人脚本时就得留个心眼。Ubuntu 20.04 LTS自带的Bash版本是5.0.x支持${var^^}这类大小写转换语法比老服务器上那些4.2、4.3的Bash强不少。这里有个实际经验写脚本时第一行务必写#!/bin/bash而不是#!/bin/sh。原因是在Ubuntu上/bin/sh是dash的软链接dash比Bash精简、启动快但语法不完全兼容[[ ]]、数组操作、local的某些用法都会出错。我踩过最典型的一次是脚本里用了echo -esh下直接把-e当普通字符打印出来了排查半天才发现是shebang写错。所以记住写脚本就写#!/bin/bash除非你有明确的理由用别的。2.2 检查与切换默认Shell想知道当前用户用的是哪个Shell敲echo $SHELL。注意这个变量是登录Shell的路径不一定等于你此刻正在运行的Shell想确认当前进程就ps -p $$$$是当前Shell的进程号。这两个命令的结果有时不一样比如你从bash里手动执行了一个zsh$SHELL还是/bin/bash但$$指向的进程已经是zsh了。要切换默认Shell用chsh -s /bin/bash然后输入密码重新登录生效。改之前建议用cat /etc/shells看一眼系统里装了哪些合法Shell你要是填一个不存在的路径chsh会直接拒绝。Ubuntu 20.04 LTS默认装了Bash和dash其他Shell得自己apt install。注意chsh改的是当前用户的登录Shell不要用sudo chsh去改那样改的是root的。另外容器环境比如Docker里的Ubuntu镜像通常没有chsh得改/etc/passwd但那种环境一般也没必要改。2.3 快捷键省下的是你一天几十次的手指移动这几个组合键是我建议第一天就记住的它们的价值随着你敲命令的时长线性增长快捷键作用使用场景Tab命令/路径/参数补全路径长、命令记不全时Ctrl R反向搜索历史命令想找前几天敲过的那条长命令Ctrl A光标移到行首改命令开头参数Ctrl E光标移到行尾在命令末尾追加参数Ctrl U删掉光标前所有内容整行重写Ctrl K删掉光标后所有内容保留前半段Ctrl L清屏等于clear屏幕太乱Ctrl C中断当前命令命令卡住、循环跑飞Ctrl R是这里面性价比最高的一个很多人不知道它还能连续按第一次按找到最近一条匹配的再按一次跳到更早的匹配项找到后直接回车执行或按方向键修改。另外Tab键按两次会列出所有候选项路径输入到一半拿不准后面是什么时特别有用。3. 文件和目录操作真正常用的就那么十几个3.1 定位自己在哪pwd、cd、ls 的组合拳pwd打印当前绝对路径看着简单但写脚本时它是自检的第一步——很多“明明文件就在这为什么找不到”的错误根源就是脚本执行时的当前目录和你想的不一样。我习惯在脚本开头加一句cd $(dirname $0)把工作目录切到脚本自身所在目录这样无论从哪里调用脚本相对路径都稳定。cd有几个写法要记牢cd不带参数回用户主目录cd -跳回上一个目录cd ..上一级。cd -在/etc和/var/log之间来回切的时候特别爽。注意路径里有空格时必须加引号cd my folder会被当成两个参数正确写法是cd my folder或者用反斜杠转义cd my\ folder。ls的参数组合值得单独说ls -l看详细信息ls -a显示隐藏文件ls -h让文件大小人类可读ls -t按修改时间排序ls -S按大小排序。我日常用的最多的组合是ls -lht列出详细信息、大小可读、最新的在最上面。要找特定类型文件ls *.log配合通配符或者直接上find。Ubuntu 20.04 LTS默认就带tree吗不带得sudo apt install tree装上之后tree -L 2看两层目录结构比ls直观太多。3.2 增删改mkdir、cp、mv、rm 的边界mkdir -p a/b/c里的-p能一次创建多层目录且目录已存在时不报错写脚本时必加。不加-p的话父目录不存在就报错退出。cp和mv最关键的是-r和-i。cp -r递归复制目录-i在覆盖前询问-a是归档模式等于-dpR保留权限、时间戳、软链接。备份目录时我都用cp -a因为默认的cp会把软链接解引用成真实文件权限也可能变。mv没有-r它本来就能移动目录这个和cp不一样很多人会习惯性加-r然后发现报“无效选项”。rm是最需要敬畏的命令。rm -rf能删目录-f强制不提示。永远不要在变量拼出来的路径上直接跑rm -rf因为你不知道变量为空时会变成什么。我见过最惊险的一次是脚本里写了rm -rf $TARGET_DIR/结果TARGET_DIR没赋值命令变成rm -rf /幸亏当时不是root大部分文件删不掉。稳妥的写法是先用set -u让未定义变量报错或者删之前echo一遍确认路径或者用${TARGET_DIR:?变量未设置}这种语法做保护。提示想给自己留条后路可以给rm加个别名alias rmrm -i但脚本里别依赖别名脚本执行的是真实的rm别名在非交互式Shell里默认不生效。3.3 看文件内容cat、less、head、tail 怎么选cat适合看短文件长文件直接糊满屏幕。它的真正用途是拼接和重定向比如cat a.txt b.txt c.txt。看长文件用less进入后/搜索、G到末尾、g到开头、q退出比more强在能上下翻。head -n 20 file看前20行tail -n 20 file看后20行。tail -f是排查日志的利器它持续输出文件新增内容。但tail -f在日志轮转log rotation后会失效因为inode变了得用tail -F大写它会在文件被删除重建后重新打开。这个区别我在一次线上排查时吃过亏日志被切割后tail -f就再也不更新了盯着屏幕等半天换成-F立刻正常。大文件里想快速统计wc -l file数行数wc -c数字节。有个小技巧grep -c ERROR app.log直接统计匹配行数比grep ... | wc -l少开一个进程在脚本里更高效。4. 重定向、管道与文本处理核心工具4.1 三个标准流与重定向的本质每个进程启动时系统都会给它打开三个文件描述符0是标准输入1是标准输出2是标准错误。默认情况下0连键盘1和2连终端。重定向的本质就是改变这些描述符指向的文件。把1指向某个文件会清空原内容是追加2重定向错误输出21把错误输出并到标准输出。这里最容易踩的坑是顺序问题。command file 21和command 21 file结果完全不同。前者的意思是“先把1指向file再把2指向1现在指的地方也就是file”两者都进文件后者是“先把2指向1现在指的地方终端再把1指向file”结果错误依然打在屏幕上、正常输出进文件。这个细节面试常考我自己也写错过一次把报错信息全漏掉了排查问题时一脸懵。还有个高频用法是丢弃输出command /dev/null 21。定时任务里常用避免没用的输出占满日志。反过来如果你只关心错误就command 21 /dev/null不过更清晰的写法是command 21 1/dev/null或者干脆只留stderr。4.2 管道用组合代替大而全管道的符号是|作用是把左边命令的标准输出接到右边命令的标准输入。它的价值在于让每个命令只做一件事通过组合完成复杂任务。比如统计当前目录下所有.log文件里出现“timeout”的次数grep -r timeout *.log | wc -l再比如找出占用内存最多的前5个进程ps aux --sort-%mem | head -n 6ps aux --sort-%mem按内存降序排head -n 6取前6行第一行是表头后面5个是进程。这种写法比去图形界面的任务管理器点来点去快得多尤其在SSH连的服务器上。管道的性能提醒中间环节太多、数据量太大时会慢因为每个|都要创建进程和缓冲区。如果只是简单过滤awk一个命令能搞定的就别串三个grep。但在脚本可读性和性能之间我通常优先可读性除非数据量到几百万行。4.3 grep、sed、awk最小可用集grep是文本搜索grep -i忽略大小写grep -v反向匹配排除grep -r递归目录grep -n显示行号grep -E用扩展正则。日常排查日志的黄金组合是grep -n 关键字 file.log带行号方便定位。sed是流编辑器最常用的是替换sed s/old/new/g file。加-i直接改文件Ubuntu下需要-i后不跟后缀比macOS省事。我只在能事后回滚的场合用sed -i比如先cp file file.bak。有个坑替换内容里含斜杠时要换分隔符sed s|/usr/local|/opt|g用竖线做分隔符更清爽。awk是文本处理的瑞士军刀语法是awk {动作} 文件。最实用的场景是按列取值和条件过滤awk -F: {print $1, $3} /etc/passwd # 以冒号分隔打印第1、3列 awk $3 1000 {print $1} /etc/passwd # 打印UID大于1000的用户名 awk {sum $1} END {print sum} nums.txt # 求和-F指定分隔符$0是整行$1是第一列NF是当前行的列数NR是行号。这三个变量用熟了很多统计任务两行就写完。我不指望你一次记住awk的所有能力但“按列取值”和“条件过滤求和”这两个套路覆盖了八成的日常需求。5. 权限、用户与软件安装的实战要点5.1 chmod 数字背后的算法chmod 755 file里的三个数字每一位是一个八进制数代表一组权限顺序是属主、属组、其他人。每一位由读4、写2、执行1相加得到。7421读写执行541读和执行642读写0无权限。所以755就是属主全权限、属组读执行、其他人读执行。为什么是八进制因为三位二进制恰好能表示0到7一位八进制数字正好对应一组权限位。这个设计现在看很优雅理解了这个644属主读写、其他人只读、600只有属主可读写私钥的标准权限就不用死记了。SSH私钥权限不对会直接报“Permissions too open”原因就是600保证了只有你自己能读别人读了就算泄露系统强制要求。改属主属组用chown user:group file递归加-R。chmod -R 777这种操作要坚决避免它把所有文件都变成任何人可读写执行安全上等于没锁门。我见过有人为了省事给整个web目录777结果被植入脚本。真实场景里目录一般755文件一般644需要写权限的目录比如上传目录才单独设775或给特定用户。5.2 新建用户与sudo的合理使用创建一个新用户并赋予sudo权限标准流程是sudo adduser zhangsan # 交互式创建自动建家目录 sudo usermod -aG sudo zhangsan # 加入sudo组adduser是Ubuntu友好的封装脚本useradd是底层命令用useradd得自己加-m建家目录、自己设密码容易漏。usermod -aG里的-a是append不加-a会把你从所有其他组里踢出去只剩sudo组这个坑很隐蔽加之前一定带上-a。为什么不用root日常操作因为root能删任何东西、改任何系统文件误操作的代价是灾难性的。用普通用户加sudo前缀等于给危险操作加了一道确认。另外很多程序比如某些数据库、构建工具拒绝以root运行逼着你用普通用户。sudo默认记忆密码15分钟sudo -k可以手动让它立刻失效公共机器上用完记得敲一下。5.3 apt 增删改查与几个实用参数Ubuntu 20.04 LTS用apt管理软件包日常四个动作更新索引sudo apt update升级已装包sudo apt upgrade安装sudo apt install 包名卸载sudo apt remove 包名。apt update只更新“有哪些包、什么版本”的列表不实际升级apt upgrade才真正下载安装。这俩经常被新手搞混以为敲了update就升级完了。加-y自动确认适合脚本apt install --no-install-recommends只装必需的不装推荐包能少装一堆用不上的东西容器镜像里特别有用。想清理不用的依赖用sudo apt autoremove想清缓存用sudo apt clean。查某个包信息用apt show 包名查文件属于哪个包用dpkg -S /path/to/file这个在做故障排查时很有用——发现一个不认识的二进制想确认是谁装的。注意生产环境的服务器上apt upgrade前先看它要升级哪些包尤其是内核和关键服务升完可能得重启。我一般先apt list --upgradable看一遍确认没问题再动手。6. 写第一个Shell脚本从能跑到跑好6.1 脚本结构与执行方式的选择一个最小的脚本长这样#!/bin/bash # 这就是个注释说明脚本用途 echo Hello, Ubuntu 20.04保存为hello.sh后执行方式有两种。一是bash hello.sh显式用Bash解释不要求文件有执行权限二是chmod x hello.sh后./hello.sh靠shebang决定用哪个解释器。两种都行但要测试脚本的通用性就用第二种因为别人拿到你的脚本正是这么跑的。./不能省当前目录通常不在PATH里直接敲hello.sh会提示命令找不到。我写脚本的习惯结构是shebang、注释头用途、作者、日期、用法、set -euo pipefail、变量定义、函数、主逻辑。set -euo pipefail这三件套值得展开-e是命令出错就退出-u是用到未定义变量就报错-o pipefail是管道中任何一个命令失败整体就算失败。不带这三个脚本里某条命令悄悄失败会继续往下跑最后得出一个错误结果比直接报错还难查。6.2 变量、引号以及 ${} 和 $() 的区别变量赋值namezhangsan等号两边不能有空格name zhangsan会被当成执行name命令。取值用$name或${name}。什么时候必须用花括号当变量名后面紧跟其他字符时${name}abc会取name变量再拼abc而$nameabc会去找一个叫nameabc的变量。这个规则写多了会形成直觉但初学阶段建议一律用${}省心。引号的区别是新手最大的坎。双引号$var会展开变量单引号$var原样输出。变量在双引号里建议永远加引号因为不加的话变量值里有空格或通配符时会被Shell再次拆分和展开。经典翻车是rm $file如果file的值是a b.txt会变成删除a和b.txt两个文件。正确写法rm $file。${}是变量替换$()是命令替换——把括号里命令的输出结果作为值。now$(date %F)把日期赋给变量files$(ls)把文件列表赋给变量。还有一对反引号date也是命令替换但不推荐因为它嵌套时转义麻烦$()更清晰。热词里总有人问${}和$()的区别记住一句话${}换的是变量的值$()换的是命令的输出。变量还有几个常用修饰${var:-默认值}在变量为空或未定义时用默认值代替注意不会修改原变量${var:默认值}会在为空时赋值给原变量${#var}取长度${var%.txt}从尾部删掉最短匹配的.txt。这几个在处理文件名和参数时非常实用${#var}统计字符串长度就一行。6.3 条件判断if 与 test 的正确姿势基础条件是if [ 条件 ]; then ... fi注意[后面、]前面都要有空格因为[其实是个命令。字符串比较用注意不是虽然[ ]里也能用数值比较用-eq、-ne、-gt、-lt。文件判断-f是普通文件-d是目录-e是存在-r可读-w可写-x可执行。if [ -f /etc/hosts ]; then echo 文件存在 fi if [ $count -gt 10 ]; then echo 超过10 fi我强烈建议用[[ ]]代替[ ]。[[ ]]是Bash的关键字支持、||直接连接条件[ ]里得用-a、-o支持正则~支持模式匹配而且不做单词拆分变量不加引号也相对安全。唯一代价是失去POSIX兼容性但前面说了脚本就是给Bash写的这个代价可以忽略。if [[ $name zhang* -d /home/$name ]]; then echo 匹配 fi6.4 for 循环几种写法覆盖不同场景热词里shell脚本for循环问的人多这里把最常见的几种列全。最基本的是遍历列表for fruit in apple banana orange; do echo $fruit done遍历文件最忌讳for f in $(ls)因为文件名带空格就会断。正确姿势是通配符直接展开for f in *.txt; do echo 处理 $f done或者用find配合while read这样连带换行的文件名都能处理find . -name *.log -print0 | while IFS read -r -d f; do echo $f done-print0配read -d 用空字符分隔是有奇葩文件名时的稳妥做法。C语言风格的循环适合需要计数for ((i1; i5; i)); do echo 第 $i 次 done还有遍历参数的for arg in $注意要加引号$会把每个参数当成独立一项且保留引号内空格$不加引号会拆分。批量重命名是for循环的经典场景把当前目录下的.jpeg全改成.jpgfor f in *.jpeg; do [ -e $f ] || continue # 没有匹配文件时直接跳过 mv -- $f ${f%.jpeg}.jpg done${f%.jpeg}删掉尾部最短匹配的.jpeg拼上.jpg就是新名字。[ -e $f ] || continue是防御性写法防止目录里一个.jpeg都没有时通配符原样保留成字面量*.jpeg被误操作。7. 那些我真实踩过、别人也大概率会踩的坑7.1 常见问题速查表现象大概率原因解决方向command not found命令没装或不在PATH里apt install或用绝对路径脚本报Permission denied没有执行权限chmod x 脚本变量值里有空格被拆开取值没加双引号写成$var脚本第一行不生效shebang写了sh但用了Bash语法改成#!/bin/bash中文文件名乱码解压时的编码不匹配用unzip -O CP936或先转编码定时任务里命令跑不了cron环境PATH和交互式不同用绝对路径、显式source环境source 脚本报错但bash 脚本正常脚本依赖交互式环境变量检查脚本里的路径假设7.2 中文乱码解压和文本处理的编码问题linux 解压文件乱码是个高频问题。根本原因是Windows打包的zip通常用GBK/CP936编码文件名而Ubuntu默认用UTF-8解读于是中文全变问号或方块。处理办法有几种一是unzip -O CP936 file.zip指定源编码如果系统的unzip不支持-O有些版本编译时没带就装p7zip用7z x file.zip或者先用convmv转换编码后再解压。文本文件乱码同理用iconv -f GBK -t UTF-8 old.txt -o new.txt转换。file -i file.txt能看文件当前编码file命令在排查“为什么打开是乱码”时是第一步。我一般的处理顺序是先file确认编码再iconv或convmv转换改完再用grep验证中文能正常匹配。提示写脚本处理中文路径时确保脚本文件本身也是UTF-8保存的并且执行环境的LANG变量设置成en_US.UTF-8或zh_CN.UTF-8。locale命令能看当前设置。环境变量信息很乱时export LANGen_US.UTF-8先临时顶上。7.3 几个藏在细节里的坑cd失败但脚本继续跑。这就是没加set -e的后果。脚本里cd /some/dir如果目录不存在脚本会留在原地继续执行后面的命令导致删错文件。加上set -e或者写成cd /some/dir || exit 1。变量存命令输出后带着换行。files$(ls)的结果末尾会有换行后面拼接时可能出问题用files$(ls | tr -d \n)清掉或者干脆改成遍历处理别存成变量。用在[ ]里。[ ]里字符串比较标准写法是虽然Bash里也能用但换到dash或别的POSIX Shell就报错。要兼容就用要省心就用[[ ]]。管道里while循环的变量出不来。这个特别隐蔽cat file | while read line; do count$((count1)); done; echo $count结果count还是0。原因是管道会为右侧命令创建子Shell循环里的变量修改只发生在子Shell里不影响父Shell。解决方法是用输入重定向代替管道while read line; do ...; done file或者用进程替换done (cat file)。这个坑我在写统计脚本时踩过数据明明处理了计数器却是0。rm -rf $dir/里引号位置放错。如果dir为空且引号写在外面$dir/会变成删除根目录下的东西非常危险。写删除逻辑前先echo确认或者用rm -rf ${dir:?}变量为空时直接报错退出。8. 调试技巧与练手思路脚本一跑就错先别急着逐行看。我最先上的是bash -x script.sh它会打印每条命令执行前的展开结果前面带号。这样一眼就能看出变量到底被替换成了什么、哪一条命令返回了非零。输出太多就配合set -x和set x在关键段打开关闭只调试可疑部分。shellcheck是个神器能静态检查脚本里的常见错误sudo apt install shellcheck装上然后shellcheck script.sh。它会指出引号没加、变量没用上、条件判断写法可疑等一系列问题还会给出修改建议。我现在的习惯是脚本写完先过一遍shellcheck能省掉大量低级调试。它对“未加引号的变量”这种坑特别敏感。想练手的话我建议从这三个小任务开始由浅入深。第一写一个脚本接受一个目录参数统计里面各类文件按扩展名的数量并排序输出。第二写一个日志清理脚本删除指定目录下7天前的.log文件并把删除记录写进一个日志。第三写一个用户检查脚本读取/etc/passwd列出所有UID大于1000的普通用户及其家目录并判断家目录是否存在。这三个做完变量、循环、条件、文本处理、文件判断就都过了一遍。shell的shift命令也值得单独提一句处理脚本参数时很有用。$1、$2是位置参数$#是参数个数shift把参数列表左移一位$2变$1常用于参数个数不定、需要循环处理的场景while [ $# -gt 0 ]; do echo 处理参数$1 shift done配合case可以写出带选项的命令行参数解析比如-v开启详细输出、-f指定文件。这是让脚本从“能用”到“好用”的关键一步。写到这里我自己的体会是Shell命令和语法本身不难难的是那些不写在文档里、只在踩坑时才暴露的边界情况——空格、引号、空变量、子Shell、编码。这些东西没人能一次全记住最有效的办法是每踩一次就把它记进自己的“坑本”下次写脚本时对着检查一遍。Ubuntu 20.04 LTS这个环境足够稳定你可以放开了在上面折腾大不了重装虚拟机犯错成本低。真正练出来的手感都是从一条条报错信息里磨出来的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门