shell脚本编程

本文最后更新于:2023年12月5日 晚上

基本用法

脚本调试

脚本错误分为三种:

  • 语法错误,导致后续命令不继续执行,使用 bash -n 检查错误
  • 命令错误,默认后续命令继续执行,使用 bash -x 检查错误
  • 逻辑错误,只能使用 bash -x 检查

变量

shell 脚本中的所有变量值都是字符串,其中的数字也是字符串

shell 中变量命名规则

- 只能使用数字、字母及下划线,且不能以数字开头
- 区分大小写
- 变量、等号、值中间不能出现任何空格

注意: 不支持短横线 “-”,这点和主机名相反

定义变量

name='value'

name='root'  # 直接字符串赋值
name="$user"    # 变量引用,注意一定要用双引号
name=`COMMAND`  或者 name=$(COMMAND)  # 命令引用

引用变量

$name
${name}

关于$:

- js中变量名前不加$,只有在上引号包裹的字符串中才使用${}引用变量
- php变量名前加$,无论什么情况,$和变量名不可分离
- shell变量名前不加$,只有在引用的时候加$,在容易起因歧义的时候,使用${}引用变量

拼接字符串:

- php使用点 .
- js使用加号 +
- shell中什么也不需要,直接写在一起就可以了,也可以用+=拼接自身

删除变量

unset name

declare ★★★

declare 是 bash 内置命令,用来声明变量

declare [-aAfFgilnrtux] [-p] [name[=value] ...]

f    # 显示已定义的函数
F    # 显示已定义的函数名
g    # 声明一个全局变量,name="value" 这种方式就是省略参数g,完整的写法是declare -g name="value",所以在函数中请使用 declare name="value",不要使用 name="value",除非你想声明一个全局变量
-p   # 显示所有变量及其属性和值,不包括函数

为变量设置属性,如果没有声明变量,则显示设置此属性的变量:
a    # 声明索引数组
A    # 声明关联数组
i    # 声明整数,可以进行算数运算
l    # 声明变量为小写字母
n    # 为value设置引用属性
r    # 声明只读变量,相当于 readonly,当前进程内,只能定义,不能修改,不能删除
t    # 为变量设置trace属性,跟踪函数从调用shell继承DEBUG和RETURN类型的。trace属性对变量没有特殊意义
u    # 声明变量为大写字母
x    # 声明环境变量,相当于 export

Shell 内建命令之 trap:https://blog.csdn.net/asty9000/article/details/88393166

  • i 示例:

    let var=算术表达式
    ((var=算术表达式))
    var=$[算术表达式]
    var=$((算术表达式))
    declare -i var=算术表达式
    echo '算术表达式' | bc
  • n 示例:

    declare a=123
    declare -n b=a  # b=123  等同于 declare b=$a

eval ★★★

先扫描一遍,进行置换,再执行命令

# 范例一
lujinkai@Z510:~$ echo {a..z}
a b c d e f g h i j k l m n o p q r s t u v w x y z
lujinkai@Z510:~$ echo {a..z} | tr -d ' '
abcdefghijklmnopqrstuvwxyz
lujinkai@Z510:~$ words="echo {a..z} | tr -d ' '"
lujinkai@Z510:~$ echo $words
echo {a..z} | tr -d ' '
lujinkai@Z510:~$ echo `echo $words`
echo {a..z} | tr -d ' '
lujinkai@Z510:~$ eval $words
abcdefghijklmnopqrstuvwxyz

# 范例二
[root@centos8 ~]# CMD=whoami
[root@centos8 ~]# echo
$CMD
whoami
[root@centos8 ~]# eval $CMD
root

# 范例三
[root@centos8 ~]# n=10
[root@centos8 ~]# echo {0..$n}
{0..10}
[root@centos8 ~]# eval echo {0..$n}
0 1 2 3 4 5 6 7 8 9 10

练习

