L o a d i n g . . .
SHIWIVI-文章

//sunny forever
while(life<end){
love++;
beAwesome :)}

    <
  • 主题:
  • + -
  • 清除背景
  • 禁用背景
目 录
  1. 1. shell脚本基础
    1. 1.1. 脚本声明
    2. 1.2. shell脚本的执行方式
    3. 1.3. 全局执行脚本方式
    4. 1.4. 注释
    5. 1.5. 脚本格式转换
    6. 1.6. 脚本退出状态码
  2. 2. shell中的运算
    1. 2.1. 各种括号使用预览
    2. 2.2. $()与``命令调用
    3. 2.3. $(())与$[]整数运算
    4. 2.4. expr表达式
  3. 3. 变量
    1. 3.1. shell中的变量类型
    2. 3.2. 自定义变量
      1. 3.2.1. 变量定义、赋值、调用
      2. 3.2.2. 只读变量
      3. 3.2.3. 删除变量
    3. 3.3. 变量值检测与替换
  4. 4. 字符串
    1. 4.1. 字符串拼接
    2. 4.2. 获取字符串长度
    3. 4.3. 截取字符串
    4. 4.4. 快速删除字符串
    5. 4.5. 查找子字符串
    6. 4.6. 可转义的字符串
  5. 5. 数组
    1. 5.1. 数组定义
    2. 5.2. 数组元素
    3. 5.3. 数组长度
    4. 5.4. 关联数组
  6. 6. 位置变量与参数传递
    1. 6.1. 给脚本传递参数
      1. 6.1.1. $0参数的使用
    2. 6.2. 移动参数shift
  7. 7. 脚本选项
    1. 7.1. 选项与参数分割符
    2. 7.2. 简单选项处理
    3. 7.3. getopt与getopts命令
      1. 7.3.1. getopt命令
      2. 7.3.2. getopts命令
    4. 7.4. 选项标准化
  8. 8. 数值声明与数值运算
    1. 8.1. 数值声明
    2. 8.2. 数值运算
      1. 8.2.1. 1.通过”$(())”或”$[]”运算式
      2. 8.2.2. 2.通过declare -i声明
    3. 8.3. 浮点数运算的解决方案
  9. 9. 输入输出
    1. 9.1. 用户输入read
    2. 9.2. 终端输出
  10. 10. 输入输出重定向
    1. 10.1. 文件描述符与标准输入输出设备
    2. 10.2. 输出重定向
      1. 10.2.1. 1.标准输出与标准错误输出的重定向
      2. 10.2.2. 2.输出错误信息
    3. 10.3. 输入重定向(不常用)
    4. 10.4. 批量重定向
    5. 10.5. 丢弃命令输出
    6. 10.6. 自定义重定向
      1. 10.6.1. 创建文件描述符
      2. 10.6.2. 重定向文件描述符
      3. 10.6.3. 恢复文件描述符的指向
      4. 10.6.4. 同时读写的文件描述符(了解)
      5. 10.6.5. 关闭文件描述符
    7. 10.7. 列出打开的文件描述符
    8. 10.8. tee同时输出到显示器和文件
  11. 11. 条件判断
    1. 11.1. test命令与[ ]
    2. 11.2. 文件相关
      1. 11.2.1. 文件类型
      2. 11.2.2. 文件权限
      3. 11.2.3. 两个文件比较
    3. 11.3. 整数比较
    4. 11.4. 字符串判断
    5. 11.5. 多重判断
  12. 12. 判断语句
    1. 12.1. if语句
    2. 12.2. case命令
  13. 13. 循环语句
    1. 13.1. for命令
    2. 13.2. while命令
    3. 13.3. until命令
    4. 13.4. break终止循环
    5. 13.5. continue跳过循环
    6. 13.6. 处理循环的输出
  14. 14. 字段分隔符
  15. 15. (())双括号命令
    1. 15.1. 数学运算
    2. 15.2. 逻辑判断
  16. 16. [[]]双方括号命令
  17. 17. 脚本控制
    1. 17.1. 捕获信号
    2. 17.2. 修改信号捕获
    3. 17.3. 删除信号捕获
  18. 18. 函数
    1. 18.1. 创建与调用函数
    2. 18.2. 变量的作用域
      1. 18.2.1. 全局变量
      2. 18.2.2. 局部变量
    3. 18.3. 函数返回值与输出
      1. 18.3.1. 默认退出状态码
      2. 18.3.2. return命令修改状态码
      3. 18.3.3. 函数输出
    4. 18.4. 函数传参
    5. 18.5. 传递数组参数
    6. 18.6. 返回数组
    7. 18.7. 创建库
    8. 18.8. 在命令行中使用函数
    9. 18.9. 在.bashrc文件中定义函数
  19. 19. 脚本合集
    1. 19.1. 判断发行版
    2. 19.2. 判断系统架构

Shell脚本语言

字数:18153 写于:2021-12-29
最新更新:2024-05-08 阅读本文预计花费您52分钟

shell脚本基础

脚本声明

shell脚本文件通常以.sh作为文件扩展名,扩展名仅作为文件标识,并不影响脚本执行。shell程序的第一行,往往需要一个#!约定标记符,用于告诉系统这个脚本需要调用哪一种Shell解释器来执行

#!bin/bash #申明shell脚本的解释器

shell脚本的执行方式

1.通过文件名直接执行

首先需要为该脚本添加可执行权限,然后通过文件路径和文件名直接运行,操作系统会调用脚本文件第一行所指定的解释器来执行该脚本,如对于名为HelloWorld.sh脚本,执行方式为:

chmod 755 HelloWorld.sh #或chmod +x HelloWorld.sh赋予可执行权限 ./HelloWorld.sh #在当前路径中执行HelloWorld.sh,如果直接写为HelloWorld.sh,则系统会前往PATH路径中查找该命令
2.通过shell解释器调用

第二种执行方式为在调用shell解释器时,将脚本作为shell程序参数传入,该方法会忽略脚本第一行所指定解释器信息,直接使用用户唤起的shell程序作为脚本解释器

bash HelloWorld.sh #通过bash执行/bin/sh HelloWorld.sh #通过sh执行

全局执行脚本方式

执行脚本一般需要在赋予脚本执行权限后,使用绝对路径调用,或者进入脚本所在目录,使用相对路径调用。那些全局可用的脚本和命令,Linux是根据PATH环境变量中所存储的脚本路径自动寻找执行实现的,可通过echo $PATH查看当前所配置的全局路径,手动安装的服务想实现全局执行脚本,有两种实现方法:

  • 将脚本通过cp命令复制到PATH中的某个路径下,如:/usr/bin下
  • 通过变量叠加的方式,将脚本路径添加到PATH变量中,格式为PATH=”$PATH”:路径

通过变量叠加的方式添加的全局变量,在系统重启后将失效,永久生效方法是将路径添加到/etc/profile文件的PATH变量后

如Nginx的启动脚本在/usr/local/nginx/sbin路径下,非全局启动方法为: 1.相对路径: 在 /usr/local/nginx/sbin 目录下执行./nginx 2. 绝对路径:执行 /usr/local/nginx/sbin/nginx 添加到全局的方法为: 执行PATH="$PATH":/usr/local/nginx/sbin 使调用命令永久生效的方法:在 /etc/profile 中最后一行添加 PATH=$PATH:/usr/local/nginx/sbin

注释

shell程序使用#开头作为注释,多行注释需要在每一行开头都添加#号,注意程序第一行的脚本声明并不是注释符

#!/bin/bash #author: shiwivi #version: v0.1.0 #data: 2023-09-01

对于需要多行注释的内容,可以将其写入花括号中定义成一个函数,然后不调用即可

脚本格式转换

在Linux中,回车符识别为$,而在windows中回车符为^M$,因此在windows中编写的sh脚本需要经过转换,才能在Linux中运行,转换命令为:
dos2unix 文件名
相反,将文件从Linux格式转为Windows格式为
unix2dos 文件名

该功能需要自行安装dos2unix软件,red hat系列操作系统安装命令为:yum -y install dos2unix

脚本退出状态码

shell中运行的每个命令都使用退出状态码(exit status)告诉shell它已经运行完毕,这个状态码可以使用$?查看。同样,我们的脚本也可以使用退出状态码优雅地结束脚本执行,并返回脚本执行的状态。

退出状态码是一个0~255的整数值,默认情况下,命令成功执行时退出状态码是0,如果命令执行错误,则退出状态码就是一个正整数值。在脚本中,可通过exit 状态码命令指定脚本结束时的退出状态码,注意!exit命令会退出脚本,exit命令之后的任何代码都将不再执行。状态码应当设置在0-255区间内,否则会对其进行求模运算,如:指定状态码为300,则进行300%256运算,最后取值44,设置状态码时也可以使用变量

#!/bin/bash echo "Hello world" exit 0

Linux中命令执行错误时的退出状态码无标准可循,但有一些可用参考:

状态码 描述
0 命令成功执行
1 一般性未知错误
2 不适合的shell命令
126 命令不可执行
127 没找到命令
128 无效的退出参数
128+x 与Linux信号相关的严重错误
130 通过Ctrl C终止的命令
255 正常范围之外的退出状态码
上述的状态码有一些常见的情况: 状态码126表明用户没有权限执行该命令,即Permission denied 状态码1表明命令发生了未知错误,如:为命令提供了无效参数

shell中的运算

各种括号使用预览

  • ()用来创建子shell执行命令组,而$()则可以用来获取命令的输出,并将其赋值给变量,其作用和 ``命令调用一致,如:today=$(date “+%D”)。如果( )中指定了多个命令,则变量获取的是最后一个命令的输出
  • [ ]用来进行条件判断,它是test命令的替代,可以结合if-then等条件判断语句进行数值比较、字符串比较、文件比较
  • $(())和$[]用来进行整形数值运算,如:num=$[11+22]
  • (())可以用来运算高级数学表达式,如:++、>>(右移位)、&&(逻辑与)等运算,也可以搭配if-then语句进行条件判断
  • 双方括号[[]]命令用来进行高级字符串比较,允许使用*和?通配符,支持模式匹配,但部分shell可能不支持
  • 大括号{ }用来进行变量引用,支持字符串拼接

$()与``命令调用

shell中最有用的特性之一是可以将命令的输出赋值给变量,然后通过变量来任意调用,有两种方法可以将命令输出赋值给变量:

  • 通过反引号``
  • 通过$( )
