Shell 基础与基本工具

Administrator 1

Shell 基础与基本工具


目录

  1. 什么是 Shell
  2. 管道 | 与重定向 > >> < 2>&1
  3. 进阶管道:tee 与 xargs
  4. 退出码 $? 与 && / ||
  5. 变量与命令替换
  6. 正则表达式基础:grep -E
  7. 环境变量与 Shell 展开
  8. 帮助文档:man / info / --help
  9. 历史与补全:history / !! / Tab / Ctrl+R
  10. 总结与练习

一、什么是 Shell

Shell(壳) 是用户与操作系统内核(Kernel)之间的命令行解释器。用户在 Shell 里输入命令,Shell 负责解析、执行,并把结果返回给用户。

常见 Shell

  • Bash(Bourne Again Shell):Linux 默认 shell,本博客主角。
  • sh(Bourne Shell):最早的经典 shell,Bash 兼容它。
  • zshfish:功能更强的现代 shell。

查看当前 shell:

echo $SHELL        # 查看当前用户的默认 shell
echo $0            # 查看当前正在运行的 shell

参考:Bash 官方手册菜鸟教程 - Shell 教程


二、管道 | 与重定向 > >> < 2>&1

2.1 三个标准数据流(stdin / stdout / stderr)

每个 Linux 进程运行时都有三个标准文件描述符(File Descriptor,FD):

文件描述符 名称 编号 默认指向 含义
stdin 标准输入 0 键盘 程序读取的输入来源
stdout 标准输出 1 屏幕 程序正常的输出结果
stderr 标准错误 2 屏幕 程序的错误/诊断信息

为什么要区分? 因为正常结果和错误信息要能分开处理。例如 ls /etc /no-such-dir 会同时产生正常输出(/etc 内容,走 stdout)和错误提示(no such dir,走 stderr)。

2.2 重定向(Redirection)

重定向就是改变数据流的方向,把输出写到文件、从文件读取输入等。

# 标准输出重定向(> 覆盖,>> 追加)
ls > list.txt          # 把 ls 的正常输出写入 list.txt(覆盖原内容)
ls >> list.txt         # 追加到 list.txt 末尾(不覆盖)

# 标准错误重定向
ls /no-such 2> err.txt      # 把错误信息写入 err.txt
ls /no-such 2>> err.txt     # 错误信息追加到 err.txt

# 标准输入重定向(< 从文件读)
sort < names.txt            # 从 names.txt 读取内容并排序
wc -l < file.txt            # 统计文件行数

# ★ 同时重定向 stdout 和 stderr
cmd > out.txt 2> err.txt       # stdout 到 out.txt,stderr 到 err.txt(分开)
cmd > all.txt 2>&1             # stdout 和 stderr 都写入 all.txt
cmd >> all.txt 2>&1            # 都追加到 all.txt
cmd &> all.txt                 # 简化写法,等价于 2>&1(都写入)
cmd 2>&1 | less                # 把 stdout 和 stderr 一起交给管道

2>&1 详解

  • 数字 2 表示 stderr,1 表示 stdout,>& 表示"重定向到"。
  • 2>&1 的意思是:把标准错误(2)重定向到标准输出(1)当前所指向的地方
  • 顺序很重要:> file 2>&1 先让 stdout 指向文件,再把 stderr 也指向同一个文件;而 2>&1 > file 顺序反了,stderr 会指向屏幕而不是文件。
  • &> 是 Bash 提供的简写,cmd &> file 等价于 cmd > file 2>&1

参考:Bash 手册 - Redirection

2.3 管道(Pipe)|

管道 |前一个命令的 stdout 作为后一个命令的 stdin,实现命令间的数据流动,无需中间文件。

cat file.txt | sort              # 显示并排序
cat file.txt | sort | uniq       # 排序并去重(多重管道)
ls -l | grep '^d'                # 只显示目录(以 d 开头的行)
ps aux | grep httpd              # 从进程列表里找 httpd
dmesg | tail -20                 # 只查看系统启动信息最后 20 行
cat /var/log/messages | grep -i error | head -20   # 综合运用

管道的特点

  • 管道只传递 stdout,stderr 不会进管道(除非用 2>&1 合并,见上文)。
  • 管道是流式的:前一命令持续输出,后一命令就开始处理,不必等全部完成。
  • 管道中的每个命令在独立的子进程中运行。

经典组合示例