1、编写脚本 systeminfo.sh,显示当前主机系统信息,包括:主机名,IPv4 地址,操作系统版本,内核版本,CPU 型号,内存大小,硬盘大小

主机名:hostname
ipv4地址:hostname -I
操作系统版本:os-release NAME VERSION
内核版本:echo `uname -s;uname -r`
内存大小:`free -m | awk '/Mem:/{print $2}'`
硬盘大小:lsblk -m | awk '/sda/{print $2}'| head -n1

2、编写脚本 backup.sh,可实现每日将 /etc/ 目录备份到 /backup/etcYYYY-mm-dd 中

3、编写脚本 disk.sh,显示当前硬盘分区中空间利用率最大的值

4、编写脚本 links.sh,显示正连接本主机的每个远程主机的 IPv4 地址和连接数,并按连接数从大到小排序

环境变量

对所有用户生效的环境变量:/etc/profile
对特定用户生效的环境变量:~/.bashrc 或者 ~/.bash_profile
临时有效的环境变量:脚本或命令行使用 export 定义

常用环境变量:

PATH、HOME、LOGNAME、PWD、HISTFILE、HISTSIZE、HOSTNAME、SHELL、PS1、TMOUT、IFS、OFS
# 声明环境变量,除了使用 declare -x 还可以使用export
export name=VALUE

显示所有环境变量:

env
printenv
export
declare -x

位置变量

位置变量,在 bash shell 中内置的变量,在脚本代码中调用通过命令行传递给脚本的参数

1,2...   对应第一个,第二个等参数,当参数小于10个的时候,$n可以表示,当参数大于10个的时候,使用${n}表示
0        命令本身,包括路径在内
*        传递给脚本所有的参数,全部参数合为一个字符串,每个参数区别对待
@        传递给脚本的所有参数,每个参数为独立字符串,所有参数视为一个整体
#        传递给脚本的参数个数

注意:$@$* 只有在被双引号包起来的时候才会有差异,在 for in 循环中,不加引号的时候,$*$@ 都可以正常遍历,加双引号的时候,$* 只有一个字符串变量

清空所有位置变量:

set --

范例:利用软链接实现同一个脚本不同功能

退出状态码

用于无条件终止当前脚本的执行 exit n

n == 0         #脚本执行成功
n == 1-125     #出错,这些对应的错误值由用户在脚本中定义
  n == 1        #一般未知错误
  n == 2        #不合适的shell命令
n == 126       #文件不可执行
n == 127       #不存在该命令
n == 128       #无效的退出参数
n == 128+x     #与linux信号x相关的严重错误
n == 130       #通过ctrl+c终止的命令
n >  255       #正常范围之外的退出状态码

1-125 这些错误值可以由用户自定义
如果不给定 n 的值,而直接使用 exit,那么返回 exit 之前最后一条语句的状态,等于 exit $?

‘’ “” `` $()

单引号不解析变量,双引号解析变量,反引号中必须是有输出的命令,$() 和 ``是等价的, 但是 $() 可以嵌套

脚本安全和 set

set 命令:可以用来定制 shell 环境,set + 关闭设置,set - 打开设置

lujinkai@Z510:~/data/test$ echo $-
himBHs
h #hashall,外部命令使用过一次后就会被hash下来,后面再使用这个命令就优先从hash中获取,通过`set +h`将h选项关闭
i #interactive-comments,说明当前shell是交互式shell,在脚本中,i选项是被关闭的
m #monitor,打开监控
B #braceexpand,大括号扩展
H #history,H选项打开,可以展开历史列表中的命令,可以通过!来完成,例如!!返回上一个历史命令,!n返回第n个历史命令

set 命令实现脚本安全:

参考:http://www.ruanyifeng.com/blog/2017/11/bash-set.html?utm_source=tool.lu

