系统启动shell程序取决于个人的用户ID配置,在 /etc/passwd 文件中用户ID记录的第7个字段中列出了默认的shell程序,不过一般默认bash shell为默认shell。

脚本基础

Shebang与脚本结构

#!/bin/bash
# -*- coding: utf-8 -*-
# 脚本说明
 
# 严格模式(推荐)
set -euo pipefail   # -e: 出错退出  -u: 未定义变量报错  -o pipefail: 管道失败检测
 
# 脚本内容
echo "Hello, World!"

脚本执行

# 添加执行权限
chmod +x script.sh
 
# 执行方式
./script.sh                         # 直接执行
bash script.sh                      # 指定解释器
source script.sh                    # 在当前shell执行
. script.sh                         # 同source

变量

变量定义与使用

# 变量定义(等号两边不能有空格)
name="John"
age=25
 
# 使用变量
echo $name
echo ${name}                        # 推荐使用花括号
echo "My name is ${name}"           # 在字符串中使用
 
# 只读变量
readonly PI=3.14
 
# 删除变量
unset name

特殊变量

变量说明
$0脚本名称
$1~$9第1~9个参数
${10}第10个及以后的参数
$#参数个数
$*所有参数(作为单个字符串)
$@所有参数(作为独立字符串)
$?上条命令退出状态码
$$当前进程PID
$!最后一个后台进程PID

环境变量(environment variable)

bash shell使用环境变量的特性来存储有关shell会话和工作环境的信息,这项特性允许在内存中存储数据,以便程序或shell中运行的脚本能够轻松访问它们。

环境变量分为两类:全局环境变量(对所有子进程可见)和局部环境变量(仅在当前shell有效)。

全局环境变量

全局环境变量(也称导出变量)通过 export 命令设置,对所有子进程和子shell可见。

# 查看所有全局环境变量
env                                  # 显示所有环境变量
printenv                             # 同上
printenv HOME                        # 查看指定变量
 
# 设置全局环境变量
export MY_VAR="value"                # 导出为全局变量
export PATH="$PATH:/new/path"        # 追加路径
 
# 常用全局环境变量
$HOME       # 用户家目录
$PWD        # 当前工作目录
$PATH       # 命令搜索路径(冒号分隔)
$LANG       # 系统语言设置
$USER       # 当前用户名
$SHELL      # 当前shell路径
$EDITOR     # 默认编辑器
$DISPLAY    # X11显示器地址
 
# 持久化配置(写入配置文件)
# ~/.bashrc 或 ~/.bash_profile(用户级)
# /etc/environment 或 /etc/profile(系统级)

局部环境变量

局部环境变量仅在当前shell会话中有效,不会传递给子进程。适用于临时计算、脚本内部状态等场景。

# 定义局部变量(不使用export)
local_var="only in this shell"
echo $local_var                      # 当前shell可访问
 
# 在函数中声明局部变量
my_func() {
    local count=0                    # local关键字,函数作用域
    count=$((count + 1))
    echo $count
}
 
# 特殊的局部变量
$RANDOM     # 随机数(0-32767),每次访问生成新值
$SECONDS    # 脚本运行秒数
$LINENO     # 当前行号
$BASH_VERSION  # Bash版本
$BASHPID    # 当前进程PID(不同于$$)
 
# 查看所有变量(含局部)
set                                  # 显示所有变量和函数
declare -p                           # 显示所有变量的声明

变量作用域对比

# 示例:理解作用域差异
global_var="I am global"
local_var="I am local"
 
export global_var                    # 导出为全局
 
bash -c 'echo "global: $global_var"'  # 输出: global: I am global
bash -c 'echo "local: $local_var"'    # 输出: local: (空)
 
# 删除变量
unset global_var                     # 删除全局变量
unset local_var                      # 删除局部变量

用户自定义变量

除了系统预定义的环境变量,用户可以根据需要创建自己的变量。

设置局部用户自定义变量

# 基本定义(等号两边不能有空格)
my_name="John"
my_age=25
my_path="/home/user/documents"
 
# 使用变量
echo $my_name                        # John
echo "Hello, ${my_name}!"            # 推荐使用花括号
 
# 变量命名规则
# - 只能包含字母、数字、下划线
# - 不能以数字开头
# - 区分大小写
 
# 合法命名
_user_var="valid"
userVar="valid"
USER_VAR="valid"
 