e.g.每天使用当前日期为文件名创建一个文件,并将bin目录信息写入其中 today=`date` 或 today=$(date "+%D") ls /usr/bin -al > log.$today

$(())与$[]整数运算

  • 变量名=$((运算式))
  • 变量名=$[运算式]

$[]是一种旧的Bash算术扩展方式,现代Shell更推荐使用$(())进行算术运算

示例: [root@localhost ~]# num1=$((11+22)) [root@localhost ~]# num2=$[11+22]

expr表达式

expr是由Bourne shell提供的数学工具,bash shell为了保持跟Bourne shell的兼容而包含了expr命令。尽管现代脚本中大部分功能被更强大的 $(()) 算术求值或 ${} 字符串操作所取代,但 expr 在经典UNIX和早期shell脚本中仍然非常重要,expr表达式支持以下功能:

  • 整数运算:加减乘除、取模运算等
  • 字符串操作:获取字符串长度、子串提取、模式匹配等
  • 条件判断:执行条件判断并返回 0 或 1 表示真假

expr通常搭配``$()来获取expr表达式最后的计算值:

num=`expr 2 + 2` 注意表达式和运算符之间要有空格,即+号前后都要有一个空格,正确形式为2 + 2,省略空格2+2会被解析为一个字符串,而不是数值运算
示例: [root@localhost ~]# a=11 [root@localhost ~]# b=22 [root@localhost ~]# num1=$a+$b //字符串运算 [root@localhost ~]# echo $num1 //输出11+22 [root@localhost ~]# num2=$(expr $a + $b) //数值运算 [root@localhost ~]# echo $num2 //输出33

expr命令能够识别以下数学和字符串操作符,特殊字符的转义 expr 中的某些字符,如 *, &, <, >, | 等,在 shell 中具有特殊含义,需要用反斜杠 \ 进行转义处理

操作符 描述
ARG1 + ARG2 返回ARG1和ARG2的算术运算和
ARG1 - ARG2 返回ARG1和ARG2的算术运算差
ARG1 \* ARG2 返回ARG1和ARG2的算术乘积,需要将*号转义
ARG1 / ARG2 返回ARG1被ARG2除的算术商
ARG1 % ARG2 返回ARG1被ARG2除的算术余数
ARG1 \< ARG2 如果ARG1小于ARG2,返回1;否则返回0
ARG1 <= ARG2 如果ARG1小于或等于ARG2,返回1;否则返回0
ARG1 = ARG2 如果ARG1等于ARG2,返回1;否则返回0
ARG1 != ARG2 如果ARG1不等于ARG2,返回1;否则返回0
ARG1 >= ARG2 如果ARG1大于或等于ARG2,返回1;否则返回0
ARG1 \> ARG2 如果ARG1大于ARG2,返回1;否则返回0
A
ARG1 \| ARG2 如果ARG1既不是null也不是零值,返回ARG1;否则返回ARG2
ARG1 \& ARG2 如果没有参数是null或零值,返回ARG1;否则返回0
STRING : REGEXP 如果REGEXP匹配到了STRING中的某个模式,返回匹配的子字符串或长度
match STRING REGEXP 如果REGEXP匹配到了STRING中的某个模式,返回该模式匹配
substr STRING POS LENGTH 返回起始位置为POS(从1开始计数)、长度为LENGTH个字符的子字符串
index STRING CHARS 返回在STRING中找到CHARS字符串的位置;否则,返回0
length STRING 返回字符串STRING的数值长度
+ TOKEN 将TOKEN解释成字符串,即使是个关键字
(EXPRESSION) 返回EXPRESSION的值
1. 数值运算 expr 3 \* 4 #输出12 2. 获取字符串长度 expr length "hello" # 输出:5 3. 截取子字符串(substr) expr substr "hello world" 1 5 # 输出:hello 4. 查找字符位置(index) expr index "hello world" "o" # 输出:5 5. 字符串模式匹配(返回匹配的子字符串或长度) expr "abc123" : 'abc[0-9]*' # 输出:6 6. 大于、小于判断 expr 4 \> 3 # 输出:1 (表示 true) expr 4 \< 5 # 输出:1 (表示 true) 7. 逻辑与和或运算 expr 0 \| 1 # 输出:1 expr 0 \& 1 # 输出:0

变量

shell中的变量类型

shell程序的变量可以分为以下几种:

  • 自定义变量,即程序员自定义的变量
  • 预定义变量:bash预定义的变量
  • 环境变量:由操作系统或用户设置的特殊变量,用于保存操作系统环境相关的数据,以及配置 Shell 的行为和执行环境
  • 位置参数变量:用于向脚本中传递参数和数据,变量名不能自定义
可以通过 set 命令查看所有变量,包括系统预定义的变量

自定义变量

  • 变量名规则同C语言,允许由字母、数字、下划线组成,但不能以数字开头
  • bash中变量默认类型都为字符串类型
  • 变量赋值时,若值包含空格,则需要使用单引号或双引号包裹
  • 变量值中可以使用\进行转义
  • 变量名通常使用下划线分隔单词,如:user_name,server_ip
  • 自定义的环境变量名、常量的变量名一般使用大写,如:readonly MAX_VLAUE=10
变量定义、赋值、调用

定义自定义变量不需要int、var等关键字,可以直接使用变量名=值形式定义并赋值,默认情况下,变量通常被视为字符串。对于定义过的变量,可以使用${变量名}调用,花括号用于识别变量边界,可以不加,但加上花括号是个好的编程习惯

1. 赋值时等号两侧避免使用空格 name="Ken" 2. 除了显式地直接赋值,还可以用语句给变量赋值 for file in `ls /etc` 3. 可以用$调用变量,使用花括号可以识别变量边界,如果有花括号,则以下变量会被识别为$nameChou echo "my name is ${name}Chou" 4. 变量可以重新定义,即重新赋值,重新赋值不需要使用$ name="new"
只读变量

使用readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变,且只读变量不能使用unset删除

myName="Ken" readonly myName myName="New" #执行脚本会报错myName:This variable is read only
删除变量

可以通过unset 变量名命令删除变量,但无法删除只读变量

myName="Ken" unset myName echo $myName #无任何输出

变量值检测与替换

类似于C语言三元表达式的一种功能,即检测变量y是否存在,或者其值是否为空/非空,然后根据y的状态赋予变量x不同值

语句 未定义变量y 变量y为空 变量y非空 说明
x=${y:-默认值} x=默认值 x=默认值 x=$y 如果变量y未定义或为空,则x使用指定的默认值
x=${y:=默认值} x=y=默认值 x=y=默认值 x=$y,y值不变 如果变量y未定义或为空,则赋予y默认值,且x也使用默认值
x=${y:+替代值} x为空 x为空 x=替代值 如果变量y已定义且非空,则x使用替代值
x=${y:?错误消息} 输出消息到标准错误输出 输出消息到标准错误输出 x=$y 如果变量y未定义或为空,输出错误信息并退出
不带:的语法只检查变量是否已定义,不检查变量是否为空值(即使y为空也视为有效)
x=${y-默认值} x=默认值 x=$y(空) x=$y 如果y未定义,则x使用默认值
x=${y=默认值} x=y=默认值 x=$y(空) x=$y,y值不变 如果变量y未定义,则x和y都赋值为默认值
x=${y+替代值} x为空 x=替代值 x=替代值 如果变量y已定义(即使为空),则x使用替代值
x=${y?错误消息} 输出消息到标准错误输出 x=$y(空) x=$y 如果变量y未定义,则输出错误消息并退出
1. 如果变量filePath未定义或路径为空,则使用默认的路径 filePath=${filePath:-/usr/local/etc/myfile} 2. 当变量port未定义或为空时,为newPort赋值为2000,且port也赋值为2000(虽然2000依旧会被解析为字符串而不是数值) newPort=${port:=2000}

字符串

shell中的字符串可以使用单引号声明,也可以使用双引号声明,二者的特性:

  • 单引号里的字符会原样输出,无法使用转义字符和变量,双引号里字符串的可以使用转义字符、引用变量
  • 二者都可以用于字符串拼接

字符串拼接

可以直接在字符串前后使用${变量名}拼接字符串,不需要像其他语言一样添加+号等字符串拼接符

text="1234" str1="aaa"${text}"bbb" #可以直接将多个字符串拼接在一起,并拼接变量,输出aaa1234bbb str2="aaa${text}bbb" #可以直接在字符串中引用变量,输出aaa1234bbb str3='aaa'${text}'bbb' #单引号也可以拼接字符串,输出aaa1234bbb str4='aaa${text}bbb' #但不能在单引号声明的字符串中引用变量,会原样输出aaa${text}bbb echo str1 str2 str3 str4

获取字符串长度

