Shell脚本学习笔记

第一章 Shell基础认知

一、什么是Shell?

作为命令解释器和操作系统内核之间的接口,负责解析并执行用户输入的命令。

二、Bash/Zsh的区别

特性BashZsh
稳定性稳定较稳定,插件过多可能启动慢
定制化定制化复杂定制化简单,支持丰富插件
版本特性较老旧较新

三、脚本文件结构

1. 脚本声明行

通过#!提示系统选择Shell解释器,需放在脚本第一行。
示例:

#!/bin/bash
2. 注释部分

#开头的内容为注释,不参与脚本执行,用于说明代码功能。
示例:

# 这是单行注释
name="John"  # 定义用户名变量(行尾注释)
3. 多行注释

通过<<COMMENTCOMMENT包裹实现多行注释。
示例:

#!/bin/bash
<<COMMENT
这是第一行注释内容
这是第二行注释内容
可以有多行注释内容在此处
COMMENT
echo "脚本继续执行"
4. 变量定义

用于存储数据(字符串、数字等),等号两边不能有空格,引用变量需加$
语法:变量名="值"
示例:

name="John"  # 字符串变量
age=25       # 数字变量
echo "Hello, $name!"  # 引用变量,输出:Hello, John!
5. 函数定义

将特定功能代码封装,方便重复调用,调用时需写函数名。
语法1(带function关键字):

function 函数名() {
函数体
}

语法2(简化版):

函数名() {
函数体
}

示例:

# 定义函数
function greet() {
echo "Welcome to the script!"
}
# 调用函数(必须调用才执行)
greet
6. 主程序逻辑

脚本核心部分,包含文件操作、条件判断、循环等核心任务。
示例(判断文件是否存在):

# 判断当前目录下是否存在test.txt文件
if [ -f "test.txt" ]; then
echo "File test.txt exists."
else
echo "File test.txt does not exist."
fi
7. 退出状态和返回值
  • 退出状态码:0表示执行成功,非0表示失败。
  • 通过exit指定退出状态码。

完整脚本示例:

#!/bin/bash
# 这是一个完整的Shell脚本示例,用于判断目录是否存在并创建新文件
# 定义变量
dir_name="my_directory"
# 定义函数,用于创建文件
create_file() {
touch "$1/new_file.txt"
echo "File new_file.txt created in $1."
}
# 主程序逻辑
if [ -d "$dir_name" ]; then
echo "Directory $dir_name exists."
create_file "$dir_name"
else
echo "Directory $dir_name does not exist. Creating it..."
mkdir "$dir_name"
create_file "$dir_name"
fi
# 脚本执行成功,返回退出状态码0
exit 0

四、文件权限管理

Shell脚本需添加执行权限才能运行,通过chmod命令设置:

chmod +x 脚本.sh  # 给脚本添加执行权限

五、执行脚本的4种方式