# 非法命名(会报错)
# 2var="invalid"                    # 不能数字开头
# my-var="invalid"                   # 不能包含连字符
 
# 在脚本中定义临时变量
#!/bin/bash
input_file="data.txt"
output_dir="result/"
count=0
while read line; do
    count=$((count + 1))
done < "$input_file"
echo "共处理 $count 行"

设置全局环境变量

# 方法1:export 已定义的变量
my_global="shared value"
export my_global
 
# 方法2:定义并导出(一步完成)
export MY_CONFIG="/etc/myapp/config"
export API_KEY="your-secret-key"
 
# 方法3:追加到已有环境变量
export PATH="$PATH:/opt/myapp/bin"
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib"
 
# 持久化配置
# 用户级:编辑 ~/.bashrc 或 ~/.bash_profile
# 系统级:编辑 /etc/environment 或 /etc/profile.d/*.sh
 
# 示例:添加到 ~/.bashrc
echo 'export JAVA_HOME="/usr/lib/jvm/java-17"' >> ~/.bashrc
echo 'export PATH="$JAVA_HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc                      # 立即生效

删除环境变量

# 删除变量
unset my_var                         # 删除局部变量
unset MY_GLOBAL                      # 删除全局环境变量
 
# 检查变量是否存在
if [ -z "${MY_VAR+x}" ]; then
    echo "变量未定义"
fi
 
# 清空变量值(不删除变量)
MY_VAR=""
 
# 删除数组元素
arr=(a b c d)
unset arr[1]                         # 删除索引1的元素
echo ${arr[@]}                       # a c d
 
# 删除关联数组元素
declare -A config
config=([host]="localhost" [port]="8080")
unset config[port]                   # 删除port键

变量操作速查表

操作命令说明
定义局部变量var=value仅当前shell有效
定义全局变量export var=value子进程可见
查看变量值echo $varprintenv var输出变量值
查看所有变量setdeclare -p含局部变量
查看所有环境变量envprintenv仅全局变量
删除变量unset var完全删除
清空变量var=""变量存在但为空
检查变量存在[ -v var ][ -z "${var+x}" ]条件测试

数据类型

字符串操作

str="Hello World"
 
# 字符串长度
echo ${#str}                         # 11
 
# 子串提取
echo ${str:0:5}                      # Hello
echo ${str:6}                        # World
 
# 字符串替换
echo ${str/World/Linux}              # Hello Linux(替换第一个)
echo ${str//o/O}                     # HellO WOrld(替换所有)
 
# 删除匹配
echo ${str#Hello }                   # World(删除开头匹配)
echo ${str% World}                   # Hello(删除结尾匹配)
 
# 大小写转换
echo ${str^^}                        # HELLO WORLD
echo ${str,,}                        # hello world

数组操作

# 定义数组
arr=(1 2 3 4 5)
arr=([0]=1 [2]=3 [4]=5)              # 索引数组
declare -A assoc                     # 关联数组(需要bash 4.0+)
assoc=([name]=John [age]=25)
 
# 访问数组
echo ${arr[0]}                       # 第一个元素
echo ${arr[-1]}                      # 最后一个元素
echo ${arr[@]}                       # 所有元素
echo ${#arr[@]}                      # 数组长度
echo ${!arr[@]}                      # 所有索引
 
# 数组切片
echo ${arr[@]:1:3}                   # 从索引1开始取3个
 
# 添加元素
arr+=(6 7)
 
# 删除元素
unset arr[2]

数值运算

# 算术运算
echo $((1 + 2))                      # 3
echo $((5 * 3))                      # 15
echo $((10 / 3))                     # 3(整数除法)
echo $((10 % 3))                     # 1(取模)
 
# 自增自减
i=5
echo $((i++))                        # 5(先输出后自增)
echo $((++i))                        # 7(先自增后输出)
 
# 使用let
let i=i+1
let "i += 1"
 
# 使用expr
expr 1 + 2
 
# 使用bc(支持浮点)
echo "scale=2; 10/3" | bc            # 3.33

流程控制

条件判断

# if语句
if [ condition ]; then
    # code
fi
 
# if-else
if [ condition ]; then
    # code
else
    # code
fi
 
# if-elif-else
if [ condition1 ]; then
    # code
elif [ condition2 ]; then
    # code
else
    # code
fi

测试条件

# 文件测试
[ -e file ]      # 文件存在
[ -f file ]      # 是普通文件
[ -d file ]      # 是目录
[ -r file ]      # 可读
[ -w file ]      # 可写
[ -x file ]      # 可执行
[ -s file ]      # 文件非空
[ file1 -nt file2 ]  # file1比file2新
 
# 字符串测试
[ -z str ]       # 字符串为空
[ -n str ]       # 字符串非空
[ str1 = str2 ]  # 字符串相等
[ str1 != str2 ] # 字符串不等
 
# 数值测试
[ n1 -eq n2 ]    # 等于
[ n1 -ne n2 ]    # 不等于
[ n1 -gt n2 ]    # 大于
[ n1 -ge n2 ]    # 大于等于
[ n1 -lt n2 ]    # 小于
[ n1 -le n2 ]    # 小于等于
 
# 逻辑运算
[ condition1 -a condition2 ]  # 与
[ condition1 -o condition2 ]  # 或
[ ! condition ]               # 非
 
# 使用[[ ]](推荐,支持正则)
[[ $str == pattern* ]]
[[ $str =~ regex ]]

循环语句

# for循环
for i in 1 2 3 4 5; do
    echo $i
done
 
for i in {1..5}; do
    echo $i
done
 
for i in $(seq 1 5); do
    echo $i
done
 
# C风格for循环
for ((i=0; i<5; i++)); do
    echo $i
done
 
# while循环
while [ condition ]; do
    # code
done
 
# 读取文件
while read line; do
    echo $line
done < file.txt
 
# until循环
until [ condition ]; do
    # code
done
 
# 循环控制
break        # 跳出循环
continue     # 跳过本次迭代

case语句

case $var in
    pattern1)
        # code
        ;;
    pattern2)
        # code
        ;;
    *)
        # 默认处理
        ;;
esac
 
# 示例
case $1 in
    start)
        echo "Starting..."
        ;;
    stop)
        echo "Stopping..."
        ;;
    restart)
        echo "Restarting..."
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        ;;
esac

