Shell脚本是一种简单的脚本语言,运行在Unix-like的操作系统上,像Linux,mac, unix等。Shell脚本的解析器是shell,Unix-like系统很多,所以出现了很多不同的shell,像tcsh, csh, ash, bash, dash等。
像Ubuntu默认使用的Shell是dash,其特点是解析执行速度快,缺点是支持的语法特性少。如果要查看自己系统上使用的是何种解释器,可以使用如下命令:
ls /bin/sh -al
# or
echo $SHELL
bash使用更为便捷,此文主要以bash解释器为基础来进行讲解。Shell有一些老的语法形式,不推荐使用,此文不讲解。
因为系统内置了shell解释器,所以可以直接在控制台窗口上输入Shell脚本来解释执行。也可以将Shell脚本编写到以.sh为后缀的脚本文件当中。然后通过sh/bash调用执行,也可以将脚本文件修改为可执行模式,直接运行。
因为Shell脚本的解释器有很多版本,不同版本之间的功能略有差异,为了防止编写的脚本应用时使用的解释器不同而导致脚本功能异常,系统在调用解释器时会先检查脚本文本的第一行,如果有指定解释器路径,则以此解释器来执行当前脚本。
#!/bin/bash
在代码中添加注释,可以提高代码的可读性,降低开发维护难度。
# 此行是注释
echo start
# 此行是注释
# 此行是注释
# 此行是注释
:<<!
Copyright 2022, Ys Co. Ltd.
All rights erserved.
Revision: 1.0.0
!
$variable_name
${variable_name}
”$variable_name“
”${variable_name}“
加双引号是为了防止引用变量中有空格时,作为形参传递给第三方应用程序时被截断。所以推荐第4种用法,但是一些常见的$1,$2等也遵循惯例。关于语法形式,推荐使用shellcheck来校验。
unset variable_name
在非数字操作的场景中,所有变量都被当作字符串,字符串可以加单引号,也可以加双引号,也可以不加引号。
string="abcd"
echo ${#string} # 输出 4
str="abcdefg"
# 从第1个到第4个(从0开始)
echo ${str:1:4}
# 从第1个到倒数第2个(从0开始)
echo ${str:1:-2}
ip="172.16.36.40"
# 只替换第1个.:172-16.36.40
echo ${ip/./-}
# 替换所有:172-16-36-40
echo ${ip//./-}
[root@payqa1 work]# echo ${ip}
172.16.36.40
# 从头开始删除到第1个.
[root@payqa1 work]# echo ${ip#*.}
16.36.40
# 从头删除到最后1个.
[root@payqa1 work]# echo ${ip##*.}
40
# 从尾部开始删除
[root@payqa1 work]# echo ${ip%.*}
172.16.36
[root@payqa1 work]# echo ${ip%%.*}
172
name="new line\n"
echo -e ${name}
# 访问第0个元素
echo $array_name[0]
# 访问所有元素
echo ${array_name[@]}
# 取得数组元素的个数
length=${#array_name[@]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
declare -A high=(["mike"]=178 ["will"]=190 ["jack"]=172)
high["lily"]=165
echo "${high[@]}"
echo "${high["mike"]}"
arr=(1 2 3)
unset arr[1]
echo ${arr[@]}
echo "param1" "param2"
echo $p1 $2
./run.sh "${p1}" "${p2}"
# $1是脚本名字
echo "param1:" $1
echo "param2:" $2
echo "param count:" $#
echo "All param:" $@
Shell脚本只支持整形计算,包括负数,其范围为64bit大小。
Shell脚本支持加、减、乘、除和取余操作,其语法主要有两形式。
let a=4*4
let a=a*8
b=8
# 推荐
(( b=b*8 ))
echo $a
echo $b
逻辑判断语法形式:
if expr; then
# do something
elif expr
# do something
else
# do something
fi
逻辑表达式,针对不同的情况,有不同的表达式
a=10
b=11
if [ $a -eq $b ]; then
echo "$a -eq $b : a 等于 b"
fi
if [ $a -ne $b ]; then
echo "$a -ne $b: a 不等于 b"
fi
if [ $a -gt $b ];then
echo "$a -gt $b: a 大于 b"
fi
if [ $a -lt $b ]; then
echo "$a -lt $b: a 小于 b"
fi
if [ $a -ge $b ]; then
echo "$a -ge $b: a 大于或等于 b"
fi
if [ $a -le $b ]; then
echo "$a -le $b: a 小于或等于 b"
fi
# 另外一种方式直接符号比较
if (( $a <= $b )); then
echo "$a -le $b: a 小于或等于 b"
fi
if (( $a != $b )); then
echo "$a -ne $b: a 不等于 b"
fi
字符串比较有两种形式使用[],[[]],后者功能更强大,推荐后者。
if [[ $a == z* ]]; then
# $a只要以z开头即为true
fi
if [[ $a == "z*" ]]; then
# $a为”z*“时才为true
fi
if [[ "$a" > "$b" ]]; then
echo "a greater than b"
fi
if [[ -z "$a" ]]; then
echo "$a length is 0"
fi
if [[ -n "$a" ]]; then
echo "$a length is not 0"
fi
if [[ "$a" ]]; then
echo "$a length is not null"
fi
file="/var/test.sh"
if [ -r $file ]; then
echo "文件可读"
fi
if [ -w $file ]; then
echo "文件可写"
fi
if [ -x $file ]; then
echo "文件可执行"
fi
if [ -f $file ]; then
echo "文件为普通文件"
fi
if [ -d $file ]; then
echo "文件是个目录"
fi
if [ -s $file ]; then
echo "文件不为空"
fi
if [ -e $file ]; then
echo "文件存在"
fi
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
|| 逻辑的 OR [[ $a -lt 100 || $b -gt 100 ]] 返回 true
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
# 3个命令都会执行
cmd && cmd2 && cmd3
# 只要有一个失败,后面的就不执行
cmd || cmd2 || cmd3
# 按空格分割遍历in后面的内容
for loop in 1 2 3 4 5; do
echo "The value is: $loop"
done
for str in This is a string; do
echo $str
done
#${#array[@]}获取数组长度用于循环
for(( i=0;i<${#array[@]};i++ )); do
echo ${array[i]};
done;
# 不带数组下标
for element in ${array[@]}; do
echo $element
done
# 带数组下标
for i in "${!array[@]}"; do
printf "%s\t%s\n" "$i" "${array[$i]}"
done
while 循环用于不断执行一系列命令,也用于从输入文件中读取数据。
int=1
while (( int <= 5 )); do
echo $int
(( int++ ))
done
# 读取用户键盘输出
while read -r line; do
echo "${line}"
done
# 读取文件util.sh,按行输出
while read -r line; do
echo "${line}"
done < util.sh
until循环到满足条件
a=0
# 输出0到9
until [ ! $a -lt 10 ]; do
echo $a
(( a++ ))
done
read aNum
case $aNum in
1) echo '1'
;;
2) echo ' 2'
;;
3) echo '3'
;;
4) echo '4'
;;
*) echo 'default'
;;
esac
name="mike"
case "$name" in
# ”mi"*匹配任何以mi开头的字符串
"mi"*) echo "mike"
;;
"jack") echo "jack"
;;
"sam") echo "sam"
;;
esac
跳出所有循环。
for(( i=0;i<${#array[@]};i++ )); do
if (( i -eq 2 )); then
echo ${array[i]}
break
fi
done;
退出当前循环,继续下一次循环
for(( i=0;i<${#array[@]};i++ )); do
if (( i -eq 2 )); then
continue
echo ${array[i]}
fi
done;
select in 循环用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。
通常select是配合case来使用。
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"; do
echo $name
done
echo "You have selected $name"
read [-optins] [variables]
选项 | 说明 |
---|---|
-a | array 把读取的数据赋值给数组 array,从下标 0 开始。 |
-d | delimiter 用字符串 delimiter 指定读取结束的位置,而不是一个换行符(读取到的数据不包括 delimiter)。 |
-e | 在获取用户输入的时候,对功能键进行编码转换,不会直接显式功能键对应的字符。 |
-n | num 读取 num 个字符,而不是整行字符。 |
-p | prompt 显示提示信息,提示内容为 prompt。 |
-r | 原样读取(Raw mode),不把反斜杠字符解释为转义字符。 |
-s | 静默模式(Silent mode),不会在屏幕上显示输入的字符。当输入密码和其它确认信息的时候,这是很有必要的。 |
-t | seconds 设置超时时间,单位为秒。如果用户没有在指定时间内输入完成,那么 read 将会返回一个非 0 |
-u | fd 使用文件描述符 fd 作为输入源,而不是标准输入,类似于重定向。 |
read -r -t 10 -s -p "Enter password in 20 seconds(once) : " pass1 && printf "\n"
read -r -t 10 -s -p "Enter password in 20 seconds(again): " pass2 && printf "\n"
if [ "$pass1" == "$pass2" ]; then
echo "Valid password"
else
echo "Invalid password"
fi
variable=$(cmd)
shell运行命令替换符号中的命令,并将其输出赋值给变量variable。
cmd可以是函数,可以是命令,也可以是可执行程序。
var=$(echo "abcd")
var=$(bc "1+2")
a=$(echo "1+2" | bc)
echo string,默认将字符串输出到标准终端控制台。
# 普通输出
echo "abcd"
# 不换行输出
echo "abc\r"
# 输出变量
echo $1
# 默认转义
echo "\"abc\""
# 关闭转义
echo "abc\n"
# 覆盖输入到文件
echo "abc">file
# 附加输出到文件
echo "abc">>file
printf,格式化输出。
# printf默认不带换行
printf "abcd\n"
# format-string为双引号
printf "%d %s\n" 1 "abc"
# 保留位数
printf "%-10s %-8s %-4.2f\n" lily 女 47.9876
将代码封装成函数,提升代码复用性。
[ function ] funname [()]
{
action;
[return int;]
}
# 无参数无返回,推荐写function标识
test() {
echo "This is function"
}
# 执行函数
test
# 有参数有返回值
add() {
(( ret = $1 + $2 ))
return $ret
}
# 函数return值通过$?获取
add 2 3
echo $?
大多数命令的输出默认是输出到终端的标准输出(STDOUT),错误信息则输出到标准错误输出(STDERR)中。
命令 | 说明 |
---|---|
command > file | 将输出重定向到 file。 |
command < file | 将输入重定向到 file。 |
command >> file | 将输出以追加的方式重定向到 file。 |
n > file | 将文件描述符为 n 的文件重定向到 file。 |
n >> file | 将文件描述符为 n 的文件以追加的方式重定向到 file。 |
n >& m | 将输出文件 m 和 n 合并。 |
n <& m | 将输入文件 m 和 n 合并。 |
<< tag | 将开始标记 tag 和结束标记 tag 之间的内容作为输入。 |
# 重定位到文件file
echo abc 1>file
# 重定位到文件file,文件描述符1可以省略
echo abc>file
# 重定向文件输入
command1 < file1
# 不显示在屏幕上
command > /dev/null
屏蔽 stdout 和 stderr
command > /dev/null 2>&1
一些公共代码可以封装到一个文件中,供不同项目使用
# .和文件中必须要有空格
. base.sh
# 推荐
source base.sh
# base.sh
# 输出进程号
echo $$
# test.sh
# 输出进程号
echo $$
source base.sh
./base.sh
bash base.sh
# 打印退出码
echo $?
exec ./base.sh
组命令,就是将多个命令划分为一组,或者看成一个整体。
Shell 组命令的写法有两种:
{ command1; command2;. . .; }
( command1; command2;. . . )
使用花括号{}时,花括号与命令之间必须要有一个空格,并且最后一个命令必须用一个分号或一个换行符结束组命令可以将多条命令的输出结果合并在一起,在使用重定向和管道时会特别方便。
两种写法的重要不同:由{}包围的组命令在当前 Shell 进程中执行,由()包围的组命令会创建一个子Shell,所有命令都会在这个子 Shell 中执行。
在子 Shell 中执行意味着,运行环境被复制给了一个新的 shell 进程,当这个子 Shell 退出时,新的进程也会被销毁,环境副本也会消失,
所以在子 Shell 环境中的任何更改都会消失(包括给变量赋值)。因此,在大多数情况下,除非脚本要求一个子 Shell,
否则使用{}比使用()更受欢迎,并且{}的进行速度更快,占用的内存更少。
子进程的概念是由父进程的概念引申而来的。在 Linux 系统中,系统运行的应用程序几乎都是从 init(pid为 1 的进程)进程派生而来的,所有这些应用程序都可以视为 init 进程的子进程,而 init 则为它们的父进程。
Shell 脚本是从上至下、从左至右依次执行的,即执行完一个命令之后再执行下一个。如果在 Shell 脚本中遇到子脚本(即脚本嵌套,但是必须以新进程的方式运行)或者外部命令,就会向系统内核申请创建一个新的进程,以便在该进程中执行子脚本或者外部命令,这个新的进程就是子进程。子进程执行完毕后才能回到父进程,才能继续执行父脚本中后续的命令及语句。
使用pstree -p命令就可以看到 init 及系统中其他进程的进程树信息(包括 pid):
systemd(1)─┬─ModemManager(796)─┬─{ModemManager}(821)
│ └─{ModemManager}(882)
├─NetworkManager(975)─┬─{NetworkManager}(1061)
│ └─{NetworkManager}(1077)
├─abrt-watch-log(774)
├─abrt-watch-log(776)
├─abrtd(773)
├─accounts-daemon(806)─┬─{accounts-daemon}(839)
│ └─{accounts-daemon}(883)
├─alsactl(768)
├─at-spi-bus-laun(1954)─┬─dbus-daemon(1958)───{dbus-daemon}(1960)
│ ├─{at-spi-bus-laun}(1955)
│ ├─{at-spi-bus-laun}(1957)
│ └─{at-spi-bus-laun}(1959)
├─at-spi2-registr(1962)───{at-spi2-registr}(1965)
├─atd(842)
├─auditd(739)─┬─audispd(753)─┬─sedispatch(757)
│ │ └─{audispd}(759)
│ └─{auditd}(752)
echo $$输出当前进程ID,echo $PPID输出父shell ID。
除了 $,Bash 还提供了另外两个环境变量——SHLVL 和 BASH_SUBSHELL,用它们来检测子 Shell 非常方便。
SHLVL 是记录多个 Bash 进程实例嵌套深度的累加器,每次进入一层普通的子进程,SHLVL 的值就加 1。而 BASH_SUBSHELL 是记录一个 Bash 进程实例中多个子 Shell(sub shell)嵌套深度的累加器,每次进入一层子 Shell,BASH_SUBSHELL 的值就加 1。
管道的主要是作用是将一个命令的输出作为另一个命令的输入。管道符号为"|"。
echo "1+3" | bc
dmesg | grep "inth"
后台执行,也就是异步执行,非阻塞式的。
nohup cmd &
(cmd1; cmd2)&
{
cmd1
cmd2
sleep
cmd3
}&
wait
信号(Signal):信号是在软件层次上对中断机制的一种模拟,通过给一个进程发送信号,执行相应的处理函数(即捕获信号)。
一种是标准信号,编号1-31,称为非可靠信号(非实时),不支持队列,信号可能会丢失,比如发送多次相同的信号,进程只能收到一次,如果第一个信号没有处理完,第二个信号将会丢弃。其中比较典型的有一种是内核检测到系统事件,比如键盘输入CTRL+C会发送SIGINT信号。
另一种是通过系统调用kill命令来向一个进程发送信号。
设置信号号为50的信息捕获
trap “echo catch signal” 50
发送信息使用kill指令
给当前进程发送50号信息
kill -n 50 $$
custom_sig=50
trap "echo abc" $custom_sig
sleep 2
kill -n $custom_sig $$
# 屏蔽Ctrl+C功能,响应必须为空
trap "" INT
脚本退出码通过exit函数设置,可以通过$?获取退出码。
$?也可以获取第三方可执行程序的退出码。
退出码,默认0为正常,非0为异常。
unix-like哲学是以进程来作为模块完成功能,所以unix-like系统中有大量的功能都是小工具来完成的。
变 量 | 功 能 |
---|---|
BASH | 调用 Shell 的完整文件名 |
BASHOPTS | 启用 Bash Shell 的选项列表 |
BASHPID | 当前 Bash Shell 的进程 ID |
BASH_ALIASES | 含有当前所用别名的关联数组 |
BASH_ARGC | 当前子函数或 Shell 脚本中的参数总数的数组变量 |
BASH_ARCV | 当前子函数或 Shell 脚本中的参数的数组变量 |
BASHCMDS | 关联数组,包括 Shell 执行过的命令的所在位置 |
BASH_COMMAND | 当前正在被执行的命令名 |
BASH_ENV | 如果设置,每个 Bash 脚本都会尝试在运行前执行由该变量定义的起始文件 |
BASH_EXECUTION_STRING | 在 Bash -c 命令行选项中用到的命令 |
BASH_LINENO | 含有脚本中每个命令的行号的数组变量 |
BASH_REMATCH | 只读数组,含有与指定的正则表达式匹配的文本元素的数组 |
BASH_SOURCE | 含有 Shell 中已声明函数所在源文件名的数组变量 |
BASH_SUBSHELL | 当前 Shell 生成的子 Shell 数目 |
BASH_VERS INFO | 含有当前运行的 Bash Shell 的主版本号和次版本号的数组变量 |
BASH_VERS ION | 当前运行的 Bash Shell 的版本号 |
BASH_XTRACEFD | 当设置一个有效的文件描述符整数时,跟踪输出生成,并与诊断和错误信息分离开文件描述符必须设置 -x 启动 |
COLUMNS | 当前 Bash Shell 实例使用的终端的宽度 |
COMP_CWORD | 变量 COMP_WORDS 的索引值,COMP_WORDS 包含当前光标所在的位置 |
COMP_KEY | 调用 Shell 函数补全功能的按键 |
COMP_LINE | 当前命令行 |
COMP POINT | 当前光标位置相对于当前命令起始位置的索引 |
COMP_TYPE | 一个整数值,表示所尝试的补全类型,完成 Shell 函数的补全 |
COMP_WORDBREAKS | 在进行单词补全时用作单词分隔符的一组字符 |
COMP_WORDS | 含有当前命令行上所有单词的数组变量 |
COMPREPLY | 含有由 Shell 函数生成的可能补全代码的数组变量 |
COPROC | 占有未命名的协程的 I/O 文件描述符的数组变量 |
DIRSTACK | 含有目录栈当前内容的数组变量。 |
EMACS | 如果设置了该环境变量,则 Shell 认为其使用的是 emacs Shell 缓冲区,同时禁止进行编辑功能 |
ENV | 如果设置了该环境变量,每个 Bash 脚本在运行之前都会执行由该环境变量所定义的起始文件 |
EUID | 当前用户的有效用户 ID (数字形式) |
FCEDIT fc | 命令使用的默认编辑器 |
FIGNORE | 用分隔的后缀名列表,在文件名补全时会被忽略 |
FUNCNAME | 当前执行的 Shell 函数的名称 |
FUNCNEST | 当设置成非 0 值时,嵌套函数的最髙层级 |
GLOBIGNORE | 以分隔的模式列表,定义了在进行文件名扩展时要忽略的文件名集合 |
GROUPS | 含有当前用户属组列表的数组变量 |
histchars | 控制历史记录展幵的字符(最多可有 3 个字符) |
HISTCMD | 当前命令在历史记录中的编码 |
HISTCONTROL | 控制哪些命令留在历史记录列表中 |
HISTFILE | 保存 Shell 历史记录列表的文件名(默认是 .Bash_history ) |
HISTFILESIZE | 保存在历史文件中的最大行数 |
HISTIGNORE | 以分隔的模式列表,用来决定哪些命令会被忽略 |
HISTSIZE | 最多在历史文件中保存多少条命令 |
HISTIMEFORMAT | 如果设置且非空,用作格式化字符,决定历史文件条目的时间戳 |
HOSTFILE | 含有Shell在补全主机名时读取的文件的名称 |
HOSTNAME | 当前主机的名称 |
HOSTTYPE | 当前运行Bash Shell的机器 |
IGNOREEOF | Shell在退出前必须收到连续的EOF字符的数量。如果这个值不存在,则默认是1 |
INPUTRC | readline初始化文件名(默认是.inputrc ) |
LANG | Shell的语言环境分类 |
LC_ALL | 定义一个语言环境分类,它会覆盖LANG变量 |
LC_COLLATE | 设置对字符串值排序时用的排序规则 |
LC_CTYPE | 决定在进行文件名扩展和模式匹配时,如何解释其中的字符 |
LC_MESSAGES | 决定解释前置美元符($)的双引号字符串的语言环境设置 |
LC_NUMERIC | 决定格式化数字时所使用的语言环境设置 |
LINENO | 脚本中当前执行代码的行号 |
LINES | 定义了终端上可见的行数 |
MACHTYPE | 用“cpu-公司-系统”格式定义的系统类型 |
MAILCHECK | Shell 查看邮件的频率(以 s 为单位,默认值是 60s) |
MAPFILE | 含有 mapfile 命令所读入文本的数组,当没有给出变量名的时候,使用该环境变量 |
OLDPWD | Shell之前的工作目录 |
OPTERR | 设置为1时,Bash Shell会显示getopts命令产生的错误 |
OSTYPE | 定义了 Shell所在的操作系统 |
PIPESTATUS | 含有前台进程退出状态列表的数组变量 |
POSIXLY_CORRECT | 如果设置了该环境变量,Bash 会以 POSIX 模式启动 |
PPID | Bash Shell 父进程的 PID |
PROMPT COMMAND | 如果设置该环境变量,在显示命令行主提示符之前会执行这条命令 |
PS3 | select命令的提不符 |
PS4 | 如果使用了 Bash 的 -x 选项,在命令行显示之前显示的提示符 |
PWD | 当前工作目录 |
RANDOM | 返回一个 0〜32767 的随机数,对其赋值可作为随机数生成器的种子 |
READLINE_LINE | 当使用 bind -x 命令时,保存了 readline 行缓冲区中的内容 |
READLINE_POINT | 当使用 bind -x 命令时,当前 readline 行缓冲区的插入点位置 |
REPLY | read 命令的默认变量 |
SECONDS | 自从 Shell 启动到现在的秒数,对其赋值将会重置计时器 |
SHELL | Bash Shell 的全路径名 |
SHELLOPTS | 已启用 Bash Shell 选项列表,用“”分隔开 |
SHLVL | 表明 Shell 的层级,每次启动一个新的 Bash Shell 时该值增加1 |
TIMEFORMAT | 指定 Shell 显示的时间值的格式 |
TMOUT | select 和 read 命令在没输入的情况下等待多久(以 s 为单位)。默认值为零,表示无限长 |
TMPDIR | 如果设置成目录名,Shell 会将其作为临时文件目录 |
UID | 当前用户的真实用户 ID (数字形式) |
打开终端时系统自动调用:/etc/profile 或 ~/.bashrc。其中profile文件是所有用户共享,而bashrc则是每个用户一个,存放在home的当前用户目录中。
我们根据情况可以选择在profile或.bashrc中创建环境变量或是一些简短的别名。
export kconfig=config_gyf
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
# 因为.bashrc只在启动的时候执行一次,如果再立即使用,则需要手动执行一下
source .bashrc
for file in ./* ;do
echo "$file"
done
# 包括子目录
traverse_files() {
for file in "$1"*; do
if [ -d "$1""$file" ]; then
traverse_files "$1""$file"/
else
echo "$1""$file"
fi
done
}
traverse_files
get_char()
{
SAVEDSTTY=$(stty -g)
stty -echo
stty cbreak
dd if=/dev/tty bs=1 count=1 2> /dev/null
stty -raw
stty echo
stty "$SAVEDSTTY"
}
# for debug
enable_pause=1
ys::pause()
{
if [ "x$1" != "x" ] ;then
echo "$1"
fi
if [ $enable_pause -eq 1 ];then
echo "Press any key to continue!"
get_char
fi
}
可以在脚本开始设置如下代码进行调试。
# 开启显示脚本执行的详细过程
sudo bash -x test.sh