可以使用${#字符串变量名}来获取字符串长度

str="abcd echo ${#str} #当变量为字符串时,${#str} 等价于 ${#str[0]}

截取字符串

使用${字符串变量名:索引1:索引2}来截取从索引1到索引2的字符串,字符串索引从0开始

str="He said, one day you will leave this world behind." echo ${str:1:5} #输出e sai

快速删除字符串

shell中可以使用###%%%符快速删除匹配的部分字符串

  • ${字符串变量#pattern}删除pattern所匹配字符串之前的最短匹配部分
  • ${字符串变量##pattern}删除pattern所匹配字符串之前的最长匹配部分
  • ${字符串变量%pattern}删除pattern所匹配字符串之后的最短匹配部分
  • ${字符串变量%%pattern}删除pattern所匹配字符串之后的最长匹配部分
str="path/to/file.txt" # 删除开头的部分 echo ${str#*/} # 输出:to/file.txt(最短匹配 "path/") echo ${str##*/} # 输出:file.txt(最长匹配 "path/to/") # 删除结尾的部分 echo ${str%/*} # 输出:path/to(最短匹配 "/file.txt") echo ${str%%/*} # 输出:path(最长匹配 "/to/file.txt")

查找子字符串

查找字符 i 或 o 的位置(哪个字母先出现就计算哪个) str="He said, one day you will leave this world behind." echo `expr index "${str}" io`

可转义的字符串

Bash支持使用$’字符串’语法来解析字符串中ANSI C 样式的转义序列,允许在字符串中插入特殊字符,如:换行符(\n)、制表符(\t)等,这可用于将转义字符赋值给IFS(字段分隔符变量)等

1.写于''或""号的转义字符不会被解析,会原样输出(以echo举例,虽然echo有支持转义的选项) echo 'AAAA\nAAAA' #输出AAAA\nAAAA echo "AAAA\nAAAA" #输出AAAA\nAAAA echo $'AAAA\nAAAA' 输出 AAAA AAAA 2.可以通过IFS变量将字段分隔符修改为换行 IFS.OLD=$IFS #保存原变量 IFS=$'\n'

数组

数组定义

bash只支持一维数组,不支持多维数组,bash中的一维数组不限制数组大小,使用括号囊括数组元素,数组元素之间使用空格分隔:数组名=(值1 值2 值3)

1. 数组支持整体赋值 array1=(10 20 30) 2.数组元素间可以使用空格,也可以用换行符 array1=(10 20 30) 3.也支持数组元素单独赋值 array1[0]=10 array1[1]=20 array1[2]=30 4. 数组元素默认都被声明为字符串类型,因此以下语句也正确 array1=(100 "Hello" abc)

数组元素

bash中数组使用${数组名[下标]}表示数组元素,与绝大多数语言一样,数组下标从0开始,可以用${数组名[@]}${数组名[*]}表示数组中的所有元素

echo ${array1[1]} #输出20 echo ${array1[@]} #输出10 20 30 echo ${array1[*]} #输出10 20 30

数组长度

获取数组长度的方法与获取字符串长度的方法类似,即在需要获取的对象前加上#号,该方法可以获得整个数组的长度,也可以用于获取单个数组元素的长度

1. 获取数组长度 echo ${#array1[@]} 或 echo ${#array1[*]} 2. 获取单个数组元素的长度 echo ${#array1[1]} 3. 注意${#array1}返回的是数组第一个元素的长度(数组名代表数组首元素的起始地址) array1=(12345 "Hello" abcd) echo ${#array1} #输出字符串12345的长度5

关联数组

Bash 支持关联数组,可以使用任意的字符串、或者整数作为下标来访问数组元素,关联数组的键是唯一的
关联数组使用 declare 命令来声明,语法格式为:
declare -A 数组名
数组元素访问方式为:${数组名[“键名”]},通过${数组名[@]}${数组名[*]}可以获取所有元素,在此基础上,在数组名前加上!可以获取数组所有键,在数组名前加上#可以获取数组长度

1.关联数组可以在定义同时赋值 declare -A singer=(["周杰伦"]="手写的从前" ["陈奕迅"]="十年" ["林俊杰"]="江南") 也可以先声明,再赋值 declare -A singer singer["周杰伦"]="手写的从前" singer["陈奕迅"]="十年" singer["林俊杰"]="江南" 2. 数组元素、键访问方式 echo ${singer["周杰伦"]} #访问单个元素 echo "数组的元素有" ${singer[*]}#访问所有元素 echo "数组的键有" ${!singer[*]} #访问所有键 echo "数组的元素有" ${#singer[*]}#访问数组长度

位置变量与参数传递

给脚本传递参数

在使用命令执行脚本时,可以向脚本中传递参数,在脚本中可以使用一些预定义变量获取到这些参数

变量 作用
$n n为数字,$0代表命令本身(包含文件路径),$1-$9代表命令第1-9个参数,10以上用{}包裹,如:${10}
$* 代表命令行所有参数,$*将命令行所有参数作为一个整体进行处理,在遍历时循环只执行一次
$@ 代表命令行所有参数,$@会将参数依次拆开
$# 代表传递到脚本的参数总个数
$? 返回上一个命令的执行状态,如果返回0则说明执行正确,返回值非0则说明上一个命令执行不正确,用于检测上一个命令是否正确执行,方便后续处理
$$ 脚本运行当前进程的进程号(PID)
$! 后台运行最后一个进程的进程号(PID)
$- 显示shell所使用的当前设置(如:是否使用监视模式,是否只读取指令,而不实际执行),与set命令功能相同
定义一个test.sh脚本 #! /bin/bash echo $1 #输出10 echo $* #将参数 10 20作为一个整体输出 echo $@ #将参数依次处理为10和20两个参数输出 echo $# #输出参数个数 执行该脚本并传参 chmod 755 test.sh ./test.sh 10 20
在使用$1、$2..调用参数之前,应当使用-n等命令测试用户是否传递了参数,如果直接调用了参数,而用户忘记传参,则脚本可能出现严重错误,示例:
if [ -n "$1" ] then #正常执行功能 else echo "请输入参数" fi
$*和$@的区别 for i in "$*";do echo $i done for i in "$@";do echo $i done 执行 ./test.sh 1 2 3 第一个语句相当于传递了1个参数,输出1 2 3 第二个语句相当于传递了3个参数,输出 1 2 3
$0参数的使用

$0参数用于获取shell在命令行启动的脚本名,如果调用脚本时包含了路径,该参数会获取完整的脚本路径

执行命令./test.sh 则 $0=./test.sh 执行命令 bash /home/myshell/test.sh 则 $0=/home/myshell/test.sh

如果不想获取文件路径,而只需要脚本名,则可以结合basename命令来剥离脚本名

name=$(basename $0) echo $name # 执行命令 /home/myshell/test.sh 输出值为test.sh

基于该方法,可以写一个脚本,当用户输入不同的命令(不同脚本名),实现不同功能,但它们本质上是同一个脚本

e.g.执行addem 10 20命令返回10+20,执行multem 10 20返回10*20 #!/bin/bash name=$(basename $0) if [ $name = "addem" ] then total=$[ $1 + $2 ] elif [ $name = "multem" ] then total=$[ $1 * $2 ] fi echo echo 最后计算结果为: $total # 将该文件命名为addem,然后执行ln -s addem multem为该文件创建一个软链接,执行任何一个脚本名都会执行同一个脚本,脚本随后根据$0参数识别对应的命令

移动参数shift

bash shell的shift命令能够用来操作命令行参数,在使用shift命令时,默认情况下它会将每个参数变量向左移动一个位置。所以,变量$3的值会移到$2中,变量$2的值会移到$1中,而变量$1的值则会被删除(注意,变量$0的值,也就是程序名,不会改变)

在不知道到底有多少参数时,可以先处理$1参数,然后用该命令依次移动参数,然后继续操作第一个参数,由此实现了对参数的遍历。但注意,如果某个参数被移出,它的值就被丢弃了,无法再恢复。shift也支持一次移动多个数据,只需要指定一个参数即可:shift 参数,因此,shift也常用来跳过不需要的参数

#!/bin/bash echo "The original parameters: $*" shift 2 echo "Here's the new first parameter: $1"

脚本选项

除了参数,绝大部分命令还会带有选项,如:ls -alh中的alh,同样,编写的shell脚本如果需要选项来加持命令的功能,则需要在脚本中对选项进行处理

选项与参数分割符

在Linux中,有些命令同时包含了参数和选项,为了方便将二者区分开来,Linux使用一个特殊标记符:双短线-‌-表示选项列表结束,参数列表开始。该符号可以用于提醒命令和脚本,-‌-符号后的都为命令(脚本)参数,该符号主要有两个使用场景:

  • 选项输入结束,参数输入开始。如:test -a -b -c -‌- value1 data2;使用-‌-可以很清晰的划分出命令选项和参数的界限
  • 如果参数中包含-号,可能会被解析为选项,此时使用-‌-分割可以防止参数被解析错误。如:创建名为-rTest的文件,如果使用touch -rTest会将-r解析为选项,因而返回错误,而执行 touch -‌- -rTest可成功创建该文件

简单选项处理

表面上看,命令行选项紧跟在脚本名之后,就跟命令行参数一样。因此对于简单的选项,可以像处理命令行参数一样处理命令行选项

#!/bin/bash while [ -n "$1" ] do case "$1" in -a) echo #添加-a的脚本功能 ;; -b) echo #添加-b的脚本功能 ;; -c) echo #添加-c的脚本功能;; *) echo "无 ${1} 该选项" ;; esac shift #移动参数,依次处理 done

如果选项中同时含有选项和参数,可以使用-‌-符号分隔

该方法适合类似./test.sh -c -a -b -- data1 data2 选项和参数分开的情况 #!/bin/bash # extracting options and parameters echo while [ -n "$1" ] do case "$1" in -a) echo 这是一个选项;; -b) echo 这是一个选项;; -c) echo 这是一个选项;; --) shift break ;; #选项结束,退出循环 *) echo "$1 is not an option";; esac shift done # count=1 for param in $@ do echo "Parameter #$count: $param" count=$[ $count + 1 ] done

当选项和参数混合时,需要对选项做进一步处理

处理./testing.sh -a data1 -b -c -d data2此类参数和选项混合的情况 while [ -n "$1" ] do case "$1" in -a) echo "Found the -a option";; -b) param="$2" echo "Found the -b option, with parameter value $param" shift ;; -c) echo "Found the -c option";; --) shift break ;; *) echo "$1 is not an option";; esac shift done # count=1 for param in "$@" do echo "Parameter #$count: $param" count=$[ $count + 1 ] done

getopt与getopts命令

上述方法不太方便处理选项合并输入等情况(如:-alh),事实上Linux提供了专门用于处理选项输入的命令——getopt,而bash也提供了更高级的内建命令——getopts

getopt命令

getopt命令用于解析命令行参数和选项,其命令格式为

getopt [选项] 解析的选项 需要解析的参数表列

getopt命令的选项有:

  • -o 单个字符:指定命令短选项的字符串(如:-v),表示脚本支持的短选项(单个字符选项)
  • -‌-long 字符串1,字符串2…:指定命令长选项的字符串(如:-‌-version),表示脚本支持的长选项(多字符选项)
  • -n 替代字符串:指定一个名字来代替默认命令名称”getopt”
  • -q:不生成错误信息(用户输入了指定选项之外的字符时)
  • -s chars:指定分隔选项参数的字符集,通常用于指定选项和其参数之间的分隔符,默认是空格。
  • -l:输出选项的字符串表示形式。
  • -options:将选项和非选项分开,选项在参数列表的前面,非选项在参数列表的后面

指定命令解析的选项指该命令有效的选项字母,并指定选项需不需要参数值,如果需要则在选项后加上:号,随后getopt命令会根据指定的选项,解析getopt命令最后跟随的参数