1. 作为可执行程序执行
  • 步骤1:添加执行权限(chmod +x test.sh
  • 步骤2:指定路径执行(当前目录需加./
    示例:
./test.sh  # 当前目录执行
/home/user/test.sh  # 绝对路径执行
2. 使用bash或者sh命令执行

无需添加执行权限,直接通过解释器调用。
示例:

bash test.sh  # 使用bash执行
sh test.sh    # 使用sh执行(sh通常指向bash)
3. 通过环境变量(PATH)执行
  • 步骤1:将脚本目录添加到PATH(export PATH=$PATH:..表示当前目录)
  • 步骤2:直接执行脚本
    示例:
export PATH=$PATH:.  # 临时添加当前目录到PATH
test.sh              # 直接执行脚本
4. 使用source命令执行

在当前Shell环境中执行,脚本内变量/函数会影响当前Shell。
示例:

source 脚本.sh  # 方式1
. 脚本.sh       # 方式2(.后需加空格)

第二章 变量和运算符

一、变量的定义

1. 基本变量的定义
  • 规则:变量名由字母、数字、下划线组成,不能以数字开头,等号两边无空格。
    示例:
# 字符串变量
name="John"
# 数字变量
age=25
2. 环境变量的定义
  • 作用域:当前Shell及子Shell可访问,通过export定义。
    示例:
# 定义并导出环境变量
export MY_VARIABLE="Hello, World!"
3. 多组变量定义(数组)
  • 存储多个值,元素间用空格分隔,索引从0开始。
    示例:
# 方式1:直接初始化
fruits=("apple" "banana" "cherry")
# 方式2:按索引赋值
my_array[0]="apple"
my_array[1]="banana"
my_array[2]="cherry"
# 打印数组元素
echo ${my_array[0]}  # 输出:apple
echo ${my_array[1]}  # 输出:banana
echo ${my_array[2]}  # 输出:cherry

二、变量的拼接和引用

1. 基本变量引用

通过$变量名引用,示例:

name="John"
echo "My name is $name."  # 输出:My name is John.
2. 变量值的拼接

直接将变量与字符串拼接,示例:

greeting="Hello"
name="John"
message="$greeting, $name!"
echo $message  # 输出:Hello, John!
3. 数组变量的引用
  • 单个元素:${数组名[索引]}
  • 整个数组:${数组名[@]}${数组名[*]}
    示例:
fruits=("apple" "banana" "cherry")
echo ${fruits[0]}  # 输出:apple
echo ${fruits[@]}  # 输出:apple banana cherry
4. 变量的间接引用

通过另一个变量的值引用目标变量,语法:${!变量名}
示例:

var1="name"
name="John"
echo ${!var1}  # 输出:John
5. 变量的作用域
类型定义位置作用域范围关键字
局部变量函数内部仅函数内可见local
全局变量函数外部整个脚本可见

示例(局部变量):

function test_function {
local local_var="This is a local variable."
echo $local_var  # 函数内可输出
}
test_function
echo $local_var     # 函数外输出为空

示例(全局变量):

global_var="This is a global variable."
function test_function {
echo $global_var  # 函数内可输出
}
test_function
echo $global_var     # 函数外可输出

三、环境变量和局部变量

1. 环境变量
  • 作用域:整个系统或特定Shell会话,所有用户/进程可访问。
  • 常见环境变量:
    • PATH:可执行文件搜索路径
    • HOME:当前用户主目录
    • LANG:系统语言和字符集
2. export命令
  • 导出局部变量:让子Shell可访问。
    示例:
# 局部变量(子Shell不可见)
local_variable="Hello, World!"
bash -c 'echo $local_variable'  # 输出空
# 导出为环境变量(子Shell可见)
export global_variable="Hello, Global!"
bash -c 'echo $global_variable'  # 输出:Hello, Global!
  • 导出函数:export -f 函数名,让子Shell可调用。
    示例:
# 定义函数
my_function() {
echo "This is a function."
}
# 导出函数
export -f my_function
# 子Shell调用
bash -c 'my_function'  # 输出:This is a function.
3. 局部变量
  • 使用场景:临时存储数据、控制循环/条件判断。
  • 定义:函数内用local关键字,示例:
function test_func {
local var="local value"  # 局部变量
echo $var
}

四、算数运算

Shell仅支持整数运算,常用方式有3种:

1. $((a+b))

语法简洁,支持+-*/%
示例:

a=5
b=3
result=$((a + b))
echo "a + b 的结果是: $result"  # 输出:a + b 的结果是: 8
2. let

执行算术表达式,变量名无需加$
示例:

a=5
b=3
let result=a+b
echo "a + b 的结果是: $result"  # 输出:a + b 的结果是: 8
3. expr

外部命令,运算符与操作数间需有空格。
示例:

a=5
b=3
result=$(expr $a + $b)
echo "a + b 的结果是: $result"  # 输出:a + b 的结果是: 8

五、字符串操作

1. 字符串拼接
  • 直接拼接:
str1="Hello"
str2=" World"
result=$str1$str2
echo $result  # 输出:Hello World
  • 双引号拼接:
str1="Hello"
str2=" World"
result="$str1$str2"
echo $result  # 输出:Hello World
2. 获取字符串长度

语法:${#字符串}
示例:

str="Hello, World!"
length=${#str}
echo "字符串的长度是: $length"  # 输出:字符串的长度是: 13
3. 字符串的提取

语法:${string:offset:length}(offset起始位置,length可选)
示例:

str="Hello, World!"
# 从索引7开始提取
substr1=${str:7}
echo "从索引7开始的子串是: $substr1"  # 输出:从索引7开始的子串是: World!
# 从索引0开始,提取长度5的子串
substr2=${str:0:5}
echo "从索引0开始,长度为5的子串是: $substr2"  # 输出:从索引0开始,长度为5的子串是: Hello

第三章 控制结构入门

一、条件判断

1. if语句

语法:

if [ 条件判断 ]; then
# 条件为真执行
elif [ 另一个条件判断 ]; then
# 前条件为假,此条件为真执行
else
# 所有条件为假执行
fi

示例:

num=10
if [ $num -gt 20 ]; then
echo "数字大于20"
elif [ $num -gt 5 ]; then
echo "数字大于5但不大于20"
else
echo "数字小于等于5"
fi  # 输出:数字大于5但不大于20
2. case语句

用于多分支判断,语法:

casein
模式1)
# 匹配模式1执行
;;
模式2)
# 匹配模式2执行
;;
*)
# 所有模式不匹配执行
;;
esac

示例:

fruit="apple"
case $fruit in
apple)
echo "这是苹果"
;;
banana)
echo "这是香蕉"
;;
*)
echo "未知水果"
;;
esac  # 输出:这是苹果
3. test命令

