Linux服务器管理完全攻略 第6篇:Shell脚本基础
Linux Server Administration Complete Guide Part 6
Linux服务器管理完全攻略系列
第5篇:SSH与远程管理 | 第6篇:Shell脚本基础 (当前) | 第7篇:Shell脚本高级
前言:用Shell脚本进入自动化世界
手动处理重复性工作既浪费时间又容易出错。利用Shell脚本可以自动化复杂任务,实现一致的服务器管理。本篇讲解Bash Shell脚本的基础知识。
1. Shell脚本入门
1.1 第一个脚本
#!/bin/bash
# 第一个Shell脚本
# 文件名: hello.sh
echo "Hello, World!"
echo "当前时间: $(date)"
echo "当前用户: $USER"
echo "当前目录: $PWD"
1.2 执行脚本
# 创建脚本文件
vim hello.sh
# 赋予执行权限
chmod +x hello.sh
# 执行方法
./hello.sh # 从当前目录执行
bash hello.sh # 用bash直接执行
/path/to/hello.sh # 用绝对路径执行
# 无执行权限时执行
bash hello.sh
sh hello.sh
1.3 理解Shebang
#!/bin/bash # 使用Bash shell
#!/bin/sh # 使用POSIX兼容shell
#!/usr/bin/env bash # 从环境中查找bash(可移植性好)
#!/usr/bin/python3 # Python脚本
#!/usr/bin/perl # Perl脚本
2. 变量
2.1 变量声明和使用
#!/bin/bash
# 变量声明(=两边不能有空格)
name="张三"
age=25
readonly PI=3.14159 # 只读变量
# 使用变量
echo "姓名: $name"
echo "年龄: ${age}岁" # 推荐使用大括号
# 删除变量
unset name
# 设置默认值
echo "${undefined_var:-默认值}" # 如果为空则输出默认值
echo "${undefined_var:=新值}" # 如果为空则赋值新值
echo "${name:?变量不存在}" # 如果为空则输出错误
2.2 特殊变量
#!/bin/bash
# special_vars.sh arg1 arg2 arg3
echo "脚本名: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "所有参数: $@"
echo "所有参数(字符串): $*"
echo "参数个数: $#"
echo "当前进程ID: $$"
echo "最后一个后台进程ID: $!"
echo "最后一个命令退出码: $?"
2.3 环境变量
#!/bin/bash
# 主要环境变量
echo "主目录: $HOME"
echo "路径: $PATH"
echo "当前Shell: $SHELL"
echo "用户名: $USER"
echo "主机名: $HOSTNAME"
echo "终端类型: $TERM"
# 设置环境变量
export MY_VAR="my value"
# 查看所有环境变量
env
printenv
2.4 数组
#!/bin/bash
# 数组声明
fruits=("苹果" "香蕉" "橙子" "葡萄")
# 数组访问
echo "第一个: ${fruits[0]}"
echo "第三个: ${fruits[2]}"
echo "所有元素: ${fruits[@]}"
echo "数组长度: ${#fruits[@]}"
# 修改数组
fruits[1]="芒果"
fruits+=("猕猴桃") # 添加元素
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 关联数组(Bash 4+)
declare -A person
person[name]="张三"
person[age]=25
person[city]="北京"
echo "姓名: ${person[name]}"
echo "所有键: ${!person[@]}"
3. 运算符
3.1 算术运算
#!/bin/bash
a=10
b=3
# 算术运算方法
echo "加法: $((a + b))"
echo "减法: $((a - b))"
echo "乘法: $((a * b))"
echo "除法: $((a / b))"
echo "取模: $((a % b))"
echo "幂运算: $((a ** 2))"
# let命令
let "c = a + b"
echo "let结果: $c"
# expr命令(需要空格)
result=$(expr $a + $b)
echo "expr结果: $result"
# 自增运算符
((a++))
echo "自增后: $a"
# 小数计算(使用bc)
result=$(echo "scale=2; 10 / 3" | bc)
echo "小数计算: $result"
3.2 字符串操作
#!/bin/bash
str="Hello, World!"
# 字符串长度
echo "长度: ${#str}"
# 子字符串
echo "子串: ${str:0:5}" # Hello
echo "后5个字符: ${str: -5}" # orld!
# 字符串替换
echo "替换: ${str/World/China}" # 只替换第一个
echo "全部替换: ${str//o/O}" # 替换所有o为O
# 字符串删除
filename="document.txt.bak"
echo "删除扩展名: ${filename%.*}" # document.txt
echo "删除全部扩展名: ${filename%%.*}" # document
echo "删除路径: ${filename#*/}"
echo "删除前缀: ${filename##*.}" # bak
# 大小写转换(Bash 4+)
text="Hello World"
echo "大写: ${text^^}"
echo "小写: ${text,,}"
4. 条件语句
4.1 if语句
#!/bin/bash
age=20
# 基本if语句
if [ $age -ge 18 ]; then
echo "成年人"
fi
# if-else
if [ $age -ge 18 ]; then
echo "成年人"
else
echo "未成年人"
fi
# if-elif-else
if [ $age -lt 13 ]; then
echo "儿童"
elif [ $age -lt 20 ]; then
echo "青少年"
else
echo "成年人"
fi
# 使用[[ ]](更多功能,Bash专用)
name="张三"
if [[ $name == "张三" && $age -ge 18 ]]; then
echo "成年人张三"
fi
4.2 比较运算符
#!/bin/bash
# 数字比较
a=10
b=20
[ $a -eq $b ] # 等于(equal)
[ $a -ne $b ] # 不等于(not equal)
[ $a -lt $b ] # 小于(less than)
[ $a -le $b ] # 小于等于(less or equal)
[ $a -gt $b ] # 大于(greater than)
[ $a -ge $b ] # 大于等于(greater or equal)
# 字符串比较
str1="hello"
str2="world"
[ "$str1" = "$str2" ] # 相等
[ "$str1" != "$str2" ] # 不相等
[ -z "$str1" ] # 空字符串
[ -n "$str1" ] # 非空
# [[ ]]中的字符串比较
[[ $str1 == $str2 ]] # 相等
[[ $str1 < $str2 ]] # 字典序比较
[[ $str1 =~ ^h ]] # 正则匹配
4.3 文件测试
#!/bin/bash
file="/etc/passwd"
dir="/home"
# 文件存在和类型
[ -e $file ] # 存在
[ -f $file ] # 普通文件
[ -d $dir ] # 目录
[ -L $file ] # 符号链接
[ -b $file ] # 块设备
[ -c $file ] # 字符设备
[ -S $file ] # 套接字
# 文件权限
[ -r $file ] # 可读
[ -w $file ] # 可写
[ -x $file ] # 可执行
# 文件属性
[ -s $file ] # 大小不为零
[ -O $file ] # 所有者是当前用户
[ -G $file ] # 所属组是当前组
# 文件比较
[ $file1 -nt $file2 ] # 更新(newer than)
[ $file1 -ot $file2 ] # 更旧(older than)
[ $file1 -ef $file2 ] # 同一文件(同一inode)
# 示例
if [ -f "/etc/passwd" ] && [ -r "/etc/passwd" ]; then
echo "passwd文件存在且可读"
fi
4.4 case语句
#!/bin/bash
echo "请选择水果(1-4):"
echo "1) 苹果"
echo "2) 香蕉"
echo "3) 橙子"
echo "4) 退出"
read choice
case $choice in
1)
echo "您选择了苹果"
;;
2)
echo "您选择了香蕉"
;;
3)
echo "您选择了橙子"
;;
4)
echo "退出"
exit 0
;;
*)
echo "无效的选择"
;;
esac
# 模式匹配示例
read -p "输入文件名: " filename
case $filename in
*.txt)
echo "文本文件"
;;
*.jpg|*.png|*.gif)
echo "图像文件"
;;
*.sh)
echo "Shell脚本"
;;
*)
echo "未知文件类型"
;;
esac
5. 循环
5.1 for循环
#!/bin/bash
# 列表遍历
for fruit in 苹果 香蕉 橙子 葡萄; do
echo "水果: $fruit"
done
# 范围遍历
for i in {1..5}; do
echo "数字: $i"
done
# 指定步长
for i in {0..10..2}; do
echo "偶数: $i"
done
# C风格for循环
for ((i=0; i<5; i++)); do
echo "索引: $i"
done
# 文件遍历
for file in /var/log/*.log; do
echo "日志文件: $file"
done
# 命令结果遍历
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "用户: $user"
done
5.2 while循环
#!/bin/bash
# 基本while
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
# 无限循环
while true; do
read -p "输入'q'退出: " input
if [ "$input" = "q" ]; then
break
fi
done
# 读取文件
while IFS= read -r line; do
echo "行: $line"
done < /etc/passwd
# 与管道配合
cat /etc/passwd | while read line; do
echo "$line"
done
# 读取多个字段
while IFS=: read user pass uid gid desc home shell; do
echo "用户: $user, UID: $uid, 主目录: $home"
done < /etc/passwd
5.3 until循环
#!/bin/bash
# until:条件为真时停止
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done
# 服务等待示例
until nc -z localhost 80 2>/dev/null; do
echo "等待Web服务器启动..."
sleep 1
done
echo "Web服务器已启动!"
5.4 break和continue
#!/bin/bash
# break:退出循环
for i in {1..10}; do
if [ $i -eq 5 ]; then
echo "在5处退出"
break
fi
echo "数字: $i"
done
# continue:跳过当前迭代
for i in {1..10}; do
if [ $((i % 2)) -eq 0 ]; then
continue # 跳过偶数
fi
echo "奇数: $i"
done
# 嵌套循环中的break
for i in {1..3}; do
for j in {1..3}; do
if [ $j -eq 2 ]; then
break 2 # 退出外层循环
fi
echo "i=$i, j=$j"
done
done
6. 函数
6.1 函数定义和调用
#!/bin/bash
# 函数定义方法1
function greet {
echo "你好!"
}
# 函数定义方法2(推荐)
greet2() {
echo "欢迎!"
}
# 函数调用
greet
greet2
6.2 函数参数
#!/bin/bash
# 接收参数的函数
print_info() {
echo "姓名: $1"
echo "年龄: $2"
echo "所有参数: $@"
echo "参数个数: $#"
}
print_info "张三" 25
# 默认值设置
greet() {
local name=${1:-"访客"}
echo "你好,${name}!"
}
greet "张三"
greet
6.3 返回值
#!/bin/bash
# return返回退出码(0-255)
is_even() {
if [ $(($1 % 2)) -eq 0 ]; then
return 0 # 成功/真
else
return 1 # 失败/假
fi
}
if is_even 4; then
echo "4是偶数"
fi
# 返回值(使用echo)
get_double() {
echo $(($1 * 2))
}
result=$(get_double 5)
echo "5的两倍: $result"
# 返回多个值
get_stats() {
local sum=$(($1 + $2))
local diff=$(($1 - $2))
echo "$sum $diff"
}
read sum diff <<< $(get_stats 10 3)
echo "和: $sum, 差: $diff"
6.4 局部变量
#!/bin/bash
global_var="全局"
test_scope() {
local local_var="局部"
global_var="在函数中修改"
echo "函数内局部变量: $local_var"
echo "函数内全局变量: $global_var"
}
test_scope
echo "函数外全局变量: $global_var"
echo "函数外局部变量: $local_var" # 空
7. 输入输出
7.1 用户输入
#!/bin/bash
# 基本输入
echo "请输入您的姓名:"
read name
echo "你好,$name!"
# 带提示符
read -p "请输入年龄: " age
# 密码输入(不显示)
read -sp "密码: " password
echo ""
# 超时设置
read -t 5 -p "5秒内输入: " input
# 字符数限制
read -n 1 -p "继续吗? (y/n): " answer
echo ""
# 读入数组
read -a colors -p "喜欢的颜色(空格分隔): "
echo "第一个颜色: ${colors[0]}"
7.2 输出
#!/bin/bash
# echo
echo "普通输出"
echo -n "不换行"
echo -e "制表符\t换行符\n特殊字符"
# printf(格式化)
printf "姓名: %s, 年龄: %d\n" "张三" 25
printf "小数: %.2f\n" 3.14159
printf "%-10s | %5d\n" "苹果" 100
printf "%-10s | %5d\n" "香蕉" 50
# 彩色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
echo -e "${RED}错误消息${NC}"
echo -e "${GREEN}成功消息${NC}"
7.3 重定向
#!/bin/bash
# 标准输出重定向
echo "日志内容" > logfile.txt # 覆盖
echo "追加内容" >> logfile.txt # 追加
# 标准错误重定向
command 2> error.log # 仅错误
command > output.log 2>&1 # 输出和错误都
command &> all.log # 输出和错误都(Bash)
# 标准输入重定向
while read line; do
echo "$line"
done < input.txt
# Here Document
cat << EOF
多行
文本
一次输出。
变量替换: $USER
EOF
# Here String
grep "pattern" <<< "要搜索的字符串"
# 管道
cat /etc/passwd | grep "root" | cut -d: -f1
8. 命令执行与替换
8.1 命令替换
#!/bin/bash
# 使用$()(推荐)
current_date=$(date)
file_count=$(ls -1 | wc -l)
# 使用反引号(旧式)
current_date=`date`
# 可嵌套
inner=$(echo "Hello $(whoami)")
echo "当前日期: $current_date"
echo "文件数: $file_count"
8.2 检查命令执行结果
#!/bin/bash
# 检查退出码
ls /nonexistent 2>/dev/null
if [ $? -eq 0 ]; then
echo "成功"
else
echo "失败"
fi
# 在条件语句中直接使用
if grep -q "root" /etc/passwd; then
echo "root用户存在"
fi
# && 和 ||
mkdir test_dir && echo "目录创建成功"
rm nonexistent 2>/dev/null || echo "文件不存在"
# 命令组
{ echo "第一行"; echo "第二行"; } > output.txt
9. 实用脚本示例
9.1 系统信息脚本
#!/bin/bash
# system_info.sh - 显示系统信息
echo "========================================"
echo " 系统信息报告"
echo "========================================"
echo ""
echo "主机名: $(hostname)"
echo "操作系统: $(uname -o)"
echo "内核版本: $(uname -r)"
echo "架构: $(uname -m)"
echo ""
echo "--- CPU信息 ---"
echo "CPU: $(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2)"
echo "核心数: $(nproc)"
echo ""
echo "--- 内存信息 ---"
free -h | grep -E "Mem|Swap"
echo ""
echo "--- 磁盘使用 ---"
df -h | grep -E "^/dev"
echo ""
echo "--- 网络信息 ---"
ip -4 addr show | grep inet | awk '{print $2}'
echo ""
echo "--- 系统运行时间 ---"
uptime
echo ""
echo "========================================"
9.2 备份脚本
#!/bin/bash
# backup.sh - 简单备份脚本
# 配置
SOURCE_DIR="/home/$USER/documents"
BACKUP_DIR="/home/$USER/backups"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 执行备份
echo "开始备份: $SOURCE_DIR"
if tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" -C "$(dirname $SOURCE_DIR)" "$(basename $SOURCE_DIR)" 2>/dev/null; then
echo "备份完成: ${BACKUP_DIR}/${BACKUP_FILE}"
echo "文件大小: $(du -h ${BACKUP_DIR}/${BACKUP_FILE} | cut -f1)"
else
echo "备份失败!"
exit 1
fi
# 删除旧备份(7天以上)
echo ""
echo "清理旧备份..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
echo "完成!"
9.3 日志监控脚本
#!/bin/bash
# log_monitor.sh - 监控日志文件
LOG_FILE="${1:-/var/log/syslog}"
KEYWORD="${2:-error}"
if [ ! -f "$LOG_FILE" ]; then
echo "文件不存在: $LOG_FILE"
exit 1
fi
echo "开始监控日志: $LOG_FILE"
echo "关键词: $KEYWORD"
echo "退出: Ctrl+C"
echo "----------------------------"
tail -f "$LOG_FILE" | while read line; do
if echo "$line" | grep -qi "$KEYWORD"; then
echo "[$(date '+%H:%M:%S')] $line"
fi
done
10. 调试
10.1 调试模式
#!/bin/bash
# 调试整个脚本
bash -x script.sh
# 在脚本内启用调试
set -x # 开始调试
# 命令...
set +x # 结束调试
# 只调试特定部分
debug() {
if [ "$DEBUG" = "1" ]; then
echo "[DEBUG] $*" >&2
fi
}
DEBUG=1
debug "这是调试消息"
10.2 错误处理
#!/bin/bash
# 出错时立即退出
set -e
# 使用未定义变量时报错
set -u
# 检测管道错误
set -o pipefail
# 全部设置
set -euo pipefail
# 错误处理器
trap 'echo "错误发生: 行 $LINENO"; exit 1' ERR
# 退出时清理
cleanup() {
echo "执行清理..."
rm -f /tmp/tempfile
}
trap cleanup EXIT
总结
我们学习了Shell脚本的基础知识。通过组合变量、条件语句、循环和函数,可以创建各种自动化脚本。下一篇将讲解正则表达式、文本处理、高级模式等Shell脚本高级技巧。