e.g.该命令的用法解析 getopt ab:cd -a -b test1 -cde test2 test3 该命令中,getopt命令定义了四个有效选项字母:a、b、c和d。字母b后有一个冒号(:),表示b选项需要一个参数值。当getopt命令运行时,它会检查提供的参数列表(-a -b test1 -cde test2 test3),并基于提供的optstring进行解析。注意,它会自动将-cde选项分成三个单独的选项,并插入双破折线(--)来分隔行中的选项和参数,因此,输入的参数会被解析为以下形式: -a -b 'test1' -c -d -e -- 'test2' 'test3' 但由于getopt命令只指定了abcd四个有效选项,而用户输入了选项e,因此默认情况下,getopt命令会产生一条错误消息: getopt: invalid option -- e 此时,可以添加为getopt命令添加-q选项来忽略错误消息: getopt -q ab:cd -a -b test1 -cde test2 test3

shell脚本中使用getopt命令,可以搭配set命令使用,set命令可以将其接收的参数按空格分割,并赋值给位置参数,可以将getopt解析出来的值作为set命令的输入参数,此后就可以使用$位置值的方式调用getopt命令解析出来的值

e.g.写一个名为testGetopt.sh脚本,脚本需要支持输入选项:-a选项和-b选项(完整选项名为-aaa和-bbb),这两个选项需要提供一个参数,选项-c、-ccc、-d不需要提供参数,可以提供任意数量的参数在脚本中调用 分析: a:b:cd -‌-long aaa:,bbb:,ccc为解析的选项 $@为用户输入到该脚本中的参数,也是getopt命令需要解析的参数表列 通过-‌-分隔getopt命令的选项和参数部分,更为清晰 如果用户执行: bash getoptPra.sh -a 100 -b 200 -cd --ccc lua ajax 则该命令被getopt命令解析后,$args的值为: -a '100' -b '200' -c -d --ccc -- 'lua' 'ajax' getopt命令帮我们分割好了选项和参数,此时将该串字符串作为set命令的参数输入,set命令会以空格为分隔符,将该串字符串分割并依次存储在$1、$2、$3等位置变量中,方便后续依次判断和调用 通过case命令和shift命令依次为各选项写功能,选项处理完毕,最后的剩余的参数另作处理 执行
#! /bin/bash
# 对用户输入的选项和参数进行处理
  args=$(getopt -o a:b:cd --long aaa:,bbb:,ccc -- $@)
  if [ $? -ne 0 ] 
     then
         echo"解析错误"
        exit 1 
  fi
# 查看getopt命令解析出来的选项和参数
 echo "---------"
 echo $args
 echo "---------"
 set -- $args
# 遍历所有选项,给与不同的功能
 while true
   do
    case $1 in
      -a|--aaa) echo "输入了-a或-aaa选项,参数值为$2" ; shift 2 ;;
      -b|--bbb) echo "输入了-b或-bbb选项,参数值为$2" ; shift 2 ;;
      -c)       echo "输入了-c选项" ; shift ;;
      -d)      echo "输入了-d选项" ; shift ;;
      --ccc)    echo "输入了--ccc长选项" ; shift; break ;;
      --)       shift; break ;;
    esac
   done
# 选项外的参数另作处理
 echo "额外的输入参数有:$@"
getopts命令

getopts命令是bash shell的内建命令,为了弥补getopt命令的不足(如:不好处理带空格和引号的参数),它提供了一些高级功能。

getopts命令与getopt命令不同,每次调用getopts命令,它只处理命令行上检测到的一个参数,因此往往需要循环调用,处理完所有的参数后,它会退出并返回一个大于0的退出状态码。getopts命令的格式与getopt命令相似:

getopts 解析的选项 变量

同样,解析选项中可以指定命令需要识别的有效字母选项,如果该选项需要参数,就加一个冒号。如果需要getopts命令忽略错误信息,可以在整个解析选项字符串前加个冒号,相当于getopt命令的-q选项。getopts命令会将输入的参数表列保存在指定的变量中,方便遍历。

e.g.脚本支持选项abc,其中-b选项需要跟随一个参数。 getopts :ab:c opt # ab:c前的:表示让getopts命令忽略错误(如:用户输入abc以外的选项)

getopts命令还预定义了两个环境变量:如果选项需要跟随一个参数值,OPTARG环境变量会保存该值,而变量OPTIND变量保存了参数列表中getopts正在处理的参数位置。

getopts命令有以下特点:

  • 该命令不直接支持长选项,但处理单字母选项很方便
  • 解析命令行选项时该命令会移除开头的单破折线,所以在case定义中不用单破折线
  • 该命令支持在参数值中包含空格,只需要将带空格的参数值用双引号包裹即可
  • 可以将选项字母和参数值放在一起使用,而不用加空格,如:-abvalue,命令可以根据case定义的字符正确解析出-a和-b选项,以及value参数值
e.g.脚本需要支持-a、-b、-c命令,选项ab需要输入参数 #!/bin/bash while getopts ":a:b:c" opt; do case $opt in a) echo "输入了选项-a,其参数值为$OPTARG" ;; b) echo "输入了选项-b" ;; c) echo "输入了选项-c" ;; *) echo "未定义的选项" ;; esac done #使用shift命令和OPTIND变量处理剩余的参数 shift $[ $OPTIND - 1 ] echo "剩余的参数:" for param in $@ do echo $param done

选项标准化

部分选项在Linux中有一些墨守成规的用途,遵守这些规定会让脚本看起来更友好一些

选项 说明 选项 说明
-a 显示所有对象 -n 使用非交互模式(批处理)
-c 生成一个计数 -o 将所有输出重定向到的指定的输出文件
-d 指定一个目录 -q 以安静模式运行
-e 扩展一个对象 -r 递归地处理目录和文件
-f 指定读入数据的文件 -s 以安静模式运行
-h 显示命令的帮助信息 -v 生成详细输出
-i 忽略文本大小写 -x 排除某个对象
-l 产生输出的长格式版本 -y 对所有问题回答yes

数值声明与数值运算

默认情况下,Bash中的变量会被声明为字符串类型,如:num=22,Bash会将22作为字符串处理,诸如11+22的式子也会被识别为字符串,想要进行数值运算,需要使用特殊声明语句和运算方式

数值声明

该语句用于特殊声明变量类型,使变量不再只限于字符串类型
declare [+或-] [选项] 变量名

  • - 给变量设置类型属性
  • + 取消变量的类型属性
  • -i 将变量声明为整形(integer)
  • -x 将变量声明为环境变量
  • -p 查看变量的类型
declare -i num1=11+22

数值运算

进行数值运算可以使用以下几种方式:

1.通过”$(())”或”$[]”运算式
  • 变量名=$((运算式))
  • 变量名=$[运算式]
示例: [root@localhost ~]# num1=$((11+22)) [root@localhost ~]# num2=$[11+22]
2.通过declare -i声明

declare -i 变量名

示例: [root@localhost ~]# declare -i num1=11+22

浮点数运算的解决方案

z shell提供了完整的浮点数运算操作,但在bash shell中,需要借助内建的bash运算器——bc

输入输出

用户输入read

read [选项] [变量名] 从键盘(默认)读取一行数据,然后将输入的数据分解成字段,并将字段分别赋值给这些变量

  • -p “提示信息”  输出提示信息(prompt 提示)
  • -t 秒数  read命令只等待指定时间(timeout 超时),如果超时未输入,read命令会以非零状态码退出
  • -n 字符数 read命令只接收指定数量字符
  • -s  隐藏输入的内容(silent 沉默)
  • -i 文本 将指定文本作为默认输入值
  • -a 数组名 将输入的数据赋值给数组变量
  • -d 字符 指定一个定界符,当入户输入该字符时,read命令将停止读取输入(delimit 划定界限)
  • -e 允许用户使用Readline功能,该功能允许用户使用快捷键进行快速操作(如快速跳转到本行开头)
  • -r 禁用转义,\不再会被解析为转义字符
  • -u 文件描述符 从指定的文件描述符中读取,如标准输入设备的文件描述符为0
  • -c 在读取输入时不需要按回车确认,即实时输入
  • 如果在命令中不指定变量,则read读入的数据会保存在环境变量REPLY
示例: #!/bin/bash read -t 30 -p "input name:" name #30s内输入姓名并赋值给name read -s -t 30 -p "input password:" passwd #30s内隐藏输入密码并赋值给passwd

read也支持一次输入多个数据,数据之间用空格隔开,然后赋值给多个变量。如果输入的数据个数多于变量个数,则多出来的数据连同空格会被赋值给最后一个变量。read命令也支持将多个数据直接存入数组中,如果数据间的分隔符是,或者其他符号,也可以通过IFS变量修改分隔符(Linux中分隔符默认为空格、制表符、换行符),方便从文件中读入数据并作处理

1. 可以一次读取多个数据 read data1 data2 输入10 20 30,则data1=001,data2=20 30,空格会视为第一个数据输入完毕,多余数据赋值给最后一个变量 2. 对于使用其他符号分隔的数据,可以重新定义分隔符 read -p "输入数据,以逗号隔开" data echo "当前数据" $data IFS=',' #修改IFS变量的值,将分隔符改为,号 read -a data_array <<< $data echo "处理完的数组为:" ${data_array[@]} 3. read命令可以从文件中读取数据,每次调用read命令,都将从文件中读取一行文本,当文件中没有内容时,read命令会退出并返回非零状态码。读取文件时,可以借助cat命令和管道符 cat test.txt | while read content do echo $content #内容被保存到content变量中 done

终端输出

echo [选项] [输出内容] 用于向终端输出文本信息

  • -e 启用转义字符\,可以输出如换行符(\n)、制表符(\t)等,也可以修改输出文字的样式
  • -E 禁用转义字符,原样输出文本
  • -n 不换行输出文本,输出完毕后光标停留在同一行
1. echo输出字符串文本时默认情况下可以不需要引号 echo Hello World 2. 但如果字符串中有单引号/双引号,则字符串需要使用另外一种引号包裹(字符里有单引号,就用双引号包裹,反之亦然) echo 'he said:"One day you will leave.."' #单引号用于划定字符串,不会输出显示 3. echo也可以一次性输出多个变量,变量间用空格分隔 echo "当前值为:" $num1 $num2 #输出文本后输出变量num1和num2

输入输出重定向

文件描述符与标准输入输出设备