用于数据比较和文件测试,语法:test 条件(等价于[ 条件 ][]两边需有空格)。

数值比较
运算符描述
-eq等于
-ne不等于
-gt大于
-ge大于等于
-lt小于
-le小于等于
字符串比较
运算符描述
=等于
!=不等于
-z字符串长度为零
-n字符串长度不为零
文件测试
运算符描述
-e文件/目录存在
-f文件存在且为普通文件
-d目录存在
-r文件可读
-w文件可写
-x文件可执行

二、“[]” 和 “[[]]” 命令的区别

1. 语法差异
  • [][]与操作数间需有空格,否则语法错误。
    示例:if [ "$a" = "$b" ]; then
  • [[]]:语法要求宽松,建议加空格但非强制。
    示例:if [[ "$a" = "$b" ]]; then
2. 字符串比较差异
  • []:不支持模式匹配,特殊字符需转义。
  • [[]]:支持模式匹配(如*?),无需转义。
    示例:
    str="abc123"
    if [[ $str = abc* ]]; then
    echo "字符串以 abc 开头"  # 输出此内容
    fi
3. 正则表达式匹配差异
  • []:不支持。
  • [[]]:支持=~进行正则匹配。
    示例:
    str="abc123"
    if [[ $str =~ ^abc[0-9]+$ ]]; then
    echo "字符串符合正则表达式"  # 输出此内容
    fi
4. 逻辑操作差异
  • []:用-a(与)、-o(或)。
    示例:if [ $a -gt 5 -a $b -lt 30 ]; then
  • [[]]:用&&(与)、||(或)。
    示例:if [[ $a -gt 5 && $b -lt 30 ]]; then
5. 短路求值差异
  • []:不支持,无论前条件结果,后条件都会求值。
  • [[]]:支持,前条件可确定结果时,后条件不求值。
6. 兼容性差异
  • []:POSIX标准,兼容所有Shell。
  • [[]]:Bash扩展,仅支持Bash、Zsh等。

三、基础循环

1. for循环遍历列表

语法:

for 变量名 in 取值列表; do
命令序列
done

示例:

# 遍历数字1-5
for i in {1..5}; do
echo "数字: $i"
done
2. while循环基础用法

语法:

while [ 条件判断 ]; do
# 条件为真执行
命令序列
done

示例:

count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))  # 变量自增
done
3. until循环语句

while相反,条件为假时执行循环。
语法:

until 条件; do
命令序列
done

第四章 高级控制结构

一、case多分支选择结构

示例(根据输入输出星期):

#!/bin/bash
# 提示用户输入一个数字
echo "请输入一个1 - 7之间的数字:"
read day
# 使用case语句根据输入的数字输出对应的星期几
case $day in
1)
echo "星期一"
;;
2)
echo "星期二"
;;
3)
echo "星期三"
;;
4)
echo "星期四"
;;
5)
echo "星期五"
;;
6)
echo "星期六"
;;
7)
echo "星期日"
;;
*)
echo "输入无效,请输入1 - 7之间的数字。"
;;
esac

二、break循环语句

  • 作用:终止循环,break [n]可终止n层循环。
    示例(for循环中使用):
for i in {1..10}; do
if [ $i -eq 5 ]; then
break  # 遇到5终止循环
fi
echo $i
done
echo "Loop finished."  # 输出:1 2 3 4 + Loop finished.

示例(while循环中使用):

count=1
while [ $count -le 10 ]; do
if [ $count -eq 3 ]; then
break  # 遇到3终止循环
fi
echo $count
((count++))
done
echo "Loop finished."  # 输出:1 2 + Loop finished.

