Shell编程规范与变量
Shell是一个命令行解释器,它是用户与Linux内核之间的桥梁。当你在终端输入命令时,Shell负责解析这些命令,并将其传递给操作系统内核执行。bash(Bourne Again Shell):Linux默认Shell,功能强大,本文基于bash。sh(Bourne Shell):Unix早期Shell,bash兼容sh。zsh:功能更丰富的Shell,macOS Catalina开始默认使用。
第一部分:Shell脚本概述
1.1 什么是Shell?
Shell是一个命令行解释器,它是用户与Linux内核之间的桥梁。当你在终端输入命令时,Shell负责解析这些命令,并将其传递给操作系统内核执行。
Linux系统中常见的Shell有:
-
bash(Bourne Again Shell):Linux默认Shell,功能强大,本文基于bash。
-
sh(Bourne Shell):Unix早期Shell,bash兼容sh。
-
zsh:功能更丰富的Shell,macOS Catalina开始默认使用。
-
csh/tcsh:类C语法Shell。
查看当前系统使用的Shell:
bash
echo $SHELL # 输出示例:/bin/bash
1.2 Shell脚本的作用
Shell脚本是将一系列Shell命令写入文件,通过解释器执行,实现自动化任务。它的主要作用包括:
| 应用场景 | 示例 |
|---|---|
| 系统管理与维护 | 日志清理、备份、监控脚本 |
| 软件部署 | 一键安装LNMP环境 |
| 批量处理 | 批量重命名文件、批量创建用户 |
| 任务自动化 | 定时备份数据库、自动同步文件 |
| 工具开发 | 定制化的运维小工具 |
第一个Shell脚本:
bash
#!/bin/bash # 这是我的第一个脚本 echo "Hello, World!" echo "当前时间: $(date)"
-
#!/bin/bash:Shebang,指定使用哪个解释器执行脚本。 -
#:注释行,解释脚本用途。 -
echo:输出文本。
执行脚本:
bash
chmod +x first.sh ./first.sh
1.3 重定向与管道操作——数据流动的艺术
在Shell中,每个命令都有三个默认的I/O流:
-
标准输入(stdin):文件描述符0,默认从键盘读取。
-
标准输出(stdout):文件描述符1,默认输出到屏幕。
-
标准错误(stderr):文件描述符2,默认输出到屏幕。
重定向就是改变这些默认的输入输出源。
1.3.1 输出重定向
| 操作符 | 作用 | 示例 |
|---|---|---|
> |
将标准输出重定向到文件(覆盖) | ls > filelist.txt |
>> |
将标准输出重定向到文件(追加) | echo "new line" >> filelist.txt |
2> |
将标准错误重定向到文件 | ls /nonexist 2> error.log |
&> |
将标准输出和标准错误都重定向 | ls /home /nonexist &> all.log |
2>&1 |
将标准错误合并到标准输出 | ls /home /nonexist > all.log 2>&1 |
实战示例:
bash
# 将命令输出保存到文件 date > now.txt # 追加内容 echo "Hello" >> now.txt # 分别保存正确和错误输出 find / -name "*.conf" > find_ok.txt 2> find_err.txt # 丢弃错误输出(常见技巧) find / -name "*.conf" 2> /dev/null
1.3.2 输入重定向
| 操作符 | 作用 | 示例 |
|---|---|---|
< |
从文件读取输入 | wc -l < /etc/passwd |
<< |
Here Document,从当前脚本读取多行输入直到指定分隔符 | cat << EOF > test.txt |
Here Document示例:
bash
#!/bin/bash # 使用Here Document生成配置文件 cat << EOF > /tmp/myconfig.conf # This is a config file username=admin password=123456 port=8080 EOF
1.3.3 管道操作
管道(|) 将一个命令的标准输出作为下一个命令的标准输入,实现命令间的数据传递。这是Shell最强大的特性之一。
基本语法:
bash
command1 | command2 | command3
经典示例:
bash
# 查看进程,筛选包含nginx的行,并统计行数
ps aux | grep nginx | wc -l
# 查看日志,提取IP,排序并去重
cat access.log | awk '{print $1}' | sort | uniq -c | sort -nr
# 找出当前目录最大的5个文件
du -sh * | sort -rh | head -5
# 使用xargs将前一个命令的输出作为后一个命令的参数
find . -name "*.log" | xargs rm -f
管道与重定向的区别:
-
管道:连接命令,传递数据流。
-
重定向:连接命令和文件,改变输入输出源。
第二部分:Shell变量深度解析
变量是脚本中存储数据的容器。理解变量类型和作用域是编写健壮脚本的关键。
2.1 变量的作用
变量让我们能够:
-
存储临时数据:如用户输入、命令结果。
-
提高脚本可维护性:修改一处变量定义,多处生效。
-
传递参数:在脚本内外、函数之间传递数据。
2.2 变量的命名规则
-
由字母、数字、下划线组成,不能以数字开头。
-
区分大小写:
name和NAME是不同的变量。 -
避免使用Shell关键字(如
if、then、else、fi等)。 -
惯例:环境变量全大写,自定义变量小写或驼峰。
bash
# 正确示例 name="John" file_name="data.txt" MAX_COUNT=100 _db_host="localhost" # 错误示例 1st_name="Tom" # 数字开头 user-name="joe" # 包含连字符(会被解释为减法)
2.3 变量的定义和使用
2.3.1 基本语法
bash
# 定义变量(等号两边不能有空格)
变量名=变量值
# 引用变量
$变量名
${变量名} # 推荐使用花括号,避免歧义
示例:
bash
name="Alice"
echo $name
echo "My name is ${name}" # 输出:My name is Alice
2.3.2 命令替换
将命令的执行结果赋值给变量:
bash
# 旧语法:反引号 current_date=`date` # 新语法:$(),推荐,支持嵌套 current_date=$(date) files_count=$(ls -l | wc -l) echo "今天日期: $current_date" echo "文件数量: $files_count"
2.3.3 变量的作用范围
默认定义的变量是局部变量,只在当前Shell进程有效,不会传递给子进程。
bash
# 定义局部变量 name="local" # 启动子Shell bash echo $name # 输出为空 exit # 使用export导出为环境变量,可传递给子进程 export name="global" bash echo $name # 输出:global
2.4 变量类型详解
Shell变量主要分为以下几类:
2.4.1 自定义变量(用户变量)
用户在当前Shell或脚本中定义的变量,默认是字符串类型。
高级赋值技巧:
bash
# 直接赋值 a=100 b="hello world" # 从命令结果赋值 c=$(pwd) # 从其他变量赋值 d=$a # 交互式赋值 read -p "请输入你的名字: " user_name echo "你好, $user_name" # 只读变量(常量) readonly PI=3.14159 PI=3.14 # 报错:PI: readonly variable # 删除变量 unset a echo $a # 输出为空
2.4.2 环境变量
环境变量是系统或用户设置的全局变量,影响Shell和所有子进程的行为。
常用环境变量:
bash
echo $HOME # 用户主目录 echo $PATH # 命令搜索路径 echo $USER # 当前用户名 echo $SHELL # 当前Shell echo $PWD # 当前工作目录 echo $LANG # 系统语言 echo $UID # 当前用户ID
查看所有环境变量:
bash
env # 显示所有环境变量 set # 显示所有变量(包括局部变量和环境变量) export # 显示所有已导出的环境变量
设置环境变量:
bash
# 临时生效(当前Shell) export MY_VAR="hello" # 永久生效(写入配置文件) echo 'export MY_VAR="hello"' >> ~/.bashrc source ~/.bashrc
2.4.3 位置变量(命令行参数)
位置变量用于接收脚本或函数执行时传递的参数:
| 变量 | 含义 |
|---|---|
$0 |
脚本名称 |
$1、$2... |
第1、2...个参数 |
${10} |
第10个参数(大于9需要花括号) |
$# |
参数个数 |
$* |
所有参数(作为一个字符串) |
$@ |
所有参数(每个作为独立字符串) |
示例脚本 args.sh:
bash
#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数个数: $#"
echo "所有参数: $*"
echo "所有参数: $@"
# 遍历参数
for arg in "$@"; do
echo "参数: $arg"
done
执行:
bash
./args.sh a b c d # 输出: # 脚本名称: ./args.sh # 第一个参数: a # 第二个参数: b # 参数个数: 4 # 所有参数: a b c d # 参数: a # 参数: b # 参数: c # 参数: d
$*和$@的区别:
-
不加引号时,两者行为相同。
-
加双引号时:
"$*"将所有参数合并为一个字符串,"$@"保持参数的独立性。
bash
# 测试脚本 set -- "a b" "c d" echo "使用\$*:" for i in $*; do echo $i; done echo "使用\"\$*\":" for i in "$*"; do echo $i; done echo "使用\"\$@\":" for i in "$@"; do echo $i; done
2.4.4 预定义变量(特殊变量)
Shell提供了一些特殊变量,用于获取脚本运行状态:
| 变量 | 含义 |
|---|---|
$? |
上一个命令的退出状态码(0表示成功,非0表示失败) |
$$ |
当前Shell进程的PID |
$! |
最后一个后台进程的PID |
$_ |
上一个命令的最后一个参数 |
实战示例:
bash
#!/bin/bash # 演示预定义变量 echo "当前脚本的PID: $$" # 后台执行命令 sleep 30 & echo "最后一个后台进程的PID: $!" # 测试命令执行状态 ls /etc/passwd > /dev/null 2>&1 echo "ls /etc/passwd 执行状态: $?" # 0 ls /nonexist > /dev/null 2>&1 echo "ls /nonexist 执行状态: $?" # 非0 # 获取上一个命令的最后一个参数 echo "之前命令的最后一个参数: $_"
常用退出码含义:
-
0:成功 -
1:一般性错误 -
2:Shell内建命令使用错误 -
126:命令不可执行 -
127:命令未找到 -
128:无效退出参数 -
128+n:信号n的致命错误 -
130:Ctrl+C终止
2.5 变量高级技巧
2.5.1 变量默认值
在处理可能未定义的变量时,可以设置默认值:
| 语法 | 作用 |
|---|---|
${var:-default} |
如果var未定义或为空,返回default,var不变 |
${var:=default} |
如果var未定义或为空,将var设为default,并返回default |
${var:+value} |
如果var已定义且非空,返回value,否则返回空 |
${var:?message} |
如果var未定义或为空,打印message并退出脚本 |
示例:
bash
#!/bin/bash
# 变量默认值演示
name=""
echo "1: ${name:-张三}" # 输出:张三(name仍为空)
echo "name: $name" # 输出:name:
echo "2: ${name:=李四}" # 输出:李四(name被设为李四)
echo "name: $name" # 输出:name: 李四
age=18
echo "3: ${age:+成年}" # 输出:成年
echo "age: $age" # 输出:age: 18
echo "4: ${not_exist:?变量未定义}" # 报错并退出
2.5.2 变量字符串操作
| 语法 | 作用 |
|---|---|
${#var} |
获取字符串长度 |
${var:offset} |
从offset开始截取到结尾 |
${var:offset:length} |
从offset开始截取length长度 |
${var#pattern} |
从开头删除最短匹配 |
${var##pattern} |
从开头删除最长匹配 |
${var%pattern} |
从结尾删除最短匹配 |
${var%%pattern} |
从结尾删除最长匹配 |
${var/old/new} |
替换第一个匹配 |
${var//old/new} |
替换所有匹配 |
示例:
bash
file="/home/user/docs/report.txt"
echo "文件名: ${file##*/}" # 输出:report.txt
echo "目录: ${file%/*}" # 输出:/home/user/docs
echo "扩展名: ${file##*.}" # 输出:txt
echo "无扩展名: ${file%.*}" # 输出:/home/user/docs/report
text="hello world hello"
echo "长度: ${#text}" # 输出:17
echo "替换: ${text/hello/hi}" # 输出:hi world hello
echo "全部替换: ${text//hello/hi}" # 输出:hi world hi
2.5.3 变量类型声明
bash支持使用declare或typeset声明变量类型:
bash
declare -i num=10 # 整数类型 num="20" # 自动转为整数 num="abc" # 赋值为0(因为不是数字) declare -r PI=3.14 # 只读变量(同readonly) declare -a arr # 数组类型 arr=(1 2 3 4 5) declare -A map # 关联数组(需要bash 4.0+) map[name]="Tom" map[age]=25 declare -x global_var # 导出为环境变量(同export) declare -f # 列出所有函数 declare -F # 只列出函数名
数组操作示例:
bash
# 索引数组
fruits=("apple" "banana" "orange")
echo ${fruits[0]} # apple
echo ${fruits[@]} # 所有元素
echo ${#fruits[@]} # 数组长度
# 关联数组
declare -A user
user=([name]="Alice" [age]=30 [city]="Beijing")
echo ${user[name]} # Alice
echo ${!user[@]} # 输出所有键:name age city
第三部分:实战案例与最佳实践
3.1 实战:系统信息收集脚本
结合变量、管道和重定向,编写一个实用的系统信息收集脚本:
bash
#!/bin/bash
# 文件名:sysinfo.sh
# 用途:收集系统关键信息并输出报告
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # 无颜色
# 主机信息
HOSTNAME=$(hostname)
OS_VERSION=$(cat /etc/redhat-release 2>/dev/null || cat /etc/os-release | grep "^PRETTY_NAME" | cut -d= -f2 | tr -d '"')
KERNEL=$(uname -r)
UPTIME=$(uptime | awk '{print $3,$4}' | tr -d ',')
# 硬件信息
CPU_MODEL=$(lscpu | grep "Model name" | cut -d: -f2 | xargs)
CPU_CORES=$(nproc)
MEM_TOTAL=$(free -h | awk '/^Mem:/ {print $2}')
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}')
# 网络信息
IP_ADDR=$(ip -4 addr show | grep inet | grep -v 127.0.0.1 | awk '{print $2}' | cut -d/ -f1 | head -1)
# 输出报告
cat << EOF
========================================
系统信息报告
========================================
主机名: $HOSTNAME
操作系统: $OS_VERSION
内核版本: $KERNEL
运行时间: $UPTIME
----------------------------------------
CPU型号: $CPU_MODEL
CPU核心数: $CPU_CORES
内存总量: $MEM_TOTAL
根分区使用: $DISK_USAGE
----------------------------------------
IP地址: $IP_ADDR
========================================
EOF
# 检查关键服务
echo -e "\n${GREEN}服务状态检查:${NC}"
for service in sshd httpd crond; do
if systemctl is-active --quiet $service; then
echo -e "$service: ${GREEN}运行中${NC}"
else
echo -e "$service: ${RED}未运行${NC}"
fi
done
3.2 最佳实践:编写高质量Shell脚本
-
脚本头部规范
bash
#!/bin/bash # 文件名:script.sh # 作者:Your Name # 日期:2025-02-26 # 描述:脚本的用途说明 # 版本:1.0
-
使用set命令增强脚本健壮性
bash
set -e # 遇到错误立即退出 set -u # 使用未定义变量时报错 set -x # 调试模式,显示执行的命令 set -o pipefail # 管道中任何命令失败都返回失败状态 # 通常组合使用 set -euo pipefail
-
变量使用规范
-
始终使用
${变量}形式,避免歧义。 -
使用有意义的变量名。
-
局部变量用
local声明(函数内)。 -
常量用
readonly声明。
-
-
引号使用
-
始终给变量加双引号,防止单词分割和路径名扩展。
bash
# 错误示例 file="my document.txt" rm $file # 实际执行:rm my document.txt(删除了两个文件!) # 正确示例 rm "$file"
-
-
函数化
-
将重复代码封装为函数。
-
函数应具有单一职责。
-
-
错误处理
bash
command || { echo "命令失败"; exit 1; } # 或 if ! command; then echo "错误信息" >&2 exit 1 fi -
注释充分
-
解释复杂的逻辑。
-
说明输入输出格式。
-
注明特殊技巧的原因。
-
3.3 常见陷阱与解决方法
| 陷阱 | 示例 | 解决方法 |
|---|---|---|
| 等号两边空格 | name = "Tom" |
name="Tom" |
| 忘记$引用 | name="Tom"; echo name |
echo $name |
| 未加引号导致分词 | file="a b.txt"; rm $file |
rm "$file" |
| 使用未定义变量 | rm -rf /${dir}/(dir未定义则删根) |
set -u,或使用默认值${dir:-} |
| 命令替换中的换行符 | files=$(ls) |
使用数组files=($(ls)),但更推荐find |
| 管道丢失退出状态 | command1 | command2 |
set -o pipefail |
更多推荐


所有评论(0)