Shell 笔记
Shell 笔记
参考教程 https://www.runoob.com/linux/linux-tutorial.html
Shell 基本介绍
Shell 是一种用于访问系统内核服务的应用程序
实际上 Windows Explorer 属于一种图形界面的 Shell 程序
而在 Linux 中的 Shel 程序如 bahs, 主要使用命令的方式交互
其中用于控制 Shell 完成功能的命令语言也是编程语言则称为 Shell 脚本
以下 Shell 笔记以介绍 bash 下的 Shell 脚本的编写为主
Shell 程序
在 Linux 中, 常用的 Shell 程序有
- sh 最基础的 shell, 解释器一般位于
bin/sh
(注意该笔记中的部分语法可能不被 sh 支持) - bash 在 sh 基础上改进的 shell, 通常是 Linux 系统的默认解释器, 解释器一般位于
bin/bash
- zsh 功能更强大的 shell, 但一般额外需要安装, 语法与 bash 存在一定差异, 安装后解释器位于
bin/zsh
Shell 脚本
可以创建文本文件, 并在文本内写入 Shell 脚本供 Shell 程序解释并执行
约定 Shell 脚本的第一行为 #!<解释器路径>
通过该语句规定执行该脚本所用的解释器, 对于 php, python 等脚本同理
Shell 脚本文件的拓展名则无具体要求, 一般以该脚本使用的 Shell 程序为拓展名, 如 .zsh
, .sh
等
运行脚本
可通过以下四种方式执行脚本
- 通过指定的解释器执行脚本文件
<解释器> <脚本文件路径>
注意应当前目录下的文件应当使用相对路径./<脚本文件名>
- 将脚本视为可执行程序运行
<脚本文件路径>
使用该方法时, 脚本文件需要有执行权限 - 使用当前 Shell 环境执行脚本文件
source <脚本文件路径>
- 通常可以使用
.
代替source
- 以上两种方式都是在新终端中执行脚本, 但
source
语句是在当前终端执行脚本, 因此其中定义的变量都会被作为环境变量保留下来, 因此该语句一般用于执行终端的初始化脚本 - 此外也可将该语句用于 bash 脚本中, 类似于导入其他的 bash 脚本
- 通常可以使用
- 直接向 Shell 终端程序输入脚本, 如以下的 bash 脚本语句均可在 bash 终端下输入并运行, 此时需要使用
;
代替换行
基本语法
在 Shell 脚本中, 使用 #
作为单行注释
使用 : '
作为注释开头, '
作为注释结尾, 实现多行注释
除了如 if
等程序控制关键字以及定义的子命令 (函数), 每个语句都相当与执行一个程序 (对于命令实际对应的程序可通过指令 whereis 查询), 且都是 程序路径 + 参数
的格式, 一般也将这些语句称为命令
因此可通过 bash 脚本组织一系列命令的执行, 减少重复输入命令
使用命令时注意
- 当程序在环境变量
PATH
下时, 可直接输入程序名以执行这些程序, 对于绝大部分的 Linux 命令都是以方式执行的 - 否则需要给出程序的完整路径, 例如执行当前目录下的 python 脚本程序使用
./script.py
, 运行bash
程序使用/bin/bash
- Shell 使用换行区分语句, 在终端中则可使用
;
代替换行, 因此<命令1>;<命令2>
可在终端中执行多个命令
在 bash 中, 最常使用的命令即 echo "<字符串>"
, 该命令可向交互界面打印字符串
变量
变量定义
使用 <变量名>=<变量初值>
的语法即可定义一般变量
- 定义变量时,
=
两侧不应有空格, 否则可能导致错误 - 一般使用字母, 数字, 下划线的组合作为变量名 (不能以数字开头)
- 如果要定义常量, 则应在变量定义后, 使用
readonly
修饰变量, 且常量一般使用大写字母作为变量名 - 不建议在 bash 中使用浮点数与浮点运算, bash 仅会将这些数作为字符串处理
- 使用命令
read <变量名>
则将从控制台读取用户输入为变量赋值, 如果变量不存在将自动创建
变量初值除了整数, 以及接下来介绍的字符串与数组, 可以使用 $(命令)
, 此时将被包裹的内容视为脚本并运行, 然后将结果返回
- 可使用此方法来获取查询命令的结果, 如
a=$(pwd)
可获取当前工作目录,a=$(cat ${path})
可读取 path 中的文件内容 - 在命令中使用
${变量名}
也将自动取变量值, 可使用此方法将变量值传入命令 - 语法
`命令`
与$(命令)
效果相同, 笔记中以后者为主
使用变量
- 通过
${<变量名>}
即可访问变量的值 (一般情况下可省略大括号) - 对变量重新赋值的方式与变量定义相同, 不需要加
${...}
- 使用
unset <变量名>
即可删除指定名称的变量 - 对于环境变量, 可使用
$<变量名>
的方式访问 - 此外还有一些特殊变量
$0
表示当前脚本路径 (不包含参数与 Shell 程序), 可配合 dirname 获取脚本所在目录${n}
运行脚本时给出的第 n 个参数 (当 n 小于 10 时, 可省略大括号)$#
传递给脚本的参数数量$?
上一个命令的退出状态 (一般执行成功时为0
, 失败时为1
或任意非零值)
- 使用命令
declare -p <变量名>
可以获取变量的类型与值信息
字符串
可以使用 '
或 "
包裹字符表示字符串, 但两者存在区别
- 使用
'
时, 不会对字符串内的内容做转换 (包括变量与转移字符) - 使用
"
时, 能解析${<变量值>}
对应的变量值, 以及使用\
转义
对于字符串变量 str
, 可使用以下语法访问字符串的信息
${#str}
获取字符串的长度${str:<ind>:<size>}
获取字符串在索引ind
(从 0 开始索引) 及之后最多size
个字符的子字符串
当size = 1
, 即可以访问特定索引位置的字符${str/<str1>/<str2>}
将str
中第一个匹配str1
的子字符串替换为str2
${str//<str1>/<str2>}
与上一个相同, 但该命令可以替换所有匹配的子字符串${str/<str1>}
将str
中第一个匹配str1
的子字符串删除${str//<str1>}
与上一个相同, 但该命令可以删除所有匹配的子字符串
使用命令 expr index "${str}" <字符>
可用于查询字符在字符串中最早出现位置的索引
如果希望保存结果, 则需要 res=$(exper index "${str}" <字符>)
对于其他字符串处理, 也可通过命令 expr
完成, 具体见 expr --help
数组
使用语法 (元素1 元素2 ...)
可以定义一个一维数组
- 数组的索引从 0 开始
- 注意使用空格划分元素
- bash 不支持多维数组
使用语法 declare -a <变量名>=([索引1]=元素1 [索引2]=元素2 ...)
可以定义关联数组
- 使用改语法时不一定要声明初值, 可以定义后再赋值
修改数组时 (对于数组 x
为数组, 对于关联数组 x
为键)
- 使用
${arr[x]}
访问数组arr
的第x
个元素 - 使用
arr[x]=<新值>
为数组第x
个元素赋值, 如果对不存在的索引赋值则将自动创建, 因此数组的索引可能不连续 - 使用
${arr[*]}
获取数组内的所有元素, 通常使用此方法打印数组内的所有元素 - 使用
${#arr[*]}
获取数组的长度 - 使用
${!arr[*]}
获取数组所有有效的索引
运算
数学运算
使用 $[数学运算表达式]
或 $((数学运算表达式))
表示数学运算 (注意 bash 仅支持整数运算)
主要支持的运算有
$[${a} + ${b}]
相加$[${a} - ${b}]
相减$[${a} * ${b}]
相乘$[${a} / ${b}]
相除 (向下取整)$[${a} % ${b}]
取余
如果希望计算浮点数与数学函数, 可能需要借助 $()
并执行如 bc
等计算指令
关系运算
使用 [[ 关系运算表达式 ]]
表示关系运算 (注意 [[]]
与表达式之间要有空格)
除了以上关系表达式, 也可使用 test <关系运算表达式>
当关系表达式被运算后, 如果表达式成立, 则相当于命令执行成功, 此时特殊变量 $?=0
, 否则 $?=1
对于多个关系运算, 可使用以下运算符进行连接
!
对运算结果取反||
逻辑或 (短路求值)&&
逻辑与 (短路求值)
数值关系运算
常用的数值关系运算表达式有 (此处省略了 [[ ]]
)
${a} -eq ${b}
检测两个数是否相等${a} -ne ${b}
检测两个数是否不相等${a} -gt ${b}
检测变量a
是否大于b
${a} -lt ${b}
检测变量a
是否小于b
${a} -ge ${b}
检测变量a
是否大于等于b
${a} -le ${b}
检测变量a
是否小于等于b
字符串关系运算
常用的字符串关系运算表达式有
${str1} = ${str2}
检测两个字符串是否相等 (注意只有一个等号)${str1} != ${str2}
检测两个字符串是否不同-z ${str}
检测字符串是否为空 (长度为 0)-n ${str}
检测字符串是否不为空 (长度不为 0)${str}
检测是否有字符串, 即任意字符串的表达式结果均为真, 不存在 false, 0 等表述
文件关系运算
常用的文件关系运算表达式有 (注意, 以文件的路径字符串表示被检测的文件)
-f/d/c/L ${path}
检测该路径是否指向一般文件, 目录, 设备, 连接-r/w/x ${path}
检测脚本对该文见是否有读取, 写入, 执行权限
流程控制
注意, 使用流程控制语句时, 换行方式最好与基本格式保持一致
如果在终端中, 则可使用 ;
代替换行
if 语句
if 语句格式如下
if 条件1
then
满足条件时执行 ...
elif 条件2
then
满足条件时执行 ...
else
否则执行 ...
fi
使用 if 时注意
- 允许有多个
elif
分支, 且elif
与else
不是必须的 - if 中不允许有空的分支
- if 语句的条件本质为执行命令, 并根据命令是否执行成功判断 (
$?=0
), 因此可使用关系运算作为条件
case 语句
case 语句格式如下
case 判断值 in
情况1)
满足情况执行 ...
;;
情况2)
满足情况执行 ...
;;
esac
使用 case 注意
- 当判断值为数字时, 一般直接读取变量
${a}
即可 - 当判断值为字符串变量
str
时, 判断值需要表示为"${str}"
- 使用
|
可以并列表示多个情况 - 对于其他情况, 可使用
*
代替
for 循环
指定循环次数时, for 循环的格式为 (注意空格)
for (( <定义>; <边界>; <步长> ))
do
循环执行的语句 ...
done
使用这种语法的 for 循环注意
- 其中的定义, 边界, 步长与 C 语言类似, 且定义时不需要知名类型
例如(( i = 0; i < 10; i++ ))
, 则将进行十次循环 (+=
运算也支持) - 该语法仅在部分 Shell 中支持, 如 zsh 与 bash, 但对于 ubuntu 的默认 Shell 程序 dash 不支持
- 迭代变量可通过
${迭代变量名}
访问
除了指定循环次数, for 循环还可用于遍历数组, 基本格式为
for <迭代变量名> in <迭代序列>
do
循环内容 ...
done
使用时注意
- 最简单的可通过
元素1 元素2 ...
表示迭代序列, 将按空格划分元素 - 如果给出一个字符串, 则也将一类似的方式解析, 即按空格或换行划分字符串, 迭代各个子字符串 (该特性仅用于 bash, 在 zsh 中将视为一个整体)
- 如果希望迭代数组, 可通过数组信息, 使用
${arr[*]}
迭代数组元素, 或${!arr[*]}
迭代数组索引 - 此外, 还可以使用命令
seq <序列长度>
生成指定长度的序列, 如$(seq ${n})
将生成一个 1 - n 的序列, 完成n
次循环, 实现类似指定次数循环的效果
while 循环
while 循环有基本结构
while 条件
do
循环内容 ...
done
使用时注意
- while 循环的条件与 if 语句相同
- 当条件为字符串
true
时, 为无限循环, 此时可通过 Ctrl+D 强制退出脚本 - 将 while 改为 until, 则变为 do-until 循环
循环控制
对于 for 与 while 循环, 有如下循环控制语句
break
退出循环continue
退出当此循环
函数
使用如下方式即可定义函数, 其中关键字 function
不是必要的
function fun(){
函数内容 ...
return 返回值;
}
使用函数时注意
- 使用调用命令的方式调用函数, 如
<函数名> [参数1] [参数2] ...
- 函数中同样使用
${n}
,$#
等特殊变量接收参数以及参数数量, 而不需要而外定义 - 函数执行后, 返回值将赋给特殊变量
$?
- 函数的返回值只能是 0-255 的整数, 且一般执行成功时返回 0, 也可以不设置返回值, 此时返回最后一条命令的状态
- 当函数的返回值为
0
(表示 true) 或非0
值时, 可将函数用于 if 语句的条件中 - 如果希望从函数中获取复杂信息, 则可使用
$(命令)
的方式调用函数, 接收函数通过echo
输出的内容 - 可通过
source
指令执行其他脚本的方式, 引入其他脚本中的函数 - 函数中定义的变量默认为全局有效, 如果希望定义局部变量, 需要在变量定义前加上关键字
local
高级使用技巧
重定向
关于重定向的更多内容, 详见有关教程的介绍, 此处仅介绍基本使用
Linux 程序通过标准输入 stdin
读取信息, 并将信息输出到标准输入 stdout
中, 其中错误则输出到标准错误 stderr
中
一般情况下标准输入, 标准输出, 标准错误均为终端, 且这三者在 Linux 都被视为文件, 分别有文件描述符 (类似文件句柄) 0, 1, 2
, 文件本体则位于目录 /dev
下
通过将 stdout
重定向为指定文件, 可实现将命令的输出保存到指定文件中
command > file
将命令的标准输出重定向为文件file
(可用变量, 字符串表示), 此时输出将覆盖文件file
内原有的内容command >> file
将命令的标准输出重定向为文件file
, 此时输出将在file
原有内容的基础上追加
通过将 stdin
重定向为指定文件, 可实现以已有的文件内容代替用户输入
配合 grep 等命令对结果内容进一步筛选
command < file
将标准输入重定向为已有文件file
command << TAG ... TAG
将标准输入重定向为使用TAG
包裹的多行内容 (TAG
可以替换为任意英文组合), 可使用此方法在终端中输入多行内容 (注意这不是命令的参数)
使用时注意
command
为包括参数的完整命令- 重定向输出时, 文件
file
不存在时将自动创建; 重定向输入时, 文件file
必须存在 - 对于
stderr
的重定向, 则使用2>
与2>>
代替 - 对于同一命令可设置多个重定向, 如
command < infile > outfile 2> errfile
将标准输入重定向为文件infile
, 标准输出重定向为文件outfile
, 标准错误重定向为文件errfile
- 系统中有特殊文件
/dev/null
, 该文件无法读取到任何内容, 且任何输入内容都会被丢弃, 因此可将标准输出重定向到该文件中, 以实现不显示命令输出的效果
管道
重定向使命令能与文件之间进行交互, 如果希望多个命令之间进行交互, 则可使用管道符
管道的基本格式为 命令1 | 命令2 | 命令3 ...
- 管道连接的各个命令将从做向右执行, 且上一个命令的标准输出都将重定向为下一个命令的标准输入
- 管道的命令无法从终端获输入, 但可通过重定向为第一个命令提供输入以及重定向最后一个命令的输出
- 管道可以配合 grep 等命令对结果内容进一步筛选, 实现查询命令结果的效果, 如
ls | grep test
将列出所有名称带有test
的文件
参数传递过滤器
注意, 无论时重定向还是管道, 都只能设置标准输入, 而无法设置命令的参数
如果希望将标准输入转化为命令参数, 则可以使用命令 xargs
使用 xargs
时, 首先将从标准输入接收信息, 然后解析接收到的信息
- 按指定规则将信息分割为多个命令参数
- 按指定规则, 将这些参数分组
- 将每组划分的参数与给出的命令拼接得到完整命令, 并执行各组命令
命令的基本格式如下
xargs [-d<ch> | -0] [-n<x>] [-r] [-p] [-e<ch>] <command>
-d<ch>
设置参数间的分界符为ch
, 默认为空格-0
设置参数间的分界符为\0
(推荐将分界符设置为\0
, 以防止误将参数识别为分界符)-n<x>
以x
个参数为一组执行命令, 默认为按换行划分-r
如果从标准输入没有接收到信息, 则停止执行命令-p
在执行各组命令前进行询问-e<ch>
当接收到字符ch
时, 停止接收数据的处理command
待拼接的命令 (该命令也可以带参数, 拼接的参数将跟在后面)
例如查找文件时, 可使用此方法将查找结果分组作为 ls -l
的参数, 实现显示查找结果信息的效果, 如find ... -print0 | xargs -0 ls -l
(该例子通过 -print0
与 -0
选项, 约定了以 \0
为参数分界符, 防止误将文件名识别为分界符)
启动时执行
在 Shell 启动时 (不是系统启动, 系统启动执行参见定期执行任务), 将会执行一段特殊的 Shell 脚本
可以在此脚本中完成设置环境变量, 定义函数等操作
一般情况下
- 针对所有 Shell 脚本, 启动时都将调用脚本
/etc/profile
(全局) 与~/.profile
(用户) - 针对特定脚本程序, 这些文件一般具有名称
<Shell 程序名> + rc
(不固定)- 位于文件夹
/etc/
下时, 任何用户的 Shell 启动时都会运行该脚本 - 位于文件夹
~/
下时, 该用户的 Shell 启动时将运行该脚本
- 位于文件夹
注意, 编写启动时执行的脚本时, 应当使用绝对路径