Linux系统将每个对象当作文件处理,并使用文件描述符(file descriptor)来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件,bash shell保留了前三个文件描述符(0、1和2)作为标准输入、标准输出、标准错误输出的文件描述符,每个进程一次最多可以有九个文件描述符。

设备 Linux文件名 文件描述符 类型
键盘 /dev/stdin 0 标准输入
显示器 /dev/stdout 1 标准输出
显示器 /dev/stderr 2 标准错误输出

输出重定向

一般情况下,Linux输出信息时默认的输出位置为屏幕,可以用重定向符将命令的输出重定向到文件等地方,而不再显示到屏幕上。如果重定向位置为文件而该文件不存在,则会新建文件

类型符号作用
标准输出重定向 >以覆盖方式将正确命令返回内容输出到指定文件或设备中
>>以追加方式将正确命令返回内容输出到指定文件或设备中
标准错误输出重定向2>以覆盖方式将错误命令返回内容输出到指定文件或设备中
2>>以追加方式将错误命令返回内容输出到指定文件或设备中
同时输出正确和错误命令返回信息 &> 以覆盖方式,把正确和错误返回信息保存到同一文件中(Bash特有简写,也可以写为>&)
&>> 以追加方式,把正确和错误返回信息保存到同一文件中(Bash特有简写)
cmd>file 2>&1 以覆盖方式,把正确和错误返回信息保存到同一文件中(POSIX标准写法,兼容性强)
cmd>>file 2>&1 以追加方式,把正确和错误返回信息保存到同一文件中(POSIX标准写法,兼容性强)
cmd>>file1 2>>file2 把正确命令返回信息追加到文件1中,错误命令返回信息追加到文件2中
在脚本中输出错误消息(输出到stderr) >&2 在脚本中自定义一个消息作为错误输出
批量重定向 exec 1> 将脚本执行期间所有标准输出重定向到指定位置
exec 2> 将脚本执行期间所有错误输出重定向到指定位置
1.标准输出与标准错误输出的重定向

标准输出是指程序执行正确时,会向用户输出普通信息流(如:程序的执行结果、信息或数据等)。标准错误输出则用来显示错误信息,当程序遇到错误或异常时,它会将错误信息发送到标准错误输出流。默认情况下,标准输出和标准错误输出的输出位置都为屏幕,但可以使用重定向命令将程序的正确执行结果或错误执行结果输出到屏幕以外的地方。

单独输出正确信息时,可以在重定向符中省略文件描述符1,但如果同时重定向了标准输出和标准错误输出,则需要加上文件描述符

1. 输出命令正确执行信息到文件中 eg:将/usr/bin目录文件信息保存到以log.日期为文件名的日志文件中,%y%m%d表示提取日期中两位数的年月日,如:log.240522 today=$(date +%y%m%d) ls /usr/bin -al > log.$today 2. 输出命令错误执行信息到文件中 e.g.文件test不存在时 ls -al test.txt 2> fail.log 3. 将正确信息和错误信息输出到不同文件时,需要明确添加文件描述符 ls -a test1 test2 1>success.log 2>fail.log 4. 将正确信息和错误信息输出到同一文件 ls -a test1 test2 &> message.log
2.输出错误信息

如果需要在脚本中输出错误信息,则可以将输出信息重定向到STDERR文件描述符,语法为:输出信息 >&2。在终端中输出语句时看起来和正常echo语句无区别,但对系统而言明确了这是一个错误输出信息

e.g.在脚本中输出一个错误消息 echo "程序抛出一个异常" >&2

输入重定向(不常用)

将文件等内容重定向到命令,下列命令省略了文件描述符0

类型 符号 作用
输入重定向 n< 从文件描述符n输入,n默认为0,n为0时可以省略
标准输入重定向 < 将文件或设备内容重定向到命令
内联输入重定向 << 在命令行中指定多个文本输入内容

如:使用wc命令统计文件中的字符

# wc < test 统计输出test文件内容的行数、单词数、字节数

内联输入重定向(inline input redirection)用于从命令行输入多行数据。在使用时,必须指定一个文本用于标记输入数据的开始和结尾,完整的语法为:

命令 << 标记文本 输入的数据 ... 标记文本
可以添加选项-让shell忽略所输入字符串开头的制表符
1.输入多行文本给命令 wc << EOF #指定标记文本,开始输入 >This is line1 >This is line2 > EOF #遇到标记文本,终止输入 2.当输入文本开头有制表符时,可以添加-选项来让shell忽略制表符 wc <<- EOF #指定标记文本,开始输入 > This is line1 > This is line2 > EOF #遇到标记文本,终止输入

批量重定向

当脚本中有大量信息需要重定向,如果为每个命令都进行一次重定向过于繁琐,此时可以搭配exec命令指定shell将脚本执行期间的信息都重定向到某个位置。exec会替换当前shell的执行上下文环境,将标准输出输入和错误输出重定向到指定位置

重定向输出 1. 将所有的标准输出重定向后,依旧可以指定将某个语句输出到指定位置 e.g.将所有命令执行正确的信息输出到文件out.txt,而将部分指定的错误消息输出到屏幕 exec 1>out.txt echo "正常输出到out.txt文件" echo "将该错误输出显示到屏幕" >&2 2. 也可以将标准输出、标准错误输出都完全重定向 exec 1>out.txt exec 2>error.txt echo "输出到out.txt" echo "输出到error.txt" >&2 ls -a test.txt #若test文件存在,输出信息到out,若不存在,输出到error 输入重定向 1. 将标准输入重定向到文件后,read等命令将从文件中读取数据 exec 0> test.txt while read data do echo $data done

丢弃命令输出

如果需要某个输出既不显示到屏幕上,也不保存到文件中,可以将其重定向到一个名为null的特殊文件中。Linux中该文件的位于/dev/null路径下,所有重定向到该文件中的数据都会被丢弃,该文件中无任何内容。因此也可以在输入重定向中将其作为输入文件,可以用来快速清空文件内容,将该文件变为空白文件,常用于日志文件中清除内容(也可以删除再创建)

1. 丢弃信息 ls -al test.txt > /dev/null ls -al test.txt 2> /dev/null 2.快速清空文件 cat /dev/null > test.txt

自定义重定向

在shell中最多可以有9个打开的文件描述符。除了标准输入0,标准输出1和标准错误输出2,其他6个从3~8的文件描述符均可用作输入或输出重定向。可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们

创建文件描述符

可以直接使用exec命令直接给输入、输出分配文件描述符。和标准的文件描述符一样,一旦将另一个文件
描述符分配给一个文件,这个重定向就会一直有效,直到其被重新分配。

e.g.分配一个文件描述符3,并重定向到test3文件 #!/bin/bash exec 3>>test3 echo "正常输出到屏幕" echo "将被追加到文件test3" >&3
重定向文件描述符
语法 说明
n>file 以覆盖方式,将文件描述符n重定向到文件file
n>>file 以追加方式,将文件描述符n重定向到文件file
n>&- 关闭文件描述符n
n>&m 将n的输出合并到m
n>&m- 移动文件描述符,将n重定向到m,然后关闭m
<n 从n输入
m<&n 将n的输入与m合并
n<&m 复制文件描述符,将文件描述符m复制到n,对n读取等同于对m读取
n<&m- 移动文件描述符,将文件描述符m移动到n,随后关闭m
n<>file 允许通过n同时对文件进行读操作和写操作
1. 以下两个语句等价,只是标准输出和标准错误输出的写入顺序不同 # 先写入标准输出信息,再先入错误信息 ls >>file 2>>&1 # 先写入错误信息,再先入标准输出信息 ls 2>>file 1>>&2 2. 移动输出文件描述符 # 打开文件描述符3指向 output.txt exec 3>output.txt # 将文件描述符4重定向到3,然后关闭文件描述符3,之后只能通过4来写入output.txt exec 4>&3-
恢复文件描述符的指向

通过exec命令重定向的文件描述符会在整个脚本运行期间指向指定的位置,如果需要将其恢复到原来的指向位置,可以创建一个文件描述符作为一个缓存变量,并在代码功能执行完后将需要恢复的文件描述符重定向到新建的文件描述符

e.g.恢复标准输出的指向 #!/bin/bash exec 3>&1 exec 1>out.txt echo "输出内容到out.txt" exec 1>&3
e.g.标准输入也类似 #!/bin/bash exec 6<&0 exec 0< testfile count=1 while read line do echo "Line #$count: $line" count=$[ $count + 1 ] done exec 0<&6
同时读写的文件描述符(了解)

shell也支持打开单个文件描述符来作为输入和输出,即可以用同
一个文件描述符对同一个文件进行读写。shell会维护一个内部指针,指明在文件中的当前位置,任何读或写都会从文件指针上次的位置开始,因此可能出现内容被错误读写的问题,不太好用

#!/bin/bash exec 3<> testfile read line <&3 echo "Read: $line" echo "This is a test line" >&3
关闭文件描述符

shell会在脚本退出时自动关闭文件描述符,若要手动关闭文件描述符,则需要将该文件描述符重定向到特殊符号&-,文件描述符关闭后,如果输出数据shell将抛出错误

e.g.关闭文件描述符3 #!/bin/bash exec 3> test.txt echo "输出数据到test文件" >&3 exec 3>&-

列出打开的文件描述符

lsof命令会列出整个Linux系统打开的所有文件描述符。由于该命令会向非系统管理员用户提供Linux系统的信息,因此在很多Linux发行版中隐藏了该命令。普通用户要想运行该命令,必须使用全路径来引用该命令:/usr/sbin/lsof

管理员用户可以直接使用:

lsof [选项]
  • -p PID:指定对应进程PID所打开的文件描述符
  • -d 值:指定需要显示的文件描述符编号
  • -a 对上述两个选项结果进行布尔AND运算
可以结合环境变量$$(当前shell的PID)来查看当前shell进程打开的文件描述符 /usr/sbin/lsof -a -p $$ -d 0,1,2

该命令返回信息包括:

参数 说明
COMMAND 正在运行的命令名的前9个字符
PID 进程的PID
USER 进程属主的登录名
FD 文件描述符号以及访问类型(r代表读,w代表写,u代表读写)
TYPE 文件的类型(CHR代表字符型,BLK代表块型,DIR代表目录,REG代表常规文件)
DEVICE 设备的设备号(主设备号和从设备号)
SIZE 如果有的话,表示文件的大小
NODE 本地文件的节点号
NAME 文件名

