Skip to content

Latest commit

 

History

History
2916 lines (2052 loc) · 50.1 KB

File metadata and controls

2916 lines (2052 loc) · 50.1 KB

Shell 基础命令超级详细教程

适用对象:第一次学习 Shell、正在使用 Windows 和 Codex 桌面端终端的初学者。

本教程以 Bash 为主,同时说明 PowerShell 中的对应写法。

如果你需要把每条命令拆成“命令名、选项、源参数、目标参数和实际作用”,请配合阅读同目录下的 shell-command-dictionary.md。


目录

  1. Shell 是什么
  2. Codex 终端为什么没有 touch
  3. Windows 上选择哪种终端
  4. 命令的基本结构
  5. 认识当前目录
  6. 目录和文件操作
  7. 查看文件内容
  8. 输出、重定向和管道
  9. 搜索文件和文本
  10. 变量和环境变量
  11. 引号、通配符和特殊符号
  12. 编写第一个 Shell 脚本
  13. 权限、进程和任务
  14. 常见报错及解决方法
  15. 完整练习项目
  16. 命令速查表

1. Shell 是什么

Shell 是一个用来和操作系统对话的程序。你输入命令,Shell 负责解析命令并调用系统功能。

常见 Shell 有:

Shell 常见平台 特点
Bash Linux、macOS、Git Bash、WSL Linux 教程最常用
PowerShell Windows Windows 默认终端,功能强大
Command Prompt(CMD) Windows 较老的 Windows 命令行
Zsh macOS、Linux Bash 的常见替代品

本教程中的命令如果写成这样:

$ pwd

开头的 $ 只是提示符,不要把 $ 一起输入。实际输入的是 pwd

PowerShell 的提示符通常类似这样:

PS C:\Users\Lenovo>

2. Codex 终端为什么没有 touch

你之前看到的错误:

touch : 无法将“touch”项识别为 cmdlet、函数、脚本文件或可运行程序

说明当前终端是 PowerShelltouch 是 Bash/Linux 中常见的命令,不是 PowerShell 的内置命令。

2.1 在 PowerShell 中创建文件

New-Item hello.txt

简写:

ni hello.txt

2.2 在 Codex 当前终端临时进入 WSL

如果电脑已经安装 WSL,可以输入:

wsl

进入后提示符会变化,此时可以使用 Bash 命令:

touch hello.txt
ls

退出 WSL:

exit

2.3 在 Codex 当前终端启动 Git Bash

如果已经安装 Git for Windows,可以在 PowerShell 输入:

& "C:\Program Files\Git\bin\bash.exe"

然后就能运行:

touch hello.txt
ls -la

如果电脑没有 Git Bash,可以安装 Git for Windows

2.4 三种环境怎么选

  • 想学习 Linux/Shell:选择 WSLGit Bash
  • 只做 Windows 文件操作:使用 PowerShell
  • 想运行和服务器完全一致的 Linux 命令:优先使用 WSL

3. Windows 上选择哪种终端

3.1 PowerShell

PowerShell 是 Codex 在 Windows 上常见的默认环境。它的命令示例:

Get-Location
Get-ChildItem
New-Item hello.txt

它也支持许多别名,所以以下命令可能可以运行:

pwd
ls
cat file.txt

但这些别名并不代表你已经进入 Bash。比如 touchgrepchmod 仍可能不可用。

3.2 Git Bash

Git Bash 提供比较接近 Linux 的 Bash 环境,适合学习本教程。它可以直接使用:

pwd
ls
touch hello.txt
grep hello hello.txt

3.3 WSL

WSL 是 Windows Subsystem for Linux,可以在 Windows 内运行真实的 Linux 发行版。它最适合学习服务器、Linux 和 Bash。

在 PowerShell 中检查 WSL:

wsl --status

如果还没有安装,可以用管理员 PowerShell 执行:

wsl --install

安装后可能需要重启电脑。


4. 命令的基本结构

Shell 命令通常由四部分组成:

命令 选项 参数

例如:

ls -la /tmp
  • ls:命令名称
  • -la:选项,表示显示详细信息和隐藏文件
  • /tmp:参数,表示查看哪个目录

查看命令帮助:

ls --help

查看 Bash 命令手册:

man ls

在 Windows PowerShell 中查看帮助:

Get-Help Get-ChildItem

Ctrl + C 可以中止正在运行的命令。


5. 认识当前目录

Shell 中最重要的概念之一是“当前工作目录”。很多相对路径都以它为起点。

5.1 查看当前目录

pwd

示例输出:

/home/user/project

PowerShell:

Get-Location

5.2 查看目录内容

ls
ls -l       # 详细信息
ls -a       # 包含隐藏文件
ls -la      # 两者一起使用

PowerShell:

Get-ChildItem
Get-ChildItem -Force

5.3 切换目录

cd folder       # 进入 folder
cd ..           # 返回上一级
cd .            # 当前目录,通常没有实际变化
cd ~            # 回到用户主目录
cd -            # 返回上一次所在的目录

Windows 路径也可以写成:

cd /c/Users/Lenovo/Documents

PowerShell 写法:

Set-Location C:\Users\Lenovo\Documents

路径中包含空格时,要使用引号:

cd "My Project"

6. 目录和文件操作

6.1 创建目录

mkdir demo
mkdir -p project/src/components

-p 表示父目录不存在时一起创建。

PowerShell:

New-Item -ItemType Directory demo
New-Item -ItemType Directory project/src/components -Force

6.2 创建文件

Bash:

touch hello.txt

PowerShell:

New-Item hello.txt

6.3 复制文件和目录

Bash:

cp hello.txt copy.txt
cp -r demo demo-backup

PowerShell:

Copy-Item hello.txt copy.txt
Copy-Item demo demo-backup -Recurse

6.4 移动和重命名

Bash:

mv hello.txt demo/
mv old-name.txt new-name.txt

PowerShell:

Move-Item hello.txt demo/
Rename-Item old-name.txt new-name.txt

6.5 删除

Bash:

rm file.txt
rm -r folder
rm -i file.txt

PowerShell:

Remove-Item file.txt
Remove-Item folder -Recurse
Remove-Item file.txt -Confirm

rmRemove-Item 通常不会进入回收站。删除前先确认路径:

pwd
ls

不要在不确定时执行危险命令,特别是 rm -rf


7. 查看文件内容

7.1 输出全部内容

cat file.txt

PowerShell:

Get-Content file.txt

7.2 查看开头和结尾

head file.txt
head -n 20 file.txt
tail file.txt
tail -n 20 file.txt

持续查看日志:

tail -f app.log

Ctrl + C 停止。

PowerShell 等价命令:

Get-Content app.log -Wait

7.3 分页阅读

less large-file.txt

less 中:

  • Space:向下翻页
  • b:向上翻页
  • /文字:搜索文字
  • q:退出

8. 输出、重定向和管道

8.1 echo 输出文字

echo "Hello Shell"

PowerShell 也支持:

Write-Output "Hello Shell"

8.2 > 覆盖写入

echo "first line" > notes.txt

如果文件已经存在,原内容会被覆盖。

8.3 >> 追加写入

echo "second line" >> notes.txt

这会把内容追加到文件末尾。

8.4 管道 |

管道把前一个命令的输出交给后一个命令:

cat names.txt | sort

常见组合:

cat names.txt | sort | uniq
ls -la | less
grep "ERROR" app.log | tail -n 20

许多时候可以省略不必要的 cat

sort names.txt | uniq

8.5 错误输出

Shell 中:

  • 1:标准输出
  • 2:错误输出

只保存正常输出:

command > output.txt

同时保存正常输出和错误:

command > all.txt 2>&1

8A. 第 8 章以后慢速精讲

从第 8 章开始,Shell 的重点变成“把命令组合起来”。先记住这条主线:

输入数据 -> 命令处理 -> 输出结果 -> 下一个命令继续处理

8A.1 先理解输出

echo "hello"

这条命令把 hello 输出到屏幕。这个输出叫标准输出。

echo "hello" > hello.txt

这次屏幕上没有 hello,因为 > 把标准输出改送到 hello.txt。读取文件:

cat hello.txt

可以把 > 读成“把左边命令打印的结果写进右边文件”。

8A.2 > 和 >> 的区别

echo "第一行" > notes.txt
echo "第二行" > notes.txt

第二条命令会覆盖第一条,文件只剩第二行。

echo "第一行" > notes.txt
echo "第二行" >> notes.txt

两行都会保留。记忆方法:

  • 是重新写,旧内容会消失。

  • 是接着写,旧内容会保留。

练习:

echo "Alice" > names.txt
echo "Bob" >> names.txt
echo "Carol" >> names.txt
cat names.txt

8A.3 管道 | 的逐步过程

先准备文件:

printf "orange\napple\nbanana\n" > fruits.txt

查看:

cat fruits.txt

排序:

cat fruits.txt | sort

执行过程是:

  1. cat 打开 fruits.txt。
  2. cat 原本要把内容显示在屏幕上。
  3. | 截住 cat 的输出,交给 sort。
  4. sort 排序后再把结果显示在屏幕上。

结果:

apple
banana
orange

多个管道就是多个处理步骤:

cat fruits.txt | sort | head -n 2

人话翻译:读取文件,排序,只显示前两行。

8A.4 为什么有时不用 cat

这两条通常结果相同:

cat app.log | grep "ERROR"
grep "ERROR" app.log

第一条是 cat 读文件、grep 读管道;第二条是 grep 自己读文件。能直接把文件交给命令时,第二条更简单。需要多个处理步骤时才使用管道:

grep "ERROR" app.log | sort | uniq -c

翻译:

  1. 只保留 ERROR 行。
  2. 排序,让相同内容相邻。
  3. 统计重复行。

8A.5 正常输出和错误输出

命令可能同时产生正常结果和错误提示:

ls existing.txt missing.txt

保存正常结果:

ls existing.txt missing.txt > result.txt

保存错误:

ls existing.txt missing.txt 2> errors.txt

把两者放到同一个文件:

ls existing.txt missing.txt > all.txt 2>&1

入门阶段先记住:

  • file:保存正常结果。

  • 2> file:保存错误信息。
  • file:追加正常结果。

8A.6 grep 搜索文本

准备日志:

printf "INFO started\nERROR database down\nINFO retrying\nERROR timeout\n" > app.log

完整查看:

cat app.log

只显示含 ERROR 的行:

grep "ERROR" app.log

显示行号:

grep -n "ERROR" app.log

只显示数量:

grep -c "ERROR" app.log

忽略大小写:

grep -i "error" app.log

显示不含 ERROR 的行:

grep -v "ERROR" app.log

在当前目录的所有文件中递归搜索:

grep -rn "ERROR" .

这里的 r 是递归,n 是显示行号。

8A.7 find 查找文件

从当前目录开始显示所有路径:

find .

查找名字以 .log 结尾的文件:

find . -name "*.log"

只找普通文件:

find . -type f -name "*.log"

只找目录:

find . -type d -name "data"

如果 find 太难,先分成两步:

find . -type f -name "*.log" > log-files.txt
cat log-files.txt

8A.8 变量:给数据起名字

name="Alice"
echo "$name"

第一行创建变量,第二行读取变量。等号两边不能有空格:

name="Alice"

错误:

name = "Alice"

读取变量要加美元符号。下面输出的是变量内容:

echo "$name"

下面输出的是普通文字 name:

echo "name"

把文件名保存到变量:

file="app.log"
grep "ERROR" "$file"

8A.9 命令替换

把一个命令的输出保存到变量:

error_count="$(grep -c "ERROR" app.log)"
echo "错误数量:$error_count"

执行顺序:

  1. 先运行 grep。
  2. 得到数字,例如 2。
  3. 把数字放入 error_count。
  4. echo 打印这个变量。

$(...) 的意思就是“先执行括号里面的命令”。

8A.10 为什么变量通常要加双引号

file="my notes.txt"
cat "$file"

双引号保证整个 my notes.txt 被当成一个文件名。

不加引号时:

cat $file

Shell 可能把它拆成 my 和 notes.txt 两个参数。初学阶段遵守这个规则:

使用变量时优先写成 "$变量名"

8A.11 if 判断逐行理解

if [ -f "app.log" ]; then
    echo "文件存在"
else
    echo "文件不存在"
fi

解释:

  1. if 开始判断。
  2. [ -f "app.log" ] 检查 app.log 是否是文件。
  3. then 表示条件为真时执行。
  4. else 表示条件为假时执行。
  5. fi 表示 if 结束。

常用测试:

[ -f file ]       # 是文件
[ -d folder ]     # 是目录
[ -e path ]       # 路径存在
[ -z "$text" ]    # 字符串为空
[ -n "$text" ]    # 字符串非空
[ "$a" = "$b" ]   # 字符串相等
[ "$n" -gt 10 ]   # 数字大于 10

8A.11.1 重点:方括号 [ ] 和双括号 [[ ]] 到底是什么

在 Bash 中,方括号不是装饰符号。单方括号实际上是一个叫 test 的命令:

[ -f "app.log" ]

它和下面这条命令意思相同:

test -f "app.log"

test 不会直接打印 true 或 false,而是通过退出状态表示结果:

  • 返回 0:条件为真。
  • 返回非 0:条件为假。

所以它通常放进 if:

if [ -f "app.log" ]; then
    echo "app.log 是文件"
fi

把这条命令拆开:

[        开始执行 test 命令
-f       检查后面的路径是否为普通文件
"app.log" 被检查的路径
]        test 命令的结束标记
; then   if 条件结束,开始执行真分支

方括号两边为什么必须有空格

正确:

[ -f "app.log" ]

错误:

[-f "app.log"]

Shell 会按空格拆分命令,所以 [ 和 ] 必须是独立参数。

then 也有两种写法:

if [ -f "app.log" ]; then
    echo "存在"
fi

或者:

if [ -f "app.log" ]
then
    echo "存在"
fi

如果 then 和条件在同一行,中间需要分号;如果换行,就不需要分号。

文件判断

[ -e "path" ]       # 路径存在
[ -f "file.txt" ]   # 是普通文件
[ -d "logs" ]       # 是目录
[ -r "file.txt" ]   # 可读
[ -w "file.txt" ]   # 可写
[ -x "run.sh" ]     # 可执行

完整例子:

if [ ! -f "config.conf" ]; then
    echo "缺少配置文件"
    exit 1
fi

! 表示取反,所以这句话是“如果 config.conf 不是文件”。

字符串判断

name="Alice"

if [ -n "$name" ]; then
    echo "name 非空"
fi

if [ -z "$name" ]; then
    echo "name 为空"
fi

if [ "$name" = "Alice" ]; then
    echo "名字匹配"
fi

常见写法:

  • -n:字符串非空。
  • -z:字符串为空。
  • =:字符串相等。
  • !=:字符串不相等。

变量为空时也要保留引号:

name=""
[ -z "$name" ]

不推荐:

[ -z $name ]

变量为空时,不加引号可能造成参数数量不正确。

数字判断

数字比较要使用专门的运算符:

count=3

[ "$count" -eq 3 ]   # 等于
[ "$count" -ne 3 ]   # 不等于
[ "$count" -gt 2 ]   # 大于
[ "$count" -ge 3 ]   # 大于或等于
[ "$count" -lt 5 ]   # 小于
[ "$count" -le 3 ]   # 小于或等于

例子:

error_count=2

if [ "$error_count" -gt 0 ]; then
    echo "发现错误"
else
    echo "没有错误"
fi

记忆方法:

  • eq:equal,等于。
  • ne:not equal,不等于。
  • gt:greater than,大于。
  • lt:less than,小于。

多个条件

两个条件都为真:

if [ -f "app.log" ] && [ -r "app.log" ]; then
    echo "文件存在并且可读"
fi

至少一个条件为真:

if [ "$env" = "dev" ] || [ "$env" = "test" ]; then
    echo "这是非生产环境"
fi

双方括号 [[ ]]

双方括号是 Bash 提供的条件语法:

if [[ -f "app.log" ]]; then
    echo "文件存在"
fi

它和单方括号用途相近,但更适合 Bash 脚本,因为变量为空时更安全,并支持模式匹配和正则表达式。

字符串比较:

name="Alice"
if [[ "$name" == "Alice" ]]; then
    echo "匹配"
fi

模式匹配:

file="backup-2026.sql"
if [[ "$file" == backup-*.sql ]]; then
    echo "这是 SQL 备份文件"
fi

正则表达式:

version="v1.2.3"
if [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "版本号格式正确"
fi

[ ] 和 [[ ]] 怎么选

场景 推荐
兼容普通 sh [ ]
明确使用 Bash [[ ]]
简单文件判断 两者都可以
Bash 模式匹配 [[ ]]
Bash 正则表达式 [[ ]]
阅读已有脚本 按原脚本写法理解

如果脚本第一行是:

#!/bin/sh

不要随意把 [ ] 改成 [[ ]],普通 sh 不一定支持双方括号。

如果脚本第一行是:

#!/usr/bin/env bash

那么 [[ ]] 通常可以使用。

数据库脚本中的典型判断

检查配置:

if [[ -z "$DB_NAME" ]]; then
    echo "DB_NAME 未设置" >&2
    exit 1
fi

检查客户端:

if ! command -v psql >/dev/null 2>&1; then
    echo "找不到 psql" >&2
    exit 1
fi

检查连接:

if psql "$DB_NAME" -c "SELECT 1;" >/dev/null 2>&1; then
    echo "数据库连接成功"
else
    echo "数据库连接失败" >&2
    exit 1
fi

看到方括号时,先翻译成人话:

if [[ ! -f "$config_file" ]]; then

就是:

如果 config_file 不是一个文件,就执行下面的内容。

看到:

if [ "$count" -gt 0 ]; then

就是:

如果 count 大于 0,就执行下面的内容。

8A.12 for 循环逐步理解

for file in *.log; do
    echo "正在处理:$file"
done

如果目录中有 a.log、b.log、c.log,循环会执行三次。每次执行时,file 的值不同。

对每个日志统计错误:

for file in *.log; do
    count="$(grep -c "ERROR" "$file")"
    echo "$file: $count"
done

翻译:对每个 .log 文件执行相同的两行操作。

8A.13 函数:给一组动作起名字

run_query() {
    local sql="$1"
    echo "准备执行:$sql"
}

run_query "SELECT 1;"

读函数时只问三件事:

  1. 接收哪些参数。
  2. 内部执行什么命令。
  3. 最后输出什么或返回什么状态。

数据库脚本中的函数可能是:

run_query() {
    local sql="$1"
    psql "$DB_NAME" -c "$sql"
}

人话:接收一条 SQL,交给 psql 执行。

8A.14 source:为什么公共脚本没有明显入口

另一个脚本可能这样写:

source "./database-common.sh"
run_query "SELECT 1;"

source 会在当前 Shell 中加载文件。文件里的函数加载完成后,后面的脚本就能调用它们。

直接运行:

bash database-common.sh

和 source 不一样,直接运行会开启另一个进程。公共函数通常需要 source,而不是单独运行。

8A.15 数据库命令怎么拆

看到一条长命令:

psql \
    --host "$DB_HOST" \
    --port "$DB_PORT" \
    --username "$DB_USER" \
    --dbname "$DB_NAME" \
    --command 'SELECT 1;'

不要整体死记,拆成三部分:

  1. psql:使用 PostgreSQL 客户端。
  2. --host、--port、--username、--dbname:连接参数。
  3. --command:要执行的 SQL。

其他常见客户端:

mysql "连接参数" --execute "SELECT 1;"
sqlite3 "$DB_FILE" "SELECT 1;"

8A.16 Here Document:多行 SQL

psql "$DB_NAME" <<'SQL'
SELECT id, name
FROM users
WHERE active = true;
SQL

解释:

  1. psql 等待从标准输入读取 SQL。
  2. <<'SQL' 表示开始多行输入。
  3. 中间的几行是 SQL。
  4. 单独一行的 SQL 表示输入结束。

看到这种结构时,把它整体翻译成“把下面这段 SQL 交给 psql”。

8A.17 退出状态码和错误处理

命令成功通常返回 0,失败返回非 0。可以这样判断:

if psql "$DB_NAME" -c 'SELECT 1;' >/dev/null; then
    echo "连接成功"
else
    echo "连接失败" >&2
    exit 1
fi

这条也很常见:

cd "$PROJECT_ROOT" || exit 1

翻译:进入目录失败就退出,不要在错误目录继续执行。

8A.18 事务先记三个词

数据库修改脚本中寻找:

BEGIN;
UPDATE users SET active = true WHERE id = 10;
COMMIT;
  • BEGIN:开始事务。
  • COMMIT:提交修改。
  • ROLLBACK:发生问题时撤销修改。

读脚本时重点确认:失败时有没有停止、有没有回滚、是否真的提交了。

8A.19 trap 和清理

tmp_sql="$(mktemp)"
trap 'rm -f "$tmp_sql"' EXIT

第一行创建临时文件,第二行表示脚本退出时删除它。看到 trap 时,先问“脚本结束时要清理什么”。

8A.20 实战练习:从输入到报告

先创建日志:

printf "INFO start\nERROR database down\nINFO retry\nERROR timeout\n" > app.log

再统计错误:

count="$(grep -c "ERROR" app.log)"
echo "错误数量:$count"

加上判断:

if [ "$count" -gt 0 ]; then
    echo "发现错误"
else
    echo "没有错误"
fi

最后写入报告:

{
    echo "日志报告"
    echo "错误数量:$count"
} > report.txt
cat report.txt

大括号表示把里面多条命令的输出作为一个整体,再由 > 写入文件。

8A.21 读脚本的固定笔记模板

看到任何 database-common.sh 代码,可以按这个模板写中文旁注:

解释器:用什么 Shell?
配置:变量从哪里来?有没有默认值?
入口:脚本是 source 还是直接运行?
函数:每个函数接收什么、执行什么、返回什么?
数据库:使用 psql、mysql 还是 sqlite3?
输入:SQL 来自 -c、-f、重定向还是多行输入?
输出:结果显示在屏幕、文件还是变量?
错误:失败后退出、重试还是继续?
清理:临时文件和连接如何处理?

先能回答这些问题,再研究复杂的 SQL 细节。

8A.22 你现在应该按这个顺序练

不要一次学习全部内容。每一步运行成功后再进行下一步:

echo "ERROR one" > app.log
echo "INFO two" >> app.log
grep "ERROR" app.log
count="$(grep -c "ERROR" app.log)"
echo "$count"
if [ "$count" -gt 0 ]; then echo "有错误"; fi
for file in *.log; do echo "$file"; done

如果某一行不懂,就只研究那一行,不要继续复制后面的命令。

9. 搜索文件和文本

9.1 grep 搜索文本

grep "hello" file.txt
grep -i "hello" file.txt        # 忽略大小写
grep -n "hello" file.txt        # 显示行号
grep -v "hello" file.txt        # 显示不包含 hello 的行
grep -rn "hello" .              # 递归搜索当前目录

PowerShell 对应:

Select-String -Pattern "hello" -Path file.txt

9.2 find 查找文件

find . -name "*.sh"
find . -type f -name "*.txt"
find . -type d -name "data"

含义:

  • .:从当前目录开始
  • -name:按名称匹配
  • -type f:只找文件
  • -type d:只找目录

PowerShell:

Get-ChildItem -Recurse -Filter *.sh
Get-ChildItem -Recurse -File -Filter *.txt

9.3 统计文本

wc file.txt       # 行数、单词数、字节数
wc -l file.txt    # 只统计行数
sort file.txt     # 排序
uniq file.txt     # 删除相邻重复行
sort file.txt | uniq -c

注意:uniq 只处理相邻重复行,所以通常先 sortuniq


10. 变量和环境变量

10.1 创建变量

Bash 中等号两边不能有空格:

name="Tom"
age=18

读取变量要加 $

echo "$name"
echo "${name} is ${age} years old"

错误写法:

name = "Tom"

10.2 环境变量

echo "$HOME"
echo "$PATH"
env

临时设置一个环境变量并运行命令:

MODE=development bash script.sh

PowerShell:

$name = "Tom"
Write-Output $name
$env:Path
Get-ChildItem Env:

10.3 PATH 是什么

PATH 是一组目录。你输入命令时,Shell 会在这些目录中寻找可执行程序。

查看 Bash 的命令位置:

which bash
which git

PowerShell:

Get-Command git
Get-Command bash

如果看到“command not found”,可能是软件没有安装,或者安装目录没有加入 PATH


11. 引号、通配符和特殊符号

11.1 双引号

双引号会展开变量:

name="Tom"
echo "Hello, $name"

输出:

Hello, Tom

11.2 单引号

单引号中的内容基本按原样处理:

echo 'Hello, $name'

输出:

Hello, $name

11.3 通配符

ls *.txt          # 当前目录中的所有 txt 文件
ls data-?.csv     # ? 代表一个字符
ls file[0-9].txt  # [0-9] 代表一个数字

11.4 常见特殊符号

符号 含义
. 当前目录
.. 上一级目录
~ 用户主目录
/ 路径分隔符或根目录
* 任意多个字符
? 任意一个字符
` `
> 覆盖重定向
>> 追加重定向
&& 前一个命令成功后再运行后一个
`
# 注释开始

示例:

mkdir demo && cd demo
test -f config.txt || echo "config.txt does not exist"

12. 编写第一个 Shell 脚本

12.1 创建脚本

新建 hello.sh

#!/usr/bin/env bash

name="Tom"
echo "Hello, $name!"

第一行叫 shebang,告诉系统使用哪个解释器运行脚本。

12.2 运行脚本

最简单的方式:

bash hello.sh

Linux、WSL 或 Git Bash 中也可以这样运行:

chmod +x hello.sh
./hello.sh

PowerShell 中可以调用 Bash:

bash .\hello.sh

12.3 接收命令行参数

脚本 greet.sh

#!/usr/bin/env bash

name="$1"

if [ -z "$name" ]; then
    echo "用法:bash greet.sh 姓名"
    exit 1
fi

echo "你好,$name"

运行:

bash greet.sh Alice

常用参数:

  • $0:脚本名称
  • $1:第一个参数
  • $2:第二个参数
  • $#:参数数量
  • $@:全部参数
  • $?:上一条命令的退出状态码

12.4 条件判断

if [ -f "hello.txt" ]; then
    echo "文件存在"
else
    echo "文件不存在"
fi

常用测试:

[ -f file ]     # 是否为文件
[ -d folder ]   # 是否为目录
[ -z "$text" ]  # 字符串是否为空
[ "$a" = "$b" ] # 字符串是否相等
[ "$n" -gt 10 ]  # 数字是否大于 10

12.5 循环

for file in *.txt; do
    echo "正在处理:$file"
done

数字循环:

for i in {1..5}; do
    echo "$i"
done

13. 权限、进程和任务

13.1 查看权限

ls -l

示例:

-rwxr-xr-- 1 user user 120 hello.sh

其中:

  • r:read,读取
  • w:write,写入
  • x:execute,执行

13.2 修改权限

chmod +x hello.sh
chmod 644 notes.txt
chmod 755 run.sh

Windows PowerShell 不使用完全相同的 Linux 权限模型,因此 chmod 主要在 WSL、Linux 或 Git Bash 中使用。

13.3 查看进程

ps
ps aux
top

PowerShell:

Get-Process

结束当前前台程序:

Ctrl + C

13.4 后台运行

Bash:

long-command &
jobs
fg

& 会让命令在后台运行,jobs 查看后台任务,fg 把任务切回前台。


14. 常见报错及解决方法

command not found

含义:Shell 找不到这个命令。

检查:

which command-name

PowerShell:

Get-Command command-name

可能原因:

  • 命令属于另一种 Shell。
  • 软件尚未安装。
  • 软件没有加入 PATH
  • 命令拼写错误。

No such file or directory

含义:文件或目录不存在,或路径写错。

检查:

pwd
ls -la

使用 Tab 键自动补全文件名,可以减少拼写错误。

Permission denied

含义:没有执行或读取权限。

Linux/WSL 中可能需要:

chmod +x script.sh

Is a directory

含义:你把目录当成文件操作,或者把文件当成目录操作。

先确认类型:

ls -ld path

命令执行后没有输出

这不一定是错误。很多成功的命令本来就没有输出,例如:

mkdir demo
touch file.txt

ls 检查结果:

ls -la

15. 完整练习项目

下面的练习会创建一个小型日志项目。建议在工作区中执行。

15.1 创建项目结构

mkdir -p shell-practice/logs shell-practice/scripts
cd shell-practice
touch logs/app.log

如果当前是 PowerShell:

New-Item -ItemType Directory shell-practice/logs -Force
New-Item -ItemType Directory shell-practice/scripts -Force
Set-Location shell-practice
New-Item logs/app.log

15.2 写入日志

Bash:

echo "INFO server started" >> logs/app.log
echo "INFO user logged in" >> logs/app.log
echo "ERROR database unavailable" >> logs/app.log
echo "INFO retry completed" >> logs/app.log
echo "ERROR request timeout" >> logs/app.log

查看日志:

cat logs/app.log

15.3 搜索错误并统计

grep "ERROR" logs/app.log
grep -c "ERROR" logs/app.log

预期结果:错误数量为 2

15.4 生成报告

{
    echo "日志报告"
    echo "========"
    echo "错误数量:$(grep -c "ERROR" logs/app.log)"
    echo "信息数量:$(grep -c "INFO" logs/app.log)"
} > report.txt

查看报告:

cat report.txt

这里的 $(...) 叫命令替换,表示先运行括号里的命令,再把结果放进文字中。

15.5 备份项目

cp -r logs logs-backup
find . -type f

15.6 写一个自动报告脚本

创建 scripts/report.sh

#!/usr/bin/env bash

log_file="logs/app.log"

if [ ! -f "$log_file" ]; then
    echo "找不到日志文件:$log_file"
    exit 1
fi

echo "日志报告"
echo "========"
echo "总行数:$(wc -l < "$log_file")"
echo "错误数:$(grep -c "ERROR" "$log_file")"
echo "信息数:$(grep -c "INFO" "$log_file")"

运行:

bash scripts/report.sh

如果使用 Git Bash 或 WSL,还可以:

chmod +x scripts/report.sh
./scripts/report.sh

16. 命令速查表

目录导航

pwd                 # 当前路径
ls -la              # 查看目录
cd folder           # 进入目录
cd ..               # 返回上一级
cd ~                # 用户主目录

文件操作

mkdir folder        # 创建目录
touch file.txt      # 创建文件
cp a b               # 复制
mv a b               # 移动/重命名
rm file.txt          # 删除文件
rm -r folder         # 删除目录

查看和搜索

cat file.txt
head file.txt
tail file.txt
grep "text" file.txt
find . -name "*.sh"
wc -l file.txt

脚本

bash script.sh
chmod +x script.sh
./script.sh
echo "$variable"

终端控制

Ctrl + C       停止当前命令
Ctrl + L       清屏
Tab            自动补全
↑ / ↓          浏览历史命令
Ctrl + R       搜索历史命令

学习建议


17. 目标进阶:读懂 database-common.sh

前面的内容足够入门,但如果你的明确目标是读懂一个名为 database-common.sh 的数据库公共脚本,还需要掌握本节。

当前工作区里没有名为 database-common.sh 的文件,因此本节按照常见的数据库 Bash 公共脚本来准备。它通常负责读取数据库配置、建立连接、执行 SQL、记录日志、处理错误、重试操作和清理临时文件。

17.1 先不要逐行读:先做结构扫描

拿到陌生脚本时,先快速找出解释器、变量、函数和外部命令:

head -n 20 database-common.sh
grep -nE '^(function[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*[[:space:]]*\(\)' database-common.sh
grep -nE 'source|psql|mysql|sqlite3|DATABASE|DB_|PASSWORD|BEGIN|COMMIT|ROLLBACK' database-common.sh
nl -ba database-common.sh | less

建议按这个顺序阅读:

  1. 第一行是什么解释器,是 Bash 还是普通 sh。
  2. 文件是否使用 source 被其他脚本加载。
  3. 哪些变量来自环境变量,哪些变量有默认值。
  4. 对外提供哪些函数,每个函数需要几个参数。
  5. 调用了什么数据库客户端。
  6. 命令失败时是否退出、重试或回滚。
  7. 最后才逐行研究每个函数的细节。

17.2 source:公共脚本为什么能提供函数

database-common.sh 往往不是让用户直接运行的,而是这样被加载:

source "./database-common.sh"
run_query "SELECT 1;"

下面两种写法等价:

source "./database-common.sh"
. "./database-common.sh"

source 表示在当前 Shell 中读取并执行文件,所以文件中定义的变量和函数会继续存在。直接运行则会启动新的进程:

bash database-common.sh

这也是为什么公共文件通常没有明显的“主程序入口”,而是集中定义函数。

17.3 严格模式 set -euo pipefail

数据库脚本开头经常出现:

set -euo pipefail

它由三部分组成:

  • set -e:命令失败时尽早退出,避免错误继续传播。
  • set -u:使用未定义变量时报错。
  • set -o pipefail:管道中任意命令失败时,整条管道算失败。

例如:

set -u
echo "$UNDEFINED_VARIABLE"

为了安全地提供默认值,常见写法是:

DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"

不要把 set -e 理解成“任何情况下都立即退出”。if 条件、while 条件和某些测试命令有特殊规则,所以仍然要阅读显式的 if、|| 和错误处理。

调试时可能看到:

set -x

它会打印实际执行的命令。数据库脚本中如果命令行含有密码,不能随意开启 set -x,否则密码可能进入日志。

17.4 数据库配置变量

常见配置写法:

DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:?DB_NAME is required}"
DB_USER="${DB_USER:?DB_USER is required}"

这些形式必须能看懂:

写法 含义
${VAR} 读取变量
${VAR:-default} 为空时使用默认值
${VAR:=default} 为空时设置并使用默认值
${VAR:?message} 为空时显示错误并退出
${VAR:+value} 有值时使用 value

推荐从环境变量读取密码,而不是写死在文件里:

export DB_PASSWORD='example-password'
bash migrate.sh

检查数据库相关环境变量:

env | grep '^DB_'

17.5 函数:公共脚本真正提供的 API

常见日志函数:

log_info() {
    printf '[INFO] %s\n' "$*"
}

log_error() {
    printf '[ERROR] %s\n' "$*" >&2
}

调用函数:

log_info "开始连接数据库"
log_error "连接失败"

数据库函数通常把 SQL 作为第一个参数:

run_query() {
    local query="$1"
    psql "$DB_NAME" -c "$query"
}

run_query 'SELECT 1;'

需要记住:

  • local:变量只在当前函数内有效。
  • $1、$2:函数的第一个、第二个参数。
  • $@:所有参数。
  • return 0:成功返回。
  • return 1:失败返回。

17.6 Bash 如何调用数据库

常见数据库客户端如下:

数据库 客户端
PostgreSQL psql
MySQL/MariaDB mysql
SQLite sqlite3
Redis redis-cli

PostgreSQL 可能写成:

psql \
    --host "$DB_HOST" \
    --port "$DB_PORT" \
    --username "$DB_USER" \
    --dbname "$DB_NAME" \
    --command 'SELECT 1;'

MySQL 可能写成:

mysql \
    --host="$DB_HOST" \
    --port="$DB_PORT" \
    --user="$DB_USER" \
    --database="$DB_NAME" \
    --execute='SELECT 1;'

SQLite 通常只需要数据库文件:

sqlite3 "$DB_FILE" 'SELECT 1;'

读长命令时,把它拆成三部分:客户端名称、连接参数、SQL 参数。

17.7 反斜杠和多行 SQL

行尾反斜杠表示命令还没有结束:

psql \
    --host "$DB_HOST" \
    --dbname "$DB_NAME" \
    --command 'SELECT 1;'

它等价于一行命令。反斜杠后面不要再加空格。

数据库脚本也常用 Here Document:

psql "$DB_NAME" <<'SQL'
SELECT id, name
FROM users
WHERE active = true;
SQL

最后单独一行的 SQL 是结束标记。写成 <<'SQL' 时,Shell 不会展开其中的 Bash 变量;写成 <<SQL 时,变量可能会被展开。

17.8 -c、-f 和重定向

执行一条 SQL:

psql "$DB_NAME" -c 'SELECT count(*) FROM users;'

执行 SQL 文件:

psql "$DB_NAME" -f migrations/001_init.sql

通过标准输入执行:

psql "$DB_NAME" < migrations/001_init.sql

看到这些写法时,它们本质上都是“把 SQL 交给数据库客户端执行”,区别只是 SQL 来源不同。

17.9 退出状态码和失败处理

每条命令都会返回状态码:0 表示成功,非 0 表示失败。

if psql "$DB_NAME" -c 'SELECT 1;' >/dev/null; then
    echo "数据库连接成功"
else
    echo "数据库连接失败" >&2
    exit 1
fi

也可能看到:

cd "$PROJECT_ROOT" || exit 1
psql "$DB_NAME" -c "$query" || return 1

含义是:前一个命令失败时,不允许继续执行后面的危险操作。

PostgreSQL 脚本还可能使用:

psql "$DB_NAME" -v ON_ERROR_STOP=1 -f migration.sql

它要求 SQL 出错时客户端返回失败状态,而不是继续执行剩余内容。

17.10 事务和回滚

涉及数据修改时,重点寻找:

BEGIN;
UPDATE users SET active = true WHERE id = 10;
COMMIT;

BEGIN 开始事务,COMMIT 提交修改,ROLLBACK 撤销事务中的修改。

阅读脚本时问自己:

  1. 是否开启事务。
  2. SQL 失败时是否停止。
  3. 是否一定执行 COMMIT。
  4. 出错路径是否执行 ROLLBACK。

不要在不清楚影响范围时直接运行生产数据库迁移脚本。

17.11 trap 和临时文件清理

公共脚本可能先生成临时 SQL,再交给客户端:

tmp_sql="$(mktemp)"
trap 'rm -f "$tmp_sql"' EXIT

cat > "$tmp_sql" <<'SQL'
SELECT 1;
SQL

psql "$DB_NAME" -f "$tmp_sql"

trap ... EXIT 表示脚本结束时执行清理,即使中途失败也会尽量删除临时文件。

17.12 case:按环境选择配置

数据库脚本可能用 case 区分开发、测试和生产:

case "${APP_ENV:-development}" in
    development)
        DB_HOST="localhost"
        DB_NAME="app_dev"
        ;;
    test)
        DB_HOST="test-db"
        DB_NAME="app_test"
        ;;
    production)
        DB_HOST="prod-db"
        DB_NAME="app_prod"
        ;;
    *)
        echo "未知环境:$APP_ENV" >&2
        exit 1
        ;;
esac

把 case 理解成多个 if/elif 分支即可。

17.13 引号是数据库脚本的安全边界

不推荐:

psql --dbname $DB_NAME
rm -rf $BACKUP_DIR

推荐:

psql --dbname "$DB_NAME"
rm -rf -- "$BACKUP_DIR"

未加引号的变量可能因空格被拆成多个参数,也可能触发通配符展开。数据库名、用户、路径和 SQL 变量通常都应该使用双引号。

17.14 数组保存数据库参数

长参数列表可能使用 Bash 数组:

psql_args=(
    --host "$DB_HOST"
    --port "$DB_PORT"
    --username "$DB_USER"
    --dbname "$DB_NAME"
)

psql "${psql_args[@]}" -c 'SELECT 1;'

数组可以避免手动拼接命令字符串,也能更好地处理包含空格的参数。

17.15 用 ShellCheck 辅助读脚本

检查整个文件:

shellcheck database-common.sh

优先关注这些提示:

  • 变量没有加引号。
  • 函数内部变量没有使用 local。
  • cd 失败后仍继续执行。
  • 管道忽略了前面命令的失败。
  • 临时文件没有清理。
  • 密码可能出现在命令行或调试日志中。

17.16 一段典型代码的完整拆解

set -euo pipefail

DB_HOST="${DB_HOST:-localhost}"
DB_NAME="${DB_NAME:?DB_NAME is required}"

run_query() {
    local query="$1"
    psql \
        --host "$DB_HOST" \
        --dbname "$DB_NAME" \
        --command "$query"
}

逐段理解:

  1. 开启严格模式。
  2. DB_HOST 可被环境变量覆盖,默认是 localhost。
  3. DB_NAME 必须存在,否则脚本退出。
  4. 定义 run_query 函数。
  5. 函数把第一个参数保存为 SQL。
  6. 调用 psql 连接数据库。
  7. 使用双引号保护所有变量。
  8. psql 失败时返回失败状态,上层脚本可以停止。

可能的调用方式:

export DB_NAME="app_dev"
source ./database-common.sh
run_query 'SELECT 1;'

17.17 读懂 database-common.sh 的最低检查清单

打开实际文件后,至少回答这些问题:

  • 使用 Bash 还是 sh。
  • 是直接运行还是被 source。
  • 必须设置哪些环境变量。
  • 哪些变量有默认值。
  • 哪些变量为空时会退出。
  • 提供了哪些函数。
  • 每个函数接受几个参数。
  • 调用的是 psql、mysql 还是 sqlite3。
  • SQL 通过 -c、-f、重定向还是 Here Document 传入。
  • 数据库客户端失败时会不会停止。
  • 是否有事务、提交和回滚。
  • 是否创建临时文件,退出时是否清理。
  • 密码是否可能泄露到日志。
  • 路径和变量是否正确加引号。

如果这些问题都能回答,你就不只是会背 Shell 命令,而是已经具备用“读懂数据库公共脚本”的能力。

17.18 针对本工作区现有脚本的对应练习

本工作区的 solutions 文件已经包含了很多同类语法,可以按下面顺序阅读:

  1. solutions/Solution1.sh:练习 cd、mkdir、cp、mv、wc 和失败退出。
  2. solutions/Solution2.sh:练习命令替换、sort、uniq、循环和 awk。
  3. solutions/Solution3.sh:练习 find、grep、变量、条件判断和重定向。
  4. setup.sh:练习 shebang、mkdir -p、Here Document、for 循环和变量展开。

先不要追求一次看懂全部内容。每次只回答一个问题:这个变量从哪里来、这个函数输出什么、这个命令失败时会发生什么。


18. Bash 常见语法全覆盖:以后遇到就回查

这一章不是要求一次背完,而是一个长期速查表。遇到陌生代码时,先找对应关键词。

18.1 位置参数和特殊变量

脚本运行:

bash greet.sh Alice Bob

脚本中常见的特殊变量:

echo "$0"   # 脚本名称
echo "$1"   # 第一个参数
echo "$2"   # 第二个参数
echo "$#"   # 参数数量
echo "$@"   # 所有参数
echo "$?"   # 上一条命令的退出状态
echo "$$"   # 当前 Shell 的进程号

把所有参数转发给另一个命令:

some_command "$@"

18.2 shift 和 getopts

shift 会丢弃当前第一个参数:

while [ "$#" -gt 0 ]; do
    echo "当前参数:$1"
    shift
done

getopts 用来解析 -f 文件、-n 数量这类选项:

while getopts "f:n:" option; do
    case "$option" in
        f) file="$OPTARG" ;;
        n) count="$OPTARG" ;;
        *) echo "用法:$0 -f 文件 -n 数量"; exit 1 ;;
    esac
done

18.3 算术运算

a=10
b=3
echo $((a + b))
echo $((a - b))
echo $((a * b))
echo $((a / b))
echo $((a % b))

增加和减少:

count=0
((count++))
((count += 2))

双括号也能直接判断数字:

if (( count > 0 )); then
    echo "数量大于零"
fi

18.4 case 多分支选择

case "$action" in
    start)
        echo "启动"
        ;;
    stop)
        echo "停止"
        ;;
    restart|reload)
        echo "重启或重新加载"
        ;;
    *)
        echo "未知操作"
        exit 1
        ;;
esac

每个分支用双分号结束,星号分支相当于 else。

18.5 while、until 和循环控制

while:条件为真时重复:

count=0
while [ "$count" -lt 3 ]; do
    echo "$count"
    ((count++))
done

until:条件变真之前一直重复:

until ping -c 1 "$DB_HOST" >/dev/null 2>&1; do
    echo "等待数据库主机..."
    sleep 2
done

循环控制:

for file in *.log; do
    [ -f "$file" ] || continue
    [ "$file" = "stop.log" ] && break
    echo "$file"
done
  • continue:跳过本次循环。
  • break:结束整个循环。

18.6 函数的输入、输出和返回值

get_count() {
    local file="$1"
    grep -c "ERROR" "$file"
}

count="$(get_count app.log)"

函数通过 echo 或 printf 输出文字,调用者可以用命令替换保存输出。

函数通过 return 返回成功或失败:

require_file() {
    if [ ! -f "$1" ]; then
        return 1
    fi
    return 0
}

if require_file app.log; then
    echo "文件存在"
fi
  • return 结束函数。
  • exit 结束整个脚本。

18.7 通配符和文件名匹配

*.log          # 任意多个字符
file?.txt      # 任意一个字符
file[0-9].txt  # 一个数字
file[ab].txt   # a 或 b

遍历通配符结果:

for file in logs/*.log; do
    [ -e "$file" ] || continue
    echo "$file"
done

没有匹配文件时,先用 -e 检查,避免把原始的 logs/*.log 当成文件名。

18.8 引号、数组和转义

Bash 数组

Bash 数组用于保存多个值:

files=("a.log" "b.log" "file with spaces.log")

读取某一个元素:

echo "${files[0]}"
echo "${files[1]}"

遍历全部元素:

for file in "${files[@]}"; do
    echo "$file"
done

读取数组数量:

echo "${#files[@]}"

数据库命令的参数也可以放进数组:

args=(--host "$DB_HOST" --dbname "$DB_NAME")
psql "${args[@]}" -c "SELECT 1;"

数组比手动拼接字符串更安全,尤其适合参数很多或参数中含空格的情况。

双引号会展开变量:

name="Alice"
echo "Hello, $name"

单引号不会展开变量:

echo 'Hello, $name'

反斜杠可以转义字符:

echo "他说:\"你好\""

包含空格的路径要加引号:

cat "folder with spaces/file.txt"

18.9 重定向全表

command > file        # 覆盖标准输出
command >> file       # 追加标准输出
command 2> file       # 覆盖错误输出
command 2>> file      # 追加错误输出
command > file 2>&1   # 正常输出和错误都写入 file
command < file        # 从文件读取标准输入
command <<EOF         # Here Document
command <<< "$text"   # Here String

丢弃所有输出:

command >/dev/null 2>&1

/dev/null 可以理解为输出黑洞。

18.10 子 Shell、大括号和进程替换

圆括号会开启子 Shell:

(cd /tmp && echo "子 Shell:$PWD")
echo "外层 Shell:$PWD"

子 Shell 的目录变化不会影响外层。

大括号可以把多条命令的输出作为整体重定向:

{
    echo "第一行"
    echo "第二行"
} > output.txt

进程替换把命令输出伪装成文件:

diff <(sort old.txt) <(sort new.txt)

看到 <(command) 时,理解成“把 command 的输出当作临时输入文件”。

18.11 常用文本处理工具

cut:按分隔符取列。

cut -d',' -f1 data.csv

awk:按列计算。

awk -F',' 'NR > 1 {sum += $3} END {print sum}' data.csv

sed:替换或选取行。

sed 's/old/new/g' file.txt
sed -n '1,10p' file.txt

tr:转换字符。

echo "hello" | tr 'a-z' 'A-Z'

xargs:把输入变成命令参数。

find . -name "*.tmp" -print0 | xargs -0 ls -l

18.12 环境和命令搜索

echo "$PWD"       # 当前目录
echo "$OLDPWD"    # 上一次目录
echo "$HOME"      # 用户目录
echo "$PATH"      # 命令搜索路径
echo "$USER"      # 当前用户
echo "$SHELL"     # 默认 Shell

检查命令是否存在:

command -v git
command -v psql

如果出现 command not found,依次检查拼写、软件安装情况和 PATH。

18.13 后台进程和任务

后台运行:

long_command &

查看和切换任务:

jobs
fg
bg

查看进程:

ps aux
pgrep -af process_name

结束进程:

kill PID
kill -TERM PID
kill -KILL PID

优先使用 TERM,让程序有机会清理;KILL 只在最后使用。

18.14 重试、超时和清理

简单重试:

for attempt in 1 2 3; do
    if command; then
        break
    fi
    echo "失败,准备重试"
    sleep 2
done

限制执行时间:

timeout 30s psql "$DB_NAME" -c "SELECT 1;"

退出时清理:

tmp_file="$(mktemp)"
cleanup() {
    rm -f "$tmp_file"
}
trap cleanup EXIT

18.15 set 选项和调试

set -e          # 命令失败时退出
set -u          # 未定义变量报错
set -o pipefail # 管道失败不会被隐藏
set -x          # 打印执行的命令
set +x          # 关闭命令打印

含密码的数据库命令不要在 set -x 下运行,否则密码可能出现在日志。

18.16 读 database-common.sh 的最终流程

遇到新文件时按这个顺序:

  1. 查看第一行解释器。
  2. 搜索 set、export、source 和 trap。
  3. 找出所有函数名称。
  4. 找出 DB_、DATABASE、PASSWORD 变量。
  5. 找出 psql、mysql 或 sqlite3。
  6. 看 SQL 从哪里进入客户端。
  7. 看命令失败时返回什么。
  8. 看事务、提交和回滚。
  9. 看临时文件和清理逻辑。
  10. 用 ShellCheck 检查不熟悉的写法。

你不需要一次理解每个字符。先给每段写一句中文摘要,再回头确认细节。

建议按照这个顺序练习:

  1. pwdlscd
  2. mkdirtouchcpmvrm
  3. catheadtail
  4. grepfindwc
  5. >>>|
  6. 变量、条件、循环
  7. 编写和运行 .sh 脚本

每天用终端完成一个小任务,比只背命令更有效。对于删除、覆盖写入和批量处理命令,先用 pwdls -la 检查位置,再执行操作。