-u 变量未定义就报错, 等同 `set -o nounset`
-e 遇到错误(exit不等于0)就退出,等同 `set -o errexit`
-o option 显示,打开或关闭选项
  显示选项 `set -o`
  打开选项 `set -o 选项`
  关闭选项 `set -o 选项`
-x 当执行命令的时候,打印命令及其参数,类似 `bash -x`
lujinkai@Z510:~/data/test$ set -o
allexport       off
braceexpand     on
emacs           on
errexit         off
errtrace        off
functrace       off
hashall         on
histexpand      on
history         on
...

格式化输出 printf

printf "指定的格式" "文本1" "文本2"...

%s       #字符串
%f       #浮点数
%b       #解析转义符
%c       #ASCI字符串,即显示对应参数的第一个字符
%d,%i    #十进制整数
%o       #八进制值
%u       #不带正负号的十进制值
%x       #十六进制值(a-f)
%X       #十六进制值(A-Z)
%%       #表示%本身
%n       #指定输出宽度为n,不足补空格,默认右对齐,例如:%-10s 表示10个字符宽,- 表示左对齐

常用转义字符:

\a    #警告字符
\b    #后退
\f    #换页
\n    #换行
\r    #回车
\t    #水平制表符,就是tab键
\v    #水平制表符
lujinkai@Z510:~/data/test$ printf "%.2f\n" 1 2 3
1.00
2.00
3.00
lujinkai@Z510:~/data/test$ printf "hello: %s\n" daming xiaohong tom
hello: daming
hello: xiaohong
hello: tom

算术运算

bash 只支持整数运算,不支持小数运算。复杂的运算请使用 bc

加减乘除、自加自减,取余,异或取反,三元运算等都和 php 一样。

let var=算术表达式
((var=算术表达式))
var=$[算术表达式]
var=$((算术表达式))
declare -i var=算术表达式
echo '算术表达式' | bc

$RANDOM:内建的随机数生成器变量

$RANDOM 取值范围: 0-32767 (2^15=32768)

示例:生成 0-49 随机数

echo $[$RANDOM%50] # 除以50取余

示例:鸡兔同笼

#!/bin/bash

head=$1
foot=$2

let tu=($foot-2*$head)/2
let ji=$head-$tu

echo 兔子:$tu
echo 鸡:$ji

逻辑运算 &&、||、

&&||!

短路运算

使用 read 命令来接受输入

read 是 shell 内置命令,从标准输入中读取数据并赋值给变量。如果没有重定向,默认是从键盘读取用户输入的数据,如果进行了重定向,那么可以从文件中都去数据。

# read [options] [name ...]
read: read [-ers] [-a array] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ...]

如果没有提供变量名,那么读取的数据默认存放到环境变量 REPLY 中。

-a array     #读取的数据赋值给数组
-d delimiter #用字符串delimiter指定读取结束的位置,而不是一个换行符(读取到的数据不包括delimiter)
-n num       #指定输入字符串的长度,到达指定长度自动退出
-t seconds   #设置超时时间,用户超过时间没有输入,自动退出,exit非零
-s           #静默输入,一般用于密码
-p prompt    #显示提示内容,提示内容不能换行
-e           #在用户输入的时候,对功能键进行编码转换,更人性化
-r           #不允许反斜杠转义,选项一般都要加上,pycharm中如果不加会警告提示
-u           #从文件描述符fd中读取内容

read 就像表单提交,所有用户提交的信息都是不可信的,一定要做好校验

条件测试命令

bash shell 的配置文件

bash shell 配置文件有: /etc/profile~/.bash_profile~/.bash_login~/.profile~/.bashrc/etc/bashrc/etc/profile.d/\*.sh,不同的启动方式会加载不同的配置文件

生效范围

/etc 目录下的配置文件是全局生效,家目录下的配置文件是对当前用户生效

配置文件太多,为了避免混乱,默认:对所有用户添加配置,添加到 /etc/profile.d/ 目录下,当前用户添加配置,添加到 ~/.bashrc 中