# 查看磁盘使用量最大的几个目录/文件
du -sh /var/* 2>/dev/null | sort -rh | head -5

# 查看当前系统监听端口(配合 ss 或 netstat)
ss -tlnp | head -20

# 统计日志中某关键词出现次数
grep -c "error" /var/log/messages
grep "error" /var/log/messages | wc -l

三、进阶管道:tee 与 xargs

3.1 tee —— 边看边存(既显示又写文件)

tee 把 stdin 的数据同时写到 stdout(屏幕)和指定的文件中,就像一个"三通"。适合"既要实时看,又要保存"的场景。

command | tee output.txt            # 屏幕显示 + 写入 output.txt
command | tee -a output.txt         # -a 追加而不是覆盖
command | tee file1 file2           # 同时写入多个文件
command | tee >(grep error)         # 高级:分叉处理(进程替换)
sudo tee /etc/hosts < newhosts      # 常用:以 root 权限写入文件

经典用法

# 安装软件时既看输出又存日志
sudo dnf install -y httpd 2>&1 | tee install.log

# 备份:把命令输出存文件,同时仍能看到
df -h | tee disk-usage.txt

# 以 root 权限写入系统文件(echo 单独无法写受保护文件)
echo "127.0.0.1 myhost" | sudo tee -a /etc/hosts

3.2 xargs —— 把输出当作参数

xargsstdin 读取数据,并把它们作为参数传给另一个命令。它解决了"管道只能传数据、不能传参数"的问题。

管道 vs xargs 的区别

  • 管道 |:把前一个命令的输出内容作为后一个命令的输入数据
  • xargs:把前一个命令的输出内容作为后一个命令的命令行参数
# 基本用法:把 find 的结果作为 rm 的参数
find . -name "*.tmp" | xargs rm

# -n 2:每批只传 2 个参数给命令
echo "1 2 3 4" | xargs -n 2 echo

# -I {}:把每项替换到命令中的指定位置({} 是占位符)
ls *.txt | xargs -I {} cp {} /backup/

# -0:以空字符分隔(配合 find -print0,处理含空格/换行的文件名)
find . -name "*.log" -print0 | xargs -0 rm

# 并行执行(-P 指定并发数)
cat urls.txt | xargs -P 4 -I {} curl -O {}

# 显示将要执行的命令(-t,调试用)
echo "1 2 3" | xargs -t echo

注意事项

  • 文件名含空格/特殊字符时,推荐用 -0 配合 find -print0find -exec,避免参数被错误拆分。
  • 现代 Linux 推荐用 find -deletefind -exec 替代部分 xargs 场景,更安全。

参考:GNU findutils - xargs 手册


四、退出码 $? 与 && / ||

4.1 退出码(Exit Status)

每个命令执行完都会返回一个退出码(0~255 的整数),表示执行结果:

  • 0:命令成功执行。
  • 非 0(1、2、127 等):命令失败或异常。
ls /etc
echo $?            # 输出 0,表示上一条命令成功
ls /no-such-dir
echo $?            # 输出 2,表示出错(常见 1=一般错误,2=用法/参数错误,127=命令找不到)
  • $? 保存的是上一条命令的退出码,用一次后会被下一条命令覆盖。
  • 常见退出码:1 一般性错误、2 用法错误、126 有权限但不能执行、127 命令未找到、130 被 Ctrl+C 中断。

4.2 条件执行:&& 和 ||

把退出码和逻辑运算结合,可以控制命令的条件执行

符号 含义 行为
&& (AND) 只有**前一条成功(退出码 0)**才执行后一条
` `
; 顺序分隔 无论成败都依次执行
# && 示例:前一个成功才执行后一个
mkdir -p /tmp/newdir && cd /tmp/newdir    # 建目录成功才进入
sudo dnf install -y httpd && systemctl start httpd

# || 示例:前一个失败才执行后一个
cd /no-such-dir || echo "目录不存在,无法进入"
which python3 || echo "python3 未安装"

# 结合使用:成功做 A,失败做 B
grep "keyword" file.txt && echo "找到关键词" || echo "未找到"

# ; 示例:不管成败都执行
cd /tmp; ls; pwd

注意&&|| 的优先级(&& 高于 ||),以及它们判断的是退出码而非"真/假值",初学时注意区分。

参考:Advanced Bash-Scripting Guide - Exit and Exit Status


五、变量与命令替换

5.1 变量:$VAR vs ${VAR}

name="rocky"
echo $name              # 输出 rocky
echo ${name}            # 输出 rocky(更严谨)

# 为什么推荐 ${VAR}?——边界清晰,避免歧义
echo $name_home          # 会被解析为变量 name_home(可能为空)
echo ${name}_home        # 输出 rocky_home(明确)
  • $VAR${VAR} 效果相同,但 ${VAR} 能在变量名后紧接其他字符时避免歧义,是更严谨的写法。
  • 赋值时等号两边不能有空格name=rocky 正确,name = rocky 错误。
  • 命名规则:以字母或 _ 开头,可含字母、数字、下划线。

变量的常见操作

name="Alice"
echo ${name:-default}        # 变量为空时用默认值
echo ${#name}                # 变量长度(5)
echo ${name:0:3}             # 截取子串(Ali)
unset name                   # 删除变量
readonly PI=3.14             # 只读变量

5.2 命令替换:$( ) 与反引号

命令替换:把命令的输出作为另一个命令/赋值的一部分。

# 两种写法:$(命令) 和 `命令`
today=$(date +%F)            # 把 date 的输出赋给 today
echo "今天日期: $today"

# 推荐 $( ),反引号在嵌套时容易出错
echo "当前路径: $(pwd)"
files=$(ls *.txt)
echo $(echo hello)           # 嵌套:$( ) 支持,反引号不行

# 典型用法
count=$(grep -c "error" /var/log/messages)
echo "错误数量: $count"
mkdir $(date +%Y%m%d)        # 创建以日期命名的目录
  • $(命令)`命令` 功能相同,但 $() 更推荐:支持嵌套、可读性更好。
  • 注意区分:命令替换 $() 是把命令的输出当结果;变量替换 $VAR 是把变量的值当结果。

参考:Bash 手册 - Command Substitution


六、正则表达式基础:grep -E

正则表达式(Regular Expression, regex) 是一种用于匹配文本模式的强大工具。grep 是 Linux 中最常用的文本搜索工具。

6.1 grep 基础用法

grep "pattern" file.txt       # 在文件中搜索包含 pattern 的行
grep "pattern" *.log           # 搜索多个文件
cat file.txt | grep "pattern"  # 配合管道
grep -i pattern file            # 忽略大小写
grep -n pattern file            # 显示行号
grep -v pattern file            # 反向匹配(不含 pattern 的行)
grep -c pattern file            # 统计匹配行数
grep -r pattern dir/            # 递归搜索目录
grep -w word file               # 整词匹配
grep -E "pattern" file          # 使用扩展正则

6.2 基本 vs 扩展正则(BRE vs ERE)

  • 基本正则(BRE):默认 grep 使用,部分元字符需转义(如 +?|() 需要 \)。
  • 扩展正则(ERE)grep -Eegrep 使用,元字符无需转义,更直观。

强烈建议用 grep -E,避免转义烦恼。

6.3 核心元字符

元字符 含义 示例
. 匹配任意单个字符(除换行) a.c 匹配 abc、a1c
* 匹配前一个字符 0 次或多次 ab*c 匹配 ac、abc、abbbc
+ 匹配前一个字符 1 次或多次(扩展) ab+c 匹配 abc,不匹配 ac
? 匹配前一个字符 0 次或 1 次(扩展) ab?c 匹配 ac、abc
^ 行首锚点 ^root 匹配以 root 开头的行
$ 行尾锚点 bash$ 匹配以 bash 结尾的行
[abc] 字符类,匹配其中任一字符 [aeiou] 匹配任一元音
[a-z] 字符范围 [0-9] 匹配任意数字
[^abc] 否定字符类 [^0-9] 匹配非数字
` ` 或(扩展)
() 分组(扩展) (ab)+ 匹配 ab、abab
{n,m} 重复次数(扩展) a{2,4} 匹配 aa~aaaa
\b 单词边界 \bcat\b 匹配独立单词 cat

注意区分:正则里的 * 是"前一个字符重复";而**通配符(glob)**里的 * 是"匹配任意多个任意字符"。两者不是一回事!

6.4 实例

# 查找以 http 开头的行
grep -E "^http" /etc/services

# 查找以 .conf 结尾的行
grep -E "\.conf$" /etc/services

# 查找以 root 开头的行(匹配 passwd 文件里的 root 用户)
grep "^root" /etc/passwd

# 匹配任意数字(如 IP 片段)
grep -E "[0-9]{1,3}\.[0-9]{1,3}" /etc/hosts

# 匹配 email 格式(简单版)
grep -E "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" emails.txt

# 在系统用户文件中查找非注释、非空行
grep -Ev "^#|^$" /etc/fstab

# 查找 IP 地址
ip addr | grep -E "inet " | grep -v "127.0.0.1"

6.5 grep 家族

  • grep:基本正则。
  • grep -E / egrep:扩展正则(推荐)。
  • grep -F / fgrep:固定字符串(不做正则解释)。
  • zgrep:直接搜索 .gz 压缩文件。

参考:GNU grep 官方文档菜鸟教程 - Linux grep 命令


七、环境变量与 Shell 展开

7.1 环境变量与 $PATH

环境变量是定义在 shell 环境中的命名值,影响命令和程序的行为。

echo $PATH          # 查看 PATH(命令搜索路径)
echo $HOME          # 用户主目录
echo $USER          # 当前用户名
echo $SHELL         # 当前 shell
echo $LANG          # 语言/编码
echo $HOSTNAME      # 主机名
env                  # 查看所有环境变量
printenv PATH        # 另一个查看方式

$PATH 详解

  • PATH 是用冒号 : 分隔的目录列表,当你在终端输入一个命令名时,shell 会按顺序在这些目录里查找可执行文件。
  • 例如:echo $PATH 输出 /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:...
  • 命令 which ls 之所以能找到 /usr/bin/ls,就是因为在 PATH 里搜到的。

export —— 把变量导出为环境变量

MYVAR="hello"          # 普通 shell 变量(仅当前 shell 可见)
export MYVAR             # 导出为环境变量(子进程也能看到)
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk   # 常见用法
export PATH="/opt/mytools:$PATH"   # 把新目录加到 PATH 前面
  • 普通变量 vs 环境变量:环境变量会传递给子进程,普通变量不会。
  • export 用于把变量升级为环境变量。
  • 想让变量在每次登录都生效,写入 ~/.bashrc~/.bash_profile

7.2 常用的 Shell 展开(Expansion)

Shell 在执行命令前会进行多种展开,理解展开顺序是掌握 shell 的关键。

波浪号展开 ~

echo ~              # 当前用户主目录(/home/rocky)
cd ~/docs           # 进入主目录下的 docs
echo ~other         # other 用户的主目录

变量展开 $VAR / ${VAR}

echo $USER          # 展开为用户名
echo ${HOME}/data   # 展开为 /home/rocky/data

路径名展开(通配符)* ? [...]

ls *.txt            # * 匹配任意多个字符
ls file?.log        # ? 匹配单个字符
ls file[12].txt     # 字符类

花括号展开 {}

echo {1..5}             # 输出 1 2 3 4 5
echo {a,b,c}.txt        # 输出 a.txt b.txt c.txt
mkdir -p dir/{src,bin,conf}   # 一次创建多个子目录
touch file_{01..10}.log       # 创建 file_01.log ~ file_10.log
echo {1..10..2}          # 步长 2:1 3 5 7 9

算术展开 $(( ))

echo $((3 + 5))          # 输出 8
echo $((10 / 3))         # 输出 3(整数除法)

命令替换 $( )

echo $(date +%H:%M)      # 输出当前时间

参考:Bash 手册 - Shell Expansions


八、帮助文档:man / info / --help

学会查阅帮助是 Linux 学习者的必备能力,永远不要背命令参数,要学会查。

8.1 --help(快速查看用法)

ls --help            # 快速查看命令的简要用法和常用选项
grep --help

绝大多数 GNU 命令都支持 --help,适合快速回忆选项。

8.2 man

man ls               # 查看 ls 的完整手册
man -k keyword       # 关键词搜索相关手册(等价于 apropos)
man 5 passwd         # 查看第 5 节(文件格式)的 passwd
man -f ls            # 简要描述(whatis)

man 手册的章节(Section)

章节 内容 例子
1 用户命令 man 1 ls
2 系统调用 man 2 open
3 C 库函数 man 3 printf
5 文件格式与配置 man 5 passwdman 5 hosts
7 杂项/约定 man 7 regex
8 系统管理命令 man 8 systemctl

在 man 页内操作(与 less 相同,man 使用 less 显示):

按键 作用
空格/f 向下翻页
b 向上翻页
/关键词 向下搜索(n 下一个)
?关键词 向上搜索
g/G 到开头/结尾
q 退出

读 man 的常见段落NAME(简介)、SYNOPSIS(语法)、DESCRIPTION(详解)、OPTIONS(选项)、EXAMPLES(示例)、SEE ALSO(相关)。

8.3 info(更详细的手册)

info bash             # 查看 bash 的详细文档(超链接式)
info coreutils
  • info 是 GNU 提供的更详细的文档系统,支持超链接导航。
  • 多数命令的 info 文档比 man 更详尽,但 man 更常用。
  • 通常先看 man,需要更深入再看 info

参考:man 手册 - man-dbLinux 手册页官网


九、历史与补全:history / !! / Tab / Ctrl+R

9.1 命令历史 history

history               # 显示命令历史列表
history 20            # 只显示最近 20 条
history -c            # 清空当前会话历史
!5                    # 执行历史中第 5 条命令
!!                    # ★ 执行上一条命令
!ls                   # 执行最近一条以 ls 开头的命令
!$                    # 上一条命令的最后一个参数
history | grep dnf    # 在历史中搜索某命令

常用快捷历史操作

操作 效果
上下箭头 浏览历史
!! 执行上一条命令
!$ 上一条命令的最后一个参数
!string 执行最近的以 string 开头的命令
Ctrl+R 反向增量搜索历史
Ctrl+P / Ctrl+N 上一条/下一条(等价上下箭头)

典型场景sudo !! 经常用于"上一条命令没加 sudo,重新用 sudo 执行一次"。

9.2 Tab 补全

  • 命令补全:输入 sysTab,会补全为 systemctl(如果唯一)。
  • 路径补全:输入 cd /etTab,补全为 /etc/
  • 多候选:按一次 Tab 无反应,再按一次会列出所有可能。
  • 参数补全:部分命令支持参数补全(如 systemctl sta + Tab)。
  • 养成用 Tab 的习惯,又快又不易出错

9.3 Ctrl+R 反向搜索

# 按 Ctrl+R,然后输入关键词,即可反向搜索历史命令
(reverse-i-search)`dnf`: sudo dnf update   # 匹配到后按回车执行
  • Ctrl+R 进入反向增量搜索:边输入边匹配历史。
  • 再次按 Ctrl+R 跳到上一个匹配项。
  • 回车 执行匹配到的命令;按 Esc 或方向键可编辑后再执行;Ctrl+C 退出搜索。

参考:Bash 手册 - Bash Features(History 与 Readline)


十、总结与练习

10.1 核心速查

需求 语法/命令
管道(串联命令) `cmd1
输出覆盖/追加到文件 cmd > file / cmd >> file
从文件读入 cmd < file
错误与输出都进文件 cmd > file 2>&1
边看边存 `cmd
输出当参数 `cmd
上一条命令退出码 echo $?
成功才执行 / 失败才执行 A && B / `A
变量 / 命令替换 $VAR / $(cmd)
扩展正则搜索 grep -E "pattern"
查看/导出环境变量 echo $PATH / export VAR=value
查看帮助 man cmd / cmd --help / info cmd
执行上一条命令 !!
补全 / 反向搜索 Tab / Ctrl+R

10.2 动手练习建议

# 1. 重定向练习
echo "hello world" > test.txt
cat test.txt
echo "second line" >> test.txt
cat test.txt
ls /no-such-dir 2> err.log; cat err.log

# 2. 管道练习
cat /etc/passwd | grep -v "^#" | cut -d: -f1 | sort
ps aux | grep ssh | grep -v grep

# 3. tee 练习
date | tee date.txt
cat date.txt

# 4. xargs 练习
mkdir -p /tmp/xargtest && cd /tmp/xargtest
touch a.log b.log c.log
ls *.log | xargs -I {} cp {} /tmp/
ls /tmp/*.log

# 5. 退出码与条件执行
true && echo "成功"
false || echo "失败"
test -f /etc/hosts && echo "hosts 存在"

# 6. 变量与命令替换
myname="Rocky"
echo "你好, ${myname}!"
echo "今天: $(date +%F)"

# 7. grep 正则
printf "root:0\nuser1:100\nroot2:200\n" | grep -E "^root[0-9]?:"
grep -E "^[0-9]" /etc/services | head -5

# 8. 环境变量
export MYTEST="hello env"
echo $MYTEST
echo $PATH | tr ":" "\n" | head

# 9. 历史与补全
history | tail -10
echo "第一条命令"
!!    # 再次执行上一条(echo "第一条命令")

提示:# 开头的行是注释,复制练习时请去掉不需要的部分。逐条亲手敲一遍,才是掌握 Shell 的最佳方式。


参考资料