三、continue循环语句

  • 作用:跳过当前循环剩余部分,直接进入下一次循环。
    示例:
for i in {1..10}; do
if [ $i -eq 5 ]; then
continue  # 遇到5跳过后续echo
fi
echo $i
done  # 输出:1 2 3 4 6 7 8 9 10

四、shell退出码($?)

  • 规则:命令执行成功返回0,失败返回非0
  • 查看方式:$?获取上一命令的退出码。
    示例:
grep "pattern" file.txt
if [ $? -eq 0 ]; then
echo "找到了匹配的内容。"
else
echo "未找到匹配的内容或者出现错误。"
fi

第五章 函数和模块化

一、函数定义和参数传递(1−1-1n的用法)

  • 函数内通过$1-$n获取参数($1第一个参数,$n第n个参数)。
    示例:
# 定义函数(计算两数之和)
add() {
echo $(( $1 + $2 ))  # $1第一个参数,$2第二个参数
}
# 调用函数(传递参数3和5)
result=$(add 3 5)
echo $result  # 输出:8

二、返回值处理(return与输出结果的区别)

  • return:返回状态码(0成功,非0失败),仅支持整数。
    示例:
function check_number() {
if [ $1 -gt 10 ]; then
return 0  # 成功
else
return 1  # 失败
fi
}
check_number 15
echo "函数返回状态码: $?"  # 输出:0
  • 输出结果:通过echo输出计算结果,外部用$(函数名)捕获。
    示例:
function add() {
echo $(( $1 + $2 ))  # 输出结果
}
result=$(add 2 3)
echo $result  # 输出:5

三、局部变量和作用域(local关键字)

  • local:函数内定义局部变量,仅函数内可见。
    示例:
# 定义函数
function test_function {
local var="I am a local variable"  # 局部变量
echo $var
}
# 调用函数
test_function  # 输出:I am a local variable
# 函数外部访问
echo $var  # 输出空

第六章 输入输出处理

一、shell脚本中的数据流

数据流编号默认来源/目的地
标准输入(stdin)0键盘
标准输出(stdout)1屏幕
标准错误(stderr)2屏幕

二、重定向技巧(>,>>,2>,&>)

1. > 覆盖重定向

覆盖写入文件,示例:

# 将ls结果覆盖写入file_list.txt
ls -l > file_list.txt
2. >> 追加重定向

在文件末尾追加,示例:

# 将日期追加写入log.txt
date >> log.txt
3. 2> 错误输出重定向

将错误信息覆盖写入文件,示例:

# 错误信息写入error.log
cat no_such_file.txt 2> error.log
4. &> 合并输出重定向

合并标准输出和错误,覆盖写入文件,示例:

# 所有输出写入all.log
bash script.sh &> all.log
5. 黑洞文件(/dev/null)

丢弃输出,示例:

# 丢弃所有输出
command &> /dev/null

三、管道的高级应用(多命令组合)

  • 管道(|):前一命令的stdout作为后一命令的stdin。
    示例(筛选并排序文件大小):