select语句

select语句用于创建交互式菜单,常用于需要用户选择的场景。

# 基本语法
select var in option1 option2 option3; do
    case $var in
        option1)
            echo "选择了选项1"
            break
            ;;
        option2)
            echo "选择了选项2"
            break
            ;;
        *)
            echo "无效选择,请重试"
            ;;
    esac
done
 
# 示例:文件操作菜单
PS3="请选择操作: "  # 设置提示符
select action in "查看" "编辑" "删除" "退出"; do
    case $action in
        "查看")
            echo "查看文件..."
            ;;
        "编辑")
            echo "编辑文件..."
            ;;
        "删除")
            echo "删除文件..."
            ;;
        "退出")
            break
            ;;
        *)
            echo "无效选择"
            ;;
    esac
done

getopts参数解析

getopts用于解析命令行选项,适合处理短选项(如 -a-b value)。

# 基本语法
while getopts ":a:b:c" opt; do
    case $opt in
        a)
            echo "选项 -a,参数值: $OPTARG"
            ;;
        b)
            echo "选项 -b,参数值: $OPTARG"
            ;;
        c)
            echo "选项 -c,无参数"
            ;;
        \?)
            echo "无效选项: -$OPTARG"
            exit 1
            ;;
        :)
            echo "选项 -$OPTARG 需要参数"
            exit 1
            ;;
    esac
done
shift $((OPTIND-1))  # 移除已处理的选项
 
# 示例:带选项的脚本
#!/bin/bash
# usage: script.sh -f file -o output -v
 
verbose=false
 
while getopts ":f:o:v" opt; do
    case $opt in
        f) input_file="$OPTARG" ;;
        o) output_file="$OPTARG" ;;
        v) verbose=true ;;
        \?) echo "未知选项: -$OPTARG" >&2; exit 1 ;;
        :) echo "选项 -$OPTARG 需要参数" >&2; exit 1 ;;
    esac
done
 
if [ -z "$input_file" ]; then
    echo "必须指定 -f 参数"
    exit 1
fi
 
[ "$verbose" = true ] && echo "处理文件: $input_file"

函数

函数定义与调用

# 函数定义
function_name() {
    # code
    return value
}
 
# 或使用function关键字
function function_name {
    # code
}
 
# 调用函数
function_name arg1 arg2
 
# 函数内访问参数
$1, $2, ...                         # 函数参数

函数返回值

