我最早接触 Shell,纯粹是被逼的。那时候天天要在一台服务器上部署项目,点鼠标点得手指头都快抽筋了,后来一个老同事看不过去,丢给我一句话:“你把这串命令粘进去就行。”从那以后,我就发现命令行这玩意儿虽然第一次见面黑漆漆的挺吓人,但熟悉之后,它比任何图形界面都来得干脆利落。特别是当你能把一大串操作装进一个 Shell 脚本里,敲一下回车就自动跑完的时候,那种掌控感是鼠标点一万次都换不来的。
这篇内容就是围绕“Shell 基础与命令行操作”来展开的,我会把概念、常用命令、脚本写法、调试技巧、跨平台玩法一次讲透。无论你是刚开始接触 Shell 的小白,还是已经会敲几条命令但想系统补一补的进阶选手,这篇都能给你一些直接能用的东西。我也会把实际干活时踩过的坑、总结出来的经验混在里面讲,争取让你看完就能上手,少走一些弯路。
1. 先把概念捋清楚:Shell、终端、命令行到底是什么关系
很多新手一上来就懵,终端窗口、命令行、Shell、Bash 这几个词反复出现,到底谁是谁?我先把这层窗户纸捅破,后面再讲命令就不会发怵。
1.1 Shell 就是个“翻译官”
Shell 的中文意思是“壳”,它包在操作系统内核外面,负责接收你敲进去的命令,然后翻译给内核去执行。你可以把内核想象成一个只会说机器语言的老板,Shell 就是那个把老板的话翻译成员工能听懂的指令的秘书。没有这个秘书,普通用户根本没法直接操作计算机。
其实图形界面也是一种“壳”,只不过它把操作变成了点击和拖拽。而命令行 Shell 是直接用文本交互,好处是精确、可重复、可批量。你点十次鼠标做的事,可能一行命令就搞定了,而且这行命令还能写进脚本里下次继续用。
1.2 常见 Shell 种类与选择
市面上常见的 Shell 有好几种,但绝大多数 Linux 发行版默认用的是 Bash(Bourne Again Shell)。除此之外还有 Zsh、Fish、Sh、Ksh、Tcsh 等。
- Bash:最通用、最保守的选择,教程和网上的资料最多,几乎所有 Linux 都预装。
- Zsh:对 Bash 做了大量增强,有自动补全、拼写修正、主题插件,macOS 现在默认就是它。但写脚本时还是建议按 Bash 的语法来,跨机器兼容性更好。
- Fish:交互体验更好,语法更友好,但不完全兼容 Bash,写脚本的话容易被环境卡住。
- Sh:比较老的 Shell,很多系统里其实是指向 Bash 的符号链接,但在某些精简系统里行为可能不太一样。
对于刚入门的人来说,直接用系统默认的 Bash 就好,等熟练了再折腾 Zsh 也不迟。我在文章里聊到的命令和脚本,默认都是以 Bash 环境为准。
1.3 打开一个能用的命令行环境
在 Linux 桌面上一般是按Ctrl + Alt + T打开终端,或者直接在应用菜单里找 Terminal。在 macOS 上是打开“终端”应用。Windows 的话,我后面单独讲,那边有自己的一套玩法。
打开之后,你会看到一个提示符,类似于:
user@hostname:~$这串东西告诉你的信息其实不少:当前登录的用户名是 user,机器名是 hostname,~表示当前目录是你的家目录,$表示你现在是普通用户权限。如果你是 root 用户,那个$会变成#。看到#的时候要小心,这时候任何命令都是最高权限,手误打错一个字就可能把系统搞坏。
2. 基础命令这块地基,打牢了后面才不慌
命令行操作层面,其实不需要记几百条命令,真正高频的就是文件、目录、查看内容、查找过滤这几类。把这些练熟了,日常干活基本就不慌了。
2.1 文件与目录操作的“肌肉记忆”命令
我总结了下面这一批最常用的命令,建议新手动不动就敲一遍,直到形成肌肉记忆:
| 命令 | 作用 | 典型用法 | 备注 |
|---|---|---|---|
pwd | 打印当前所在目录 | pwd | 迷路时第一反应就是敲它 |
cd | 切换目录 | cd /etc、cd ..、cd ~ | 不加参数直接回用户家目录 |
ls | 列出目录内容 | ls -lah | -l 长列表,-a 显示隐藏文件,-h 人性化大小 |
mkdir | 创建目录 | mkdir -p a/b/c | -p 自动创建父目录 |
touch | 创建空文件或更新时间戳 | touch test.txt | 常用在快速建文件 |
cp | 复制文件/目录 | cp -r dir1 dir2 | -r 复制目录时必带 |
mv | 移动或重命名 | mv old.txt new.txt | Linux 里重命名就是移动 |
rm | 删除文件/目录 | rm -rf dir | -r 递归,-f 强制,慎用! |
tree | 树状显示目录结构 | tree -L 2 | 需要单独安装,但太好用了 |
这里必须重点提醒一句:rm -rf是无数人的噩梦。它不会进回收站,删了就真没了,尤其在家目录或 root 权限下,一个路径拼写错误就可能把重要数据抹掉。我个人的习惯是尽量用mv把暂时不用的东西移到一个临时目录里,确认没问题再真正删除,给自己留条退路。
2.2 查看与检索文件内容,没那么难
文件内容查看这边,我最想推荐你掌握的是四个命令:
cat:适合看小文件。直接cat xxx.log,内容就哗啦啦全出来了。文件大了就不推荐,刷屏刷到你怀疑人生。less:适合看大文件。less xxx.log进去之后可以用方向键上下翻,按/搜索关键词,按q退出。文件再大也不会卡,因为它是分页加载的。tail:适合看日志尾部。最常用的是tail -f xxx.log,它可以持续跟踪文件新增内容,调试程序时跑起来,新日志一行一行往外蹦,比图形界面爽多了。grep:在文件或输出里过滤关键词。比如grep "ERROR" app.log,就能把所有包含 ERROR 的行列出来。加-i忽略大小写,加-n显示行号,加-r递归搜索目录。
这几个配合起来,排查问题的效率是成倍往上走的。比如你想看某个服务今天的报错,一条命令就搞定了:
grep "2026-01-01" /var/log/service.log | grep "ERROR" | tail -n 50这里还看到了一个高频操作——管道符|。它的作用是把左边命令的输出,当作右边命令的输入继续处理。这是命令行最核心的组合拳思路,单个命令能力有限,但串起来就威力无穷。
2.3 通配符、重定向与管道,组合拳的核心
光会单个命令不行,Shell 的爽点在于组合。我拿几个真实场景来说。
场景一:批量清理日志
假设一个目录下有一堆按日期命名的日志文件,你想把上个月的都删掉:
rm -f app-2025-11-*.log这里的星号*是通配符,表示匹配任意字符串。上面的命令会把所有以app-2025-11-开头、以.log结尾的文件全删掉。注意,这种命令执行前最好先ls app-2025-11-*.log看一下到底匹配了哪些文件,确认无误再删。
场景二:把命令输出保存到文件
ls -lah > filelist.txt这里的>是重定向符号,它的作用是把原本要打印到屏幕的内容写到文件里去。如果文件不存在会自动创建,如果存在会覆盖。要追加而不是覆盖,用>>。
场景三:统计某个关键词出现次数
grep -c "ERROR" app.log-c就是统计计数。如果想更花哨一点,统计每个状态码出现的次数,可以用cut加sort加uniq组合:
grep "HTTP" access.log | awk '{print $9}' | sort | uniq -c | sort -rn这句的意思是:过滤出包含 HTTP 的行,取第 9 列(一般是状态码),排序,统计去重后的次数,再按次数从大到小排序。你没猜错,awk是按列切分的利器,它在后面的脚本部分还会频繁出场。
3. Shell 脚本编程,才是真正的“生产力工具”
如果说命令行是手工单杀,那 Shell 脚本就是大招连招。把一系列命令按逻辑写进一个文件,加上变量、条件、循环、函数,让它自动处理复杂任务,这才是 Shell 的精髓。
3.1 第一个脚本和它的执行方式
写一个脚本非常简单。新建文件hello.sh,写入:
#!/bin/bash echo "Hello, Shell!" echo "当前时间: $(date)"第一行#!/bin/bash叫 shebang,用来告诉系统用哪个解释器来执行这个文件。echo是打印输出,$(date)是命令替换,意思是先执行括号里的 date 命令,把结果拼到字符串里。
执行脚本有三种常见方式:
# 方式一:直接指定解释器执行 bash hello.sh # 方式二:给文件加执行权限后执行 chmod +x hello.sh ./hello.sh # 方式三:用 sh 执行 sh hello.sh有人会问:为什么方式二要用./hello.sh,不能直接hello.sh?因为系统在当前目录找命令时,默认不会把.(当前目录)加进搜索路径。./就是在告诉 Shell:别去系统路径里找了,就在当前目录执行这个文件。
3.2 变量、特殊变量和 shift 命令
Shell 脚本里的变量没有类型的概念,一切按字符串处理。赋值时等号两边不能有空格,这一点是新手踩坑重灾区。
name="Tom" echo "Hello, $name"花括号的用法也要会,尤其是变量后面紧跟其他字符时,需要用花括号把变量名边界隔开:
prefix="log" echo "${prefix}_2026-01-01.txt"特殊变量是脚本里的隐藏神器:
| 变量 | 含义 |
|---|---|
$0 | 脚本本身的名字 |
$1、$2 | 第 1 个、第 2 个位置参数 |
$# | 参数个数 |
$@ | 所有参数列表 |
$? | 上一条命令的退出码,0 表示成功 |
$$ | 当前进程的 PID |
这里特别讲一下shift命令,它在处理不定量参数时非常好用。shift的作用是把位置参数整体左移一位,原来的$2变成$1,$1被丢弃。
举个例子,写一个累加脚本sum.sh:
#!/bin/bash total=0 while [ $# -gt 0 ]; do total=$((total + $1)) shift done echo "总和: $total"执行bash sum.sh 1 2 3 4 5,它会依次把参数移出来累加,最后输出 15。这就是shift核心用法的经典演示。在解析命令行参数时,shift还可以配合case做比较优雅的参数解析。
3.3 条件判断与循环,让脚本有了逻辑
条件判断最常见的格式是if [ 条件 ]; then ... fi。注意中括号两边必须有空格,这是另一个高频坑。常见的比较运算符有:
- 数字比较:
-eq(等于)、-ne(不等于)、-gt(大于)、-lt(小于)、-ge(大于等于)、-le(小于等于) - 字符串比较:
=、!=、-z(空字符串)、-n(非空字符串) - 文件判断:
-f(是普通文件)、-d(是目录)、-e(存在)、-r(可读)、-w(可写)、-x(可执行)
来看一个实际点的例子,检查一个目录是否存在,不存在就创建:
#!/bin/bash backup_dir="/data/backup" if [ ! -d "$backup_dir" ]; then mkdir -p "$backup_dir" echo "目录不存在,已创建: $backup_dir" else echo "目录已存在: $backup_dir" fi这里!表示取反,-d是判断目录是否存在。命令执行后,$?也可以直接当作条件来用,因为 if 判断的本质就是看命令的退出码。
循环这块,for循环是使用频率最高的。最经典的遍历文件用法:
#!/bin/bash for file in *.log; do echo "处理文件: $file" # 这里可以写你想对每个文件做的操作 done带步长的循环可以用seq或者花括号展开:
for i in $(seq 1 10); do echo "第 $i 次" done for i in {1..10}; do echo "第 $i 次" donewhile循环则适合“不知道循环多少次”的场景,比如实时读取文件每一行进行处理。经典写法:
while IFS= read -r line; do echo "读取到: $line" done < input.txt这里的IFS=是为了保留行首行尾的空格,-r是为了防止反斜杠被转义。如果你处理的行内容里可能有特殊字符,这两个参数建议固定带着。
3.4 函数和返回值,让代码优雅起来
脚本长了之后,重复代码多了,就得用函数来封装。定义函数有两种写法:
# 写法一 function log_info() { echo "[INFO] $(date '+%Y-%m-%d %H:%M:%S') $1" } # 写法二 log_error() { echo "[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $1" >&2 }调用函数就是直接写函数名:
log_info "开始备份" # 中间干点啥 log_info "备份完成"函数里$1、$2是函数自己的参数,跟脚本的参数互不干扰。函数返回值比较特殊,它不支持像别的语言那样直接返回字符串,返回值只能是一个整数,表示成功或失败:
check_file() { if [ -f "$1" ]; then return 0 else return 1 fi } if check_file "/etc/passwd"; then echo "文件存在" else echo "文件不存在" fi如果想从函数里返回字符串,标准做法是用echo输出,调用时用命令替换接住:
get_version() { echo "1.2.3" } version=$(get_version) echo "当前版本: $version"4. 实战:写一个批量文件重命名脚本
说了这么多基础,咱们动手做一个稍微完整点的东西。假设你有一堆照片文件,命名乱七八糟,有些带了空格,有些后缀是大写的.JPG,你现在想统一改成photo_001.jpg这种格式。这个需求很常见,非常适合用来练手。
4.1 脚本整体设计与关键点拆解
先想一下思路:遍历目录下所有.jpg和.JPG文件,去掉文件名里的空格,把后缀统一成小写,再按顺序编号。
这个需求里有两个关键点必须处理:
- 文件名含空格时必须给变量加双引号,否则 Shell 会把名字切成多段。
- 需要维护一个计数器,保证编号是连续的。
最终脚本写出来长这样:
#!/bin/bash count=1 for file in *.JPG *.jpg; do # 跳过不存在的文件匹配项 [ -e "$file" ] || continue # 去掉空格 new_name=$(echo "$file" | tr -d ' ') # 小写化,并构造带编号的新名字 base=$(echo "$new_name" | tr 'A-Z' 'a-z' | sed 's/\.jpg$//') target=$(printf "photo_%03d.jpg" "$count") # 如果目标名和原文件名不同,才执行重命名 if [ "$file" != "$target" ]; then mv "$file" "$target" echo "已重命名: $file -> $target" fi count=$((count + 1)) doneprintf "photo_%03d.jpg" "$count"会生成photo_001.jpg这样的固定宽度数字,%03d表示用 0 补齐到三位。tr -d ' '用来删除空格,tr 'A-Z' 'a-z'用来大写转小写,sed 's/\.jpg$//'用来去掉结尾的.jpg以便重新构造名字。
4.2 手册之外的经验补充
实际跑这个脚本之前,还有几个建议:
第一,先在一个临时目录里放几个测试文件跑一遍。写脚本最重要的习惯就是先在安全环境验证,再放到真实数据上执行。第二,执行前把mv换成echo先打印一遍看看逻辑对不对,确认没问题再真正执行。第三,批量处理文件时一定做好备份,至少确保操作可回退。
我还喜欢在脚本里加一个“调试开关”,比如用环境变量控制是否打印详细过程:
[ -n "$DEBUG" ] && echo "准备重命名: $file -> $target"这样在正式执行时不需要改代码,只需要在跑脚本时加个DEBUG=1前缀就能看到中间信息:
DEBUG=1 bash rename.sh这个习惯在写更复杂脚本时能救大命,因为很多脚本问题是出在“中间状态跟我想的不一样”。
4.3 扩展:结合 find 处理子目录
上面脚本只处理当前目录,如果文件分散在子目录里,就需要配合find命令。find本身就很强大,它不仅能找文件,还能对找到的每个文件执行命令:
find . -type f -name "*.jpg" -exec basename {} \;更高阶的玩法是用while read逐行接住find的输出,然后做处理:
find . -type f \( -name "*.jpg" -o -name "*.JPG" \) | while read -r file; do echo "找到文件: $file" # 这里可以做你想做的任意处理 done这里要注意,用管道接到while时,while是在子 Shell 里执行的,里面的变量修改不会带出到外部。如果你需要在循环里汇总结果,应该用进程替换while read ... done < <(find ...),或者把逻辑都写进循环体内,别依赖循环外变量。
5. 常见坑和排查技巧,都是拿时间换出来的
Shell 脚本写起来灵活,坑也不少。我把这些年踩过、帮别人排查过的高频问题整理一下,按“现象→原因→解决”的方式写,方便你遇到问题直接对着查。
5.1 高频入坑清单
| 坑现象 | 根本原因 | 解决办法 |
|---|---|---|
$name输出为空或错位 | 变量赋值等号两边有空格 | 严格写成name="value" |
中括号判断报错[: too many arguments | 变量没加引号,被拆成了多个词 | 条件里统一加引号[ "$var" = "a" ] |
| 文件名字带空格,处理时被拆开 | 没给变量加引号 | 所有引用变量的地方一律用双引号 |
| 脚本在 Linux 上正常,Windows 上跑报错 | Windows 的换行符是\r\n,Linux 只认\n | 用sed -i 's/\r$//' script.sh清理 |
执行./script.sh提示 Permission denied | 脚本没有可执行权限 | chmod +x script.sh |
for 循环遍历时*.log没有匹配项,会保留字面量 | Shell 默认把未匹配的通配符原样传给命令 | 加 `[ -e "$file" ] |
crontab里跑脚本不生效,但手动执行正常 | cron 的环境变量和交互式终端不一致 | 脚本开头export PATH=/usr/local/bin:/usr/bin:/bin |
其中换行符这个坑我特别有感触。Windows 下用记事本写的脚本,传到 Linux 上经常出现“命令未找到”的诡异报错,因为\r被当成命令的一部分了。用cat -A script.sh能看到行尾有^M$,$是行尾,^M就是多余的\r。
5.2 调试三板斧
我在调试 Shell 脚本时,基本靠三个工具:
第一是bash -n,它只检查语法,不执行命令。用来抓缺fi、少done、括号不匹配这类低级错误非常快。
bash -n script.sh如果语法没毛病,什么都不会输出。有错会直接报行号。
第二是bash -x,执行时把每一步展开打印。这个是我最常用的调试手段,它会显示每条被执行的命令以及变量的实际值,能直观看到脚本执行到哪一步、哪一步出了问题。
bash -x script.sh执行结果类似于:
+ count=1 + for file in '*.JPG' '*.jpg' + '[' -e '*.JPG' ']' + continue每行开头的+表示这是被展开后的命令,你一眼就能看出变量值是什么,逻辑卡在哪。看到continue跳过了,说明目录里根本没有匹配到的文件。
第三是加打印语句。写复杂逻辑时我习惯在关键节点加echo "[DEBUG] 当前变量值为: $var",跑完确认没问题再删掉。这个办法土,但最直接。
5.3 一个真实问题的排查示范
有一次同事写了个脚本,部署到服务器上后日志总是不输出。我过去看了一眼,脚本大概长这样:
#!/bin/bash source config.env echo "开始执行任务" # 下面是一堆业务逻辑报错信息是:
config.env: No such file or directory这个是最典型的坑之一:source相对路径依赖当前工作目录。脚本在哪个目录下执行,它就在哪个目录下找config.env。如果从别的目录敲bash /opt/script/deploy.sh,当前工作目录就不是/opt/script,自然找不到配置文件。
解决方法是用脚本所在目录拼出绝对路径:
#!/bin/bash script_dir=$(cd "$(dirname "$0")" && pwd) source "$script_dir/config.env"dirname "$0"获取脚本所在目录,cd进去后再pwd得到绝对路径。这样不管从哪里执行脚本,都能准确找到同目录下的配置文件。这种问题用bash -x一扫就能看到source那一步实际用了什么路径,定位很快。
6. 跨平台与场景延伸:不止 Linux 需要命令行
很多人以为命令行是 Linux 专属,其实 Windows、Android 调试、甚至日常办公都能用上。我把热词里提到的几个场景一并说一说。
6.1 Windows 下的命令行生态
Windows 上有几种命令行环境:传统的 CMD、现代的 PowerShell,以及 WSL(Windows Subsystem for Linux)。
CMD 比较古老,批处理语法也跟 Linux Shell 差异很大。现在新装机推荐直接用 PowerShell,它比 CMD 强太多,可以直接调 .NET,对象管道比文本管道高级不少。比如把当前目录下所有文件按大小排序:
Get-ChildItem | Sort-Object Length -Descending | Select-Object -First 10如果你主要工作在 Linux 生态,建议装个 WSL,在里面直接用熟悉的cd、ls、grep、管道这些,体验几乎和真实 Linux 一样。用 PowerShell 安装 WSL 的命令是wsl --install,不过这个东西在国内环境下有时候下载很慢,等它跑完就好,别中途关窗口。
Windows 命令行里还有一个比较常用的操作:按分隔符按行输出。比如你有一堆用逗号分隔的文本,想变成一行一个。在 PowerShell 里可以这样:
Get-Content input.txt | ForEach-Object { $_ -split "," } | Where-Object { $_ -ne "" }这个用法在处理导出的数据文件时非常实用,比用 Excel 手工拆分要高效得多。
6.2 adb shell 与 Android 调试
热词里频繁出现adb shell,这是 Android 调试桥的产物。连接手机或模拟器后,可以用它进入一个精简版 Linux Shell 环境,直接操作 Android 系统内部。
常用操作比如:
# 查看设备是否在线 adb devices # 进入设备 Shell adb shell # 在设备上执行单条命令 adb shell ls /sdcard/ adb shell ping -c 4 1.1.1.1adb shell后面可以直接跟命令,也可以不带命令直接进入交互式 Shell。进入之后,很多 Linux 命令都能用,但因为 Android 的 Shell 环境精简过,有些命令可能不存在或者参数受限。比如没有grep时,可以用toybox内置的命令替代,busybox v1.30.1 built-in shell也是这类精简环境的常见提示。
需要注意,Android 的/storage/emulated/0/是内置存储的路径,里面是模拟的 SD 卡目录,备份文件、推送文件经常会用到这个路径。常见操作:
# 从电脑推文件到手机 adb push local.txt /sdcard/Download/ # 从手机拉文件到电脑 adb pull /sdcard/Download/remote.txt . # 在设备上执行脚本 adb shell sh /sdcard/test.sh这个场景对做自动化测试、App 逆向分析、设备维护的人来说,是绕不开的基本功。
6.3 命令行在开发工具链里的地位
命令行不只是自己直接敲,很多开发工具本身就是命令行的形态,或者跟命令行结合非常紧密。
Java 项目的 Maven,最常用的就是:
mvn clean install mvn clean install -DskipTests第一句会清空旧的编译产物,重新编译、测试、打包。第二句跳过测试,适合本地快速构建。真实项目中,命令行参数还能指定 profile:
mvn clean install -Pprod这样就能按不同环境打包出不同的配置,完全是命令行掌控的节奏。
音视频处理的 FFmpeg 也是典型的命令行王者。比如你想把视频片尾裁掉,先用ffmpeg -i input.mp4查看时长,再用-ss和-to参数精准切出想要的区间:
ffmpeg -i input.mp4 -ss 00:00:00 -to 01:30:00 -c copy output.mp4这个命令的意思是:从第 0 秒到 1 小时 30 分直接复制流,不做重新编码,速度飞快。命令行参数化的好处在这里体现得淋漓尽致:同样的逻辑可以套用在批量文件上,写个 for 循环就能一键处理整个文件夹。
Windows 命令行还有一些实用小技巧,比如用 7z 加密压缩:
7z a -p"your_password" -mhe=on archive.7z folder/-p后跟密码,-mhe=on加密文件头,这样列出压缩包内容也需要密码。注意-p后面不要有空格,直接把密码跟上。
6.4 GUI 工具与命令行的关系不是对立
最后想说个观念问题:命令行和图形界面不是非此即彼。现代 Linux 桌面环境做得很好,文件管理器、文本编辑器都很成熟,日常操作完全可以用 GUI。
比如在 UOS 这样的国产 Linux 桌面版上,用 DDE 文件管理器就能直接管理文件权限、连接 FTP。右键属性,权限标签页下把“所有者”“群组”“其他”的读写执行权限勾一勾就行。对刚接触 Linux 的人来说,这种方式比记一串chmod参数要友好得多。
但我还是要说,命令行是底层能力,GUI 是表层便利。当你需要批量处理、自动化、远程服务器维护时,GUI 就不好使了。打个比方,GUI 像自动挡车,好开;命令行像手动挡,你的操作空间更大。自动挡车主不一定需要学手动挡,但想真正玩转系统、做深度运维,命令行的掌控力和表达力 GUI 替代不了。
我自己实际用下来的体会是:命令行跟 GUI 混着用效率最高。能用鼠标快速浏览的时候不排斥 GUI,一旦发现操作重复出现三次以上,就开始想怎么用命令行一次解决。这个思路,就是学习 Shell 最好的起点。