返回值结果举例

COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME bash 3344 rich 0u CHR 136,0 2 /dev/pts/0 bash 3344 rich 1u CHR 136,0 2 /dev/pts/0 bash 3344 rich 2u CHR 136,0 2 /dev/pts/0

由于STDIN、STDOUT和STDERR文件描述符都指向终端,因此其文件类型是字符型,输出文件的名称就是终端的设备名,且所有3种标准文件都支持读和写。

tee同时输出到显示器和文件

Linux提供了一个特殊命令tee,用于将输出同时发送到标准输出(显示器)和文件,而不用重定向多次:

tee [选项] 文件1 文件2...
  • 默认情况下,如果指定的文件不存在,则会新建该文件,如果文件存在,tee命令会覆盖原文件内容
  • -a 将数据追加到文件中
  • -i 忽略中断信号,例如通过 Ctrl+C 触发的信号

tee命令会重定向STDIN输入的数据,并同时发往两处:STDOUT和tee命令指定的文件,因此可以搭配管道符使用。

e.g.将某个数据同时发送到屏幕和test文件 echo "数据" | tee test

条件判断

在bash shell中,if 语句的判断依据为命令的退出状态码,不能像其他语言一样直接用 if 语句判断条件语句,但bash shell提供了两个用于条件判断的方法:

  • test 命令
  • [ ] 使用中括号测试条件

test命令与[ ]

test命令可用于做数值比较、字符串比较和文件比较,当test命令中列出的条件成立,test命令就会正常退出并返回退出状态码0,这样if等语句就可以正常工作了。此外,test命令有一种替代形式,可以直接将条件语句写于中括号[ ]中,注意,第一个中括号后和第二个中括号前都必须加上一个空格,否则将报错。如果test命令后或者中括号中不写条件语句,则它们都将视为条件不成立,二者的语法类似:

  • test 条件语句
  • [ 条件语句 ]
e.g.检测某个变量是否存在且不为空,提示:返回值在终端上可以用$?查看 str="" if test $str then echo "该变量存在" else echo "该变量不存在" fiif [ $str ] #取变量值$str前后均有空格 then echo "该变量存在" else echo "该变量不存在" fi

文件相关

上述两个方法用于测试文件或目录是否满足相关要求时的语法为:

  • test 选项 文件/目录名
  • [ 选项 文件/目录名 ]
文件类型
测试选项 作用
-e file 判断file是否存在
-d file 判断file是否存在,且是否为目录
-f file 判断file是否存在,且是否为为文件
-b file 判断file是否存在,且是否为块设备文件(如:硬盘设备文件)
-c file 判断file是否存在,且是否为字符设备文件(如:鼠标、键盘设备文件)
-O file 判断file是否存在并属于当前用户所有
-G file 判断file是否存在并默认组与当前用户相同
-L file 判断file是否存在,且是否为链接文件
-p file 判断file是否存在,且是否为管道文件
-s file 判断file是否存在,且文件大小是否大于0(非空),非空为true
-S file 判断是否为套接字文件
-N file 判断file是否存在,且文件在上一次读取之后被修改过(mtime比atime新),则为true
e.g.在使用某个文件或目录之前,先检查其是否存在是一个好的编程习惯 location=$HOME file_name="myFile" if [ -e $location ] then if [ -e $location/$file_name ] then echo "文件存在" echo "写入数据" date >> $location/$file_name else echo "文件不存在" fi else echo "目录不存在" fi
文件权限

检测读(写/执行)权限时,文件所有者、所属组、其他任意拥有读(写/执行)权限都将返回true

测试选项 作用
-r file 判断file是否存在,且是否拥有读权限
-w file 判断file是否存在,且是否拥有写权限
-x file 判断file是否存在,且是否拥有执行权限
-u file 判断file是否存在,且是否拥有SUID权限
-g file 判断file是否存在,且是否拥有SGID权限
-k file 判断file是否存在,且是否拥有SBit权限
两个文件比较

在比较文件之前,应该确认两个文件确实存在,或用脚本验证文件是否存在并作错误处理,否则比较操作可能会返回错误的结果。

测试选项 作用
文件1 -nt 文件2 判断文件1的修改时间是否比文件2新
文件1 -ot 文件2 判断文件1的修改时间是否比文件2旧
文件1 -ef 文件2 判断文件1和文件2的i节点(Inode)是否一致,即判断是否为同一文件,常用于判断是否为硬链接

整数比较

bash shell只支持整数进行比较,这里的整数可以是变量,也可以是正值或负值常量

测试选项 作用
整数1 -eq 整数2 判断整数1与整数2是否相等
整数1 -ne 整数2 判断整数1与整数2是否不相等
整数1 -gt 整数2 判断整数1是否大于整数2
整数1 -lt 整数2 判断整数1是否小于整数2
整数1 -ge 整数2 判断整数1是否大于等于整数2
整数1 -le 整数2 判断整数1是否小于等于整数2

字符串判断

注意!以下比较符号=、!=、\<、\> 前后都需要有一个空格

测试选项 作用
-z str 字符串str长度为0,则为true(字符串为空字符串,未定义的变量也视为空)
-n str 判断字符串str长度是否为非0(非空)
str1 = str2 判断str1和str2是否相等
str1 !=s tr2 判断str1和str2是否不相等
str1 \< str2 判断str1是否比str2小(根据ASCII码)
str1 \> str2 判断str1是否比str2大(根据ASCII码)

在比较字符串是否比另外一个字符串小/大时,大于号和小于号需要进行转义,否则会当做重定向符号处理。在比较时,会依次判断字母在ASCII中的出现位置,因此大写字母会小于小写字母(sort命令使用的是系统的本地化语言设置中定义的排序顺序,小写字母出现顺序位于大写字母前,与之相反)

e.g. 比较字符Test和test str1="Test" str2="test" if [ str1 \\< str2 ] then echo "Test小于test" #由于ASCII中T小于t,所以输出该语句 else echo "Test大于test" fi

多重判断

测试选项 作用
判断1 -a 判断2逻辑与,判断1和判断2都成立,结果为真
判断1 -o 判断2逻辑或,判断1和判断2任意一个成立,结果为真
! 判断 结果取反
[ 判断1 ] && [ 判断2 ]判断1和判断2都成立,结果为真
[ 判断1 ] || [ 判断2 ]判断1和判断2任意一个成立,结果为真
e.g.判断a是否有值,并且判断a是否大于20,都成立输出 true,否则输出 false a=10 [ -n "$a" -a "$a" -gt 20] && echo "true" || echo "false"

判断语句

if语句

其他语言中的if语句,会判断if语句后式子的求值结果,并根据结果的true或false值进行对应处理。但bash shell中的if语句有所不同,bash会执行if语句后的命令,如果该命令的退出状态码是0,则会执行then部分的代码体,如果该命令的退出状态码是其他值,则执行else语句(没有else则不执行),if语句的语法为:

if 命令行 then 程序体 fi
e.g. 查询是否存在某个用户,如果存在则输出该用户的HOME目录 userName=shiwivi if grep $userName /etc/passwd then echo "找到该用户:" ls -a /home/$userName/.b* fi
在部分脚本中,该语句会被写成
if 命令;then 程序体 fi
形式,即如果then和if写于同一行,需要加上;号,这样它会与其他一些语言的语法类似,如果if和then不写在同一行上,则;号可要可不要

此外,bash shell也支持if-then-else语句和if嵌套语句,但注意,嵌套的if语句中的else-if被简化为了elif

if-then-else语句:
if 命令 then 条件成立执行 else 条件不成立时执行 fi
多分支if语句:
if 命令1 then 条件成立执行 elif 命令2 then 条件成立时执行 (.....) else 上述所有条件不成立时执行 fi

case命令

case语句类似于其他语言中的switch…case语句,用于匹配多个同类型的条件,其语法为:

case 变量 in 值1 | 值2) 程序1 ;; 值3) 程序2 ;; ..... *) 上述条件都不满足,执行该程序;; esac

case中的条件可以使用或符号同时设置多个条件,最后的星号会捕获所有与已有条件不匹配的值,相当于switch语句中的default

e.g.查询多个用户 case $USER in JayChou | EasonChan) echo "歌手";; KenThompson) echo "程序员";; *) echo "未知用户";; esac

循环语句

for命令

for语句遍历时,默认使用空格分割需要遍历的值,如果值本身包含空格,则需要使用双引号将值包裹,此时for语句不会将双引号作为值的一部分

for 变量 in 值1 值2 值3..... do 程序 done
do可以和for语句放在同一行,只需要在值列表之后加上一个分号即可。
1. for需要遍历的数据使用空格隔开 for value in data1 data2 data3 do echo "数据依次为$value" done 2. 如果数据中包含单引号等特殊字符,可以使用双引号包括数据,或使用转义符 #使用转义符表示\',或使用双引号包裹"this'll" for test in I don\'t know if "this'll" work do echo "word:$test" done 3. 数据中包含空格,也需要使用双引号包裹数据 for test in Nevada "New Hampshire" "New Mexico" "New York" do echo "Now going to $test" done 4. for遍历的数据,可以是一个变量,也可以是一个命令的输出 file="/myData/map" for state in $(cat $file) do echo "Visit beautiful $state" done 5. for也可以用来读取目录 for file in /home/user1/test/* do if [ -d "$file"] #将file用双引号包裹,避免文件名中含有空格时产生错误 then echo "这是一个目录" elif [ -f "$file" ] then echo "这是一个文件" fi done

此外,bash shell还支持一种C语言风格的for语句,但需要注意:

  • 使用双括号
  • 变量不以美元符$
  • 迭代过程的算式不需要用expr命令格式
    for((初始值;条件;变量变化)) do 程序 done
e.g.这种形式的for语句也支持遍历多个变量 for (( a=1, b=10; a <= 10; a++, b-- )) do echo "$a 与 $b" done

while命令

whlie 命令 do 程序 done
注意:while命令允许在while语句行定义多个测试命令,只有最后一个测试命令的退出状态码会被用来决定什么时候结束循环。
e.g.只有最后一个命令[ $num2 -ge 0 ]的退出状态码决定while语句何时结束 num1=5 num2=10 while echo "-------" [ $num1 -ge 0 ] [ $num2 -ge 0 ] do echo 这是第${num2}次循环 echo num1为${num1},num2为${num2} num1=$[ $num1 - 1 ] num2=$[ $num2 - 1 ] done

