系统启动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 $var 或 printenv var | 输出变量值 |
| 查看所有变量 | set 或 declare -p | 含局部变量 |
| 查看所有环境变量 | env 或 printenv | 仅全局变量 |
| 删除变量 | 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}"
;;
esacselect语句
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
donegetopts参数解析
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"
fitrap信号处理
常用信号列表
| 信号 | 数值 | 说明 | 触发方式 |
|---|---|---|---|
SIGINT | 2 | 中断信号 | Ctrl+C |
SIGQUIT | 3 | 退出信号 | Ctrl+\ |
SIGKILL | 9 | 强制终止 | kill -9 |
SIGTERM | 15 | 终止信号 | kill 默认 |
SIGHUP | 1 | 挂起信号 | 终端关闭 |
SIGSTOP | 19 | 暂停进程 | Ctrl+Z |
SIGCONT | 18 | 继续运行 | 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自动化脚本