Shell脚本编程基础
Shell脚本概述
Shell脚本是一种批处理脚本,用于自动化执行一系列命令。它允许用户将多个命令组合在一起,创建复杂的程序来完成各种任务。
什么是Shell
Shell是Linux系统的命令解释器,它接收用户输入的命令并执行相应的操作。常见的Shell包括:
- Bash (Bourne Again Shell) - 最常用的Shell
- Sh (Bourne Shell) - 传统的Unix Shell
- Zsh (Z Shell) - 功能增强的Shell
- Fish (Friendly Interactive Shell) - 用户友好的Shell
Shell脚本的优势
- 自动化 - 自动执行重复性任务
- 批处理 - 一次执行多个命令
- 可移植性 - 在不同系统间移植
- 效率 - 提高工作效率
创建和运行Shell脚本
编写第一个Shell脚本
#!/bin/bash
# 这是一个简单的Shell脚本示例
echo "Hello, World!"
echo "当前时间: $(date)"
echo "当前用户: $(whoami)"
脚本执行方式
# 1. 使用bash命令执行
bash script.sh
# 2. 给脚本添加执行权限后直接执行
chmod +x script.sh
./script.sh
# 3. 使用source或.命令在当前shell中执行
source script.sh
# 或
. script.sh
Shell脚本的基本结构
#!/bin/bash
# 脚本描述和作者信息
# 变量定义
variable=value
# 函数定义
function_name() {
# 函数体
}
# 主要代码逻辑
echo "脚本开始执行"
# 条件判断和循环
if [ condition ]; then
# 条件为真时执行
fi
# 脚本结束
echo "脚本执行完成"
变量和参数
变量定义和使用
#!/bin/bash
# 定义变量
name="Linux"
version="5.4"
number=42
# 使用变量
echo "系统名称: $name"
echo "版本号: $version"
echo "数字: $number"
# 变量拼接
message="欢迎使用 $name 系统"
echo $message
# 变量赋值时使用命令输出
current_date=$(date)
echo "当前日期: $current_date"
# 使用反引号(旧式语法)
current_user=`whoami`
echo "当前用户: $current_user"
特殊变量
#!/bin/bash
# 脚本参数
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "所有参数: $@"
echo "参数个数: $#"
# 进程相关变量
echo "当前进程ID: $$"
echo "最后运行的后台进程ID: $!"
# 上一个命令的退出状态
ls /nonexistent
echo "上一个命令的退出状态: $?"
数组
#!/bin/bash
# 定义数组
fruits=("苹果" "香蕉" "橙子")
numbers=(1 2 3 4 5)
# 访问数组元素
echo "第一个水果: ${fruits[0]}"
echo "第二个水果: ${fruits[1]}"
# 获取数组所有元素
echo "所有水果: ${fruits[@]}"
# 获取数组长度
echo "水果数量: ${#fruits[@]}"
# 添加数组元素
fruits[3]="葡萄"
fruits+=("西瓜")
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
运算符
算术运算符
#!/bin/bash
a=10
b=5
# 基本算术运算
echo "加法: $((a + b))"
echo "减法: $((a - b))"
echo "乘法: $((a * b))"
echo "除法: $((a / b))"
echo "取余: $((a % b))"
# 自增自减
echo "a自增前: $a"
((a++))
echo "a自增后: $a"
echo "b自减前: $b"
((b--))
echo "b自减后: $b"
# 使用expr命令(旧式语法)
result=$(expr $a + $b)
echo "使用expr: $result"
比较运算符
#!/bin/bash
# 数值比较
num1=10
num2=20
if [ $num1 -eq $num2 ]; then
echo "$num1 等于 $num2"
elif [ $num1 -ne $num2 ]; then
echo "$num1 不等于 $num2"
fi
if [ $num1 -gt $num2 ]; then
echo "$num1 大于 $num2"
elif [ $num1 -lt $num2 ]; then
echo "$num1 小于 $num2"
fi
# 字符串比较
str1="hello"
str2="world"
if [ "$str1" = "$str2" ]; then
echo "字符串相等"
elif [ "$str1" != "$str2" ]; then
echo "字符串不相等"
fi
# 文件测试运算符
if [ -f "/etc/passwd" ]; then
echo "/etc/passwd 是一个普通文件"
fi
if [ -d "/home" ]; then
echo "/home 是一个目录"
fi
if [ -r "/etc/passwd" ]; then
echo "/etc/passwd 可读"
fi
if [ -w "/tmp" ]; then
echo "/tmp 可写"
fi
if [ -x "/bin/bash" ]; then
echo "/bin/bash 可执行"
fi
条件判断
if语句
#!/bin/bash
# 基本if语句
age=18
if [ $age -ge 18 ]; then
echo "已成年"
else
echo "未成年"
fi
# 多重条件判断
score=85
if [ $score -ge 90 ]; then
echo "优秀"
elif [ $score -ge 80 ]; then
echo "良好"
elif [ $score -ge 70 ]; then
echo "中等"
elif [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
# 多个条件组合
user="admin"
password="123456"
if [ "$user" = "admin" ] && [ "$password" = "123456" ]; then
echo "登录成功"
else
echo "用户名或密码错误"
fi
case语句
#!/bin/bash
# case语句示例
read -p "请输入一个数字 (1-3): " number
case $number in
1)
echo "你输入了数字一"
;;
2)
echo "你输入了数字二"
;;
3)
echo "你输入了数字三"
;;
*)
echo "输入无效"
;;
esac
# 使用通配符的case语句
read -p "请输入文件扩展名: " extension
case $extension in
.txt|.log)
echo "文本文件"
;;
.jpg|.png|.gif)
echo "图片文件"
;;
.mp3|.wav)
echo "音频文件"
;;
*)
echo "未知文件类型"
;;
esac
循环结构
for循环
#!/bin/bash
# 基本for循环
for i in 1 2 3 4 5; do
echo "数字: $i"
done
# 使用范围的for循环
for i in {1..10}; do
echo "计数: $i"
done
# 使用seq命令
for i in $(seq 1 5); do
echo "序列: $i"
done
# C风格的for循环
for ((i=1; i<=5; i++)); do
echo "C风格循环: $i"
done
# 遍历数组
fruits=("苹果" "香蕉" "橙子")
for fruit in "${fruits[@]}"; do
echo "水果: $fruit"
done
# 遍历文件
for file in /etc/*.conf; do
if [ -f "$file" ]; then
echo "配置文件: $(basename $file)"
fi
done
while循环
#!/bin/bash
# 基本while循环
count=1
while [ $count -le 5 ]; do
echo "计数: $count"
((count++))
done
# 无限循环(需要break退出)
while true; do
read -p "输入'quit'退出: " input
if [ "$input" = "quit" ]; then
break
fi
echo "你输入了: $input"
done
# 读取文件内容
while read line; do
echo "行内容: $line"
done < /etc/hosts
until循环
#!/bin/bash
# until循环示例
count=1
until [ $count -gt 5 ]; do
echo "计数: $count"
((count++))
done
函数
定义和调用函数
#!/bin/bash
# 定义函数
greet() {
echo "你好, $1!"
}
# 调用函数
greet "张三"
greet "李四"
# 带返回值的函数
add() {
local sum=$(( $1 + $2 ))
echo $sum
}
# 调用带返回值的函数
result=$(add 10 20)
echo "10 + 20 = $result"
# 函数中使用全局变量和局部变量
global_var="全局变量"
test_scope() {
local local_var="局部变量"
echo "函数内: $global_var"
echo "函数内: $local_var"
}
test_scope
echo "函数外: $global_var"
# echo "函数外: $local_var" # 这会报错,因为局部变量在函数外不可访问
函数参数
#!/bin/bash
# 处理函数参数
show_args() {
echo "函数名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "所有参数: $@"
echo "参数个数: $#"
}
show_args "参数1" "参数2" "参数3"
# 参数默认值
greet_user() {
local name=${1:-"访客"} # 如果$1为空,则使用"访客"
echo "你好, $name!"
}
greet_user "张三"
greet_user # 使用默认值
输入输出
读取用户输入
#!/bin/bash
# 基本读取输入
read -p "请输入您的姓名: " name
echo "您好, $name!"
# 读取密码(不显示输入)
read -s -p "请输入密码: " password
echo
echo "密码已输入"
# 读取多个值
read -p "请输入姓名和年龄: " name age
echo "姓名: $name, 年龄: $age"
# 设置读取超时
if read -t 5 -p "请输入内容(5秒超时): " input; then
echo "您输入了: $input"
else
echo "超时未输入"
fi
输出重定向
#!/bin/bash
# 输出重定向到文件
echo "这是第一行" > output.txt
echo "这是第二行" >> output.txt
# 错误输出重定向
ls /nonexistent 2> error.txt
# 标准输出和错误输出分别重定向
command > output.txt 2> error.txt
# 标准输出和错误输出重定向到同一文件
command > all_output.txt 2>&1
# 或
command &> all_output.txt
# 丢弃输出
command > /dev/null 2>&1
格式化输出
#!/bin/bash
# 使用printf格式化输出
name="张三"
age=25
height=175.5
printf "姓名: %s\n" "$name"
printf "年龄: %d\n" "$age"
printf "身高: %.1f cm\n" "$height"
printf "格式化字符串: %s, 数字: %d\n" "测试" 123
# 颜色输出
echo -e "\033[31m红色文本\033[0m"
echo -e "\033[32m绿色文本\033[0m"
echo -e "\033[33m黄色文本\033[0m"
echo -e "\033[34m蓝色文本\033[0m"
# 背景颜色
echo -e "\033[41m红色背景\033[0m"
echo -e "\033[42m绿色背景\033[0m"
调试和错误处理
调试选项
#!/bin/bash
# 使用set命令调试
set -x # 显示执行的命令
echo "调试模式开启"
set +x # 关闭调试模式
# 使用bash -x执行脚本
# bash -x script.sh
# 显示执行过程但不显示命令
set -v
echo "显示模式开启"
set +v
# 同时开启详细模式和调试模式
set -vx
echo "详细调试模式"
set +vx
错误处理
#!/bin/bash
# 检查命令执行结果
if ! ls /nonexistent > /dev/null 2>&1; then
echo "命令执行失败"
exit 1
fi
# 使用set -e在命令失败时自动退出
set -e
echo "这条命令会执行"
# ls /nonexistent # 这条命令会导致脚本退出
echo "这条命令不会执行到"
# 捕获错误信号
trap 'echo "脚本被中断"; exit 1' INT TERM
# 捕获退出信号
trap 'echo "脚本执行完成"' EXIT
# 自定义错误处理函数
error_exit() {
echo "错误: $1"
exit 1
}
# 使用自定义错误处理
command || error_exit "命令执行失败"
实用Shell脚本示例
系统信息收集脚本
#!/bin/bash
# system_info.sh - 系统信息收集脚本
echo "=== 系统信息报告 ==="
echo "生成时间: $(date)"
echo "主机名: $(hostname)"
echo "用户: $(whoami)"
echo
echo "=== 硬件信息 ==="
echo "CPU信息:"
lscpu | head -10
echo
echo "内存信息:"
free -h
echo
echo "磁盘使用情况:"
df -h
echo
echo "=== 网络信息 ==="
echo "网络接口:"
ip addr show | grep "inet "
echo
echo "路由表:"
ip route show
echo
echo "=== 进程信息 ==="
echo "运行中的进程数: $(ps aux | wc -l)"
echo "CPU使用率最高的5个进程:"
ps aux --sort=-%cpu | head -6
echo
echo "=== 系统负载 ==="
uptime
文件备份脚本
#!/bin/bash
# backup.sh - 文件备份脚本
SOURCE_DIR="/home/user/documents"
BACKUP_DIR="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$DATE.tar.gz"
# 检查源目录是否存在
if [ ! -d "$SOURCE_DIR" ]; then
echo "错误: 源目录 $SOURCE_DIR 不存在"
exit 1
fi
# 创建备份目录
mkdir -p "$BACKUP_DIR"
# 执行备份
echo "开始备份 $SOURCE_DIR 到 $BACKUP_DIR/$BACKUP_NAME"
tar -czf "$BACKUP_DIR/$BACKUP_NAME" -C "$(dirname "$SOURCE_DIR")" "$(basename "$SOURCE_DIR")"
if [ $? -eq 0 ]; then
echo "备份成功完成"
echo "备份文件: $BACKUP_DIR/$BACKUP_NAME"
echo "备份大小: $(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)"
else
echo "备份失败"
exit 1
fi
# 清理7天前的备份文件
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
echo "已清理7天前的备份文件"
日志分析脚本
#!/bin/bash
# log_analyzer.sh - 日志分析脚本
LOG_FILE="/var/log/syslog"
OUTPUT_FILE="log_analysis_$(date +%Y%m%d).txt"
# 检查日志文件是否存在
if [ ! -f "$LOG_FILE" ]; then
echo "错误: 日志文件 $LOG_FILE 不存在"
exit 1
fi
echo "开始分析日志文件: $LOG_FILE"
echo "分析时间: $(date)" > "$OUTPUT_FILE"
echo "=== 错误日志统计 ===" >> "$OUTPUT_FILE"
grep -i "error" "$LOG_FILE" | wc -l >> "$OUTPUT_FILE"
echo "=== 警告日志统计 ===" >> "$OUTPUT_FILE"
grep -i "warning" "$LOG_FILE" | wc -l >> "$OUTPUT_FILE"
echo "=== 最常见的错误 ===" >> "$OUTPUT_FILE"
grep -i "error" "$LOG_FILE" | cut -d' ' -f5- | sort | uniq -c | sort -nr | head -10 >> "$OUTPUT_FILE"
echo "=== 活跃IP地址 ===" >> "$OUTPUT_FILE"
# 假设是Web服务器日志
grep "GET\|POST" "$LOG_FILE" | grep -oE "\b([0-9]{1,3}\.){3}[0-9]{1,3}\b" | sort | uniq -c | sort -nr | head -10 >> "$OUTPUT_FILE"
echo "日志分析完成,结果保存在: $OUTPUT_FILE"
实践练习
练习1:基础脚本编写
#!/bin/bash
# practice1.sh
# 1. 创建一个脚本,显示系统信息
echo "=== 系统信息 ==="
echo "当前时间: $(date)"
echo "当前用户: $(whoami)"
echo "工作目录: $(pwd)"
echo "系统负载: $(uptime | awk -F'load average:' '{print $2}')"
# 2. 使用变量存储信息
os_name=$(uname -s)
os_version=$(uname -r)
echo "操作系统: $os_name $os_version"
# 3. 条件判断
if [ -f "/etc/passwd" ]; then
echo "用户账户数: $(wc -l < /etc/passwd)"
else
echo "无法读取用户信息"
fi
练习2:循环和数组
#!/bin/bash
# practice2.sh
# 1. 创建数组并遍历
services=("ssh" "httpd" "mysql" "nginx")
echo "=== 服务状态检查 ==="
for service in "${services[@]}"; do
if systemctl is-active --quiet $service 2>/dev/null; then
echo "✓ $service 正在运行"
else
echo "✗ $service 未运行"
fi
done
# 2. 使用while循环计数
echo "=== 倒计时 ==="
count=10
while [ $count -gt 0 ]; do
echo "倒计时: $count"
sleep 1
((count--))
done
echo "倒计时结束!"
练习3:函数和参数处理
#!/bin/bash
# practice3.sh
# 1. 定义计算函数
calculate() {
local operation=$1
local num1=$2
local num2=$3
case $operation in
add)
echo $(($num1 + $num2))
;;
sub)
echo $(($num1 - $num2))
;;
mul)
echo $(($num1 * $num2))
;;
div)
if [ $num2 -ne 0 ]; then
echo $(($num1 / $num2))
else
echo "错误: 除数不能为零"
fi
;;
*)
echo "错误: 不支持的操作 $operation"
;;
esac
}
# 2. 处理命令行参数
if [ $# -ne 3 ]; then
echo "用法: $0 <操作> <数字1> <数字2>"
echo "操作: add|sub|mul|div"
exit 1
fi
operation=$1
num1=$2
num2=$3
result=$(calculate $operation $num1 $num2)
echo "结果: $result"
练习4:文件操作脚本
#!/bin/bash
# file_manager.sh
# 1. 创建目录结构
create_structure() {
local base_dir=$1
mkdir -p "$base_dir"/{documents,pictures,videos,music}
echo "目录结构创建完成: $base_dir"
}
# 2. 统计文件类型
count_files() {
local dir=$1
echo "=== $dir 目录文件统计 ==="
find "$dir" -type f -name "*.txt" | wc -l | xargs echo "文本文件:"
find "$dir" -type f -name "*.jpg" -o -name "*.png" | wc -l | xargs echo "图片文件:"
find "$dir" -type f -name "*.mp3" -o -name "*.wav" | wc -l | xargs echo "音频文件:"
}
# 3. 清理临时文件
clean_temp() {
local dir=$1
echo "清理临时文件..."
find "$dir" -name "*.tmp" -delete
find "$dir" -name "*~" -delete
echo "临时文件清理完成"
}
# 主程序
if [ $# -lt 1 ]; then
echo "用法: $0 <目录路径> [create|count|clean]"
exit 1
fi
target_dir=$1
action=${2:-"count"}
case $action in
create)
create_structure "$target_dir"
;;
count)
count_files "$target_dir"
;;
clean)
clean_temp "$target_dir"
;;
*)
echo "不支持的操作: $action"
echo "支持的操作: create|count|clean"
;;
esac
最佳实践
脚本编写规范
-
使用正确的Shebang
#!/bin/bash -
添加注释
#!/bin/bash
# 脚本功能:备份重要文件
# 作者:张三
# 日期:2023-01-01 -
变量命名规范
# 使用有意义的变量名
backup_directory="/backup"
current_date=$(date +%Y%m%d) -
错误处理
# 检查命令执行结果
if ! command; then
echo "命令执行失败"
exit 1
fi -
输入验证
# 验证参数
if [ $# -ne 2 ]; then
echo "用法: $0 <参数1> <参数2>"
exit 1
fi
性能优化建议
-
避免不必要的子shell
# 不好的做法
count=$(ls *.txt | wc -l)
# 更好的做法
count=$(find . -maxdepth 1 -name "*.txt" | wc -l) -
使用内置命令
# 不好的做法
if [ $(echo "$num1 > $num2" | bc) -eq 1 ]; then
# 更好的做法
if [ $num1 -gt $num2 ]; then -
批量处理
# 不好的做法
for file in *.txt; do
process_file "$file"
done
# 更好的做法
process_files *.txt
总结
Shell脚本编程是Linux系统管理的重要技能。通过掌握变量、条件判断、循环、函数等基本概念,可以编写出功能强大的自动化脚本来提高工作效率。
关键要点:
- 理解Shell脚本的基本结构和执行方式
- 掌握变量定义和使用方法
- 熟练运用条件判断和循环结构
- 学会定义和调用函数
- 掌握输入输出处理技巧
- 能够进行调试和错误处理
- 遵循脚本编写最佳实践