打开Linux终端,看到那行闪烁的光标,你能想到什么?对刚接触命令行的朋友来说,这往往意味着"又要背命令了"。但在我眼里,Shell更像是一个藏在键盘后面的翻译官——你敲下一行ls -l,它翻译给内核,内核再把文件列表交回来,最后由它格式化呈现在屏幕上。这篇博文的目标很简单:围绕Shell的基本脉络,把命令基础、脚本入门、常见坑、面试考点一次性串清楚。不管你是零基础初学者,还是已经会写几行脚本但总被小问题卡住的人,读完后都能把这些内容真正用到生产环境里。
1. Shell不是黑窗口,它是藏在命令行背后的翻译官
很多教程喜欢一上来就教你ls、cd、pwd,但从来不解释Shell到底是什么。结果就是,你确实会敲几个命令了,但遇到bash: xxx: command not found、Permission denied、脚本跑着跑着变量丢了,依然一头雾水。所以我打算先把底层逻辑讲明白,再进入命令和脚本的部分。
1.1 从"敲命令"到"写程序":Shell到底是什么
Shell的英文原意是"外壳"。计算机系统里,内核(Kernel)负责管理CPU、内存、磁盘这些硬件资源,但它没法听懂人类的自然语言。Shell就是包在内核外面的一层程序,专门负责接收你输入的命令,解释成内核能理解的任务,再执行它、把结果返回给你。
可以类比成去餐厅吃饭:你是用户,后厨是内核,Shell就是那个拿着菜单和你说"请问需要什么"的服务员。你说"帮我看看当前目录有什么文件",Shell就跑去问后厨,后端处理完,它再把菜端到你面前。所以Shell本身只是一个翻译程序,这也是为什么你在Linux里能见到好几种Shell——它们都能翻译,只是说话的方式略有不同。
更重要的是,Shell不只是命令解释器,它还是一门完整的编程语言。变量、数组、判断、循环、函数,它全都支持。这意味着你可以把一堆命令组合起来,写成一个脚本文件,让机器自动执行。我当年就是从"每天重复敲同样的备份命令"开始,慢慢把Shell从一个命令工具用成了自己最顺手的自动化工具。可以这么说:不会脚本的Shell用法,在运维和开发日常里只能算玩了个皮毛。
1.2 为什么Linux里会有那么多Shell
你在终端里常用的可能是bash,但Linux系统里其实住着不少Shell:/bin/sh、bash、zsh、dash、fish、csh、ksh……它们各自有不同的语法特性和适用场景。
bash(Bourne Again Shell):Linux发行版里最常见的默认Shell,语法最通用,我写的脚本90%都指定它。sh(Bourne Shell):Unix界的"老前辈",现在很多系统里/bin/sh其实是别的Shell的软链接。比如Ubuntu里的/bin/sh指向dash,脚本第一行写#!/bin/sh时用的其实是dash来跑。zsh:比bash更友好的交互式Shell,有自动补全、插件体系,很多开发者拿来当日常终端,但写脚本时我依然推荐bash。fish:开箱即用、语法更人性化,但很多bash语法在fish里不兼容,做生产环境脚本时不太建议。dash:极简、执行速度快,常用于系统初始化脚本,但缺少bash的数组、[[ ]]等特性。
这就能解释一个很多人踩过的坑:在Ubuntu里写了个#!/bin/sh脚本,用到了source或数组,结果报错。因为Ubuntu的/bin/sh是dash,不是bash。如果脚本必须有bash特性,第一行就用#!/bin/bash。
另外,"Shell"这个叫法在不同语境下还有别的含义。手机上常说的"adb shell"是Android系统里的命令环境;一些设备厂商的固件界面也叫Shell;甚至GNOME桌面有个美化插件叫Blur My Shell,它给桌面弹窗加毛玻璃效果。这些都不是本篇文章的主体,但你应该知道:看到"Shell"这个词时,先想清楚它指的是哪个环境。
1.3 用Shell之前必须弄明白的三件事
第一,命令本身也是程序。你以为ls、df、ps是什么内置魔法?它们其实是放在/bin、/usr/bin目录下的可执行文件。你执行ls时,Shell会按PATH环境变量的顺序去这些目录里找同名文件。找不到就会报command not found。所以排查命令行问题,第一件事就是看PATH。
第二,执行权限是"能不能运行"的开关。Windows下双击exe就能跑,Linux里一个脚本即使写得正确,没有可执行权限,直接./test.sh也只会得到Permission denied。你需要chmod +x test.sh才能把它当作程序执行。刚接触Linux的人几乎都会在这卡一次。
第三,当前目录默认不在PATH里。所以执行当前目录的脚本要写成./test.sh,这个./就代表当前目录。如果你直接敲test.sh,Shell会去PATH里找,通常找不到。这不是脚本写错了,是Shell的查找规则决定的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高频Shell命令实战:cd、ls、df、mv一个不落
有了前面这些基础,我们再回到那些被问烂了的高频命令。别觉得它们简单,很多人在实际场景里用都是"能跑就行",完全不理解它背后的运行逻辑,一旦遇到异常情况就抓瞎。
2.1 cd和ls:目录跳转与文件列表的正确姿势
cd是Shell里最常用的命令,没有之一。但很多人只会cd xxx和cd ..,其实有几个非常省时间的姿势:
bash复制cd ~ # 回到家目录
cd - # 回到上一个目录,比如你先cd /etc,再cd /var/log,执行cd -就回/etc
cd ../.. # 向上跳两级
我自己用得最频繁的是cd -,它在两个工作目录之间来回切换时特别好用。如果你把它写进脚本里,注意行为和在交互终端里基本一致,但千万别以为cd能改变父进程的目录——这就是下面坑清单里要说的"cd不生效"问题。
ls也不只是列文件名。热搜里有个词叫"ls分屏显示",说白了就是让输出别滚得一屏幕都是。常见做法是:
bash复制ls -l | less # 用less翻页,按q退出
ls --color=auto # 带颜色区分目录、文件、可执行文件
ls -lS # 按文件大小排序,方便找大文件
ls -lt # 按修改时间排序
在less里还可以按Shift+G直接到最后、按/搜索关键词。如果是处理大量日志文件,我通常还会配合ls -lt | head来看最新改动的文件,这个组合在排障时非常实用。
2.2 用mv给文件重命名:从单个文件到批量处理
Linux里给文件重命名,没有Windows那个"重命名"按钮,标准做法是mv 旧名 新名。很多人不理解为什么"移动"和"重命名"是同一个命令,其实在Linux的文件系统里,重命名就是"把文件在原地移动一个位置",关键操作就是改路径。你把它从a.txt改成b.txt,本质上和把它从/dir1移动到/dir2是同一类动作。
单文件重命名很简单:
bash复制mv old_name.txt new_name.txt
批量重命名就要动用Shell的循环能力了。比如我想把当前目录下所有.txt后缀改成.md:
bash复制for file in *.txt; do
mv "$file" "${file%.txt}.md"
done
这里的${file%.txt}是Shell的"掐尾巴"语法,把变量末尾的.txt去掉,再拼上.md。注意循环变量一定要加双引号,否则文件名里有空格时会直接炸掉。这是我踩过无数次的坑,后面会专门说。
如果你需要更复杂的批量规则,比如把文件名里的空格替换成下划线,可以使用rename命令,也可以直接用for循环加tr或sed。核心不是死记命令,而是知道"Shell可以把任意命令组合起来处理一批文件"这个思路。
2.3 df和du:磁盘空间与目录体积的排查手段
df是查看磁盘分区使用情况的命令,最常用的是:
bash复制df -h
-h表示human-readable,自动把字节换算成G、M这种单位。服务器磁盘满了这种事,基本靠它发现。但很多人不知道df还有个隐藏考点:df -i,查看的是inode使用率。
inode可以简单理解成"文件索引号",每个文件或目录都要占一个inode。有时候你执行df -h发现磁盘还剩几十G,但就是创建不了新文件,系统提示"No space left on device",这时几乎可以断定是inode用完了,执行df -i确认,然后去删大量的小文件(比如缓存目录、邮件队列)。
du则负责告诉你"某个目录到底占了多少空间":
bash复制du -sh /var/log # 只看总计
du -sh /var/log/* | sort -rh | head -n 10 # 找出最大的前10个子目录
s是总结,h是友好单位。后面接sort -rh按数值反序排列,再取前10条。排查"磁盘到底被什么吃掉了"时,这一条命令比在Windows里右键属性快多了。
2.4 管道、重定向与xargs:命令之间是怎么协作的
管道符|应该是Shell里最能体现"组合思想"的符号。它的作用是把左边命令的输出,直接当作右边命令的输入。比如:
bash复制ps -ef | grep nginx
这就是"把ps的输出交给grep去筛"。很多命令单独用一般,一组合起来威力巨大。常见组合有:
bash复制history | grep git # 在历史命令里搜用过的git命令
ls -l | awk '{print $9}' # 只取文件名这一列
cat access.log | sort | uniq -c | sort -rn | head -n 10 # 统计日志里出现最多的IP
重定向则是把命令的输出写到文件里,而不是屏幕上:
bash复制echo "hello" > out.txt # 覆盖写
echo "world" >> out.txt # 追加写
command > out.log 2>&1 # 标准输出和错误信息都写入文件
2>&1的意思是"把文件描述符2(错误输出)也指向文件描述符1(标准输出)所在的地方",也就是合并输出。定时任务里你经常看到>/dev/null 2>&1,就是把脚本的所有输出都丢弃,避免cron给系统邮箱塞垃圾信。
还有一个容易被忽略的命令是xargs,它能把前一个命令的输出,当作后一个命令的参数。举个例子:
bash复制find /tmp -name "*.tmp" | xargs rm -f
它和find /tmp -name "*.tmp" -exec rm -f {} \;效果相似,但xargs会用更高效的方式批量执行。使用xargs时建议加-d '\n',或者用find -print0配合xargs -0,避免文件名里有空格时被错误拆解。
3. 从零写一个Shell脚本:变量、for循环和交互对话
命令敲多了,你一定会冒出这个念头:能不能一条命令把这些步骤全做完?这个时候,Shell脚本就派上用场了。脚本不是高深的东西,它就是把你要在终端里逐条执行的命令,按顺序写进一个文本文件里,然后让Shell替你批量执行。
3.1 脚本的骨架:shebang、执行权限和三种执行方式
一个最基础的Shell脚本长这样:
bash复制#!/bin/bash
# 这是一个最简单的脚本
echo "Hello, Shell!"
第一行#!/bin/bash叫shebang,作用是告诉系统"这个脚本要用哪个解释器来执行"。如果你想用bash就写/bin/bash,想用sh就写/bin/sh。注意:很多系统上的/bin/sh是dash的软链接,写她bang时不要想当然。
脚本写完之后,需要给它执行权限:
bash复制chmod +x test.sh
然后就可以执行了。这里有三种执行方式,区别非常大:
bash复制./test.sh # 依赖shebang指定的解释器,需要有执行权限
bash test.sh # 显式用bash执行,不需要可执行权限
source test.sh # 也可以写成 . test.sh,在当前Shell里直接执行
source和前面两种的区别在于,前两种方式会开一个新的子Shell来跑脚本,脚本里定义的变量、cd操作不会影响你当前终端;而source是在当前Shell里直接执行,效果会保留下来。你修改了~/.bashrc之后执行source ~/.bashrc让配置生效,就是这个原理。理解这个区别,对排查"脚本跑完cd怎么没生效"这类问题特别关键。
3.2 变量和位置参数,shift到底做了什么
Shell脚本里的变量不用声明类型,直接赋值就能用:
bash复制name="world"
echo "Hello, $name"
但有一个铁律:等号两边不能有空格。写name = "world",Shell会把name当成一个命令去执行,然后报command not found。这个细节让无数新手摸不着头脑,后面坑清单里细说。
脚本还能接收外部参数,分别存放在$1、$2……这些位置参数中:
bash复制#!/bin/bash
echo "脚本名: $0"
echo "第1个参数: $1"
echo "第2个参数: $2"
echo "参数个数: $#"
echo "全部参数: $@"
当你执行./test.sh a b时,会依次打印脚本名、a、b、2、a b。这里有一个经常在面试中被问到的命令shift,它的作用是把所有位置参数整体左移一位:原来的$2变成$1,原来的$3变成$2,以此类推,原来的$1则被丢弃。常用在while循环里逐个处理参数:
bash复制#!/bin/bash
while [ $# -gt 0 ]; do
echo "正在处理参数: $1"
shift
done
假设执行./test.sh a b c,输出就是依次打印a、b、c,最后整个循环退出。这种解析参数的方式,比傻傻地只读$1灵活得多。再配合case语句,你就能写出一个支持-h、-f之类选项的小工具。
参数处理中还有两个高频变量:$?表示上一条命令的退出码,0为成功,非0为失败;$$表示当前Shell的进程ID。做错误处理时,经常看到脚本里写:
bash复制if [ $? -ne 0 ]; then
echo "上一条命令执行失败,退出"
exit 1
fi
3.3 for循环实战:批量重命名那些重复的工作
Shell的for循环是我用得最多的语法。基本格式:
bash复制for 变量 in 列表; do
命令
done
比如你要把当前目录下的.txt文件全部重命名为.md,前面已经给过一个示例。再比如,你想把目录下所有文件名统一改成小写:
bash复制for file in *.JPG; do
mv "$file" "$(echo "$file" | tr 'A-Z' 'a-z')"
done
这里用了命令替换$(...),把echo "$file" | tr 'A-Z' 'a-z'的输出作为新文件名。注意外面一定要套双引号,否则文件名含空格时会出问题。
for循环还常配合数字序列使用,写法是:
bash复制for i in $(seq 1 10); do
echo "第 $i 次循环"
done
或者用bash特有的花括号展开:
bash复制for i in {1..10}; do
echo $i
done
面试里有一道几乎必考的题:求1到100的和。用Shell写就是:
bash复制sum=0
for i in {1..100}; do
sum=$((sum + i))
done
echo $sum
$((...))是Shell的算术运算语法,加、减、乘、除、取余都可以在里面做。
如果需要在循环里判断条件,就用if语句:
bash复制if [ -f "$file" ]; then
echo "$file 是一个文件"
elif [ -d "$file" ]; then
echo "$file 是一个目录"
else
echo "未知类型"
fi
注意[后面、]前面都必须有空格,因为[实际上是一个test命令,不是语法符号。-f判断文件、-d判断目录、-e判断是否存在。数值比较用-gt、-lt、-eq,字符串比较用=、!=,比如:
bash复制if [ "$count" -gt 10 ]; then
echo "数量超过10"
fi
3.4 read交互:用Shell做一段"人机对话"
热搜词里有个很可爱的提问:"如何用Shell做对话"。其实回答非常直白——用read命令读取用户输入就行。比如写一个简单的登录模拟脚本:
bash复制#!/bin/bash
read -p "请输入你的名字: " name
echo "你好, $name!欢迎来到Shell世界。"
-p用来输出提示语,后面跟的name是存的变量名。如果你还想做多轮对话,就可以搭配while循环:
bash复制#!/bin/bash
while true; do
read -p "你说点什么(输入quit退出): " input
if [ "$input" = "quit" ]; then
echo "再见!"
break
fi
echo "你刚才说的是: $input"
done
这种交互式写法适合做小工具、安装向导,比如让用户输入数据库密码、选择安装目录。稍微进阶一点,可以用read的-t参数设置超时时间,-s隐藏输入内容(比如输入密码时不显示明文):
bash复制read -s -p "请输入密码: " password
-s特别适合写需要读密码的脚本,否则明文会直接出现在屏幕上或者被记录到history里,有安全隐患。
4. Shell里常见的坑,每一个都是验证过的教训
Shell脚本入门快,但"坑"是真的多。我见过太多人栽在这些小地方,包括我自己,当年也被这些坑折磨到怀疑人生。把它们集中写出来,就是希望你们少走这些弯路。
4.1 变量赋值等号两侧不能有空格
这是Shell课程里讲过一万遍、但每天依然有人在踩的坑。写:
bash复制name = "world"
后果是Shell把name当成命令去执行,于是报:
bash复制name: command not found
原因要从Shell的解析规则说:空格在Shell里是分隔符,name = "world"会被拆成三个词:name、=、world,Shell自然把第一个词当作命令名。而name="world"是一个完整的单词,等号是词的一部分,Shell才识别成赋值。所以记住:赋值语句里等号两边坚决不能有空格。
4.2 管道里的子Shell让变量静默消失
看下面的脚本:
bash复制echo "hello world" | read line
echo "$line"
你预期输出hello world,实际却输出一行空行。问题出在管道符:管道左右两侧的命令,会被Shell放进子Shell里执行。read line在子Shell里读到了内容,但变量line只在子Shell里存着,一旦子Shell结束,变量就跟着消失了,外层Shell里根本没有line这个变量。
解决办法之一是使用进程替换,让read在当前Shell环境里执行:
bash复制read line < <(echo "hello world")
echo "$line"
另一个场景是循环里处理生成的变量,循环结束后变量丢失。要避免所有这类问题,最简单的原则是:如果能用for file in *.txt这种形式处理,就不要用ls | while read file这种管道形式。管道写法虽然更接近自然语言,但副作用就是子Shell环境的隔离。
4.3 文件名里的空格,以及循环中的"文件分词"陷阱
Linux是允许文件名带空格的,但Shell默认以空格作为分隔符,这里就产生了剧烈的矛盾。
最常见的问题是:
bash复制for file in $(ls *.txt); do
echo "$file"
done
如果当前目录下有my notes.txt,$(ls *.txt)输出的字符串里是my和notes.txt两个词,for循环就会把它们当成两个文件分别处理,最后你会在循环里得到my和notes.txt两个不存在的文件名。
正确做法是直接用通配符,让Shell来做匹配而不是依赖ls输出:
bash复制for file in *.txt; do
echo "$file"
done
通配符展开的结果会保留空格,所以my notes.txt会被当成一个整体。另一个关键点:所有引用变量的地方尽量加双引号。mv "$file"和mv $file在文件名没有空格时确实一样,但一旦遇到空格就天差地别。把双引号养成习惯,是Shell编程卫生的底线。
4.4 "wq: command not found"不是Shell的错
热搜里有一条特别经典的错误信息:
bash复制[no write since last change]
/bin/sh: wq: command not found
shell returned 1
这几乎可以断定是新手在Vim里没有退出插入模式,直接在键盘上敲了:wq,然后系统把wq当成一个命令去执行。简单说,你在Vim里敲i进入插入模式后,再敲wq会被当成文本输入到文件里;按下Esc退出插入模式后,wq才有机会被Vim解释为保存并退出。
正确操作是:按一下Esc,确保左下角没有-- INSERT --字样,然后再输入:wq回车。如果不想保存,就用:q!强制退出。
很多人把这个报错归结于"Shell又出问题了",其实Shell只是背了锅。排查思路要清晰:看到command not found,先问自己——这个命令是给Shell执行的,还是给某个编辑器/解释器执行的?搞混了上下文,问题就永远找不到根因。
4.5 单引号、双引号、反引号的区别和乱用后果
在Shell里,引号是最容易忽略、也最容易引发血案的语法点。
- 单引号
'...':里面的内容原样输出,变量不会展开。 - 双引号
"...":里面的变量会展开,但通配符不展开。 - 反引号
`...`:里面的内容会被当作命令执行,执行结果替换到当前位置。
看个例子:
bash复制name="zhangsan"
echo 'My name is $name' # 输出 My name is $name
echo "My name is $name" # 输出 My name is zhangsan
echo "今天是 `date`" # 输出 今天是 ...
现代写法建议用$(...)代替反引号,因为反引号在嵌套时很容易弄混,而$(...)可以嵌套、逻辑更清晰:
bash复制echo "脚本所在目录: $(dirname "$0")"
如果你要在双引号里再使用双引号,记得转义\",或者改用单引号包一层。这里面的细节没有一个是可以靠"猜"解决的,都需要在实际执行中反复验证。
4.6 脚本突然找不到命令、cd不生效,先查这几处
在Windows上用记事本写脚本,拿到Linux里一执行就报各种怪错,十有八九是换行符问题。Windows文本文件用\r\n换行,Linux用\n,脚本第一行的#!/bin/bash\r会被当成"解释器路径带了回车",自然找不到。执行cat -A test.sh,如果行尾出现^M$就是这个原因。解决办法是用sed -i 's/\r$//' script.sh清理,或者直接用dos2unix工具。
脚本里cd不生效,多半是执行方式的问题。如果你用./script.sh执行,脚本在子Shell里跑,子Shell里的cd当然不会影响你当前终端;如果你希望脚本里的cd能改变当前目录,就要用source script.sh。同理,脚本里明明设置了变量,执行完脚本回到终端一echo $var却是空的,也是同一个原因。
还有一类"找不到命令"很隐蔽:你用的是#!/bin/sh,但脚本里写了数组、[[ ]]、source这类bash专用语法,而系统/bin/sh是dash,执行到那行直接报语法错误。排查命令是否兼容时,先看清楚脚本第一行写的是哪个解释器。
5. 把Shell用进实际工作流:备份、定时任务和面试考点
聊完了基础操作和坑,最后来点真正能落地的内容。我不会给那种"Hello World"级别的演示,而是直接把你推进真实的工作场景:写一个能跑的备份脚本、配一个定时任务,以及看看面试和日常排障中Shell考点到底考什么。
5.1 一个能直接用的备份脚本:df告警+tar打包+SFTP推送
先把场景设定出来:你需要把/var/log/myapp目录打包备份,本地保留7天,然后推送到远程的SFTP服务器,日志记录到/var/log/backup.log。如果磁盘空间不足,跳过备份并输出告警。
bash复制#!/bin/bash
BACKUP_DIR="/var/log/myapp"
BACKUP_ROOT="/backup"
REMOTE_DIR="/remote_backup"
LOG_FILE="/var/log/backup.log"
SFTP_HOST="192.168.1.100"
SFTP_USER="backup_user"
SFTP_PASS="your_password"
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}
# 检查磁盘空间
disk_usage=$(df -h /backup | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$disk_usage" -gt 80 ]; then
log "ERROR 磁盘使用率已超过80%,停止备份"
exit 1
fi
# 生成备份文件名
today=$(date +%Y%m%d_%H%M%S)
archive="$BACKUP_ROOT/myapp_$today.tar.gz"
# 打包
tar -czf "$archive" -C /var/log myapp
if [ $? -ne 0 ]; then
log "ERROR 打包失败"
exit 1
fi
# 删除7天前的本地备份
find "$BACKUP_ROOT" -name "myapp_*.tar.gz" -mtime +7 -delete
# 通过SFTP推送到远程服务器
lftp -u "$SFTP_USER:$SFTP_PASS" "$SFTP_HOST" -e "put $archive -o $REMOTE_DIR/$(basename $archive); bye"
if [ $? -eq 0 ]; then
log "INFO 备份成功:$archive"
else
log "ERROR SFTP推送失败:$archive"
fi
这个脚本里用到了几个核心点:df -h配合tail和awk提取磁盘使用率,$?判断命令是否成功,find -mtime +7 -delete清理旧备份,tar打包,lftp完成SFTP传输。如果你的环境里没有lftp,也可以用sftp -b批处理文件模式:
bash复制sftp -b batch.txt "$SFTP_USER@$SFTP_HOST"
batch.txt里写:
bash复制put /backup/myapp_xxx.tar.gz /remote_backup/
bye
真实场景中,我一般还会加上脚本进程锁,防止前一个备份还没跑完、后一个定时任务又启动了。实现的思路是检查一个锁文件是否存在,存在就退出,不存在就创建并继续,脚本结束时删除。
5.2 crontab定时执行脚本的完整配置
脚本写好了,怎么定时跑?我们需要crontab:
bash复制crontab -e
然后在打开的编辑器里加一行:
cron复制0 2 * * * /usr/local/bin/backup.sh >> /var/log/cron_backup.log 2>&1
这段的意思是:每天凌晨2点执行一次/usr/local/bin/backup.sh,标准输出和错误输出都追加到日志文件。cron格式是五个星号,从左到右分别是:分钟、小时、日期、月份、星期。
常见写法举几个:
bash复制*/5 * * * * # 每5分钟执行一次
0 9 * * 1 # 每周一早上9点执行
30 22 1 * * # 每月1号晚上10点半执行
一个容易被忽略的问题:crontab执行时环境变量和你的交互终端不一样,PATH可能很短,脚本里用绝对路径最稳妥;如果脚本里依赖.bashrc里定义的变量,那定时任务里往往会找不到。所以脚本内部尽量用绝对路径,或者在脚本开头先source /etc/profile。
5.3 Shell面试题高频考点速查
如果把Shell面试题做个频率排行,下面这几个几乎属于"必考题",不管你是应聘运维、测试还是后端开发,都很可能遇到。
1. 如何查看磁盘空间和内存占用?
bash复制df -h # 磁盘空间
free -m # 内存,-m表示以MB显示
2. 如何查找大于100MB的文件?
bash复制find / -type f -size +100M 2>/dev/null
3. 如何统计一个日志文件的总行数、去重后行数?
bash复制wc -l access.log
sort access.log | uniq -c | sort -rn | head
4. [ ]和[[ ]]有什么区别?
[ ]实际上是一个命令,后面必须跟空格、变量最好加引号,支持的条件也不多;[[ ]]是bash的关键字,支持&&、||、正则匹配=~,而且变量即使为空也不会因为分词产生奇怪问题。写bash脚本时我推荐优先用[[ ]]。
5. 如何提取变量的一部分?
bash复制url="https://example.com/index.html"
echo "${url#https://}" # 去掉开头匹配的最短部分 => example.com/index.html
echo "${url%.html}" # 去掉结尾匹配的最短部分 => https://example.com/index
echo "${url##*/}" # 去掉开头匹配的最长部分 => index.html
这套#、##、%、%%的贪婪/非贪婪截取,面试中经常配合文件重命名出现。
6. 如何安全地处理文件名里的空格?
答案永远是:加双引号,用通配符而不是ls结果做循环。具体写法前面已经写过了。
7. 如何调试Shell脚本?
bash复制bash -x test.sh
-x会打印每条命令的实际执行过程,变量会被替换成具体值,这是我排脚本问题时的第一利器。另一个是bash -n test.sh,只检查语法不执行。
5.4 不同设备环境下的Shell:从Ubuntu到Android再到最小化系统
你平时在Ubuntu里写习惯的脚本,换个环境可能就不灵了。这让我想到几个热搜词里的场景,看着千奇百怪,本质都是"环境差异"问题。
Android的adb shell里有一个精简版的Shell,很多Linux常用命令没有,或者参数支持不全。像"安卓11模拟手机晃动"这类需求,通常是在adb shell里通过input命令或者调试接口模拟事件,而不是直接把Linux上的脚本搬过去用。所以在嵌入式、Android环境里,写脚本前先摸清可用命令清单,比背语法更重要。
UEFI环境也有一个Shell,叫作EFI Shell,用来查看和操作固件级别的文件系统。热搜里的"efi shell cannot find required map name",就是Shell启动后找不到文件系统映射,需要你先执行map -r重新扫描磁盘,或者直接用fs0:切到第一个文件系统。这类环境连ls、cd的可用参数都和Linux不同,网上搜到的Linux命令思路不能生搬硬套。
还有一些网络设备的登录界面,你输入命令后偶尔会看到shell request failed on channel 0。这个报错发生在SSH会话层,意思是SSH通道请求Shell进程失败了,常见原因包括连接数超限、服务端Shell配置问题或者网络不稳定。它和Linux里写错一个语法是完全不同的排障方向,要检查的是SSH会话而不是.sh文件。
回到日常开发,还会遇到在Ubuntu里给交叉编译环境写脚本的场景,比如用OP-TEE做可信执行环境开发时,经常需要在命令行里设置工具链的环境变量、循环遍历多个构建目标、判断编译返回码。这些本质上都是一个套路:环境变量+循环+条件判断。把基础语法吃透,不管面对的是哪种"Shell",你都能快速适应。
我个人的习惯是,每到一个新环境,先用echo $0看当前是什么Shell,再用which看核心命令是否存在,最后再看版本和路径。花30秒确认环境,比对着教程踩半小时坑要划算得多。学Shell没有什么捷径,就是多敲、多踩、多总结,把别人踩过的坑提前变成自己的经验。