du -sh /var/log/* | grep -E "[0-9]+M" | sort -hr

四、用户交互:read -p “提示” var

  • read -p "提示" var:显示提示并读取输入到变量。
  • 常用选项:
    • -t 秒数:超时时间
    • -s:输入不回显(密码)
    • -n 字符数:读取指定字符数后结束
      示例:
# 基础交互
read -p "请输入姓名:" name
echo "你好,$name!"
# 输入密码(不回显)
read -s -p "请输入密码:" pwd
echo -e "\n密码已接收"

第七章 字符串与数组

一、数组

1. 数组的定义

示例:

# 数组定义
arr=(a b c d)
# 访问数组元素(索引从0开始)
echo ${arr[0]}  # 输出:a
echo ${arr[2]}  # 输出:c
# 访问所有元素
echo ${arr[@]}  # 输出:a b c d
echo ${arr[*]}  # 输出:a b c d
# 获取数组长度
echo ${#arr[@]}  # 输出:4
2. 数组的遍历方式
  • 方式1:for循环遍历
for item in "${arr[@]}"; do
echo $item
done
  • 方式2:按索引遍历
for ((i=0; i<${#arr[@]}; i++)); do
echo "索引 $i: ${arr[$i]}"
done
3. 数组的扩展和修改
# 添加元素
arr+=(e f)  # 数组变为(a b c d e f)
# 修改元素
arr[1]=new  # 数组变为(a new c d e f)
# 删除元素
unset arr[2]  # 删除索引2的元素

二、字符串模式匹配与截取

1. 字符串长度

示例:

str="hello world"
echo ${#str}  # 输出:11
2. # 操作符号(从左侧截取)
path="/usr/local/bin/bash"
# 移除最短匹配的前缀
echo ${path#/*/}  # 输出:local/bin/bash
# 移除最长匹配的前缀
echo ${path##/*/}  # 输出:bash
3. % 操作符号(从右侧截取)
file="document.txt.bak"
# 移除最短匹配的后缀
echo ${file%.*}  # 输出:document.txt
# 移除最长匹配的后缀
echo ${file%%.*}  # 输出:document
4. 字符串切片
str="abcdefgh"
echo ${str:2:3}  # 输出:cde(索引2开始,截取3个字符)
echo ${str:4}    # 输出:efgh(索引4开始,截取到末尾)

实践任务

任务1:开发文件类型统计脚本(分类统计目录下各类文件数量)
#!/bin/bash
# 检查参数
if [ $# -ne 1 ]; then
echo "使用方法: $0 <目录路径>"
  exit 1
  fi
  dir="$1"
  if [ ! -d "$dir" ]; then
  echo "错误: $dir 不是有效的目录"
  exit 1
  fi
  # 声明关联数组
  declare -A type_count
  # 遍历目录
  for file in "$dir"/*; do
  if [ -f "$file" ]; then
  filename=$(basename "$file")
  # 提取后缀
  if [[ "$filename" == *.* ]]; then
  ext="${filename##*.}"
  ext=$(echo "$ext" | tr 'A-Z' 'a-z')  # 转为小写
  else
  ext="无扩展名"
  fi
  ((type_count["$ext"]++))
  fi
  done
  # 输出结果
  echo "目录 $dir 中的文件类型统计:"
  echo "=========================="
  for ext in "${!type_count[@]}"; do
  echo "$ext: ${type_count[$ext]}"
  done | sort -k2nr
任务2:编写用户管理系统(通过函数实现增删查功能)
#!/bin/bash
# 声明关联数组
declare -A users
# 加载初始用户
load_users() {
users["admin"]="管理员,admin@example.com,1001"
users["guest"]="访客,guest@example.com,1002"
}
# 显示所有用户
list_users() {
echo "==================== 用户列表 ===================="
echo "用户名 | 姓名 | 邮箱 | UID"
echo "-------------------------------------------------"
for username in "${!users[@]}"; do
IFS=',' read -r name email uid <<< "${users[$username]}"
echo "$username | $name | $email | $uid"
done
echo "================================================="
}
# 添加用户
add_user() {
read -p "请输入用户名: " username
if [ -n "${users[$username]}" ]; then
echo "错误: 用户 $username 已存在"
return 1
fi
read -p "请输入姓名: " name
read -p "请输入邮箱: " email
read -p "请输入UID: " uid
users["$username"]="$name,$email,$uid"
echo "用户 $username 添加成功"
}
# 删除用户
delete_user() {
read -p "请输入要删除的用户名: " username
if [ -z "${users[$username]}" ]; then
echo "错误: 用户 $username 不存在"
return 1
fi
unset users["$username"]
echo "用户 $username 已删除"
}
# 查询用户
search_user() {
read -p "请输入要查询的用户名: " username
if [ -z "${users[$username]}" ]; then
echo "用户 $username 不存在"
return 1
fi
IFS=',' read -r name email uid <<< "${users[$username]}"
echo "==================== 用户详情 ===================="
echo "用户名: $username"
echo "姓名: $name"
echo "邮箱: $email"
echo "UID: $uid"
echo "================================================="
}
# 显示菜单
show_menu() {
echo "=============== 用户管理系统 ==============="
echo "1. 显示所有用户"
echo "2. 添加用户"
echo "3. 删除用户"
echo "4. 查询用户"
echo "5. 退出"
echo "============================================"
read -p "请选择操作 (1-5): " choice
}
# 主程序
main() {
load_users
while true; do
show_menu
case $choice in
1) list_users ;;
2) add_user ;;
3) delete_user ;;
4) search_user ;;
5)
echo "谢谢使用,再见!"
exit 0
;;
*) echo "无效的选择,请重试" ;;
esac
read -p "按回车键继续..."
clear
done
}
# 启动程序
main