Shell 脚本编程完全指南
Shell 脚本编程是 Linux 系统管理员和开发者的必备技能。通过编写 Shell 脚本,您可以自动化重复性任务、简化复杂操作并提高工作效率。本指南将详细介绍 Shell 脚本编程的核心概念,从变量开始逐步深入。
Shell 脚本简介
什么是 Shell 脚本
Shell 脚本是一种解释型编程语言,通过 Shell 命令的序列来完成特定任务。它是实现自动化运维、批处理任务和系统管理的强大工具。
主要 Shell 类型:
- Bash (Bourne Again Shell):Linux 默认 Shell,功能强大
- Sh (Bourne Shell):最原始的 Shell
- Zsh:功能丰富的交互式 Shell
- Fish:用户友好的现代 Shell
Shell 脚本的优势
- 自动化:批量处理重复性任务
- 简化操作:将复杂命令组合成简单脚本
- 可移植性:Shell 脚本在类 Unix 系统中广泛支持
- 易于学习:语法简单,上手容易
- 高效开发:快速编写和测试脚本
Shell 变量详解
变量命名规则
Shell 变量的命名必须遵循以下规则:
有效命名:
- ✅ 只能使用英文字母、数字和下划线
- ✅ 首个字符不能以数字开头
- ✅ 可以使用下划线
_ - ✅ 大小写敏感
无效命名:
- ❌ 不能包含空格
- ❌ 不能使用标点符号(如
?、*、&等) - ❌ 不能使用 Shell 关键字(如
if、for、while等)
有效变量名示例
# 标准命名
RUNOOB
LD_LIBRARY_PATH
_var
var2
my_variable_name
USER_NAME
PI=3.14159
无效变量名示例
# 错误示例
?var=123 # 以特殊字符开头
user*name=runoob # 包含特殊字符
my var=123 # 包含空格
2var=456 # 以数字开头
变量赋值
1. 直接赋值
# 基本赋值
name="张三"
age=25
salary=5000.50
is_student=true
# 空变量
empty_var=""
null_var=
2. 从命令赋值
# 使用反引号(推荐使用 $() 替代 )
current_user=`whoami`
current_date=`date +%Y-%m-%d`
file_count=`ls -1 | wc -l`
# 使用 $()(现代写法)
current_time=$(date +%H:%M:%S)
home_directory=$(echo ~)
disk_usage=$(df -h / | awk 'NR==2 {print $5}')
3. 循环赋值
# 遍历目录
for file in `ls /etc`; do
echo "文件: $file"
done
# 现代写法
for dir in $(ls /home); do
echo "用户目录: $dir"
done
# 遍历数字序列
for i in {1..5}; do
echo "数字: $i"
done
# 遍历数组元素
colors=("红" "绿" "蓝")
for color in "${colors[@]}"; do
echo "颜色: $color"
done
使用变量
基本使用
# 定义变量
your_name="qinjx"
# 使用变量(两种方式)
echo $your_name
echo ${your_name}
# 推荐:使用花括号明确边界
for skill in Ada Coffe Action Java; do
echo "I am good at ${skill}Script"
done
为什么要使用花括号?
# 正确:使用花括号
echo "我擅长 ${skill}Script"
# 输出:我擅长 JavaScript
# 错误:不使用花括号
echo "我擅长 $skillScript"
# 输出:我擅长 ($skillScript 被当作一个空变量)
变量重新定义
# 第一次赋值
your_name="tom"
echo $your_name
# 输出:tom
# 重新赋值
your_name="alibaba"
echo $your_name
# 输出:alibaba
# 注意:赋值时不要使用 $
your_name="alibaba" # 正确
# $your_name="alibaba" # 错误
只读变量
使用 readonly 命令创建只读变量,其值不能被修改:
#!/bin/bash
# 定义只读变量
myUrl="https://www.google.com"
readonly myUrl
# 尝试修改只读变量(会报错)
myUrl="https://www.runoob.com"
# 输出:/bin/sh: myUrl: This variable is read only.
实际应用场景:
#!/bin/bash
# 定义系统常量
readonly OS_NAME=$(uname)
readonly SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
readonly LOG_FILE="/var/log/myscript.log"
readonly MAX_RETRIES=3
readonly TIMEOUT=30
# 只读环境变量
export readonly API_KEY="your-secret-api-key"
删除变量
使用 unset 命令删除变量:
#!/bin/sh
myUrl="https://www.runoob.com"
echo "删除前: $myUrl"
# 输出:删除前:https://www.runoob.com
unset myUrl
echo "删除后: $myUrl"
# 输出:删除后:(空)
# 注意:unset 不能删除只读变量
变量删除的实际应用:
#!/bin/bash
# 临时变量
temp_file="/tmp/temp_$$"
echo "临时文件: $temp_file"
# 执行操作
perform_operation > $temp_file
# 清理临时文件
unset temp_file
# 只读变量无法删除
readonly CONSTANT="无法删除"
unset CONSTANT # 这行会报错
变量类型
Shell 中存在三种变量类型:
1. 局部变量
#!/bin/bash
# 局部变量:在函数或脚本 中定义
local_variable="我在函数内"
global_variable="我在函数外"
function test_function() {
local local_var="局部变量"
echo "函数内访问: $local_var"
echo "函数内访问全局: $global_variable"
}
test_function
echo "函数外访问局部: $local_variable" # 输出空
echo "函数外访问全局: $global_variable" # 输出:我...
2. 环境变量
# 查看所有环境变量
env
# 查看特定环境变量
echo $PATH
echo $HOME
echo $USER
echo $SHELL
# 设置环境变量
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export PATH=$PATH:$JAVA_HOME/bin
# 在脚本中设置环境变量
#!/bin/bash
export DB_HOST="localhost"
export DB_PORT="5432"
export DB_USER="admin"
echo "数据库配置:"
echo "主机: $DB_HOST"
echo "端口: $DB_PORT"
echo "用户: $DB_USER"
3. Shell 变量
# Shell 内置变量(自动设置)
echo "当前脚本名称: $0"
echo "第一个参数: $1"
echo "所有参数: $@"
echo "参数个数: $#"
echo "上一个命令的退出码: $?"
echo "当前进程 ID: $$"
echo "后台任务的进程 ID: $!"
echo "当前选项: $-"
# 示例
#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数个数: $#"
Shell 字符串操作
字符串是 Shell 编程中最常用的数据类型。Shell 支持单引号、双引号和无引号三种方式。
单引号 vs 双引号
单引号字符串
str='this is a string'
# 特性:
# - 原样输出,不会解析变量
# - 不能包含单引号(即使转义也不行)
# - 适用于纯文本内容
# 示例
name="张三"
echo '$name'
# 输出:$name
echo '当前用户:$USER'
# 输出:当前用户:$USER
echo '不能包含单引号:It\'s'
# 输出错误或意外结果
单引号的高级用法:
# 拼接字符串(成对出现)
greeting='hello, '$USER' !'
echo $greeting
# 输出:hello, 当前用户名 !
# 包含特殊字符
echo '文件名:test*.txt'
# 输出:文件名:test*.txt(不会展开通配符)
双引号字符串
your_name="runoob"
str="Hello, I know you are \"$your_name\"! \n"
# 特性:
# - 可以解析变量
# - 可以使用转义字符
# - 适用于包含变量的字符串
# 示例
name="李四"
echo "我的名字是 $name"
# 输出:我的名字是 李四
echo "当前目录:$(pwd)"
# 输出:当前目录:/当前路径
# 转义字符
echo "第一行\n第二行"
# 输出:
# 第一行
# 第二行
# 包含引号
echo "他说:\"Hello, World!\""
# 输出:他说:"Hello, World!"
无引号
# 适用场景:简单单词
word=hello
echo $word
# 不适用:包含空格或特殊字符
# message=hello world # 错误
字符串拼接
使用双引号拼接
your_name="runoob"
# 方法 1:直接拼接
greeting="hello, "$your_name" !"
echo $greeting
# 输出:hello, runoob !
# 方法 2:使用花括号
greeting_1="hello, ${your_name} !"
echo $greeting_1
# 输出:hello, runoob !
# 方法 3:混合使用
welcome="欢迎 "
full_message="${welcome}${your_name}"
echo $full_message
# 输出:欢迎 runoob
使用单引号拼接
your_name="runoob"
# 方法 1:成对单引号
greeting_2='hello, '$your_name' !'
echo $greeting_2
# 输出:hello, runoob !
# 方法 2:单引号不解析变量
greeting_3='hello, ${your_name} !'
echo $greeting_3
# 输出:hello, ${your_name} !
高级拼接技巧
# 多行拼接
message="第1行\
第2行\
第3行"
echo "$message"
# 输出:三行连在一起
# 使用换行符
message="第1行
第2行
第3行"
echo "$message"
# 数组拼接
parts=("第一部分" "第二部分" "第三部分")
result="${parts[0]} ${parts[1]} ${parts[2]}"
echo $result
# 输出:第一部分 第二部分 第三部分
字符串长度
string="abcd"
# 获取字符串长度
echo ${#string}
# 输出:4
# 实际应用
password="mypassword123"
password_length=${#password}
if [ $password_length -lt 8 ]; then
echo "密码长度不足8位"
else
echo "密码长度符合要求"
fi
# 计算文件路径长度
path="/home/user/documents/project/file.txt"
path_length=${#path}
echo "路径长度: $path_length"
# 动态计算
dynamic_string=$(date)
length=${#dynamic_string}
echo "当前日期字符串长度: $length"
提取子字符串
string="runoob is a great site"
# 语法:${string:start:length}
# start:从 0 开始计数
# length:提取的字符数
# 示例:从第 2 个字符开始,提取 4 个字符
echo ${string:1:4}
# 输出:unoo
# 完整示例
text="Hello World"
echo ${text:0:5} # 输出:Hello
echo ${text:6:5} # 输出:World
echo ${text:-5} # 输出:World(从右边开始取5个字符)
# 实际应用
url="https://example.com/path/to/resource"
# 提取协议
protocol=${url%%://*} # 输出:https
# 提取域名
domain=${url#*//} # 输出:example.com/path/to/resource
domain=${domain%%/*} # 输出:example.com
# 提取路径
path=${url#*//*/*/} # 输出:resource
查找子字符串
string="runoob is a great site"
# 查找字符位置(使用 expr index)
echo `expr index "$string" io`
# 输出:4(i 在第 4 个位置)
# 实际应用
search_string="hello world"
character="world"
position=$(expr index "$search_string" "$character")
if [ $position -gt 0 ]; then
echo "找到 '$character' 在位置 $position"
else
echo "未找到 '$character'"
fi
# 查找字符串包含
if [[ "$string" == *"runoob"* ]]; then
echo "字符串包含 'runoob'"
fi
# 查找字符串前缀
if [[ "$string" == runoob* ]]; then
echo "字符串以 'runoob' 开头"
fi
# 查找字符串后缀
if [[ "$string" == *.com ]]; then
echo "字符串以 '.com' 结尾"
fi
字符串替换
string="Hello World, Hello Universe"
# 替换第一个匹配项
echo ${string/Hello/Hi}
# 输出:Hi World, Hello Universe
# 替换所有匹配项
echo ${string//Hello/Hi}
# 输出:Hi World, Hi Universe
# 替换开头
echo ${string/#Hello/Hi}
# 输出:Hi World, Hello Universe
# 替换结尾
echo ${string/%Universe/Galaxy}
# 输出:Hello World, Hello Galaxy
# 实际应用
# 替换文件扩展名
filename="document.txt"
new_filename="${filename%.txt}.md"
echo $new_filename
# 输出:document.md
# 替换路径分隔符
path="C:/Users/John/Documents"
unix_path="${path//\\//}"
echo $unix_path
# 输出:C:/Users/John/Documents
# 替换空格为下划线
name="John Doe"
formatted_name="${name// /_}"
echo $formatted_name
# 输出:John_Doe
字符串删除
string="prefix_content_suffix"
# 删除前缀
echo ${string#prefix_}
# 输出:content_suffix
# 删除最长匹配前缀
echo ${string##prefix_}
# 输出:content_suffix
# 删除后缀
echo ${string%_suffix}
# 输出:prefix_content
# 删除最长匹配后缀
echo ${string%%_suffix}
# 输出:prefix_content
# 实际应用
# 提取文件名
filepath="/home/user/document.txt"
filename="${filepath##*/}"
echo $filename
# 输出:document.txt
# 提取目录路径
dirpath="${filepath%/*}"
echo $dirpath
# 输出:/home/user
# 提取文件扩展名
extension="${filename##*.}"
echo $extension
# 输出:txt
# 提取不带扩展名的文件名
basename="${filename%.*}"
echo $basename
# 输出:document
字符串操作高级技巧
条件判断
# 检查字符串是否为空
str=""
if [ -z "$str" ]; then
echo "字符串为空"
fi
# 检查字符串是否非空
if [ -n "$str" ]; then
echo "字符串非空"
fi
# 字符串比较
str1="hello"
str2="world"
if [ "$str1" = "$str2" ]; then
echo "字符串相等"
else
echo "字符串不相等"
fi
# 字符串模式匹配
string="hello123world"
if [[ "$string" =~ [0-9]+ ]]; then
echo "字符串包含数字"
fi
字符串大小写转换
# Bash 4.0+ 支持大小写转换
string="Hello World"
# 转为大写
echo ${string^^}
# 输出:HELLO WORLD
# 转为小写
echo ${string,,}
# 输出:hello world
# 实际应用
username="JohnDoe"
# 检查用户名是否为大写
if [[ "$username" == *"[[:upper:]]"* ]]; then
echo "用户名包含大写字母"
fi
# 统一转为小写处理
email="JohnDoe@Example.Com"
normalized_email="${email,,}"
echo $normalized_email
# 输出:johndoe@example.com
字符串填充
# 右填充(使用 printf)
number="123"
padded_number=$(printf "%05d" $number)
echo $padded_number
# 输出:00123
# 左填充
text="hello"
padded_text=$(printf "%10s" "$text")
echo "'$padded_text'"
# 输出:' hello'
# 自定义填充字符
text="test"
padded_text=$(printf "%10s" "$text" | tr ' ' '*')
echo "'$padded_text'"
# 输出:'******test'