登录方式

shell 登录有两种方式:交互式登录和非交互式登录

交互式

  1. 帐号密码登录;
    1. su - userName

配置文件生效和执行顺序:

Ubuntu18.4

1. /etc/profile
2. /etc/profile/*.sh
3. /etc/bash.bashrc
4. ~/.profile
5. ~/.etc/bashrc

CentOS7

1. /etc/profile
2. /etc/profile.d/\*.sh
3. ~/.bash_profile
4. ~/.bashrc
5. /etc/bashrc

非交互式

  1. su userName
  2. 图形化界面下打开的终端;
  3. 执行脚本;
  4. 任何其他的 bash 实例

非交互式登录,只加载 ~/.bashrc(centos 的~/.bashrc 中会加载/etc/bashrc 文件)

所以按照功能划分:profile 类只在交互式登录中加载,而 bashrc 类在交互式和非交互式登录中都有加载

bash 退出任务

退出 shell 时自动运行 ~/.bash_logout 文件,可以用来创建自动备份文件、清除临时文件

流程控制 - 条件选择

if

if/then/elif/else/fi

单分支

if [ -f ~/.bashrc ]; then
    . ~/.bashrc
fi

双分支

if []; then
 code...
else
 code...
fi

多分支

if []; then
 code...
elif []; then
 code...
elif []; then
 code...
 ...
else
 code...
fi

case

类似 php 中的 switch

case/esac

#! /bin/sh
echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
  echo "Good Morning!";;
[nN]*)
  echo "Good Afternoon!";;
*)
  echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
  exit 1;;
esac
exit 0

case 支持 glob 风格的通配符:*、?、[]、|

流程控制 - 循环

for in

for in/do/done

递归遍历当前目录下的所有文件

#!/bin/bash
dir=$(dirname "`readlink -f $0`")
fn() {
 for filename in `ls $1`; do
  file=${1}/${filename}
  echo -e "$file\n"
  if [ -d "$file" ]; then
   fn $file
  fi
 done
}
fn $dir

while

while/do/done

无限循环

while true; do
 循环体
done

验证密码,限制尝试输入不超过 5 次

#!/bin/bash
echo 'enter password:'
read -s try
num=1
while [[ $try != '123456' ]]; do
 num=$(($num+1))
 if [[ $num > 5 ]]; then
  echo 'no change'
  break
 fi
 echo 'try again'
 read -s try
done
if [[ $try == '123456' ]]; then
 echo 'yes'
fi

while read

while 循环的特殊用法,遍历文件或文本的每一行

# 依次读取file文件中的每一行,且将行赋值给变量X
while read X; do
循环体
done < file

练习,使用 while 实现:

  1. 编写脚本,求 100 以内所有正奇数之和
  2. 编写脚本,提示请输入网络地址,如 192.168.0.0,判断输入的网段中主机在线状态,并统计在线和离线主机各多少
  3. 编写脚本,打印九九乘法表
  4. 编写脚本,利用变量 RANDOM 生成 10 个随机数字,输出这个 10 数字,并显示其中的最大值和最小值
  5. 编写脚本,实现打印国际象棋棋盘
  6. 后续六个字符串: efbaf275cd、4be9c40b8b、44b2395c46、f8c8873ce0、b902c16c8b、ad865d2f63 是通过对随机数变量 RANDOM 随机执行命令: echo $RANDOM | md5sum | cut -c1-10 后的结果,请破解这些字符串对应的 RANDOM 值

until

while 判断条件为真则循环,until 判断条件为假则循环

无限循环

until false; do
 循环体
Done

循环控制 continue 和 break

continue 跳过本次循环,break 跳出所有循环

#!/bin/bash
set -u

i=10
while ((i--)); do
  if [ $i == 5 ]; then
      echo 'i is 5'
   # continue
      break
  fi
  echo $i
done
unset i

循环控制 shift

将参数列表最左边的参数删除,while 循环遍历位置参量列表时,常用到 shift

#!/bin/bash
while (($# > 0)); do
    echo "$@"
    shift
done

select

select 用于生成菜单,并显示 PS3 提示符,然后等待用户输入,无论输入什么都执行一遍循环,常搭配 case 来处理用户输入,select 通过 break 退出循环

select variable in list ;do
 循环体命令
done

示例:

#!/bin/bash
sum=0
PS3="请点菜(1-6): "
select MENU in 北京烤鸭 佛跳墙 小龙虾 羊蝎子 火锅 点菜结束; do
    case $REPLY in
    1)
        echo $MENU 价格是 100
        ((sum += 100))
        ;;
    2)
        echo $MENU 价格是 88
        ((sum += 88))
        ;;
    3)
        echo $MENU价格是 66
        ((sum += 66))
        ;;
    4)
        echo $MENU 价格是 166
        ((sum += 166))
        ;;
    5)
        echo $MENU 价格是 200
        ((sum += 200))
        ;;
    6)
        echo "点菜结束,退出"
        break
        ;;
    *)
        echo "点菜错误,重新选择"
        ;;
    esac
done
echo "总价格是: $sum"

函数

函数运行在当前 shell 进程

定义函数

#语法一:
func_name (){
...函数体...
}
#语法二:
function func_name {
...函数体...
}
#语法三:
function func_name () {
...函数体...
}
# 声明环境函数
declare -xf function_name
export -f function_name

调用函数

func_name  # 注意不要加()
# 参数用空格分割,在函数内部使用位置变量接收传入的参数
func_name arg1 arg2 arg3 ...

删除函数

unset func_name

函数返回值

函数返回值:

  • 使用 echo 等命令进行输出
  • 函数体中调用命令的输出结果

函数的退出状态码:

  • 默认取决于函数中执行的最后一条命令的退出状态码
  • 自定义退出状态码,其格式为:
    return 从函数中返回,用最后状态命令决定返回值
    return 0 无错误返回
    return 1-255 有错误返回

函数变量

local NAME=VALUE
# 等于
declare NAME=VALUE

脚本相关工具

trap

信号捕捉,捕捉一个或多个信号,替换为自定义操作

trap 捕捉的信号包括系统信号和 shell 信号,系统信号通过trap -lkill -l查看,shell 信号有四个:EXIT(或信号代码 0)、ERR、DEBUG 和 RETURN。

trap command signal_list # 捕捉信号列表,替换为自定义command
trap '' signal_list      # 捕捉信号列表,不做任何操作
trap '-' signal_list     # 捕捉信号列表,恢复这些信号
trap -p                  # 列出自定义捕捉信号操作
trap -l                  # 列出所有系统信

信号名是 SIG 后面的部分,捕捉的时候可以写信号的编号,也可以写信号名称,不区分大小写

由于不少信号在不同架构的计算机上数值不同,所以在不确定编号是否唯一的时候,最好写信号名称

trap "echo 'Press ctrl+c or ctrl+\ '" int quit

DEBUG 和 RETURN 这两种信号陷阱无需关注,EXIT 是退出状态码为 0 时发出的信号,退出状态码非 0 时则发出 ERR 信号

系统信号的编号从 1 到 64,EXIT 的编号是 0

finish(){
 echo finish| tee -a /root/finish.log
}
trap finish exit

mktemp

创建临时文件,可以避免文件名冲突

# X至少3个,大写X
lujinkai@Z510:~/data/test$ mktemp fileXXXXX
fileS9vLK
# -d 创建临时目录
lujinkai@Z510:~/data/test$ mktemp -d dirXXX
diresA

expect

expect 基于 Tcl( Tool Command Language )语言开发,主要应用于自动化交互式操作的场景,借助 expect 处理交互的命令,可以将交互过程如:ssh 登录,ftp 登录等写在一个脚本上,使之自动化完成。尤其适用于需要对多台服务器执行相同操作的环境中,可以大大提高系统管理人员的工作效率

expect [ -dDinN ] [ -c cmds ] [ [ -[f|b] ] cmdfile ] [ args ]

数组

定义

shell 和 php 一样,支持索引数组和关联数组。

# 索引数组
arr=(val1 val2 val3 val4 ...)
# 关联数组
declare -A brr=([name]='lujinkai' [age]='18' [gender]='男')

注意:声明索引数组可以省略 declare,声明关联数组不能省略 declare,如果省略不报错,但是数据会乱。

调用

lujinkai@Z510:~/data/test$ array=("Allen" "Mike" "Messi" "Jerry" "Hanmeimei" "Wang")
# 打印数组长度
lujinkai@Z510:~/data/test$ echo ${#array[@]}
6
# 打印元素长度
lujinkai@Z510:~/data/test$ echo ${#array[1]} # 打印第1个元素 Mike的长度
4
# 分片访问
lujinkai@Z510:~/data/test$ echo ${array[@]:1:2}
Mike Messi
# 元素内容替换
lujinkai@Z510:~/data/test$ echo ${array[@]/e/E}
AllEn MikE MEssi JErry HanmEimei Wang
lujinkai@Z510:~/data/test$ echo ${array[@]//e/E}
AllEn MikE MEssi JErry HanmEimEi Wang
# 数组的遍历
for in ${array[@]}
do
 echo $val
done

字符串

lujinkai@Z510:~$ str='abcdefghijklmnopqrstuvwxyz'

切片

假设有一个指针 pointer,始终指向字符串的“间隙”,从前往后,“间隙”号依次为 0、1、2、3…,而且显然“间隙”数量比字符数量多 1 个,如果字符串有 2 个字符,那就有 3 个“间隙”。指针默认指向 0,指针的位置就是切片位置。

# 字符串长度
lujinkai@Z510:~$ echo ${#str}
26

# 从切片位置往后截取length个字符:${str:offset:length}
lujinkai@Z510:~$ echo ${str:0:6}
abcdef
lujinkai@Z510:~$ echo ${str:4:2}
ef

# 截取后length个字符:${str:0-length} 0可以用空格代替
lujinkai@Z510:~$ echo ${str: -3}
xyz
lujinkai@Z510:~$ echo ${str:0-3}
xyz

#  截取后length个字符,然后再处理:${str:0-length:offset}、${str:0-length:-offset}
lujinkai@Z510:~$ echo ${str:0-3:1}
x
lujinkai@Z510:~$ echo ${str:0-3:-1}
xy


# 掐头去尾:${str:offset:-length}
lujinkai@Z510:~$ echo ${str:1:-1}
bcdefghijklmnopqrstuvwxy

查找替换

${str#*word} 查找 word 第一次出现的位置,删除其以及之前的部分

${str##*word} 贪婪模式,查找 word 最后一次出现的位置,删除其及其之前的部分

${str%word*} 查找 word 最后出现的位置,删除其及其之后的部分

${str%%word*} 查找 word 第一次出现的位置,删除其及其之后的部分

${str/pattern} 删除第一个匹配

${str//pattern} 删除所有匹配

大小写转换

${str^^} 转大写

${str,,} 转小写

lujinkai@Z510:~$ echo ${str^^}
ABCDEFGHIJKLMNOPQRSTUVWXYZ
lujinkai@Z510:~$ echo ${str,,}
abcdefghijklmnopqrstuvwxyz

变量测试

变量测试点比较多,而且不怎么用,主要用 if 来判断测试,所以不用深究


shell脚本编程
http://blog.lujinkai.cn/运维/基础/shell脚本编程/shell脚本编程/
作者
像方便面一样的男子
发布于
2020年12月9日
更新于
2023年12月5日
许可协议