一、Bash基础特性

1. 命令历史功能

命令历史的功能其实并不是Linux内核提供的,而是由shell提供的,命令为history

1.1 与命令历史相关的环境变量

用来设置history特性

  1. HISTSIZE:命令历史记录的条数
  2. HISTFILE:定义history命令历史的保存位置,默认为~/.bash_histroy
  3. HISTFILESIZE:命令历史文件记录历史的条数
  4. HISTCONTROL:命令历史的记录方式
    (1) ignoredups:忽略连续且相同的命令
    (2) ignorespace:忽略以空白字符开头的命令
    (3) ignoreboth:上述两种都有

1.2 history命令

格式:history [option]

option:
1. -d # [偏移量]:删除指定命令历史
2. -c:清空命令历史
3. #:显示最近#条命令历史
4. -r:读取历史文件并将内容追加到历史列表中
5. -w:将内存中的命令历史保存到文件中
6. -a: 手动追加当前会话缓冲区的命令历史至历史文件中

1.3 调用历史命令

1. !#:重复执行第#条指令
2. !$:重复执行上一条命令
3. ESC . :调用上一条命令的参数
4. Alt . :调用上一条命令的参数

2. 命令/路径补全

提升用户键入命令效率的功能
命令补全:如果是内部命令,因bash自带,所以可直接使用。但是外部命令就需要遍历指定路径才可以找到,这个路径是通过环境变量$PATH设置的。bash定义的路径中自左而右的在每个路径下搜寻以用户给定名称开头或全部匹配的文件名称,第一次找到的文件即为需要执行的命令。

路径补全:通过在用户指定目录下查找可匹配的文件名称,唯一则补全。

3. 命令行展开

  1. ~[USERNAME]:展开为用户的指定的家目录
  2. {,}:可承载一个以逗号分隔的列表,并将其展开为多个路径
示例:
/tmp/{a,b} 等价于 /tmp/a , /tmp/b
/tmp/{data,log}/file 等价于 /tmp/data/file , /tmp/log/file

4. 命令的执行结果状态

bash使用特殊变量$?保存最近一条执行状态结果

  1. 0:表示成功
    
  2. 1-255:表示失败
    

5. alias:命令别名

alias是shell的内建命令,与系统本身无关,需要在shell解释器下才能够运行

  1. 创建别名的命令格式alias [-p] [name[=value] ...]
(1) alias:不带任何参数的alias,显示当前shell进程中定义的命令别名
(2) \:如果不想执行alias定义的别名,要在命令前加 \
  1. 生效方式:在命令行中定义的别名,仅对当前shell进程有效,需长期有效需要定义在文件中
(1) 仅对当前用户:~/.bashrc
(2) 对所有用户有效:/etc/bashrc
  1. 撤销别名命令格式unalias [-a] name
    -a:撤销所有别名

6. glob:Globbing pathnames

使用通配符匹配机制来实现文件名通配

6.1 通配符:

1. *:任意长度的任意字符
2. ?:任意单个字符
3. []:明确指明匹配范围内的单个字符
(1) [0-9]:明确指明为匹配数字
(2) [a-z]:指明为匹配字母,不区分字符大小写
(3) [A-Z]:仅匹配大写字母
4. [^]:匹配指定范围之外的任意单个字符
5. |:或者,如 a|b 表示 a或b

6.2 专用字符集合

1. [:digit:]:任意数字
2. [:lower:]:任意小写字母
3. [:upper:]:任意大写字母
4. [:alpha:]:任意大小写字母
5. [:alnum:]:数字和字母
6. [:space:]:空格
7. [:blank:]
8. [:cntrl:]:控制符
9. [:graph:]:图形
10. [:print:]:可打印字符
11. [:punct:]:标点符号
12. [:xdigit:]:十六进制字符

7. bash快捷键

使用ctrl + KEYWORD

KEYWORD 列表:
1. l:清屏
2. a:光标跳转至命令首部
3. e:光标跳转至命令结尾
4. c:取消命令执行
5. u:删除光标之前的内容
6. k:删除包括光标所在处至命令行尾部的所有内容

8. I/O重定向及管道

用来指定程序的数据来源于何处以及程序的结果应该出入至何处

8.1 标准I/O

  1. 标准输入:默认位置为keyborad,文件描述符为0,stdin
  2. 标准输出:默认位置为monitor,文件描述符为1,stdout
  3. 标准错误输出:默认位置为monitor,文件描述符为2,stderr

8.2 输出重定向

格式:COMMAND > NEW_POS ,COMMAND >> NEW_POS

1. >:覆盖重定向,意为目标文件中原有内容会被清除
2. >>: 追加重定向,意为把新数据追加在文件尾部
3. 2>:错误覆盖重定向;
4. 2>>:追加重定向错误输出。
5. &>:正确与错误都覆盖输出;
6. &>>:正确与错误都追加输出;

8.3 输入重定向

1. <:改变命令的数据来源为文件。
2. <<:此处文档功能(HERE Documentation):
使用格式为:cat >> /path/somewhere << EOF

8.4 重定向相关设置

1. set <+|->C:设置是否可对已存在文件执行覆盖操作,-c为禁止,只对当前shell进程有效。
2. >|:如果启用了覆盖保护,可使用此符号来完成强制覆盖

8.5 管道

1. |:可以将上一个命令的输出信息,送给后一个命令的输入,实现组合多个小程序完成复杂任务  \\
需要注意:最后一个命令会在当前shell的子shell运行

9. 命令hash

运行过的命令,首先通过查找hash列表,不必搜索$PATH环境变量。如果命令在被hash后路径改变,就需要重新hash,不然路径已然出错。

9.1 hash管理命令

- hash:列出hash列表
- hash -d COMMAND:删除某命令hash结果。
- hash -r:清空命令hash列表。

二、bash的配置文件

配置文件的功用:通过它来指定我们长期需要但是默认没有启用的功能,bash shell也是拥有配置文件的,并且生效次序及范围也不同,可以从多个角度理解配置文件,。

