发现一个通俗易懂,风趣幽默人工智能学习网站:https://www.captainai.net/jiangyu1013/,建议收藏!!!
说起Shell编程,我想很多朋友都有过这样的经历:看着那些黑乎乎的命令行界面,心里就发怵。今天我就来和大家分享一下Shell编程的那些事儿,从最基础的概念开始,一步步带你入门。不求你看完就能成为Shell大神,但至少能让你不再害怕那个黑漆漆的终端界面。
Shell(也称为壳层)在计算机科学中指“为用户提供用户界面”的软件,通常指的是命令行界面的解析器。一般来说,这个词是指操作系统中提供访问内核所提供之服务的程序。Shell也用于泛指所有为用户提供操作界面的程序,也就是程序和用户交互的层面。因此与之相对的是内核(英语:Kernel),内核不提供和用户的交互功能。
Shell说白了就是一个命令解释器,它就像是你和操作系统之间的翻译官。你用人话告诉Shell要做什么,它就翻译成机器能理解的指令去执行。
在Linux系统中,最常见的Shell就是bash(Bourne Again Shell)。当然还有其他的,比如zsh、fish什么的,但是bash基本上是标配,学会了bash,其他的也就触类旁通了。
为什么要学Shell呢?这个问题我被问过无数次。简单来说,如果你要在Linux环境下工作,不会Shell就像是哑巴一样,很多事情都做不了。比如说批量处理文件、自动化部署、系统监控等等,这些都离不开Shell脚本。
我记得有一次,项目要求把几百个日志文件按照日期重新命名,如果手动操作的话,估计要搞一整天。但是用Shell脚本,几分钟就搞定了。这就是Shell的魅力所在。
更详细的学习地址:https://www.bookstack.cn/books/open-shell-book
image-20250908223947089
Shell中的变量定义很简单,直接写就行了:
name="张三"
age=25
注意这里有个坑,等号两边不能有空格!这个我当初也踩过,写成了name = "张三"
,结果报错半天才发现问题。
使用变量的时候要加上美元符号:
echo $name
echo "我的名字是$name,今年$age岁"
还有一种更安全的写法,用花括号把变量名括起来:
echo "我的名字是${name},今年${age}岁"
这样做的好处是避免变量名和其他字符混在一起造成歧义。
Shell里面有一些特殊的变量,这些是系统预定义的:
$0
- 脚本名称$#
- 参数个数$?
- 上一个命令的退出状态$$
- 当前进程ID举个例子,如果你有个脚本叫test.sh
,然后这样运行:
./test.sh hello world
那么在脚本里面:
$0
就是 ./test.sh
$1
就是 hello
$2
就是 world
$#
就是 2
这些特殊变量在写脚本的时候特别有用,特别是处理命令行参数的时候。
Shell的if语句语法稍微有点特别:
if [ condition ]; then
# 执行的命令
elif [ another_condition ]; then
# 另一个条件
else
# 默认执行的命令
fi
注意那个方括号,两边都要有空格!这又是一个容易踩的坑。
常用的条件判断:
# 数字比较
if [ $age -gt 18 ]; then
echo "成年了"
fi
# 字符串比较
if [ "$name" = "张三" ]; then
echo "你好,张三"
fi
# 文件判断
if [ -f "/etc/passwd" ]; then
echo "文件存在"
fi
数字比较的操作符有:-eq
(等于)、-ne
(不等于)、-gt
(大于)、-lt
(小于)、-ge
(大于等于)、-le
(小于等于)。
文件判断的操作符也很多:-f
(是否为文件)、-d
(是否为目录)、-e
(是否存在)、-r
(是否可读)等等。
当需要判断多个条件的时候,case语句比if更清晰:
case $1 in
"start")
echo "启动服务"
;;
"stop")
echo "停止服务"
;;
"restart")
echo "重启服务"
;;
*)
echo "未知参数"
;;
esac
这种写法在处理服务脚本的时候特别常见。
for循环有几种写法,最常用的是这样:
# 遍历列表
for item in apple banana orange; do
echo "水果:$item"
done
# 遍历文件
for file in *.txt; do
echo "处理文件:$file"
done
# C风格的for循环
for ((i=1; i<=10; i++)); do
echo "数字:$i"
done
我经常用for循环来批量处理文件,比如批量重命名、批量转换格式什么的。
while循环适合在不知道循环次数的情况下使用:
count=1
while [ $count -le 5 ]; do
echo "第${count}次循环"
count=$((count + 1))
done
还有一种常见的用法是读取文件内容:
while read line; do
echo "读到一行:$line"
done < /etc/passwd
这个在处理配置文件或者日志文件的时候特别有用。
Shell也支持函数,语法是这样的:
function hello() {
echo "Hello, $1!"
}
# 或者更简单的写法
hello() {
echo "Hello, $1!"
}
# 调用函数
hello "World"
函数可以有返回值,但是只能返回数字(0-255):
check_file() {
if [ -f "$1" ]; then
return 0 # 成功
else
return 1 # 失败
fi
}
if check_file "/etc/passwd"; then
echo "文件存在"
else
echo "文件不存在"
fi
如果要返回字符串,可以用echo然后用命令替换来获取:
get_date() {
echo $(date +%Y-%m-%d)
}
today=$(get_date)
echo "今天是:$today"
Shell也支持数组,虽然功能比较简单:
# 定义数组
fruits=("apple" "banana" "orange")
# 或者这样定义
fruits[0]="apple"
fruits[1]="banana"
fruits[2]="orange"
# 访问数组元素
echo ${fruits[0]} # 输出第一个元素
echo ${fruits[@]} # 输出所有元素
echo ${#fruits[@]} # 输出数组长度
# 遍历数组
for fruit in "${fruits[@]}"; do
echo "水果:$fruit"
done
数组在处理批量数据的时候很有用,比如存储服务器列表、配置项等等。
Shell的字符串处理功能还是挺强大的:
str="Hello World"
# 获取字符串长度
echo ${#str}
# 字符串截取
echo ${str:0:5} # 从位置0开始,取5个字符
echo ${str:6} # 从位置6开始到结尾
# 字符串替换
echo ${str/World/Shell} # 替换第一个匹配
echo ${str//l/L} # 替换所有匹配
# 字符串删除
echo ${str#Hello } # 从开头删除匹配的部分
echo ${str%World} # 从结尾删除匹配的部分
这些操作在处理文件名、路径等场景下特别有用。
# 创建文件
touch newfile.txt
# 复制文件
cp source.txt dest.txt
# 移动/重命名文件
mv oldname.txt newname.txt
# 删除文件
rm unwanted.txt
# 创建目录
mkdir newdir
# 删除目录
rmdir emptydir
rm -rf nonemptydir
重定向是Shell的一个重要特性:
# 输出重定向
echo "Hello" > file.txt # 覆盖写入
echo "World" >> file.txt # 追加写入
# 输入重定向
sort < unsorted.txt
# 错误重定向
command 2> error.log # 只重定向错误输出
command > output.log 2>&1 # 同时重定向标准输出和错误输出
# 管道
cat file.txt | grep "pattern" | sort
管道是个很强大的功能,可以把多个命令串联起来,前一个命令的输出作为后一个命令的输入。
说了这么多理论,来看几个实际的例子。
#!/bin/bash
# 检查系统负载
check_load() {
load=$(uptime | awk '{print $10}' | cut -d',' -f1)
if (( $(echo "$load > 2.0" | bc -l) )); then
echo "警告:系统负载过高 ($load)"
fi
}
# 检查磁盘使用率
check_disk() {
df -h | grep -vE '^Filesystem|tmpfs|cdrom' | awk '{print $5 " " $1}' | while read output; do
usage=$(echo $output | awk '{print $1}' | cut -d'%' -f1)
partition=$(echo $output | awk '{print $2}')
if [ $usage -ge 80 ]; then
echo "警告:磁盘使用率过高 $partition ($usage%)"
fi
done
}
# 检查内存使用
check_memory() {
memory_usage=$(free | grep Mem | awk '{printf("%.2f"), $3/$2 * 100.0}')
if (( $(echo "$memory_usage > 80" | bc -l) )); then
echo "警告:内存使用率过高 ($memory_usage%)"
fi
}
echo "=== 系统监控报告 $(date) ==="
check_load
check_disk
check_memory
这个脚本可以定时运行,监控系统的基本状态。
#!/bin/bash
log_file="/var/log/nginx/access.log"
output_file="log_analysis_$(date +%Y%m%d).txt"
echo "=== Nginx日志分析报告 ===" > $output_file
echo "分析时间:$(date)" >> $output_file
echo "" >> $output_file
# 统计访问量最多的IP
echo "访问量前10的IP地址:" >> $output_file
awk '{print $1}' $log_file | sort | uniq -c | sort -nr | head -10 >> $output_file
echo "" >> $output_file
# 统计最受欢迎的页面
echo "访问量前10的页面:" >> $output_file
awk '{print $7}' $log_file | sort | uniq -c | sort -nr | head -10 >> $output_file
echo "" >> $output_file
# 统计HTTP状态码
echo "HTTP状态码统计:" >> $output_file
awk '{print $9}' $log_file | sort | uniq -c | sort -nr >> $output_file
echo "分析完成,结果保存在 $output_file"
这种日志分析脚本在运维工作中经常用到。
#!/bin/bash
# 配置参数
backup_source="/var/www"
backup_dest="/backup"
backup_name="website_backup_$(date +%Y%m%d_%H%M%S).tar.gz"
keep_days=7
# 创建备份目录
mkdir -p $backup_dest
# 执行备份
echo "开始备份 $backup_source ..."
tar -czf "$backup_dest/$backup_name" -C $(dirname $backup_source) $(basename $backup_source)
if [ $? -eq 0 ]; then
echo "备份成功:$backup_dest/$backup_name"
# 清理旧备份
find $backup_dest -name "website_backup_*.tar.gz" -mtime +$keep_days -delete
echo "已清理${keep_days}天前的旧备份"
else
echo "备份失败!"
exit 1
fi
这个脚本可以放到crontab里定时执行,实现自动备份。
写Shell脚本难免会遇到各种问题,掌握一些调试技巧很重要。
# 开启调试模式
set -x # 显示执行的每一条命令
set -e # 遇到错误立即退出
set -u # 使用未定义变量时报错
# 或者在脚本开头加上
#!/bin/bash -x
还可以用bash -x script.sh
来运行脚本,这样会显示每一步的执行过程。
# 检查命令执行结果
if ! command -v git > /dev/null; then
echo "错误:未安装git"
exit 1
fi
# 使用trap捕获信号
trap 'echo "脚本被中断"; exit 1' INT TERM
# 函数中的错误处理
safe_execute() {
"$@"
local status=$?
if [ $status -ne 0 ]; then
echo "命令执行失败: $*"
exit $status
fi
}
safe_execute cp important_file.txt backup/
良好的错误处理可以让脚本更加健壮,避免出现意外情况。
写Shell脚本也有一些最佳实践需要注意。
# 避免在循环中调用外部命令
# 不好的写法
for file in *.txt; do
lines=$(wc -l < "$file")
echo "$file: $lines lines"
done
# 更好的写法
wc -l *.txt
# 使用内置命令替代外部命令
# 不好的写法
result=$(echo $string | cut -c1-5)
# 更好的写法
result=${string:0:5}
#!/bin/bash
# 脚本说明:这是一个示例脚本
# 作者:张三
# 创建时间:2024-01-01
# 设置严格模式
set -euo pipefail
# 全局变量使用大写
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly CONFIG_FILE="${SCRIPT_DIR}/config.conf"
# 函数名使用小写,用下划线分隔
check_prerequisites() {
# 函数内容
return 0
}
# 主函数
main() {
check_prerequisites
# 其他逻辑
}
# 只有在直接执行脚本时才运行main函数
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
在学习Shell的过程中,我遇到过很多坑,这里分享一些常见的问题。
Shell对空格特别敏感,很多错误都是因为空格引起的:
# 错误的写法
if [ $var= "hello" ]; then # 等号前后不能有空格(在变量赋值时)
echo "world"
fi
# 正确的写法
var="hello"
if [ "$var" = "hello" ]; then # 比较时等号前后要有空格
echo "world"
fi
什么时候用单引号,什么时候用双引号,这个也容易搞混:
name="张三"
# 单引号:原样输出,不解析变量
echo '我的名字是$name' # 输出:我的名字是$name
# 双引号:解析变量
echo "我的名字是$name" # 输出:我的名字是张三
# 不加引号:可能会有问题
echo 我的名字是$name # 如果name包含空格就会出问题
处理文件路径时要特别小心:
# 不好的写法
cd /some/path
rm -rf * # 如果cd失败,这条命令会在当前目录执行!
# 更安全的写法
cd /some/path || exit 1
rm -rf *
# 或者使用绝对路径
rm -rf /some/path/*
# 定义数组时的常见错误
files="file1.txt file2.txt file3.txt" # 这是字符串,不是数组
# 正确的数组定义
files=("file1.txt" "file2.txt" "file3.txt")
# 遍历时也要注意
for file in $files; do # 如果文件名包含空格会有问题
echo $file
done
# 更安全的写法
for file in "${files[@]}"; do
echo "$file"
done
掌握了基础语法之后,还有一些进阶的技巧可以让你的Shell脚本更加强大。
# 命令替换
current_date=$(date +%Y-%m-%d)
file_count=`ls | wc -l` # 旧式写法,不推荐
# 进程替换
diff <(sort file1.txt) <(sort file2.txt)
filename="document.pdf"
# 获取文件名和扩展名
basename=${filename%.*} # document
extension=${filename##*.} # pdf
# 设置默认值
config_file=${CONFIG_FILE:-"/etc/default.conf"}
# 参数长度
echo ${#filename} # 12
# 使用=~操作符进行正则匹配
email="user@example.com"
if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "有效的邮箱地址"
fi
# 提取匹配的部分
text="版本号:v1.2.3"
if [[ $text =~ v([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
major=${BASH_REMATCH[1]}
minor=${BASH_REMATCH[2]}
patch=${BASH_REMATCH[3]}
echo "主版本:$major,次版本:$minor,补丁版本:$patch"
fi
# 生成配置文件
cat > /etc/myapp.conf << EOF
server_name = $SERVER_NAME
port = $PORT
debug = true
EOF
# 多行字符串
message=$(cat << 'EOF'
这是一个
多行的
消息内容
EOF
)
Shell脚本的强大之处在于可以很容易地与其他工具集成。
# 连接MySQL
mysql -u root -p$PASSWORD -e "SELECT COUNT(*) FROM users;" mydb
# 使用Here Document执行复杂SQL
mysql -u root -p$PASSWORD mydb << EOF
UPDATE users SET last_login = NOW() WHERE id = 1;
SELECT * FROM users WHERE active = 1;
EOF
# 使用curl调用REST API
api_response=$(curl -s -X GET "https://api.example.com/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json")
# 解析JSON响应(需要安装jq)
user_count=$(echo $api_response | jq '.total_count')
send_alert() {
local subject="$1"
local message="$2"
echo "$message" | mail -s "$subject" admin@example.com
}
# 使用示例
if [ $disk_usage -gt 90 ]; then
send_alert "磁盘空间警告" "磁盘使用率已达到 ${disk_usage}%"
fi
学Shell编程其实没有什么捷径,就是多练多用。我当初也是从最简单的命令开始,慢慢积累经验的。
建议大家从这几个方面入手:
网上有很多不错的学习资源,比如《Shell 编程范例》《Shell编程基础》这本书,内容很全面。还有一些在线的教程网站,像菜鸟教程、实验楼等等,都有不错的Shell教程。
最重要的是要多实践,光看不练是学不会的。可以从自己工作中的实际需求出发,尝试用Shell脚本来解决问题。比如自动化一些重复性的工作,或者写一些监控脚本等等。
Shell编程虽然看起来有点复杂,但是掌握了基本语法和常用技巧之后,你会发现它其实是一个非常实用的工具。无论是系统管理、自动化运维,还是日常的文件处理,Shell脚本都能帮你提高效率。
当然,Shell也有它的局限性,比如在处理复杂的数据结构或者需要高性能计算的场景下,可能Python或者其他语言会更合适。但是对于大部分的系统管理任务来说,Shell已经足够强大了。
记住,学习任何技术都需要时间和耐心,不要指望一蹴而就。从简单的脚本开始,慢慢积累经验,相信你很快就能写出实用的Shell脚本了。
如果这篇文章对你有帮助,别忘了点赞转发支持一下!想了解更多运维实战经验和技术干货,记得关注微信公众号@运维躬行录,领取学习大礼包!!!我会持续分享更多接地气的运维知识和踩坑经验。让我们一起在运维这条路上互相学习,共同进步!
公众号:运维躬行录
个人博客:躬行笔记