until命令

until命令和while命令工作的方式相反,until命令要求你指定一个通常返回非零退出状态码的测试命令。只有测试命令的退出状态码不为0,bash shell才会执行循环中列出的命令。一旦测试命令返回了退出状态码0,循环就结束了。即:while命令在条件满足时执行,until命令在条件满足时退出。同样,until也像while一样允许多个测试命令,但只有最后一个命令有效

until 命令 do 程序 done

break终止循环

break的用法与其他语言类似,即可以终止当前的循环,但bash shell提供了一个更高级功能,可以通过break 值的方式跳出多重循环(如果不指定值,默认为1),如:在两层for语句嵌套中,break 2 可以直接停止外层for语句的循环

e.g.当b为4时,终止2个for语句的循环 for (( a = 1; a < 4; a++ )) do echo "Outer loop: $a" (( b = 1; b < 100; b++ )) do if [ $b -gt 4 ] then break 2 #终止外部for语句的循环,如果命令为break则只能终止内部循环 fi echo " Inner loop: $b" done done

continue跳过循环

continue可以跳过本次循环,continue之后的命令将不再执行而开始下一次循环,与break类似,在多层循环中,bash shell中的continue提供跳过多层循环的功能,其语法为continue 值,值默认为1

for (( var1 = 1; var1 < 15; var1++ )) do if [ $var1 -gt 5 ] && [ $var1 -lt 10 ] then continue #如果有多层for,可以指定一个值跳过外层的循环 fi echo "Iteration number: $var1" done

处理循环的输出

在shell脚本中,可以在done命令后添加管道符或重定向符来处理循环的输出

e.g.将循环的语句"值为xx"写入到文件test.txt中而不显示到终端上,等循环语句写入完毕在终端上显示"数据写入完毕"提示语句 for (( a = 1; a < 10; a++ )) do echo "值为 $a" done > test.txt echo "数据写入完毕"

字段分隔符

环境变量IFS,称为内部字段分隔符(internal field separator),用于定义bash shell内用作字段分隔的一系列字符。默认情况下,bash shell会将下列字符当作字段分隔符:

  • 空格
  • 制表符
  • 换行符

如果bash shell在数据中看到了这些字符中的任意一个,它就会假定这表明了列表中一个新数据字段的开始。在处理可能含有空格的数据(比如文件名)时,这会非常麻烦
要解决这个问题,可以在shell脚本中临时更改IFS环境变量的值来限制被bash shell当作字段分隔符的字符。一般会在修改IFS值前保存原来的默认值,使用完后再恢复它

e.g.文件file每一行保存了一个数据,且数据中可能包含空格和制表符,要使用for循环遍历这些数据,则需要先将IFS修改为换行符,使bash shell忽略空格和制表符,将每一行当作一个整体处理 file="data" IFS.OLD=$IFS #保存旧的IFS值 IFS=$'\n' for data in $(cat $file) do echo "数据:$data" done IFS=$IFS.OLD #恢复IFS值
如果要指定多个IFS字符,只要将它们在赋值行串起来就行。 IFS=$'\n':;" 这个赋值会将换行符、冒号、分号和双引号作为字段分隔符。如何使用IFS字符解析数据没有任何限制。
#!/bin/bash 
# 提取/etc/passwd文件每个字段的内容
IFS.OLD=$IFS 
IFS=$'\n' 
for entry in $(cat /etc/passwd) 
do 
 echo "当前用户整体信息:$entry" 
 IFS=: 
 for value in $entry 
 do 
 echo " $value" 
 done 
done

(())双括号命令

双括号命令(( ))在shell中用于进行数学运算和条件判断,并提供了更灵活和方便的方式来处理数值计算和逻辑操作

数学运算

双括号命令除了基本的数学运算外,还支持以下运算,且双括号中还支持使用变量

符号 描述
num++、++num 前置/后置的自增
num–、–num 前置/后置的自减
! 逻辑取反
~ 按位取反
** 冥运算
<< 左位移
>> 右位移
& 位布尔和
| 位布尔或
&& 逻辑和
|| 逻辑或
(( result = 5 * (3 + 2) )) echo "Result of arithmetic operation: $result"

逻辑判断

双括号命令也常用于if-then等语句的逻辑判断,双括号命令支持<><=>===!=运算符

if (( result > 10 )); then echo "The result is greater than 10" else echo "The result is not greater than 10" fi

[[]]双方括号命令

双方括号命令提供了针对字符串比较的高级特性,双方括号命令里的表达式支持模式匹配,可以定义正则表达式来匹配字符串,因此该命令在比较字符串时功能很强大:

  • 支持<>==!=-gt等比较符
  • 支持!&&||等逻辑组合
  • 支持-f等文件测试,如:检测文件是否存在,是否为目录等
  • 支持使用*?等通配符,并使用=~结合正则表达式进行匹配
#!/bin/bash if [[ $USER == r* ]] #r*使用模式匹配规则 then echo "Hello $USER" else echo "未知用户" fi
bash shell支持双方括号命令,但部分shell可能不支持该命令

脚本控制

脚本在执行时,终端可以通过kill命令或Ctrl+C等方式发起信号进行进程的暂停、终止操作,这些信号中有一些是可以通过脚本捕获并作相应处理的(如:在被终止时将数据写入文件做好保存,或者无视终止命令继续执行脚本)

捕获信号

trap 捕获时执行的命令 信号1 信号2 ...
  • 信号可以是信号值或信号名,但需要是可被捕获的Linux信号
  • trap命令也会捕获以下特殊的伪信号
信号名(信号值) 说明
EXIT(0) 会在脚本退出时执行
DEBUG 会在每个命令后都执行一次
ERR 当某个命令以非零状态退出时,执行一次(非零状态来自if,while,until语句时不会执行)
e.g.用户执行Ctrl+C发起SIGINT信号中断脚本执行时,输出信息并继续脚本执行,下下述代码中,echo语句包含空格,所以需要使用双引号引用整个echo语句,其他命令同理。如果命令中包含空格(如:rm -f)则也需要双引号 #!/bin/bash trap "echo '无法通过Ctrl+C停止脚本执行'" SIGINT count=1 while [ $count -le 10 ] do echo "循环次数: $count" sleep 1 count=$[ $count + 1 ] done

修改信号捕获

如果需要在脚本某个阶段修改前面设置的信号捕获语句,只需要重新使用一次trap命令即可

#!/bin/bash trap "echo '捕获到SIGINT信号执行功能1'" SIGINT count=1 while [ $count -le 5 ] do echo "Loop #$count" sleep 1 count=$[ $count + 1 ] done trap "echo '此时捕获到SIGINT信号修改为执行功能2'" SIGINT

删除信号捕获

如果脚本在某个阶段不再需要进行信号捕获了,则可以移除信号捕获,语法为trap -‌- 信号

#!/bin/bash trap "echo '捕获信号SIGINT'" SIGINT . . #脚本功能 . trap -‌- SIGINT #移除信号捕获

函数

创建与调用函数

shell脚本中创建函数的方法有两种:

function 函数名 { #函数体 }
函数名( ) { #函数体 }

注意,使用第一种方式定义函数时,函数名和{之间必须有一个空格,否则{可能会被当作函数名的一部分从而报错。调用函数与其他语言类似,只是不需要括号,直接执行函数名即可,同样,shell中的函数需要在调用前的代码中创建,否则抛出command not found错误。函数可以同名,后面的函数会覆盖前面的函数,函数调用方法为:
函数名 参数1 参数2 …

变量的作用域

在shell中,变量也分为全局变量和局部变量

全局变量

与其他语言有所不同,shell中的全局变量作用域为定义之后的整个脚本代码,即使是函数内部定义的变量,在函数外依旧有效。若无特殊声明,脚本中定义的变量默认为全局变量。

e.g.函数内定义的变量,函数外也可以访问 #! /bin/bash function test { string="手写的从前" } test echo "函数外也可以获取全局变量:$string"
局部变量

shell中的局部变量需要使用local关键字声明。局部变量如果在函数内部声明,则其作用域只在函数中,如果函数外部存在一个同名变量,二者互不干扰

e.g.想要变量作用域为局部,需要使用local关键字声明 #! /bin/bash function test { local string="手写的从前" echo "函数内可以获取到变量值:$string" } test echo "函数外无法获取该变量:$string" #为空值

函数返回值与输出

默认退出状态码

bash shell会把函数当作一个小型脚本,运行结束时会返回一个退出状态码。与其他命令一样,在函数执行结束后,可以用shell中的预定义变量$?来确定函数的退出状态码。默认情况下,函数的退出状态码是函数中最后一条命令返回的退出状态码。注意,函数的默认退出状态码只看最后一条命令的退出状态,其他命令执行正确与否无法获悉,因此该方法应用场景有限。此外,就像脚本的返回值可以使用exit修改一样,函数的返回值可以使用return语句修改

e.g.函数func的退出状态码为最后一条命令ls的退出状态码,如果none.txt文件不存在,则函数的退出状态码为1,该状态码可以用$?调用 #!/bin/bash func() { echo "执行函数" ls -l none.txt } func echo "该函数的退出状态码为 $?" #none.txt文件不存在,输出值为1 2. 可以使用return修改该返回值,即便ls命令未正确执行 #!/bin/bash func() { echo "执行函数" ls -l none.txt return 0 } func echo "该函数的退出状态码为 $?" #此时return语句才是脚本最后一条命令,$?将输出为0
return命令修改状态码

shell支持使用return命令来返回指定的退出状态码,状态码支持0-255之间的整数值,该状态码同样可以使用$?获得。与exit命令指定的脚本退出状态码类似,如果return命令指定的退出状态码大于255,会输出该值被256取模后的结果

e.g.如果函数返回值大于255,则会返回一个错误的值 #!/bin/bash function getDouble { read -p "输入一个值:" value #输入200 return $[ $value * 2] } getDouble echo "函数的返回值为 $?" #输出值为144:400%256=144
函数输出

shell支持将命令的输出赋值给变量,同样也支持将函数的输出值赋值给变量,通过该方法我们可以将任何类型的数据作为函数输出,然后通过反引号``$( )调用函数,并将获取的函数输出值保存到变量中,该方法相比于return语句功能更加强大。但注意,通过该方法从函数中输出的值并不会作为函数的退出状态码,也无法被$?捕获

e.g.函数中read等命令的提示语句并不会作为函数输出一部分 #!/bin/bash function getDouble { read -p "输入值:" value #不输出提示语句给result变量 echo $[ $value * 2 ] #输入10 } result=$(getDouble) echo "函数输出值为 $result" #输出result为20 echo "函数的退出状态码为$?" #输出状态码为0

函数传参

由于bash shell会将函数当作小型脚本来对待,因此可以像命令传参一样给函数传入参数,并使用$1、$2等位置变量来读取传入函数的参数。

由于函数内部也使用$1、$2等变量来引用函数的参数,因此脚本中的$1、$2等位置变量无法在函数内部使用,即函数中无法直接通过位置变量来读取脚本传入的参数。要在函数中使用脚本传入的参数,则需要在调用函数时手动将它们传过去

e.g.为脚本test传入两个参数,并在函数中进行求和 #!/bin/bash function add { if [ $# -ne 2 ] #判断函数参数数量 then echo -1 else echo $[ $1 + $2 ] #这是函数的位置变量$1 $2 fi } value=$(add $1 $2) #这是脚本的位置变量$1 $2 echo "函数计算结果为:$result" 执行该脚本时 ./test 10 20

传递数组参数

shell中,给函数传递数组,不能像C语言一样直接传递数组名(数组首地址),这样只会传递第一个值。需要使用${数组名[@]}将所有数组元素传递过去,然后在函数中使用$@获取所有的数组元素

理论上通过${数组名[*]}也可以传递数组元素,或用$*接收,但$*会将所有数组元素视为一个整体,在使用for-in等语句遍历时可能出现问题

e.g.传递数组 #! /bin/bash function test { echo '$@的值为'$@ for value in $@ do echo "当前值为:$value" done } array=(10 20 30 40) test ${array[@]} e.g.如果使用$*,所有数组元素会被视为一个整体 function test { #使用该语句,依旧会把元素一个一个遍历出来,这是因为即便$*使用空格作为元素分隔符,for语句会自动识别空格分隔的数据并遍历 for value in $* #使用双引号包裹$*,for语句就只执行一次了 for value in "$*" #而对于$@,无论加不加双引号,for语句都会依次遍历出数组里的元素 for value in "$@" do echo "当前值为:$value" done } array=(10 20 30 40) test ${array[*]}

返回数组

从函数返回数组时,也不能直接返回数组地址,而是需要通过echo语句输出${数组名[@]}(数组元素),然后通过命令替换等方式在脚本中调用函数,并在命令替换时在外层加上( )将其转换为一个数组

e.g.传入一个数组,反序输出 #! /bin/bash function test { #echo "函数开始执行----------" local newArray local index=$[$#-1] #获取函数接收的参数个数并-1 for value in $@ do newArray[$index]=$value index=$[${index}-1] done echo ${newArray[@]} } array=(10 20 30 40 50) result=($(test ${array[@]})) echo "返回值result $result" 注意: 1.bash在使用命令替换时,会将捕获的结果作为一个字符串返回。上述代码中,命令替换外层加了个小括号(),在没有括号的情况下,result存储的是一个字符串,添加小括号后,result存储的结果将被转换为数组 2. bash在使用命令替换时会捕获所有命令的标准输出,如果在test函数中有除了输出数组元素以外的语句,也会被捕获,并被赋值给result。由于shell支持不同类型的数据都作为同一数组的元素,因此所有输出信息都将作为数组元素,这可能会对之后使用该数组造成影响,因此,返回数组的函数,尽量不要输出多余的信息。如:如果test函数第一行被注释的echo语句正常输出,则该语句信息将作为result的数组元素 3. 命令替换会捕获所有标准输出语句,且不显示到屏幕上,因此执行result=($(test ${array[@]}))语句时,虽然函数被执行了,但不会有任何屏幕输出,这些输出信息会在使用echo输出result的结果时,才会被输出到屏幕上

创建库

如果需要在多个脚本中频繁使用同一段代码,可以将这段代码封装为函数库,然后在多个脚本中调用该库文件。但在shell中,如果直接调用或执行该文件,shell会创建一个新的shell并在其中执行该库文件中的代码,而函数的作用域仅限于定义它的shell会话,因此在其他脚本中将无法调用这些函数。

shell提供了source命令用于解决该问题,source命令会在当前shell上下文中执行命令,这样脚本就可以使用库中的函数了。source命令有个快捷别名,称为点操作符,命令也可以用.号代替,语法格式为:
. 库文件路径

e.g.在myfunc.sh定义了两个函数,在test.sh中调用这两个函数 文件路径:/myShell/myfunc.sh #!/bin/bash function add { if [ $# -ne 2 ] then echo "参数输入错误" else echo $[$1+$2] fi } function sub { if [ $# -ne 2 ] then echo "参数输入错误" else echo $[$1-$2] fi } 文件路径:/myShell/test.sh #!/bin/bash #注意:无论使用.号还是source,命令后都需要添加一个空格,路径用相对路径或绝对路径均可 . ./myfunc.sh source /myShell/myfunc.sh echo "求和运算:$(add 10 20)" echo "求差运算:$(sub 20 10)"

在命令行中使用函数

函数也可以在命令行中创建和调用,命令行中创建的函数可以在整个系统中被调用,直到当前shell退出。注意,如果命名时函数名和shell的内建命令同名,则内建命令的功能会被函数功能覆盖!

在命令行中定义函数的方法有两种:

  1. 以单行方式定义,需要在每个命令后都加上分号;,以便shell区分命令的起止

    定义: function test { pwd; ls; echo $[ $1 / $2 ]; } 调用: test 100 50
  2. 以多行方式定义,定义时,bash会使用次提示符提示输入更多命令,该方式不需要在命令后添加分号,直接回车即可,最后输入花括号}告知shell函数输入完毕

    定义: function add { > echo $[ $1 + $2 ] > } 调用: add 2 5

在.bashrc文件中定义函数

在命令行中定义的函数,在shell退出时函数也会随之失效,如果希望某个函数在shell启动时也随之被加载进内存,方便在全局调用,则可以将该函数写入$HOME/.bashrc文件,一般情况下,bash以登录交互式shell启动,或以非登录交互式shell启动都会载入该文件。绝大部分Linux发行版都已经在.bashrc文件中定义了一些函数,注意小心修改,将新添加的函数写于文件末尾即可。也可以将新写的函数写于一个单独的文件中,然后在.bashrc文件中使用source命令载入。

通过该方式从.bashrc文件载入的函数,可以在命令行中全局调用,但无法在脚本中使用,这是由于脚本执行时启动的非交互式shell不会读取.bashrc文件,函数也不会从父shell中继承。如果需要某个函数在脚本中也可以被直接调用,可以用以下方法:

  • 在脚本中使用source命令引入.bashrc文件
  • 如果不想引入.bashrc文件,则在.bashrc文件中定义完函数后,可以用export -f命令将函数导出为环境变量,这样执行脚本时创建的非交互式shell将能从父shell继承该函数,然后在脚本中直接调用
e.g.为了避免修改$HOME/.bashrc,因此将某个常用的函数定义于$HOME/myfunc文件中,然后在$HOME/.bashrc引入,然后尝试在某个脚本中直接调用。这里直接以root账户举例 创建文件/root/myfunc用于存放自定义函数 function myTest1 { echo "函数myTest1被调用" } function myTest2 { echo "函数test2被调用" } export -f myTest1 #导出其中一个函数 在文件/root/.bashrc末尾引入myfunc文件 if [ -f /root/myfunc ];then . /root/myfunc fi /myShell/test.sh #!/bin/bash echo "脚本开始执行" myTest1 #被导出的函数可以在非交互式shell执行的脚本中直接使用 myTest2 #未被导出为环境变量的函数则提示command not found

脚本合集

判断发行版

#!/bin/bash  

os_name="undefined"
os_version="0.0"

# 检查 /etc/os-release 文件是否存在  
if [ -f /etc/os-release ]; then  
    . /etc/os-release  
    os_name=$NAME
    os_version=$VERSION
    echo "发行版: $NAME"  
    echo "版本: $VERSION"  
else  
    # 作为替代,检查其他可能的文件  
    if [ -f /etc/lsb-release ]; then  
        . /etc/lsb-release  
    os_name=$DISTRIB_ID
    os_version=$DISTRIB_RELEASE
        echo "发行版: $DISTRIB_ID"  
        echo "版本: $DISTRIB_RELEASE"  
    elif [ -f /etc/debian_version ]; then  
        os_name="Debian"
    echo "发行版:Debian"
        echo "版本未知,以下为/etc/debian_version文件内容:"
        cat /etc/debian_version    
    elif [ -f /etc/centos-release ]; then  
    os_name="CentOS"
        echo "发行版:CentOS"  
        cat /etc/centos-release  
    elif [ -f /etc/redhat-release ]; then
    os_name="redhat"
        echo "发行版:redhat或centOS"
    else        
        echo "未知发行版"  
    fi  
fi

判断系统架构

#!/bin/bash
if [[ "$(uname)" == "Linux" ]];then
    case "$(uname -m)" in
    'i386'|'i686')
     machine='32';;
    'amd64'|'x86_64')
    machine='64';;
        'armv5tel')
        machine='arm32-v5';;
      'armv6l')
        machine='arm32-v6'
        grep Features /proc/cpuinfo | grep -qw 'vfp' || machine='arm32-v5';;
      'armv7' | 'armv7l')
        machine='arm32-v7a'
        grep Features /proc/cpuinfo | grep -qw 'vfp' || machine='arm32-v5';;
      'armv8' | 'aarch64')
        machine='arm64-v8a';;
      'mips')
        machine='mips32';;
      'mipsle')
        machine='mips32le';;
      'mips64')
        machine='mips64';;
      'mips64le')
        machine='mips64le';;
      'ppc64')
        machine='ppc64';;
      'ppc64le')
        machine='ppc64le';;
      'riscv64')
        machine='riscv64';;
      's390x')
        machine='s390x';;
      *)
        echo "error: The architecture is not supported."
        exit 1;;
    esac    
fi
echo $machine
上一篇:Redhat防火墙
下一篇:Shell介绍与使用
z z z z z