get_random() {
    echo $RANDOM                    # 通过echo返回
}
 
result=$(get_random)                # 捕获返回值
 
check_file() {
    [ -f "$1" ] && return 0 || return 1  # 通过return返回状态码
}
 
if check_file "/etc/passwd"; then
    echo "文件存在"
fi

局部变量

my_func() {
    local var="local value"         # 局部变量
    echo $var
}

递归函数

Bash支持函数递归调用,适合处理树形结构、阶乘等场景。

# 计算阶乘
factorial() {
    local n=$1
    if [ $n -le 1 ]; then
        echo 1
    else
        local prev=$(factorial $((n - 1)))
        echo $((n * prev))
    fi
}
 
result=$(factorial 5)               # 120
 
# 斐波那契数列
fibonacci() {
    local n=$1
    if [ $n -le 0 ]; then
        echo 0
    elif [ $n -eq 1 ]; then
        echo 1
    else
        echo $(( $(fibonacci $((n-1))) + $(fibonacci $((n-2))) ))
    fi
}
 
# 目录递归遍历
list_files() {
    local dir=$1
    for item in "$dir"/*; do
        if [ -d "$item" ]; then
            echo "目录: $item"
            list_files "$item"      # 递归调用
        elif [ -f "$item" ]; then
            echo "文件: $item"
        fi
    done
}
 
list_files "/path/to/directory"

输入输出

用户输入

# 读取用户输入
read var                            # 读取到变量
read -p "Enter name: " name         # 带提示
read -s password                     # 隐藏输入
read -t 10 var                      # 10秒超时
read -a arr                         # 读取到数组

输出

# 基本输出
echo "Hello"
printf "Name: %s, Age: %d\n" $name $age
 
# 颜色输出
echo -e "\033[31mRed\033[0m"        # 红色
echo -e "\033[32mGreen\033[0m"      # 绿色
echo -e "\033[33mYellow\033[0m"     # 黄色
echo -e "\033[34mBlue\033[0m"       # 蓝色
 
# 颜色变量
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo -e "${GREEN}Success${NC}"

Here Document

# 多行文本
cat << EOF
Line 1
Line 2
Line 3
EOF
 
# 写入文件
cat << EOF > file.txt
Line 1
Line 2
EOF
 
# 变量替换
name="John"
cat << EOF
Hello, $name
EOF
 
# 不替换变量
cat << 'EOF'
Hello, $name
EOF

正则表达式与文本处理

grep

grep "pattern" file                 # 基本搜索
grep -i "pattern" file              # 忽略大小写
grep -v "pattern" file              # 反向匹配
grep -r "pattern" dir/              # 递归搜索
grep -n "pattern" file              # 显示行号
grep -E "regex" file                # 扩展正则
grep -o "pattern" file              # 只显示匹配部分
grep -c "pattern" file              # 统计匹配行数

sed

# 替换
sed 's/old/new/' file               # 替换每行第一个
sed 's/old/new/g' file              # 替换所有
sed 's/old/new/2' file              # 替换每行第2个
sed -i 's/old/new/g' file           # 直接修改文件
 
# 删除
sed '2d' file                       # 删除第2行
sed '/pattern/d' file               # 删除匹配行
sed '1,5d' file                     # 删除1-5行
 
# 插入
sed '2i\text' file                  # 在第2行前插入
sed '2a\text' file                  # 在第2行后追加

awk

# 基本用法
awk '{print $1}' file               # 打印第一列
awk '{print $1, $3}' file           # 打印第1和第3列
awk '{print NF}' file               # 打印列数
awk '{print NR}' file               # 打印行号
 
# 指定分隔符
awk -F: '{print $1}' /etc/passwd
 
# 条件过滤
awk '$3 > 1000' file                # 第3列大于1000的行
awk '/pattern/' file                # 匹配pattern的行
 
# 内置变量
NR    # 当前行号
NF    # 当前列数
FS    # 输入字段分隔符
OFS   # 输出字段分隔符
RS    # 输入记录分隔符
ORS   # 输出记录分隔符
 
# 示例:统计日志
awk '{count[$1]++} END {for(ip in count) print ip, count[ip]}' access.log

错误处理

错误检测

# 检查命令执行结果
if ! command; then
    echo "命令执行失败"
    exit 1
fi
 
# 检查变量
if [ -z "$var" ]; then
    echo "变量未设置"
    exit 1
fi
 
# 捕获错误输出
output=$(command 2>&1)
if [ $? -ne 0 ]; then
    echo "错误: $output"
fi

trap信号处理

常用信号列表

信号数值说明触发方式
SIGINT2中断信号Ctrl+C
SIGQUIT3退出信号Ctrl+\
SIGKILL9强制终止kill -9
SIGTERM15终止信号kill 默认
SIGHUP1挂起信号终端关闭
SIGSTOP19暂停进程Ctrl+Z
SIGCONT18继续运行fg/bg
EXIT-脚本退出任意退出
# 捕获信号
trap 'echo "收到中断信号"; exit 1' INT
trap 'echo "清理临时文件"; rm -f /tmp/temp_*' EXIT
trap 'echo "收到SIGTERM"; exit 1' TERM
 
# 清理函数
cleanup() {
    rm -f /tmp/temp_*
    echo "清理完成"
}
trap cleanup EXIT
 
# 捕获多个信号
trap 'echo "收到信号,正在退出..."; exit 1' INT TERM HUP
 
# 忽略信号
trap '' INT                        # 忽略Ctrl+C

调试

# 调试模式
bash -x script.sh                   # 显示执行的每条命令
bash -n script.sh                   # 语法检查
bash -v script.sh                   # 显示读取的每行
 
# 在脚本中启用调试
set -x                              # 开启调试
set +x                              # 关闭调试
 
# 调试函数
debug() {
    [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"
}
debug "变量值: $var"

子Shell与进程替换

子Shell

子Shell是在当前Shell中创建的独立进程,变量的修改不会影响父Shell。

# 使用()创建子Shell
(cd /tmp && ls)                     # 子Shell执行,不影响当前目录
 
# 子Shell中的变量隔离
x=10
(
    x=20                            # 子Shell中修改
    echo "子Shell中: $x"             # 20
)
echo "父Shell中: $x"                 # 10,不受影响
 
# 后台任务与子Shell
(sleep 10 && echo "后台任务完成") &
 
# 子Shell继承父Shell的变量(导出的)
export MY_VAR="shared"
(
    echo "$MY_VAR"                   # 可以访问
    MY_VAR="changed"                # 不影响父Shell
)
echo "$MY_VAR"                       # 仍然是 shared

进程替换

进程替换将命令的输出/输入当作文件处理,格式为 <(cmd)>(cmd)

# 输出重定向到进程
exec > >(tee -a output.log)         # 所有输出同时显示和记录
 
# 比较两个命令的输出
diff <(ls dir1) <(ls dir2)          # 比较两个目录内容
 
# 合并多个文件内容
sort -m <(sort file1) <(sort file2) > merged.txt
 
# 将输出写入多个进程
echo "data" | tee >(process1) >(process2) > /dev/null
 
# 读取多个进程的输出
while read line1; do
    read line2 <&3
    echo "$line1 vs $line2"
done < <(command1) 3< <(command2)
 
# 实际应用:配置文件合并
comm -23 <(sort config1.conf | uniq) <(sort config2.conf | uniq)

命令替换

命令替换将命令的输出赋值给变量,有两种写法。

# 推荐写法: $()
current_dir=$(pwd)
files=$(ls *.txt)
date_str=$(date +%Y%m%d)
 
# 旧写法: `` (反引号)
current_dir=`pwd`                   # 不推荐,嵌套时不直观
 
# 嵌套示例
echo "今天是 $(date +%A),$(date +%Y年%m月%d日)"
 
# 命令替换与管道
lines=$(cat file.txt | wc -l)
biggest=$(du -sh * | sort -hr | head -1)
 
# 在循环中使用
for file in $(find . -name "*.txt"); do
    echo "处理: $file"
done
 
# 注意:命令替换会移除末尾的换行符
output=$'hello\n\n'
echo "[${output}]"                   # [hello],换行被移除

💡 脚本编程建议:

  • 始终使用 set -euo pipefail 开启严格模式
  • 使用双引号包裹变量,防止空格问题
  • 使用 [[ ]] 替代 [ ] 进行条件测试
  • 添加详细的注释和帮助信息
  • 使用函数封装重复逻辑
  • 善用 shellcheck 工具检查脚本语法
  • 重要脚本添加 --help--version 选项

🔗 相关笔记: 02.01_帮助 02.02_快捷键 10.01_Shell自动化脚本