shell 入门指南
# Variable
$HOME: 用户主目录$0: 脚本被调用时使用的名称(取决于调用方式:相对路径、绝对路径或文件名)- 示例:
./script.sh→$0 = ./script.sh;/path/to/script.sh→$0 = /path/to/script.sh - 获取纯文件名:
basename "$0"或${0##*/} - 获取完整路径:
realpath "$0"或readlink -f "$0"(Linux)
- 示例:
$1, $2, ...: 脚本的参数,$1是第一个参数,$2是第二个参数$#: 传递给脚本的参数个数$@: 所有参数的列表,每个参数都用双引号包围$_: 一个特殊变量,通常用于引用前一个命令的最后一个参数mkdir -p /path/to/directory && cd $_: 切到目标目录,没有此目录则创建
$*: 所有参数的列表,作为一个字符串$$: 当前进程的PID$?: 上一个命令的退出状态(0表示成功,非0表示失败)$!: 最后一个后台进程的PID$PWD: 当前工作目录$USER: 当前用户名$SHELL: 当前shell的路径$HOSTNAME: 主机名(可能为空,建议使用hostname命令)- 获取主机名的替代方法:
hostname: 显示主机名hostnamectl: 显示主机名和系统信息uname -n: 显示网络节点名cat /etc/hostname: 读取主机名文件
- 获取主机名的替代方法:
$RANDOM: 0到32767之间的随机数$SECONDS: 脚本运行的时间(秒)$LINENO: 当前行号$FUNCNAME: 当前函数名$BASH_SOURCE: 当前脚本的源文件名$BASH_LINENO: 当前行号数组$BASH_ARGC: 参数个数数组$BASH_ARGV: 参数值数组
# common
printf "%x\n" <102>: 转成 16 进制mktemp: 创建临时文件basename: 提取路径中的文件名dirname: 提取路径中的目录名readlink -f: 获取符号链接的真实路径which: 查找命令的完整路径type: 显示命令的类型和位置command -v: 查找命令路径(POSIX兼容)
# config
set -o vi: bash vi 模式. 后续光标移动就跟 vi 模式一样了。 vim 光标
# 父子 shell
在 shell 脚本中, 你可以运行一系列命令, 这些命令按照你编写的顺序执行。每个命令都在父 Shell 的上下文中执行
如果你使用管道
|来连接命令, 例如cmd1 | cmd2 | cmd3, 这表示 cmd1 的输出会成为 cmd2 的输入, cmd2 的输出会成为 cmd3 的输入圆括号
()可以用来创建一个子 Shell, 其中可以包含一系列命令。子 Shell 是在父 Shell 内部运行的, 但它有自己的独立环境, 包括变量。如果你在子 Shell 中执行cd /命令来改变工作目录, 这个改变只会在子 Shell 内部生效, 不会影响到父 Shell 或后续的命令。子 Shell 中定义的变量也是局部变量, 只在子 Shell 内部可见最后, 子 Shell 在括号
()内部的命令序列会被依次执行, 然后它的输出可以被父 Shell 或后续命令使用
这个概念对于在 shell 脚本中控制命令的执行流程和作用域非常重要。子 Shell 可以用来隔离一些操作, 以免影响到整个脚本的环境
# path
; 这会将 $ANDROID_BUILD_TOOL/bin 路径添加到当前 PATH 环境变量的末尾. 如果 $ANDROID_BUILD_TOOL 变量未定义,这将不起作用,或可能会导致错误
export PATH=$PATH:$ANDROID_BUILD_TOOL/bin
; 这会将 /usr/local/opt/postgresql@16/bin 路径添加到当前 PATH 环境变量的开头。这样做的好处是,新的路径会优先于其他路径
export PATH="/usr/local/opt/postgresql@16/bin:$PATH"
2
3
4
5
# 进程替换
进程替换:将命令的输出转换为临时文件,使那些只能接受文件作为参数的命令也能使用命令输出。 <(...) 语法是进程替换,允许将命令输出作为文件传递给另一个命令
# 比较两个命令的输出
diff <(ls /path1) <(ls /path2)
# 将命令输出作为文件处理
cat <(echo "Hello World")
# 进程替换与管道结合
paste <(cut -d: -f1 /etc/passwd) <(cut -d: -f3 /etc/passwd)
2
3
4
5
6
7
8
# 变量扩展语法
${ZSH_CUSTOM:-~/.oh-my-zsh/custom}:${ZSH_CUSTOM:-~/.oh-my-zsh/custom}是一种 Shell 变量扩展语法, 用于在命令中引用一个变量。在这个特定的命令中,${ZSH_CUSTOM:-~/.oh-my-zsh/custom}表示一个变量, 它的值是$ZSH_CUSTOM的值, 如果$ZSH_CUSTOM未定义, 则使用默认值~/.oh-my-zsh/customremote_name=${1:-origin}: 如果没有第一个参数, 则设置则使用默认值 origin
更多变量扩展语法:
${var:=default}: 如果变量未设置或为空,则设置为默认值${var:?message}: 如果变量未设置或为空,则显示错误信息并退出${var:+alternative}: 如果变量已设置且非空,则使用替代值${var#pattern}: 删除最短匹配的前缀${var##pattern}: 删除最长匹配的前缀${var%pattern}: 删除最短匹配的后缀${var%%pattern}: 删除最长匹配的后缀
# Here Tag
在 Linux 中, <<(称为 Here Document 或 Here Tag)是一种用于输入多行文本的特殊语法。它允许将一段文本作为输入传递给命令或脚本, 而无需使用外部文件
具体来说, <<后面可以跟一个标识符(tag), 用于标识输入的结束。输入文本的开始和结束都由这个标识符来界定。使用<<时, 输入的文本会被 shell 进程处理, 然后传递给相应的命令或脚本
示例cat << END
这是一段
多行文本输入,
会被cat命令处理
END
2
3
4
5
在上面的示例中, cat 命令接收<< END 和 END 之间的多行文本作为输入, 并将其输出到标准输出
Here Document 的用途包括但不限于:
- 在脚本中嵌入大段的文本数据
- 在命令行中直接输入多行文本, 而无需创建临时文件
- 在配置文件或模板中插入变量和文本
需要注意的是, tag 可以是用户自定义的标识符, 只要它在输入文本中没有被使用即可。通常, tag 以大写字母开头, 但并不是强制要求
cat 多行文本写入
cat << END>config.pbtx
这是一段
多行文本输入,
会被 cat 命令处理
END
2
3
4
5
作用解释如下:
<< END开始了一个Here Document区块, END 是结束标记。你可以自行选择合适的结束标记, 只要它在文本中没有出现即可- 在开始标记和结束标记之间的所有文本都将被传递给 cat 命令
- 这个多行文本被写入名为 config.pbtx 的文件
三、赋值多行字符串给变量
info=$(cat << 'EOF'
这是一段
多行文本输入,
会被cat命令处理
EOF
)
echo "$info"
2
3
4
5
6
7
8
# 数组操作
# 数组定义和访问
# 定义数组
arr=("apple" "banana" "cherry")
# 访问数组元素
echo "${arr[0]}" # 第一个元素
echo "${arr[@]}" # 所有元素
echo "${#arr[@]}" # 数组长度
# 添加元素
arr+=("date")
# 遍历数组
for item in "${arr[@]}"; do
echo "$item"
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 关联数组 (Bash 4+)
# 声明关联数组
declare -A colors
colors["red"]="#FF0000"
colors["green"]="#00FF00"
# 访问
echo "${colors["red"]}"
# 遍历
for key in "${!colors[@]}"; do
echo "$key: ${colors[$key]}"
done
2
3
4
5
6
7
8
9
10
11
12
# 函数定义
# 基本函数语法
# 函数定义
function_name() {
local var1="$1" # 第一个参数
local var2="$2" # 第二个参数
# 函数体
echo "参数1: $var1, 参数2: $var2"
# 返回值
return 0
}
# 调用函数
function_name "hello" "world"
# 获取函数返回值
echo "退出状态: $?"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 函数参数处理
process_files() {
local count=0
for file in "$@"; do
if [ -f "$file" ]; then
echo "处理文件: $file"
((count++))
fi
done
echo "共处理 $count 个文件"
}
# 调用
process_files *.txt
2
3
4
5
6
7
8
9
10
11
12
13
# 错误处理
# 基本错误处理
# 启用错误时退出
set -e
# 启用管道错误退出
set -o pipefail
# 捕获错误
if ! command_that_might_fail; then
echo "命令执行失败" >&2
exit 1
fi
2
3
4
5
6
7
8
9
10
11
# 错误重定向
# 捕获错误输出
error_output=$(command 2>&1)
# 忽略错误
command 2>/dev/null || true
# 记录错误到文件
command 2>> error.log
2
3
4
5
6
7
8
# 作业控制
# 后台任务
# 后台运行命令
command &
# 查看后台任务
jobs
# 将任务移到前台
fg %1
# 将任务移到后台
bg %1
# 杀死任务
kill %1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 信号处理
# 信号处理函数
cleanup() {
echo "收到退出信号,清理中..."
rm -f /tmp/temp_file
exit 0
}
# 注册信号处理
trap cleanup SIGINT SIGTERM
2
3
4
5
6
7
8
9
# 输入验证
# 参数验证
# 检查参数数量
if [ $# -lt 2 ]; then
echo "用法: $0 <参数1> <参数2>" >&2
exit 1
fi
# 检查文件是否存在
if [ ! -f "$1" ]; then
echo "错误: 文件 '$1' 不存在" >&2
exit 1
fi
# 检查目录是否可写
if [ ! -w "$(dirname "$output")" ]; then
echo "错误: 无法写入目录" >&2
exit 1
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 输出重定向
<command> 2>/dev/null: 将stderr重定向到/dev/null, 从而禁止错误信息的显示<command> > /dev/null 2>&1: 将stdout和stderr都重定向到/dev/null, 使得没有输出到终端
是一种 Shell 中的标准输出(stdout)和标准错误输出(stderr)重定向的语法。这个语法的作用是将标准错误输出重定向到标准输出, 这样两者都将合并在一起输出到同一个地方
具体解释如下:2表示标准错误输出(stderr)的文件描述符>是重定向操作符, 用于将输出重定向到指定位置&1表示标准输出(stdout)的文件描述符。这里的&表示引用
# IFS
读取文件中内容 line by line
IFS(Internal Field Separator). It is an environment variable in shell scripting that determines how the shell interprets word boundaries when parsing input. 用在一行行读取文本
while IFS= read -r path; do
if [ -z "$path" ]; then
continue
fi
done < "$input_file"
2
3
4
5
By setting IFS= (to an empty value) before read -r, the command ensures that:
- Whitespace (spaces and tabs) is not treated as a delimiter. This is useful when you want to read lines that may contain spaces or other whitespace characters as part of the input rather than splitting them into separate fields.
- Entire line is read as a single entry into the variable (path in this case).
-roption in read prevents backslashes from being interpreted as escape characters, which helps in reading file paths or other strings that may contain special characters.
while IFS=' ' read -r path; do
echo "$path"
done <<< "abc ddd eee fff"
2
3
# test 命令
- 数值测试:
-eq|-ne|-gt|-ge|-lt|-le - 字符串测试:
-z|-n|=|!= - 文件测试:
-e|-r|-w|-x|-s|-d|-f|-c|-b
常用测试示例:
# 文件存在性检查
if [ -f "$file" ]; then
echo "File exists"
fi
# 目录检查
if [ -d "$dir" ]; then
echo "Directory exists"
fi
# 字符串非空检查
if [ -n "$var" ]; then
echo "Variable is not empty"
fi
# 数值比较
if [ "$num1" -gt "$num2" ]; then
echo "$num1 is greater than $num2"
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# string
操作字符串处理, 更多可见 (opens new window)
${#string}: 求$string的长度${string:position}: 在$string中, 从位置$position开始提取子串${string:position:length}: 在$string中, 从位置$position开始提取长度为$length的子串${string#substring}: 从变量$string的开头, 删除最短匹配$substring的子串${string##substring}: 从变量$string的开头, 删除最长匹配$substring的子串.- 如在脚本中使用
${0##*/}:$0是脚本被调用时使用的名称(可能是相对路径、绝对路径或只是文件名,取决于调用方式)。${0##*/}的效果是:从左边删除最长匹配*/的部分,提取出纯文件名。
- 如在脚本中使用
${string%substring}: 从变量$string的结尾, 删除最短匹配$substring的子串${string%%substring}: 从变量$string的结尾, 删除最长匹配$substring的子串${string/substring/replacement}: 使用$replacement, 来代替第一个匹配的$substring${string//substring/replacement}: 使用$replacement, 代替所有匹配的$substring${string/#substring/replacement}: 如果$string的前缀匹配$substring, 那么就用$replacement 来代替匹配到的$substring${string/%substring/replacement}: 如果$string的后缀匹配$substring, 那么就用$replacement 来代替匹配到的$substring
output_file="${file%.*}_compressed.jpg"
这是一个字符串操作, 用于删除字符串的后缀。在这里, %.* 表示删除字符串中最右边的一个.(包括.本身)及其之后的所有字符。因此, ${file%.*} 将给出文件名的部分, 去除了扩展名
# 输出着色
# 带颜色的输出函数, 使用 ANSI 转义码来实现带颜色的输出
color_echo() {
local color="$1"
local message="$2"
echo -e "\033[${color}m${message}\033[0m"
}
# 红色文本
red() {
local message="$1"
color_echo "0;31" "$message"
}
2
3
4
5
6
7
8
9
10
11
12
13
# 性能优化
# 避免外部命令调用
# 慢:使用外部命令
count=$(wc -l < file.txt)
# 快:使用内置功能
while IFS= read -r line; do
((count++))
done < file.txt
2
3
4
5
6
7
# 使用内置功能
# 避免使用 cat
cat file.txt | grep "pattern" # 慢
# 直接重定向
grep "pattern" file.txt # 快
2
3
4
5
# 批量操作
# 避免循环中的重复操作
for file in *.txt; do
# 重复的预处理
done
# 一次性处理
files=(*.txt)
# 预处理一次
2
3
4
5
6
7
8
# 最佳实践
# 脚本头部
#!/bin/bash
set -euo pipefail # 严格模式
IFS=$'\n\t' # 安全的IFS设置
2
3
# 变量引用
# 总是引用变量
echo "$variable" # 正确
echo $variable # 可能有问题
# 数组引用
echo "${array[@]}" # 正确
2
3
4
5
6
# 错误处理
# 检查命令是否成功
if ! command; then
echo "命令失败: $?" >&2
exit 1
fi
# 使用 || 提供默认值
result=$(command 2>/dev/null) || result="default"
2
3
4
5
6
7
8
# 临时文件处理
# 创建临时文件
temp_file=$(mktemp)
trap "rm -f $temp_file" EXIT
# 使用临时文件
echo "data" > "$temp_file"
2
3
4
5
6
# 调试技巧
# 调试选项
# 启用调试模式
set -x # 显示执行的命令
set -v # 显示读取的输入
# 在脚本中局部调试
set -x
# 调试代码
set +x
2
3
4
5
6
7
8
# 日志记录
# 创建日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >&2
}
log "开始处理文件"
2
3
4
5
6
# 常用工具函数
# 文件操作
# 安全备份
backup_file() {
local file="$1"
if [ -f "$file" ]; then
cp "$file" "${file}.bak.$(date +%Y%m%d_%H%M%S)"
fi
}
# 检查文件是否可写
is_writable() {
local file="$1"
[ -w "$(dirname "$file")" ] && [ ! -f "$file" -o -w "$file" ]
}
2
3
4
5
6
7
8
9
10
11
12
13
# 字符串处理
# 去除前后空格
trim() {
local var="$1"
var="${var#"${var%%[![:space:]]*}"}" # 去除前导空格
var="${var%"${var##*[![:space:]]}"}" # 去除尾随空格
echo "$var"
}
# 转换为小写
to_lower() {
echo "${1,,}"
}
# 转换为大写
to_upper() {
echo "${1^^}"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# link
- runoob.com (opens new window) linux shell 学习
- Bash Guide (opens new window) - 全面的Bash指南
- ShellCheck (opens new window) - Shell脚本静态分析工具
- Google Shell Style Guide (opens new window) - Shell编程规范