1. 按生效范围划分

  • 全局生效配置文件:
    /etc/profile:
    /etc/profile.d/*.sh:如果有些应用程序配置文件过大时,可以将一个配置文件分割成多个片段,每个片段用来保存一部分配置
    /etc/bashrc:
  • 仅对用户个人生效配置文件:
    ~/.bash_profile
    ~/.bashrc:
    ~/.bash_profile:

2. 按功能划分的配置文件

  • profile类/etc/profile、/etc/profile.d/*.sh、~/.bash_profile
    • 为交互式登录的进程提供配置(通过终端输入账号密码、su - username命令登录的即为交互式登录)
    • 定义环境变量
    • 运行命令或脚本
  • bashrc类/etc/bashrc、~/.bashrc
    • 为非交互式登录的shell提供配置(使用su username、图形界面打开的终端、脚本执行过程中的登录,即为非交互式登录)
    • 定义命令别名
    • 定义本地变量

注:交互式登录与非交互式登录用的文件并没有严格的规定,也就是说交互式登录的shell有可能会用到bashrc类文件中的配置,非交互式登录的shell也有可能会用到profile类文件中的配置。

3. 配置文件的生效次序

  • 交互式登录的shell:/etc/profile --> /etc/profile.d/*.sh --> ~/.bash_profile --> ~/.bashrc --> /etc/bashrc
  • 非交互式:~/.bashrc --> /etc/bashrc --> /etc/profile.d/*.sh
  • 立即生效:定义在配置文件中的设置如果想立即生效,需手动执行以下命令,2选1即可
    • ~]# source /PATH/FILE
    • ~]# . /PATH/FILE

对于同一个变量,如果定义在多个配置文件中,那么读取次序越靠后的配置将生效


三、bash脚本编程

大多数的shell编程语言都是通过调用当前系统上的二进制命令,组织罗列、解释执行的。

1. 简单的介绍下编程语言的分类

如果想理解bash脚本编程,需要先了解以下前置的知识点。

1.1 编程模型

程序是由 指令 + 数据 组成,目前来讲,编程语言有两种风格,其一为过程式编程,也叫面向过程的编程。另一种是面向对象的编程,也称为对象式编程。两种的不同之处如下:
(1)过程式编程语言:程序编写以指令为中心,只需要考虑指令是如何运行的,而后根据指令的需要来决定数据如何存储
(2)面向对象的编程语言:以数据为中心,围绕数据来组织指令
这一段比较抽象,结合现实生活的例子,分享下我自己的理解:
例子1(过程式编程语言):接到一个任务,需要把货物运送到A点,在过程式编程语言中,假如指令和数据分别对应运送方式和货物,那么首先我们就先需要设定运送方式,可使用货车、飞机等等,也就是说我们需要先确定运送方式(指令),具体货物大小、重量(数据)我都不管,等运送方式确定了后,在将货物按照这个运送方式去码放。也就是数据服务于指令。
例子2(对象式编程语言):同样运送货物,我需要先确定货物在我的仓库中如何存放更方便,比如按重量、温度等等。等货物的存放方式确定好了。在确定如何运送,比如冷藏运送、空运等等。这里运送方式就是服务于货物了
当然不同的运送方式最终到达终点的速度也不一样,就像是不同的指令算法之间的差异

1.2 程序的执行方式

对于计算机而言,它只能理解二进制执行,而对于人来讲,阅读并理解二进制指令是极为不易的,所以后来就出现了专门的编程语言,这种编程语言离二进制指令更近,但仍然需要转换。高级语言,离人类的思维方式更近。容易掌握。但是不管是高级语言还是低级语言,它都无法在机器上直接运行,都需要一种工具,可以把编程语言转换为机器可以理解的二进制语言。
(1)编译执行:编译器把整个源代码完完全全转换为二进制指令,而后再能运行,运行程序时编译器是不参与的
(2)解释执行:运行时启动一个叫解释器的程序,由解释器通篇做语法分析后,解释一句,执行一句

2.shell脚本的格式

因为cpu只能运行二进制程序,但shell脚本是纯文本文件,不能直接在cpu上运行,这时就需要调用一个翻译,也就是在脚本起始就注明需要调用的解释器,这个机制我们叫做shebang,比如本文所讲的shell脚本,就需要在脚本文件起始处,顶格写明#!/bin/bash,#!为固定格式,这就向cpu表明本脚需要调用解释器,它的绝对路径是/bin/bash(需要注意的是解释器一定要是二进制程序)。在或者,如果你写了一个python脚本,就需要在顶格注明解释器位置,如#!/usr/bin/python。下面示例一个简单的脚本文件:

#!/bin/bash
cat /etc/fstab
wc -l /etc/fstab

脚本运行前需要给执行权限,如果不想给执行权限也可以调用bash命令。

3. 变量:命名的内存空间

变量就是在程序运行中,划给他使用的一段命名内存空间,使用时需要注意变量存储的数据类型,大小

3.1 变量命名规则

  • 不能使用程序中的保留字
  • 只能使用数字、字母及下划线且不能以数字开头
  • 尽量做到见名知意,推荐驼峰法则
  • 尽量不要使用全大写字母,因可能会与内建变量冲突

3.2 变量类型的影响

变量类型对于变量来说尤为关键,例如字符123和数值123,它们是不同的。因为计算机最小的存储单元是字节,一个字节最多可以表示256种变化,那么字符123就需要用3个字节(1用一个字节、2用一个字节、3用一个字节),那么假如123是数值,那么就需要1个字节就可以表示了,因为1个字节256种变化,已经覆盖了数值123。那么问题来了,如果你用字符123加上数值123,结果会是什么呢?
编程语言分为强类型编程语言和弱类型编程语言,对于强类型编程语言,如果计算123+123,那么两个数据都必须是数值型才可以,shell属于弱类型编程语言,它会自动将两个数据转换为字符型,帮你计算结果。但是计算出的结果是123123,它只是将字符串拼接了。

3.3 shell中变量种类

根据变量的生效范围分类

3.3.1本地变量

无需事先声明,可以直接使用,但不声明则为空,生效范围为当前shell进程,等当前shell进程终止就被自动撤销了

  • 变量赋值:
    • 直接字串赋值var=VALUE
    • 调用变量赋值:var=‘$vaule’var="$VALUE"双引号为弱引用,变量会被替换为变量中存储的值,强引用会保持原来的子串
    • 命令执行结果赋值:var=`COMMOND` var=$(COMMOND),通过反引号或$()来赋值
  • 变量引用:${name}$name:如果在代码段中,变量名称与其他代码相连,为了区别变量名称和代码段,可以使用一对花括号来区分出来变量名称和代码段,如无此种情况,则花括号可以省略
  • 显示变量:[root@node1 ~]# set
  • 撤销变量:[root@node1 ~]# unset VAR_NAME,撤销时仅需指定变量名称,不需加$

3.3.2 环境变量

生效范围为当前shell进程及其子进程

  • 变量赋值:
    • export name=value
    • declare -x name=value:声明环境变量。
    • declare -i name=value:声明整形数据。
  • 显示环境变量:
    declare -x、export、env、printenv
  • 撤销环境变量、引用环境变量与本地变量方法相同
  • 内建的环境变量:bash中有许多内建的环境变量,使用时需要避免覆盖
    $PATH、$SHELL、$UID、$HISTSIZE、$HOME、$PWD、$OLD、$HISTFILE...

3.3.3 局部变量

生效范围为当前shell进程中某代码片段,在shell中通常指函数片段

3.3.4 位置变量

用于让脚本在运行时,调用命令行传递给脚本的参数

  • $1 、$2、$3...
  • 示例脚本如何调用
#!/bin/bash
echo $1
echo $2
[root@node1 ~]# ./test.sh 1 2
1
2
  • shift:在脚本文件中,如果写入shift,意为把$1踢掉,后面的自动补上

3.3.5 特殊变量

bash shell内置的,用来保存特殊数据的

  • $?:保存上一条命令的执行返回状态码
  • $0:命令本身的值,也可以理解为是位置变量
  • $#:传递给脚本参数的个数
  • $*:传递给脚本的所有参数,把所有参数当做一个字符串
  • $@:传递给脚本的所有参数,把每个参数当独立的字符串

3.3.6 只读变量

只读变量不区分本地变量还是环境变量,只是为了定义完后,它的值不可以发生改变,它的生命周期就是当前shell进程结束为止。

  • 赋值方式:
    • [root@node1 ~]# readonly VAR_NAME
    • [root@node1 ~]# declare -r VAR_NAME

4. 算数运算相关

4.1 加减乘除

  • let var=算数表达式:使用let 后面跟一个变量名称,=号后面直接跟算数运算表达式,这要算数结果就会被保存在变量var中
    示例: let var=1+3或let var=$num1+$num2,获取结果echo $var
  • var=$[算数表达式]:=号后面跟一个$符+一对中括号,算数表达式写在中括号中,这种方式引用起来比较灵活
    示例: var=$[$num1+$num2]或echo $[$num1+$num2]
  • var=$((算数表达式))$符后面跟两对小括号,$(($num1+$num2))
  • var=$(expr arg1 arg2 arg3…):expr为固定命令,arg1为第一个数字,arg2为算数符号,arg3位第二个数字。
    示例:var=$(expr 1 + 3) 或 var=$(expr 2 \* 3),乘号在需要转义。

4.2 增强型赋值用法

  • count=$[$count+1]等于let count+=1
  • count=$[$count+1]等于let count++

5. 条件测试

5.1 测试命令的格式

  • test EXPRESSION:使用test命令,后面跟上需要测试的表达式
  • [ EXPRESSION ]:使用一对中括号,中括号两端需要有空格,这个是测试命令
  • [[ EXPRESSION ]]:使用两对中扩号,中括号两端需要有空格,这个是bash内建的保留字
    示例:test 1 -gt 3、[ 1 -gt 3 ]、[[ 1 -gt 3 ]]

5.2 测试类型

5.2.1 数值测试

[ NUM1 -gt NUM2 ]:大于
[ NUM1 -ge NUM2 ]:大于等于
[ NUM1 -eq NUM2 ]:等于
[ NUM1 -ne NUM2 ]:不等于
[ NUM1 -lt NUM2 ]:小于
[ NUM1 -le NUM2 ]:小于等于

5.2.2 字符串测试

[ $VAR1 == $VAR2 ][ $VAR1 = $VAR2 ]:是否相同
[ $VAR1 > $VAR2 ]:阿斯克码比较
[ $VAR1 < $VAR2 ]:阿斯克码比较
[ $VAR1! = $VAR2 ]:不等于
[ $VAR1 =~ PAT ]:左侧字符串是否能被右侧的模式所匹配
[ -z $VAR ]:测试字串是否为空,空位真
[ -n $name ]:测试字串是否为不空,不空为真
字符串比较都需要使用引号

5.2.3 文件测试

  • 文件存在测试
    [ -a FILE ]:测试文件是否存在,存在为真
    [ -e FILE ]:测试文件是否存在,存在为真
  • 存在及类别测试:两个条件需同是满足
    [ -b FILE ]:文件存在且为块设备文件
    [ -c FILE ]:文件存在且为字符设备文件
    [ -d FILE ]:文件存在且为目录文件
    [ -f FILE ]:文件存在且为普通文件
    [ -p FILE ]:存在且为命名管道文件
    [ -S FILE ]:存在且为套接字文件(大写S)
    [ -L FILE ][ -h FILE ]:存在且为符号链接文件
  • 文件权限测试:在脚本中,当前用户为执行脚本的用户
    [ -r FILE ]:当前用户是否有读权限
    [ -w FILE ]:当前用户是否有写权限
    [ -x FILE ]:当前用户是否有执行权限
  • 特殊权限测试
    [ -g FILE ]:文件存在且被设置了sgid
    [ -u FILE ]:文件存在且被设置了suid
    [ -k FILE ]:文件存在且拥有sticky权限
  • 文件属性测试
    [ -s FILE ]:文件是否存在且非空(小写s)
    [ -N FILE ]:文件自从上一次被读取后,是否被修改过
    [ -O FILE ]:当前用户是否为文件的属主
    [ -G FILE ]:当前用户是否为文件的属组
  • 文件是否打开测试
    [ -t fd ]:fd表示文件描述符是否已经打开且与某终端相关
  • 双目测试:
    [ file1 -ef file2 ]:文件是否指向同一个设备上相同的inode
    [ file1 -nt file2 ]:文件1是否新与file2
    [ file1 -ot file2 ]:文件1是否旧与file2

5.3 组合测试

  • [ EXPRESSION1 -a EXPRESSION2 ]:等同于[ EXP1 ] && [ EXP2 ]
  • [ EXPRESSION1 -o EXPRESSION2 ]:等同于[ EXP1 ] || [ EXP2 ]

6. 脚本中的流程控制语句

6.1 if语句

通过罗列条件来执行代码,在多分支的if语句中,条件自上至下逐条进行判断,当条件满足时,则直接执行该分支代码,完毕后退出,后续分支将不会执行

6.1.1 格式及示例

  • 单分支的if语句
if 条件判断; then
条件为真的执行代码
fi
if [ 1 -eq 2 ]; then
echo 1
fi
  • 双分支的if语句:
if  条件判断; then
条件为真的执行代码
else
条件为假的分支代码
fi
if [ 1 -eq 2 ]; then
echo 1
else
echo 2
fi
  • 多分支的if语句:
if  条件判断; then
条件为真的执行代码
elif 条件判断; then
条件为真的执行代码
elif 条件判断; then
条件为真的执行代码
else
都不满足时执行的代码
fi
if [ $num -eq 2 ]; then
echo 1
elif [ $num -lt 2 ]; then
echo 2
elif [ $num -gt 2 ]; then
echo 3
else
条件为假的分支代码
fi

6.2 case语句

  • 格式及示例
case 变量引用 in
pat1)
分支一
;;
pat2)
分支二
;;
……
*)
默认分支
;;
esac
#!/bin/bash
#
read -p 'How old are you?' year
case $year in
20)
echo 'younger man'
;;
30)
echo 'not younger man'
;;
40)
echo 'old man'
;;
*)
echo 'ok,I know.'
;;
esac

NOTE:pat支持golb机制,不支持正则表达式

6.3 for循环语句

执行的机制是将列表中的元素依次赋值给变量,每赋值一次就执行一遍循环体,直到列表中的元素耗尽

  • 格式及示例
for 变量名 in 列表;do
循环体
done
#!/bin/bash
#
for i in {1..254}; do
ping -w 2 192.168.1.$i &> /dev/null
done
  • 列表的几种生成方式
    直接给出值:arg1 arg2 arg3 …
    整数列表:{start..end}
    整数列表:$(seq 1 2 100):开始、增量、结束
    命令结果:$(command)
    glob机制:/etc/rc.d/rc3.d/K*

  • 特殊格式的for循环用法:用两个小括号包含着变量的初始值、进入循环的条件及变量修正表达式,其中变量初始化操作只在变量进入时执行一次,变量修正表达式会在整个代码段执行完毕后就修正一次,接着在判断进入条件

for循环特殊用法格式
for ((控制变量初始化;条件判断表达式;控制变量的修正表达式));do
循环体
done

-----------分割线------------

for循环特殊用法示例:求100以内整数和
#!/bin/bash
#
sum=0
for ((i=1;$i<=100;i++)); do
let sum+=$i
done
echo $sum

6.4 while循环语句

  • 格式及示例
while  循环控制表达式; do
循环体
done

循环控制表达式条件为true,则执行一次循环
求一百以内偶数之和
#!/bin/bash
#
i=2
sum=0
while [ $i -lt 101 ]; do
sum=$(($sum + $i))
let i+=2
done
echo $sum
  • 特殊格式的while循环用法:这个特殊格式的循环表示会一次从文件中读取一行信息赋值给line变量(变量名称可以自定义,此处只是为了见名知意),在循环体中对line作出处理,当文件全部行读取完毕后则循环结束
while特殊用法格式
while read line;do
循环体
done < /path/form/somefile

-----------分割线------------

while特殊用法示例:显示uid为偶数的用户
#!/bin/bash

while read line; do
uid=$(echo $line | cut -d: -f3)
if [ $[$uid%2] -eq 0 ]; then
echo $line | cut -d: -f1,3
fi
done < /etc/passwd

6.5 until循环语句

  • 格式及示例
until  循环控制表达式; do
循环体
done

循环控制表达式条件为false,则执行一次循环
计算100以内正整数之和
#!/bin/bash

num=1
i=2
until [ $i -eq 101 ]; do
num=$(($i+$num))
let i++
done
echo $num

7. 脚本控制相关参数及命令

7.1 循环控制语句

  • continue [N]:用于在循环中,一般通过循环片段中的判断引用,如满足某个条件时做continue,也就是提前结束本次循环。在多层循环嵌套语句中,使用continue后跟一个数字,表示由continue所在循环语句向外数,提前结束嵌套的几层循环
    示例:
while 循环控制表达式;do
CMD1
...
if 条件判断;then
continue
fi
CMD2
done

正常进入while语句,当执行至if语句时做条件判断,如条件满足则做continue,表示结束本圈
循环
  • break [N]:同continue,通常借助条件判断做循环控制,同样可接受一个数字表示退出几层循环,但break表示提前结束循环片段,而不是结束本圈循环

示例:

while 循环控制表达式;do
CMD1
...
if 条件判断;then
break
fi
CMD2
done

正常进入while语句,当执行至if语句时做条件判断,如条件满足则做break,表示结束循环提前结束本循环片段

7.2 read命令

可以将用户输入的变量对位保存,用于在脚本中与用户交互
格式:read [option] var1_name var2_name...

option
-p '提示信息':使用-p选项告知用户此处需要输入的信息格式
-t #:设置一个超时时间
-a:赋值数组

注:read命令后跟几个变量名称,那么就需要用户提供几个用空格分隔的变量值,假设用户提供的变量值不满足个数要求,那么后面的变量就会为空,再则,如果用户提供的值多于变量值的个数,那么多出来的变量值会被统一赋值给最后一个变量

7.3 bash命令

格式:bash [option] script.sh

option
-n:检查语法错误
-x:单步执行
--version:版本

8. 函数

函数的意义就是将一段为一段具有特定功能的代码段取一个名字,随后可以在任何的代码中片段直接使用名字来引用对应的函数,是代码重用重要组件

8.1 函数定义语法结构

  • 语法结构一:使用function关键字,后面跟函数名称,中间用一对花括号将代码片段包含
function f_name {
……函数体……
}
  • 语法结构二:使用函数名称根一对小括号,后面同样使用一对花括号将代码片段包含
f_name() {
……函数体……
}

tips:函数只有被调用才能执行,在代码片段中给定函数名即为调用。其会自动将函数名称替换为函数代码,有些函数还会带参数

8.2 函数的生命周期

在代码段中被调用时意为着函数代码被创建,使用reture命令或函数代码段最后一条命令执行完成即返回

  • return命令:可自定义函数返回状态结果,取值范围为0-255之间的数字,0表示成功,1-255表示失败,如果没使用reture做显示返回,那么函数体最后一个执行的状态结果即为整个函数的状态结果

8.3 函数返回值

函数的执行结果返回值
  • 使用echo或print命令进行输出
  • 函数体中调用命令的执行结果
退出状态码
  • 默认取决于函数体中最后一条命令的退出状态码,如需自定义需要使用reture命令

8.4 向函数传递参数

调用函数时,在函数名后面使用空白分割参数列表即可,在函数体中可使用$1、$2…调用位置变量,还可以使用$@,$*,$#,特殊变量,需要注意在代码中引用函数使用的变量,并不是运行脚本时传递的参数,而是在代码段中传递给函数的参数

示例:添加10个用户
#!/bin/bash
#

adduser () {
if id $1 &> /dev/null; then
echo "user $i is exist"
return 1
else
useradd $1 && echo "user $1 add finished."
return 0
fi
}

for i in {1..10}; do
adduser user$i
done

8.5 局部变量

局部变量的生命周期同函数一致,因函数创建而创建,因函数结束而结束。如果局部变量名同本地变量,需要使用local NAME=value来声明使得两个变量互不影响

#!/bin/bash
i=1
local_var() {
local i=1
let i++
echo "function: $i"
}

local_var
echo "script: $i"
[root@centos6 sh]# bash local_var.sh
function: 2
script: 1

8.6 函数递归

函数直接或间接的调用自身的实现方式,叫做函数递归。常见的递归实现场景如阶乘运算、斐波那契数列

fff() {
if [ $1 -eq 1 ]; then
echo 0
elif [ $1 -eq 2 ]; then
echo 1
else
echo "$[$(fff $[$1-1])+$(fff $[$1-2])]"
fi
}
read -p 'enter:' num
fff $num

9. 数组

数组可以将多个具有相似属性的元素组织在一起存储在连续的内存空间中,使用一个名称进行调用

9.1 数组的调用方式

数组其实就是多个连续的、独立的内存空间,每一个内存空间在数组中相当于一个变量,但是我们不需要给它单独的名字,而是通过数组名称+下标的方式进行引用,索引编号从0开始,标准格式为 ${arrary_name [index]}

bash的数组是支持稀疏格式的,比如0号元素、1号元素、4号元素都有值,而2号、3号都没值,这种不连续的

  • arrary_name:数组名称
  • [index]:数组中可能存在多个元素,使用索引(下标)[index]进行区分引用,下标从0开始,省略[index]表示引用下标为0的元素

9.2 数组的特殊的调用方式

${#ARRAY_NAME[*]}或${#ARRAY_NAME[@]}:表示统计属组中的元素个数
${ARRAY_NAME[@]}或${ARRAY_NAME[*]}:表示引用属组中的所有元素
${ARRAY_NAME[@]:offset:number}:数组切片
offset:表示跳过元素个数
number:表示要取出的元素个数,number省略表示偏移量之后的所有元素

9.3 数组的声明方式

  • declare -a ARRAY_NAME
  • declare -A ARRAY_NAME:此种方式定义的数组为关联数组,普通数组的索引标号是从0开始做数值索引,但是关联数组的索引编号是自定义的,不仅仅是数值格式,需要注意的是,bash程序4.0版本之前是不支持关联数组的。在其他编程语言中关联数组也被称为键值列表

定义关联数组,其索引名称(下标)不像数值索引一样有序排列,定义方式需要先声明关联数组,接着定义各个索引名称

  • 第一步:declare -A ARRAY_NAME
  • 第二步:ARRAY_NAME=([index_name1]=’val1’ [index_name2]=’val2’ …)

9.4 数组元素的赋值方式

ARRAY_NAME[INDEX]="VALUE":一次只赋值一个元素,使用数组名称加索引名称即可。如weekdays[0]="Sunday"
ARRAY_NAME=("VAL1" "VAL2"…):一次赋值多个元素,使用一对小括号进行赋值,每个值之间使用空格分隔,数组会自动给定索引号。如weekdays=("Thursday" "Sunday")
ARRAY_NAME=([0]="VAL1" [3]="VAL2"…):只赋值特定元素
ARRAY_NAME[${#ARRAY[*]}]:向数组中追加元素,需要用数组的长度作为下标
unset ARRAY_NAME[index]:删除数组中的元素
read -a ARRAY_NAME:用户交互方式赋值

10. 字符串处理工具

10.1 字符切片

对变量中的字符串做切片

  • 用法一:${var:offset:number}
    var:变量名称
    offset:偏移量,跳过几个字符
    number:取几个
  • 用法二:${var: -#}
    -#:表示取最右侧#个字符,冒号后面必须空格

10.2 基于模式取子串

  • 用法一:${var#*word}${var##*word}
    word可以使用任意字符,表示删除自左向右第一次出现word之间的所有内容,包括word字符,如果是两个#,表示删除自左向右最后一次出现word之间的字符,包括word字符
  • 用法二:${var%word*}${var%%word*},用法同上,但表示从右向左

10.3 查找替换:支持glob

  • 用法一:${var/pattern/substi}${var//pattern/substi}
    查找var表示的字符串中,第一次被pat匹配的所有字符串,用sub替换,双斜线表示所有查找到的都替换
  • 用法二:${var/#pattern/substi}${var/%pattern/substi}
    #:用法同上,锚定行首
    %:用法同上,锚定行尾

10.4 查找删除

  • 用法一:${var/pattern}${var//pattern}
    删除第一次被pat所匹配到的字符,双斜线表示删除所有被pat匹配到的字符
  • 用法二:${var/#pattern} ${var/%pattern}
    #:表示删除行首出现的pat
    %:表示删除行尾出现的pat

10.5 字符大小写转换

  • 用法:${var^^}${var,,}
    ^^:表示把var中所有字符装换成大写
    ,,:表示把var中所有字符装换成小写

10.6 变量赋值

  • 用法:${var:-value}${var:=value}${var:+value}${var:?err_info}
    -:表示var为空或没有赋值,则直接将value返回给var,但并不赋值
    =:表示var为空或没有赋值,则直接将value返回给var,但赋值
    +:表示var不空或没有赋值,则直接将value返回给var,但不赋值
    ?:表示var为空或没有赋值,则直接将返回错误信息,否则返回var值

10.7 输出信息着色

使用echo命令为将输出信息进行着色,可以修改输出信息的前景色、背景色、字体,分别使用多组数字进行控制,固定语法为\033[## word \033[0m,其中前面的\033[##和后面的\033[0m为开始及结束固定语法,中间包含着需要着色的字体即可,开始及结束之间不需要空格,这里的的空格只是为了能看清格式

  • \033[##m:两个#号可以替换为数字,也可以只有一个数字
    第一个#:3为前景色,四为背景色、一个#号时表示字体
    第二个#:取值范围是1到7,作用为控制颜色
    单独一个#:表示控制输出字体风格
#!/bin/bash
# 颜色应用脚本示例,结合echo命令使用需要加-e选项
echo -e "\033[31mhello\033[0m 31m | \033[41mhello\033[0m 41m | \033[1mhello\033[0m 1m"
echo -e "\033[32mhello\033[0m 32m | \033[42mhello\033[0m 42m | \033[2mhello\033[0m 2m"
echo -e "\033[33mhello\033[0m 33m | \033[43mhello\033[0m 43m | \033[3mhello\033[0m 3m"
echo -e "\033[34mhello\033[0m 34m | \033[44mhello\033[0m 44m | \033[4mhello\033[0m 4m"
echo -e "\033[35mhello\033[0m 35m | \033[45mhello\033[0m 45m | \033[5mhello\033[0m 5m"
echo -e "\033[36mhello\033[0m 36m | \033[46mhello\033[0m 46m | \033[6mhello\033[0m 6m"
echo -e "\033[37mhello\033[0m 37m | \033[47mhello\033[0m 47m | \033[7mhello\033[0m 7m"

上方脚本输出信息

  • 多种颜色设置组合
#!/bin/bash
多种颜色组合需要在数字之间加入分号
echo -e "\033[31;42;6mhello\033[0m 31;42;6m"
echo -e "\033[35;4mhello\033[0m 35;4m"
echo -e "\033[37;45mhello\033[0m 37;45m"
echo -e "\033[41;6mhello\033[0m 41;6m"

上方脚本实现效果

10.8 为脚本提供配置文件

定义文本文件,声明参数和值,在脚本中source此文件即可

10.9 自定义退出状态码

exit [n]:脚本中一旦遇到exit命令,脚本会立即终止

四、练习

  1. 显示/var/目录下所有以l开头,以一个小写字母结尾,且中间出现至少一位数字的文件或目录
    [root@node1 ~]# ls -d /var/l*[0-9]*[[:lower:]]
  2. 显示/etc/目录下,以任意一位数字开头,且以非数字结尾的文件或目录;
    [root@node1 ~]# ls -d /etc/[0-9]*[^0-9]
  3. 显示/etc/目录下,以非字母开头,后面跟了一个字母及其他任意长度的文件或目录
    [root@node1 ~]# ls -d /etc/[^[:alpha:]][[:alpha:]]*
  4. 复制/etc/目录下,所有以m开头,以非数字结尾的文件或目录至/tmp目录下
    [root@node1 ~]# cp -r /etc/m*[^0-9] /tmp
  5. 复制/etc/目录下,所有以.d结尾的文件目录至/tmp目录中
    [root@node1 ~]# cp -r /etc/*.d /tmp
  6. 复制/etc/目录下,所有以.conf结尾,且以m、n、r、p开头的文件或目录至/tmp目录
    [root@node1 ~]# cp -r /etc/[mnrp]*.conf /tmp
  7. 脚本:计算/etc/passwd文件中第10个和第20个用户的id之和
#!/bin/bash
user10=$(head -10 /etc/passwd | tail -1 | cut -d':' -f3)
user20=$(head -20 /etc/passwd | tail -1 | cut -d':' -f3)
echo "$(($user10+$user20))"
  1. 脚本:计算两个文件所有空白行之和
#!/bin/bash
linenum1=$(grep "^$" $1 | wc -l | cut -d' ' -f1)
linenum2=$(grep "^$" $2 | wc -l | cut -d' ' -f1)
echo "$(($linenum1+$linenum2))"
  1. 脚本:统计/etc/,/var/,/usr/目录共有多少个以及子目录和文件
#!/bin/bash
etcnum=$(ls /etc/ | wc -l)
varnum=$(ls /var/ | wc -l)
usrnum=$(ls /usr/ | wc -l)
echo "$(($etcnum+$varnum+$usrnum))"
  1. 脚本:接受一个文件路径作为参数,如果参数个数小于一,则提示用户,至少应该给一个参数,并立即退出,如果参数不小于一,则显示第一个参数所指向的文件中的空白行数
#!/bin/bash
[ $# -lt 1 ] && echo 'please input somepath' && exit 1 || grep '^$' $1| wc -l
  1. 脚本:读取/etc/rc.d/rc3.d/目录下的文件,如果以K开头,输出文件名 stop。以s开头输出文件名 start
#!/bin/bash
#
for name in $(ls /etc/rc.d/rc3.d); do
if [[ $name =~ ^K ]]; then
echo "$name stop"
else [[ $name =~ ^S ]]
echo "$name start"
fi
done
  1. 脚本:ping检测主机在线状态
#!/bin/bash
#
for i in $(seq 1 1 50); do
ping -w 1 192.168.80.$i &> /dev/null
if [ $? -eq 0 ]; then
echo "192.168.80.$i is online"
else
echo "192.168.80.$i is down"
fi
done
  1. 脚本:求100以内所有正整数之和
#!/bin/bash
#
declare -i i=1
declare -i sum=0
while [ $i -le 100 ]; do
let sum+=$i
let i++
done
echo $i
echo "sum=$sum"
  1. 脚本:求100以内所有正偶数之和
#!/bin/bash
#
declare -i i=2
declare -i sum=0
while [ $i -le 100 ]; do
let sum+=$i
let i+=2
done
echo $i
echo "sum=$sum"
  1. 脚本:while循环测试主机在线状态
#!/bin/bash
#
declare -i i=1
declare -i online=0
declare -i down=0
host='192.168.80.'
while [ $i -lt 20 ]; do
ping -w 2 ${host}$i &> /dev/null
if [ $? -eq 0 ]; then
let online++ && echo "${host}$i is online"
else
let down++ && echo "${host}$i in down "
fi
let i++
done
echo "online host: $online"
echo "down host: $down"
echo "$i"
  1. 脚本:打印九九乘法表
#!/bin/bash
#

declare -i i=1
declare -i o=1
while [ $i -le 9 ]; do
while [ $o -le $i -a $o -le 9 ];do
echo -e -n "$o x $i = $[ $o * $i ]\t"
let o++
done
echo
let o=1
let i++
done
echo
#!/bin/bash
#
declare -i i=1
declare -i o=1
for i in {1..9};do
for o in $(seq 1 1 $i);do
echo -e -n "$o x $i = $[$i*$o]\t"
done
echo
done
#!/bin/bash
#

declare -i i=1
declare -i o=1
until [ $i -eq 10 ];do
until [ $o -gt $i -o $o -eq 10 ]; do
echo -e -n "$o x $i = $[$i*$o]\t"
let o++
done
echo
let o=1
let i++
done
#!/bin/bash
#

99cf() {
for i in $(seq 1 1 $1); do
for o in $(seq 1 1 $i);do
echo -e -n "$o x $i = $[$i*$o]\t"
done
echo
done
}
99cf $1
  1. 脚本:利用RANDOM生成10个随机数字,输出这10个数字,并显示其中的最大和最小值
#!/bin/bash
#
declare -i i=1
declare -i min=60000
declare -i max=0
while [ $i -le 10 ]; do
declare -i o=$RANDOM
if [ $o -gt $max ];then
max=$o
fi
if [ $o -lt $min ];then
min=$o
fi
echo "num$i=$o"
let i++
done
echo "random min = $min"
echo "random max = $max"
  1. 脚本:用until求100以内所有正整数之和
#!/bin/bash
#
declare -i i=0
declare -i sum=0
until [ $i -gt 100 ]; do
let sum+=$i
let i++
done

echo "$sum"
  1. 找出ID号为偶数的所有用户,显示其用户名及ID号
#!/bin/bash

while read count; do
name=$(echo $count | cut -d':' -f1)
num=$(echo $count | cut -d':' -f3)
[ $[$num%2] -eq 0 ] && echo "$name: $num"
done < /etc/passwd
  1. 写一个脚本,完成如些任务
  • 显示如下菜单:
    cpu) show cpu information;
    mem) show memory information;
    disk) show disk information;
    quit) quit
  • 提示用户选项
  • 显示用户选择内容,只有用户输入quit才执行退出
#!/bin/bash

while true; do
echo "=============================="
echo "cpu) show cpu information; |"
echo "mem) show memory information;|"
echo "disk) show disk information; |"
echo "quit) quit |"
echo "=============================="
read -p 'Please enter a key:' key
if [ $key == cpu ]; then
lscpu
elif [ $key == 'mem' ]; then
free -m
elif [ $key == 'disk' ]; then
fdisk -l
elif [ $key == 'quit' ]; then
echo "Bye."
break
else
echo
echo "please enter a true key."
fi
done

case语句

#!/bin/bash

while true; do
echo "=============================="
echo "cpu) show cpu information; |"
echo "mem) show memory information;|"
echo "disk) show disk information; |"
echo "quit) quit |"
echo "=============================="
read -p 'Please enter a key:' key
case $key in
cpu)
lscpu
;;
mem)
free -m
;;
disk)
fdisk -l
;;
quit)
echo "Bye."
break
;;
*)
echo
echo "please enter a true key."
;;
esac
done
  1. 写一个脚本,完成如下要求
    (1) 脚本可以接受参数:start,stop,restart,status
    (2) 如果参数非这四个,则报错退出
    (3) 如果参数是start:则创建/var/lock/subsys/脚本名。并显示启动成功,如果处于启动中则显示is running
    (4) 如果stop,则删除/var/lock/subsys/脚本名,并显示停止完成,如本来就处于停止态,如何处理
    (5) 如果是restart,则先stop,在start,考虑如果事先没有启动,如何处理
    (6) 如果是status,假如文件存在则显示服务正在运行,如果不存在则显示服务stop
#!/bin/bash
#

server_name=$(echo $0 | egrep -o "[^/]+/?$" | cut -d/ -f1)

if [ -a /var/lock/subsys/$server_name ]; then
state=0
else
state=1
fi

file=/var/lock/subsys/$server_name

case $1 in
start)
[ $state -eq 0 ] && echo "$server_name is running." && exit 0
touch $file && echo "$server_name is start ok"
;;
stop)
[ $state -ne 0 ] && echo "$server_name is not running." && exit 0
rm -f $file && echo "$server_name is stop ok"
;;
restart)
if [ $state -ne 0 ]; then
echo "$server_name is not running."
touch $file && echo "$server_name is start ok."
else
echo "$server_name is stop ok." && rm -f $file
touch $file && echo "$server_name is restart ok."
fi
;;
status)
[ $state -ne 0 ] && echo "$server_name is stopped...." && exit 0
echo "$server_name is running...."
;;
*)
echo "$server_name: invalid option $1"
echo "please use [ start | stop | restart | status ]"
;;
esac
  1. 生成10个随机数保存于数组中,并找出其最大值和最小值
#!/bin/bash
#

declare -a oushu
max=0
mini=40000

for i in {0..9}; do
oushu[$i]=$RANDOM
echo "$i: ${oushu[$i]}"
echo
if [ ${oushu[$i]} -gt $max ]; then
max=${oushu[$i]}
fi
if [ ${oushu[$i]} -lt $mini ]; then
mini=${oushu[$i]}
fi
done
echo "max: $max"
echo "mini: $mini"
#!/bin/bash
#

declare -a rand
declare -i max=0
declare -i min=40000

for i in {0..9}; do
rand[$i]=$RANDOM
echo ${rand[$i]}
[ ${rand[$i]} -gt $max ] && max=${rand[$i]}
[ ${rand[$i]} -lt $min ] && min=${rand[$i]}
done

echo "max: $max"
echo "min: $min"
  1. 定义一个脚本,数组中的元素是/var/log/目录下所有以.log结尾的文件,要求统计其下标为偶数文件中行数之和
#!/bin/bash
#

declare -i q=0
declare -i sum=0
declare -a file
declare -a file_num

for i in /var/log/*.log; do
file[$q]=$i
file_num[$q]=$(wc -l ${file[$q]} | cut -d' ' -f1)
if [ $[$q%2] -eq 0 ]; then
let sum+=${file_num[$q]}
fi
echo "${q} file: ${file[$q]} line_count: ${file_num[$q]}"
let q++
done

echo "line sum: $sum"
  1. 脚本:生成10个随机数,升序或降序排序
#!/bin/bash
#

declare -a num_seq
declare -i swap=0

read -t 10 -p "Please enter how many count: " count
[ $? -ne 0 ] && echo "You didn't enter a number, The default value is 10."
read -t 10 -p "You wang up or down? (up/down) :" order
[ $? -ne 0 ] && echo "You have no input sort, default is descending order."

${count:=10} &> /dev/null
${order:=down} &> /dev/null

echo "random list:"
for i in $(seq 0 $[$count-1]); do
num_seq[$i]=$RANDOM
echo -n "$i: ${num_seq[$i]} "
done
echo

case $order in
down)
for i in $(seq 1 ${#num_seq[@]}); do
for q in $(seq $[${#num_seq[@]}-1] -1 $i); do
if [ ${num_seq[$q]} -gt ${num_seq[$[$q-1]]} ]; then
swap=${num_seq[$[$q-1]]}
num_seq[$[${q}-1]]=${num_seq[$q]}
num_seq[$q]=$swap
fi
done
done

;;
up)
for i in $(seq 1 ${#num_seq[@]}); do
for q in $(seq $[${#num_seq[@]}-1] -1 $i); do
if [ ${num_seq[$q]} -lt ${num_seq[$[$q-1]]} ]; then
swap=${num_seq[$[$q-1]]}
num_seq[$[$q-1]]=${num_seq[$q]}
num_seq[$q]=$swap
fi
done
done
;;
*)
echo
;;
esac


echo "sort by $order"
for i in $(seq 0 $[$count-1]); do
echo -n "$i: ${num_seq[$i]} "
done
echo
  1. 脚本
    (1) 提示用户输入一个命令名称
    (2) 获取此命令所依赖到的所有库文件列表
    (3) 复制命令至某目标目录例如/mnt/sysroot下的对应路径下
    (4) 复制此命令依赖到的所有库文件至目标目录的对应路径下
    (5) 每次复制完成一个命令后,不要退出,而是提示用户可继续复制,并重复完成上述功能,直到用户输入quit终止
#!/bin/bash

sys_root=/mnt/sysroot
while true; do
read -p "Please enter a command:" com_name
[ $com_name == quit ] && exit 0

if [ -a $com_name ]; then
echo "Start find object file."
else
echo "Command $com_name not found."
continue
fi

echo "$(file $com_name)" | grep -v "LSB" && echo 'Please enter a true command path.' && continue

while true; do
echo
file_name1=$(mktemp /tmp/filename.XXXX)
file_name2=$(mktemp /tmp/filename.XXXX)
file_name3=$(mktemp /tmp/filename.XXXX)
file_name4=$(mktemp /tmp/filename.XXXX)
file_name5=$(mktemp /tmp/filename.XXXX)
ldd $com_name >> $file_name1
line_num1=$(wc -l $file_name1| cut -d' ' -f1)
line_num2=$[$line_num1-1]
tail -n $line_num2 $file_name1 >> $file_name2
sed -in 's/[[:space:]]//g' $file_name2 > /dev/null
echo "Command $com_name need $line_num2 object file"

for i in $(cat $file_name2); do
i=$(echo $i | cut -d'>' -f2)
i=$(echo $i | cut -d'(' -f1)
echo $i
echo $i >> $file_name3
dirname $i >> $file_name4
done

read -p 'Start copy? (yes/no) ' judge

case $judge in
yes)
echo "Starting Copy"
cat $file_name4 | uniq >> $file_name5

for i in $(cat $file_name5); do
[ -a ${sys_root}${i} ] || mkdir ${sys_root}${i}
done

commond_dir=$(dirname $com_name)
[ -a ${sys_root}${commond_dir} ] || mkdir ${sys_root}${commond_dir}
[ -a ${sys_root}${com_name} ] || cp ${com_name} ${sys_root}${com_name}

for i in $(cat $file_name3); do
[ -a ${sys_root}${i} ] || cp $i ${sys_root}${i}
done

echo "Copy finish."
rm -rf $file_name1
rm -rf $file_name2
rm -rf $file_name3
rm -rf $file_name4
rm -rf $file_name5
break
;;

no)
echo "Ok~~"
break
;;

*)
echo "Bad Answer."
;;

esac

done

done