Skip to content

Latest commit

 

History

History
2860 lines (2141 loc) · 96.6 KB

shell.md

File metadata and controls

2860 lines (2141 loc) · 96.6 KB

Shell多个命令组合执行的三种方式,以docker删除所有容器为例: $()与``功能一样,都是执行里面的命令,区别: # 示例去搜前面,这段话是从下面复制上来这里方便看的

  • ``是所有linux系统支持的,兼容性较好;
  • $()不是所有linux系统支持,兼容性没那么好,但是不容易看混淆。
  1. docker rm `docker ps -a -q`` # 注意右边是是单个符号(为了防止它变化)
  2. docker rm $( docker ps -a -q ) # 推荐使用这种新的方式,即$(command),这是shell通用的格式,而不是`command``
  3. ps -a -q | xargs docker rm
  • shell中激活conda的python环境: shell脚本里面写上 conda activate my_env_name 是无法激活虚拟环境的,会报错, 要用source /root/anaconda3/bin/activate a_env_name,centos7实测可行,ubuntu22.04LTS实测可行,更多的其它方式看

  • 很多时候写脚本文件的时候是第一行是写的#!/usr/bin/bash,这种就是写死了,万一路径不一样呢,所以更推荐的写法是#!/usr/bin/env bash,env可以在系统的PATH目录中查找,同事env还规定一些环境变量,激活python交互终端就可以是env python, 所以要用到python解释器的脚本,可以这么写#!/usr/bin/env python (注意这要写到第一行,写到其它行就不行, 后面的python也可以给一个虚拟环境的python的路径,即#!/usr/bin/env /root/anaconda3/bin/python

  • 当某些命令执行失败,shell脚本还是会往下执行,可能就会有影响,设置成出错就退出:

    set -o errexit 或 set -e # 写在脚本的最前面

    若有用未设置的变量即让脚本退出执行:

    set -o nounset 或 set -u

  • 当想要debug调试脚本时,在最前面或者要开始调试的语句前加 set -x 在set命令之后执行的每一条命令以及加载命令行中的任何参数都会显示出来,每一行都会加上加号(+),提示它是跟踪输出的标识。示例可以看这里。关闭则是 set +x

还有一个问题,在执行脚本时./run.sh,万一得到了这个错误“-bash: xxx: /bin/bash^M: bad interpreter: No such file or directory”,那是因为这个run.sh文件是在windows上编写的,run.sh文件的格式为dos格式,而linux只能执行格式为unix的脚本,修改方法:(先直接试这个命令sed -i "s/\r//" ./run.sh

vim run.sh

:set ff # 这就会看到文件的格式为dos,即fileformat=dos

那么修改方法就是:

:set ff=unix # 就搞定了

这是我启动受电弓的脚本,可以放这里学习(间接引用语法${!item}来获取变量名item所代表的值)

#!/usr/bin/env bash

cat <<EOF
===============================================
Sample:
    ./run.sh    # 后台启动所有程序且无日志输出

    ./run.sh v01  # 启动v01感知点(支持 v01~v10)
    ./run.sh v01 --save  # 保存检测结果图片
===============================================
EOF

source /usr/local/anaconda3/bin/activate base

cd clients

v01="CUDA_VISIBLE_DEVICES=0 python client_clothesDetect.py --conf-thres 0.6 --video-stride 4"
v02="CUDA_VISIBLE_DEVICES=0 python client_toolsTable.py --conf-thres 0.6 --video-stride 4"
v03="CUDA_VISIBLE_DEVICES=0 python client_side.py --conf-thres 0.6 --video-stride 4"
v04="CUDA_VISIBLE_DEVICES=0 python client_behind.py --conf-thres 0.65 --video-stride 4"
v05="CUDA_VISIBLE_DEVICES=1 python client_v05V06V07V08.py --source-name V05 --conf-thres 0.6 --video-stride 4"
v06="CUDA_VISIBLE_DEVICES=1 python client_v05V06V07V08.py --source-name V06 --conf-thres 0.6 --video-stride 4"
v07="CUDA_VISIBLE_DEVICES=1 python client_v05V06V07V08.py --source-name V07 --conf-thres 0.6 --video-stride 4"
v08="CUDA_VISIBLE_DEVICES=1 python client_v05V06V07V08.py --source-name V08 --conf-thres 0.6 --video-stride 4"
v09="CUDA_VISIBLE_DEVICES=1 python client_reading.py --conf-thres 0.6 --video-stride 4"
v10="CUDA_VISIBLE_DEVICES=1 python client_llj.py --conf-thres 0.6 --video-stride 4"

if (($# == 0))   # ./run.sh
then  # 脚本不给参数,默认全部后台启动,且不输出日志
    # for item in v01 v02 v03 v04 v05 v06 v07 v08 v09 v10
    for item in v0{1..9} v10     # 跟上面一行效果是一样的
    do
    	# 记得这里是 ${!item} 才能拿到上面的 v05这样的值,里面是用的"!",而非"$"
    	# 这叫“间接引用语法”
    	cmd_str=${!item}   # cmd_str就是上面v01、v02的str了
    	# 字符串截取那里,这就是截取 " python"前面的字符,
    	env=${cmd_str% python*}  # "CUDA_VISIBLE_DEVICES=1"这些
    	# 字符串截取,截取第一个空字符右边的所有字符串,得到的是后面的python命令
    	cmd_python=${cmd_str#* }  
    	# 通过双引号来拼接最终运行命令的字符串
        cmd="${env} nohup ${cmd_python} > /dev/null 2>&1 &"
        # 因为 CUDA_VISIBLE_DEVICES=1 这是设置环境变量,不是命令,所以一定要放在nohup前面,nohup后面需要紧跟的是一个可执行的命令。
        echo $cmd
        eval $cmd   # eval 是执行字符串命令
    done
elif [[ $# == 1 || $# == 2 ]]  # 一次给一(两)个参数,启动一个程序,且输出日志
then 
    item=$1
    #if [[ $item != v0[123456789] ]]   # [123456],是正则表达式,表示从里面取任意一个值,字母也ok
    if [[ $item == v0[1-9] || $item == v1[012] ]]  # 要使用 [[]]
    then
        cmd="${!item}"
        if [[ $# == 2 && $2 == "--save" ]]
        then
            cmd="$cmd --save-img"  # 存图,"--save-img" 是输给python程序的参数
        fi
        echo $cmd
        eval $cmd      # 用 eval 命令直接执行字符串命令,且会在控制台打印程序输出 
    else
        echo "感知点输入错误,like v01 "
    fi
else
    echo "参数输入错误!"
fi

杀死所有相关进程:

#!/usr/bin/env bash
# 第一次的grep是查找相关的进程,第二次的grep去去掉本身最后一行,
ps aux | grep "python client_" | grep -v grep | awk '{print $2}' | xargs kill
# 实测可以解决,不行的话,最后跟 xargs kill -9 试试看,
# 或是下面试试看
# kill -9 `ps aux | grep "python client_" | grep -v grep | awk '{print $2}'`

一、Shell简介

  • echo $SHELL:获取当前系统环境的shell解析器,centos默认是bash;
  • shell脚本首行需设置shell解析器的类型:#!/bin/bash;
  • windows获取变量不是用$,而是:echo %PATH%

脚本运行方式:

  • sh命令执行,本质就是使用Shell解析器:sh hello.sh;
  • bash命令执行,本质还是使用Shell解析器:bash hello.sh;
  • 直接执行,但先要给执行权限:./hello.sh.

1.1. 获取变量的方式

方式一:
var=$(. 123.txt; echo $name$age)
echo $var      # 结果是:zhangsan16
方式二:
var1=`date +%s`    # 获得当前时间,放其它命令也行
var1=$(date +%s)   # 这两行结果是一样的

在交互shell下,可以通过这两种方式来执行命令并把结果拼接一些字符串:

  • touch `uname -r``-test1.txt #这里多一个是为了不让它转义
  • touch $(uname -r)-test1.txt

获取一个文件的所在目录的绝对路径:

a=$(cd `dirname ../CMakeLists.txt`; pwd)

另外一种处理文本文件后,在赋值给变量

有一个123.txt文件,内容如下:

age="16" hobby="my_shell" name="zhangsan"

phone="12345"

var1=$(. 123.txt; echo "name:${name} and age:${age}")
echo $var1   # 结果是:name:zhangsan and age:16

1.2. 注释

  • 单行注释直接使用#号就行了

  • 多行注释(这是固定格式)

    :<<!

    注释内容1

    注释内容2

    !

1.3. 变量($@这些)

  1. 自定义局部变量

语法:var_name=zhanghsang #只能在当前窗口中定义、使用,定义

  • 等号两侧是不能有空格;
  • 在bash环境中,变量的默认类型都是字符串类型,无法直接数值运算;
  • 变量的值如果有空格,必须使用双引号括起来;

删除变量:unset var_name

  1. 自定义常量:

语法:var_name=lsi; readonly var_name # 先定义再锁定

或者直接readonly b=dasd # 直接定义

变量设置后不可修改的叫常量,也叫只读变量。

  1. 自定义全局变量(也是只能在当前窗口中使用,)

语法:export var_name=zhangshan

区别就是这个窗口的程序都能用,而上面那就是在一个.sh文件中创建,另外一个同窗口的.sh文件都不能访问,而自定义全局变量可以,这个也用的多一些,也建议这样加上export使用。


特殊变量:

==$n==

  • 语法:$n,含义:用于接收shell脚本文件执行时传入的参数。

    $0 #用于获取当前脚本文件名称

    $1~$9 #代表输入的第一个参数到第9个参数 # 这都不用括号(要也不影响)

    第10个以上就用${数字} 比如${12} # 后面这些都必须要括号

==$#==

  • 语法:$#,含义:获取shell脚本所有输入参数的个数(输入2个就是2个)。

    • echo "参数个数:${#}"
    • echo 参数个数:$# # 里面也不一定要引号

    Tips:

    • 强烈注意:在这里面,使用==单引号==,里面的内容会原样输出,里面加的任何变量,无论带括号不,都是原样输出;
    • 给自己立个规矩,使用==双引号==里面变量就加上{},没有引号就不要这{},(引号与{}有无在这里都没有影响)

==$== and ==$@*==

  • 语法:$*$@,含义:都是获取所有传入参数,用于后续输出所有参数。

    区别:

    • 直接原样,不用双引号括起来,那两者一样,都是原样

      echo '直接输出$*:'$*         # var1 var2 ... varn
      echo '直接输出$@:'$@         # var1 var2 ... varn   # 两者是一样的
    • 使用==双引号==括起来(不能是单引号,参上的Tips):那么"$*"跟上一样的,就是将这些拼接起来的字符串;而"$@"则是一个数组,是可以一个个取的,用循环验证:

      for item in "$*"          # 结果就是全部打印出来
      do
          echo $item
      done             
                                 # 这也是shell中的循环写法
      for item in "$@"
      do
          echo $item        # 这里也可以${item},但记得上面Tips自己的约定
      done                  # 一个参数一行打印出来

==$?==

  • $?,含义:用于获取上一个shell命令的退出状态码,或者是函数的返回值

    • echo "hello"
      echo $?        # 上一条肯定执行成功,这就会得到0

      得到0就代表执行成功,非0就是不成功

==$$==

  • $$,含义:用于获取当前shell环境的进程ID号

    • ps aux | grep bash
      echo $$           # 交互式shell下,这俩都是一样的

二、环境变量

  • 查看当前shell系统环境变量:env;

  • 查看shell变量(系统环境变量+自定义变量+函数):set;

  • 得到带命令历史文件的路径:echo $HISTFILE;

2.1. shell工作环境介绍

​ 用户进入linux系统就会初始化shell环境,这个环境会加载全局配置文件和用户个人配置文件中环境变量,每个脚本文件都有自己的shell的环境。

==2.1.1 shell工作环境分类==

  1. 按照交互:
  • 交互式shell:就是开启的一个窗口,输入一个命令,就会立即得到反馈;
  • 非交互式shell:就是.sh脚本文件。
  1. 按照登录与否:

    不同的工作环境加载环境变量流程不一样,以下就是一个简单的说明,其中橘色是系统环境变零,绿色是用户环境变量:

  • shell登录环境:执行/etc/profile(系统环境变量) ---> 执行/etc/profile.d/*.sh(系统环境变量) ---> 执行 ~/.bash_profile文件(用户环境变量) ---> 执行 ~/.bashrc文件(用户环境变量) ---> 执行 /etc/bashrc文件(系统环境变量);
  • shell非登陆环境:会继承上面的环境变量 ---> 执行 ~/.bashrc文件(用户环境变量) ---> 执行 /etc/bashrc文件(系统环境变量) ---> 执行/etc/profile.d/*.sh(系统环境变量);
    • 这是不用用户实名与密码进入到linux系统的shell环境

​ 那个继承不是很懂(暂时当做不管,没有吧),从视频中看,差不多是只有在/etc/profile中添加环境变量,就只有登录环境读取得到,而非登陆环境是读取不到的。


==2.1.2 shell环境切换==

shell登录环境的判断:在交互shell界面输入echo $0(这是数字零),结果

-bash就是登录环境 bash就是非登陆环境

在执行一个脚本那文件时可以指定具体shell环境,也就是切换shell环境执行脚本:

  • 登录环境:sh/bash -l/--login .sh脚本文件; 含义:先加载shell登录环境初始化环境变量,再执行脚本文件。

  • 非登陆环境:

    bash   # 切换到非登陆环境变量。这是单向的,再输一次也回不到登陆环境
    sh/bash .sh脚本文件

总结:

  • 需要登录才能执行的shell脚本,那就定义在:/etc/profile~/.bash_profile;
  • 不需要登录的,就定义:/etc/bashrc~/.bashrc (当然登录环境也能读取这里的环境变量)。

==2.1.3 用户切换==

  • 语法1:su 用户名 --loginsu 用户名 -l # 切换到指定用户,且加载shell登录环境变量

  • 语法2:su 用户名 # 加载shell非登录环境变量

  • 直接su或者su ~su - root就切换到root用户(后面俩不是很确定)

可以用useradd -m 新用户A来创建一个普通用户。

2.2. linux添加环境变量

​ 查看当前所有的PATH环境变量:echo $PATH # linux下的PATH跟window下的那个Path类似。

==2.2.1 临时添加==:

临时添加只对当前登录窗口有用,直接执行:export PATH=$PATH:新添加的路径

Ps:等号右边,linux是用引号括起来的,自己不添加也行,系统后面会自己给加上。


==2.2.2 永久添加==:

  • 方式1(建议):

    • vim ~/.bashrc # 对于当前用户,一般用的root,就权限很大了
    • export PATH=$PATH:新增路径 # 在最后把自己的路径添加进去 这一步或者换一种方式:
    • source ~/.bashrc # 编辑完后更新一下

    或者

    • vim ~/.bashrc
    • export CMAKE_HOME=/usr/local/cmake-3.13.3-Linux-x86_64
    • export PATH=$PATH:$CMAKE_HOME/bin # 先把这个环境定义一个名字,下面再定义(注意这里面有个转义符,复制出去时要去掉)
    • source ~/.bashrc
  • 方式2:

    ​ 在目录 /etc/ 下有一个文件 profile 这里面就是 一些环境变量(一般我们不去修改这个文件),此目录下还有一个文件夹叫 ./profile.d/ 进到这里面,全是一些 *.sh文件(还有.csh文件),要添加自己的环境变量,就自己在这里建一个.sh文件,然后写进去, ​ 例子:假如自己的cmake包解压后就是./cmake-3.13.3-Linux-x86_64/,然后绝对路径是/usr/local/cmake-3.13.3-Linux-x86_64 添加cmake的环境变量

    • vim /etc/profile.d/cmake.sh # 后面的 cmake.sh 名字是自己起
    • export CMAKE_HOME=/usr/local/cmake-3.13.3-Linux-x86_64 export PATH=$PATH:$CMAKE_HOME/bin # 添加这两行(复制时注意中间的转义符)
    • source /etc/profile # 最后更新让cmake环境生效,

查看动态库路径:echo $LD_LIBRARY_PATH (其内容类似于:/usr/local/lib:/root/anaconda3/lib/,前面的优先加载)

2.3 windows添加环境变量

linux下是export,windows下是set:

set PATH=/path/to/opencv_install/bin/;%PATH% set OpenCV_DIR=/path/to/opencv_install/cmake # 在环境变量添加一个这样的key-value变量值就可以全局使用了(这种加的路径里面一般都是这个库的.cmake文件所在的路径)

win下查看一些变量的值:echo %PATH% 然后win+r,输入比如 %JAVA_HOME% 就会来到JAVA_HOME这个环境变量所在目录

三、Shell字符串

shell中的数据类型就两种:字符串、数字。

3.1. 字符串定义(" "、' ')

  1. 单引号' '方式:

    任何字符串都会原样输出,在这里面使用的变量是无效的

    var1='zhang: ${PATH}' # 定义一个变量

    echo ${var1} # 得到的结果还是zhang: ${PATH}

  2. 双引号" "方式(推荐):

    双引号内包含了变量,那么该变量会被解析得到值,而不是原样输出

    var2="nihao ${PATH}"

    echo var2 # nihao 后面就是环境变量的值(是没有引号的)

    要是想要让输出的值里带引号,就需要转义:

    var="you are \"${var2}\""

    # 这样echo $var结果中,后面那串值就会带双引号

  3. 不用引号的方式:

    这与使用双引号类似,也会解析字符串中出现的变量,但是不能出现空格,否则==空格后面的字符串会作为其它命令解析==。

    无空格:

    • var3=${var1}nihao # 在var1变量后面+一个nihao
    • var3=niao${var1} # 就自会输出nihao,后面就没了

    以上都是用echo ${var3}输出得到。

    有空格:

    new="top"

    var5=nihao ${new} # 这里一回车就会直接执行top

    var6=${new} nihao # 一回车就报错,说没有nihao这个命令

    var7=${new} ls # 这就会执行ls命令,

    这感觉意义不大,那不如不要定义变量,这行直接写要执行的命令就好了

3.2. 字符串长度|拼接|截取

==获取字符串长度==:

​ 语法:${#字符串变量名} # 里面的#是固定写法

var="hello world"

echo ${#var} # 会得到 11


==字符串拼接==:

定义两个变量:var1=abc; var2=hello

  1. 无符号拼接:var3=${var1}${var2}

    # 中间不能有空格(有的话就是同上无引号有空格)

  2. 双引号拼接:var3="${var1} 123 ${var2}" # 任意拼接

  3. 混合拼接:var3=${var1}'123'​${var2} 或者 var3=​${var1}"123"${var2} # 就还是使用双引号吧


==字符串截取==:

​ 假设一个变量var=welcome to beijing,然后那个要找的char设定为e;

格式 说明
${变量名:start:length} 从左侧start个字符开始,
向右截取length个字符,==start从0开始==; echo ${var:0:2}
${变量名:start} 不给length,就是向右截取完; echo ${var:2}
${变量名:0-start:length} 从右侧start个字符开始,
向右截取length个字符,==start从1开始==; echo ${var:0-5:2}
${变量名:0-start} 从右侧start字符,向右截取完; echo ${var:0-2}
${变量名#*chars} 从字符串左侧第一次出现chars的位置开始,
截取char右边的所有字符; echo ${var#*e}
${变量名##*chars} 从字符串右侧第一次出现chars的位置开始,
截取char右边的所有字符; echo ${var##*e}
${变量名%chars*} 从字符串右边第一次出现chars的位置开始,
截取char左边的所有字符; echo ${var%e*}
${变量名%%chars*} 从字符串右边最后一次出现chars的位置开始,
截取char左边的所有字符; echo ${var%%e*}

Tips:这里是引用变量的字符串截取,[这里](#(2) 截取字符串)是直接对字符串的截取。

​ 上面的chars可以是一个字符,也可以是一个词组,甚至开头可以是空格。

四、数组

定义:Shell支持数据(Array),但只支持一维数组

语法:在shell中,用括号( )来表示数组,数组之间用空格来分隔

方式1:array1=(item1 item2 ...)

方式2:array2=([索引下标1]=item1 [索引下标2]=item2 ...)

注意:等号=====两边不能有空格;还有关联数组

示例:

  • arr1=(123 456 "nihao")

  • arr2[5]=100 # arr2也不用事先定义,那也只有${arr2[5]}才有值,其它都没

  • arr3=([0]=123 [3]="nihao" [9]="都可以") # 不一定非得是按照顺序来变


==元素的获取==: # 注意必须使用{ }

  1. 通过下标:${arr[index]}
  2. 获取值同时赋值给其它变量:item=${arr[index]}
  3. 使用@*可以获取数组中的所有元素:${arr[@]}${arr[*]}
  4. 获取数组的长度或个数:${#arr[@]}${#arr[*]}
  5. 获取数组指定元素的字符长度:${#arr[index]}

==数组的拼接==:就是将两个数组连接成一个数组

语法:使用@*获取数组所有元素后进行拼接

array_new=(${array1[@]} ${array2[@]} ...)

array_new=(${array1[*]} ${array2[*]} ...)


==数组的删除==:

删除数组的指定元素:

  • unset 数组名[index] # 就好比是 unset array_new[2]

删除整个数组:

  • unset 数组名 # 其实就是把这个变量名删除

五、Shell常用内置命令

​ shell内置命令,就是由Bash Shell自身提供的命令,而不是文件系统中的可执行脚本文件。

​ 使用type来确定一个命令是否是内置命令,是的话会得到 XXX is a shell builtin,不是的话就会得到这个脚本文件的地址。

​ 通常来说,内置命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘I/O,还需要fork出一个单独的进程来执行,执行完成后在退出,会有上下文的切换,而执行内置命令相当于调用当前Shell进程的一个函数,还是在当前shell环境进程内,减少了上下文切换。

alias:起别名,在linux教程中我有写。

5.1. echo

​ echo是一个shell内置命令,用于在终端输出字符串,并在最后默认加上换行符。

默认换行语法:echo 字符串

  • echo hello world # 这种加不加引号都无所谓

输出不换行语法:echo -n 字符串

  • echo -n hello 或者 echo -e "字符串\c"

echo默认是不解析特殊字符的,比如换行的\n,它还是原样输出,若是想让它解析,那就:

  • echo "hello wor\nld" # 得到的就是 hello wor\nld

  • echo -e "hello wor\nld" # 那么\n就会换行

  • echo -e "hello world\c" # c就是clear,会把换行符去掉,那这里就不会再换行了

5.2. read

​ read是shell内置命令,用于标准输入中读取数据并赋值给变量,如果没有进行重定向,默认就是从终端控制台读取用户输入的数据;如果进行了重定向,那么可以从文件中读取数据。

  • 使用read给多个变量赋值;
  • 使用read读取1个字符;
  • 使用read限制时间输入。

语法:read [-options] [var1 var2 ...]

  • options和var都是可选的,如果没有提供变量名,那么读取的数据将存放在环境变量REPLY变量中;

    在交互式shell中:

    read
    abc 12 45    # 一回车就会结束
    echo $REPLY           # abc 12 45  (得到的)
  • options,如下表所示;var表示用来存储数据的变量,可以有一个,也可以有多个。

选项 说明
-a 把读取的数据赋值给数组array,从下标0开始
-d 用字符串delimiter指定读取结束的位置,
而不是一个换行符(读取到的数据不包括delimiter)
-e 在获取用户输入的时候,对功能键进行编码转换,
不会直接显式功能键对应的字符。
==-n num== 读取num个字符,而不是整行字符
==-p prompt== 显示提示信息,提示内容为prompt
-r 原样读取(Raw mode),不把反斜杠字符解释为转义字符
==-s== 静默模式(Slient mode),不会在屏幕上显示输入的字符,
输入密码或其它确认信息时常用,
==-t seconds== 设置超时时间,如果用户没有在指定时间内输入完成
那么read将会返回一个非0的退出状态,表示读取失败。
==-u fd== 使用文件描述符fd作为输入源,而不是标准输入,
类似于重定向。

例:同时给多个变量赋值

vim一个read1.sh文件:

#!/bin/bash
read -p "请输入姓名,年龄,爱好:" name age hobbit
#打印每一个变量
echo "姓名:${name}"
echo "年龄:${age}"
echo "爱好:${hobbit}"

编辑完保存后,执行命令bash read1.sh +在同一行输入这三个值,用空格隔开,好比:bash read1.sh 张三 13 喜欢游泳就可以看到输出结果,这几个变量也只能在这个shell脚本中使用。

例:只接受一个字符

vim一个read2.sh文件:

#!/bin/bash
# 下面的-n和-p的顺序是没有影响的
read -n 1 -p "您确定要删除数据吗?(请输入y/n):" a_char
printf "\n"     # 这是换行,可以只用一个 echo,单用它就是换行
echo "您输入的字符:${a_char}"

加个-n参数后,在提示输入后,在输入一个字符后,就会直接进入到下一行命令,不需要用户回车。

例:时间限制|密码静默

#!/bin/bash
read -t 10 -sp "请输入密码(10s内):" pwd1
echo   # 这是换行(使用了-s静默模式才需要这个换行符)
read -t 10 -sp "请再次输入密码(10s内):" pwd2
printf "\n"
# 校验密码两次是否一致
if [ $pwd1 == $pwd2 ]   # 这里变量名前后一定都要有空格
then
        echo "两次密码一致,认证通过~"
else
        echo "密码不一致,验证失败!"
fi

-s就是让密码的输入不显示出来;

-t时间超过了,就好像会默认给空再执行下去。

5.3. exit

exit应用场景:

  • 直接结束当前shell进程;
  • 还可以返回不同的状态码,进程结束后用echo $?查看;
#!/bin/bash
echo "hello" nihao
exit 1      # 执行到这里就会退出,下面一句就不会执行了
echo "world"

​ 我们一般定义0为程序正常执行,然后可以自定义其它一般0~255的值去代表不同的状态、程序出错的原因。

5.4. declare

​ 介绍:declare命令用于声明shell变量,可用来声明变量并设置变量的属性,也可用来显示shell函数。若不加上任何参数,则会显示全部的shell变量与函数(与单独执行set命令的效果想通过)。

declare命令作用:

  • declare可设置变量的属性(直接使用age=20赋值变量,得到的都是字符串,可用这让20成为整形);
  • 单独使用查看全部shell变量与函数;
  • 实现关联数组变量(可理解为key成了字符串,前面的数组的索引都是数字)。

语法:declare [x/-][aArxif] [变量名称=具体值]

  • +/--用来指定变量有该属性,+是取消该属性;

  • a:array,设置为普通索引数组(跟前面数组是一样的);

  • A:Array,设置为key-value关联数组(索引是字符串);

  • r:readonly,将变量设置为只读,也可以使用readonly;

  • x:export,设置变量为环境变量,也可以使用export;

  • i:int,设置变量为整形变量;

  • f:function,设置为一个函数变量。

    • declare -f:查询所有函数的定义;
    • declare -F:查询所有的函数列表。

示例:操作一个变量,设置为整形\取消整形\设置为只读等操作

在交互式shell下(声明变量时,默认这个变量都是字符串):

declare -i age=24    # 设置一个整形变量(不加-i,24就是字符串)
age="abc"; echo $age    # 得到的是0,因为age设定的是整形,重新赋值为字符串就是错的,就会变成0
declare +i age      # 把整形属性取消
age="efg"; echo $age   # 这里得到的就是efg

declare -r name=abc  #或者 readonly name=abc
name=efg    # 错的,不被允许,name成为了只读

key-value关联数组

​ 关联数组也称为“键值对(key-value)数组”,即key是字符串的形式称为数组下标,语法(关键参数-A): ​ declare -A 数组名=([字符串key1]=值1 [字符串key2]=值2 ...)

获取指定key的值:${关联数组名[key]};

获取所有的值:${关联数组名[*]}${关联数组名[@]};

当然declare也可以创建定义普通索引数组(关键参数-a): declare -a 数组名=(值1 值2 ...)declare -a 数组名=([0]=值1 [1]=值2 ...) # 这跟一样

简单示例:vim一个abc.sh,内容如下

#!/bin/bash
# 普通数组
declare -a arr1=("zhangsan" 13 my_shell)
echo ${arr1[*]}   # 千万别忘了这里的花括号
echo ${arr1[1]}

declare -a arr2=([0]="lisi" 14 li_shell)
echo ${arr2[@]}

# 关联数组
declare -A arr3=(["name"]="wangwu" [age]=15 [hobbit]="shell")
echo ${arr3[@]}
echo ${arr3["age"]}

Tips:

  • 会发现无论是键还是值,引号都是可要可不要的。

  • 不用declare -A声明关联数组,而是直接像普通数组一样,那么是无法用key去获取值得,在交互式shell下:

    arr=(["one"]=abc ["two"]=efg ["three"]=xyz)
    echo $arr           # 
    echo ${arr[one]}    #  这三个得到的都是 xyz
    echo ${arr[two]}    #

5.5 ==test==

​ shell中的test命令用于检查某个条件是否成立,它可以进行数值、字符串和文件三个方面的测试,功能与[ ]一样,一般两种用法:

  • 交互式shell:test 表达式1 options 数字2 test 1 == 1 ;echo $? # 得到的就是 0

  • .sh脚本文件:

    if test 表达式1 options 表达式2 then ... fi

实例:test -w /root/123.sh -a 2 \> 1 -o 123 -eq 123 ; echo $? # 得到的就是 0

  • 第一个条件是文件测试运算符,直接用;
  • 每个条件间的逻辑运算符连接是用的-a-o,是不能用&&、||的(这必须要在[[ ]]或是(( ))中使用);
  • 可以直接使用判等、大于小于,但是记得转义符;
  • 注意,在只有数字的比较时是可以用-eq、-gt这些的;
  • 这些具体的用法限制是在每个单独学习时写明了的。

六、Shell算术运算符

6.1. expr

expr(evaluate expressions),表达式求值

6.1.1 整数求值表达式

直接使用expr 1 + 1就能得到结果2,需要注意的是:

  • 运算符、每个数字之间都是要有空格的,不然就是一个字符串;
  • 使用乘号的时候要使用转义符号\*;
  • 四则运算中,使用了小括号(),也需要转义\( 1 + 1 \);
  • 只对整数进行运算。

还可以直接把结果赋值给变量(在交互式shell):

res=`expr 1 + 1`
echo $res
echo `expr 2 \* 3`   # 这里一定要转义符
expr 2 \* 3   # 不要echo,这样写也是可以直接出结果
echo `expr \( 10 + 10 \) \* 2`    # 符号与数字之间也一定要转义符

Tips:整个表达式是要用反引号括起来的。

6.1.2 字符串相关

注意这下面字符串的第一个字符的下标都是从1开始的。

(1) 计算字符串长度

语法:expr length 字符串

expr length "hello" # 返回得到:5

(2) 截取字符串

语法:expr substr 字符串 start n

  • start:截取字符串的起始位置,注意是从1开始;
  • n:截取字符串的长度。

expr substr "hello" 2 1 # 返回得到的是:e

Tips:

  • 这里只能是字符串,不能是引用的变量;相反,上面[这里](#3.4. 字符串截取)截取字符串是引用变量的方式。
  • expr下标都是从1开始的,而上面字符串的操作,下标都是从0开始的。
(3) 获取第一个字符出现的位置

语法:expr index 字符串 需查找的字符

expr index "hello" e # 返回得到的是:2

(4) 正则匹配

它可以理解为,返回的是匹配的字符串的长度。

方式一:expr match 字符串 正则表达式

expr match "hello world" ".*l" # 返回得到的是:10

方式二:expr 字符串 : 正则表达式

expr "hello world" : ".*e" # 返回得到的是:2

6.2. (()) 多表达式计算推荐

​ 双小括号(()),用于进行数学运算能表达式的执行,可使用$获取表达式的结构,这和使用$获取变量是一样的。用法:

  • 括号内赋值:((变量名=整数表达式))
  • 括号外赋值:变量名=$((整数变道时)) # 这里的等号两边一定不能有空格
  • 多表达式赋值:((变量名1=整数表达式1, 变量名2=整数表达式2 ...))
  • 与if条件语句代培使用:if ((整数表达式)) # 里面可结合逻辑判断

vim一个123.sh脚本:

#!/bin/bash
# 在`(())`里可以直接赋值,不用$去取变量;且这里面有没有空格无所谓,它会自己去解析
((a=1+6))
((b = a + 1))
((c= b +1))
echo "a=${a}, b=${b}, c=${c}"
# 还可以这样输出:
echo $((a+b))

# (())可以多个表达式同时赋值(注意之间是用`,`隔开)
((a=1+1, b=a+1, c=b+2))

# 赋值写法(这可以写到一行,也可以不)
a=$((1 + 2)) b=$((a + 1)) c=$((b +1))  # 注意只有在(())里面才可以可有可无空格,但是其它该有空格、不该有空格的地方还是要严格执行,比如这里 b = $((a + 1)) 就一定是错的,这等号两边不能有空格
echo "a=${a}, b=${b}, c=${c}"

# 可用于逻辑表达式,在if中使用
if ((a>7 && b==c))   # 注意在这(())才对空格没要求,其它地方一定要严格遵守(上面就也是这里的判断,中括号与变量之间要有空格)
then
	echo "判断成立"
else
	echo "这是判断不成立"
fi

6.3. let (赋值推荐)

​ let命令和双小括号在数字计算方面功能一样,但是没有(())功能强大,let只能用于赋值计算,不可以用于if的条件判断。

一般用于赋值算是最简单的变量赋值的方法,语法:

  • let 变量名=整数计算表达式; # let后面的式子一个空格都不能有
  • let 变量名1=整数运算表达式1 变量名2=整数运算表达式2 ... # 空格隔开

在交互式shell下:

a=1 b=2
echo let a+b   # 这只会直接把后面这当字符串输出
let c=a+b      # 必须搞一个变量来接收
echo $c        # 3
echo $((a+b))  # 3 这里是可以这样输出的

vim一个123.sh

#!/bin/baslet 
let a=1+1
let b=a+1    # 这表达式之间一个空格都不能有
let c=b+1    
echo "a=${a},b=${b},c=${c}"

let a=1+6 b=a+1 c=b+1   # 这里的多表达式之间是用`空格`隔开的
echo "a=${a},b=${b},c=${c}"

6.4 $[] (直接求值输出)

它也是进行整数运算,但是只能对单个表达式的计算求值与输出。

语法:$[表达式] # 会直接对表达式计算并获取结果,且内部是不可以赋值给变量的,那也就是说这里面是没有等号=的。

在交互式shell中:

a=1
echo $[a+1] $[2+3]    # 结果是:2 5
echo "$[a+1] $[2+3]"  # 结果也是:2 5
echo $[(10+15)*2]    # 50 一定要echo
echo $(((10+15)*2))  # 4、5行都是不需要转义的
echo `expr \( 10 + 15 \) \* 2`  # 这个就是严格需要空格的,还需要转义字符和反引号
expr \( 10 + 15 \) \* 2   # 不一定要echo

6.5. bc

​ Bash shell内置了对整数运算的支持,但是并不支持浮点运算,而linux bc(basic calculator)命令可以方便的进行浮点运算,bc命令是linux简单的计算器,能进行进制间的转换,以及常见的运算。如果找不到bc命令,直接yum install bc就好了。

语法:bc [options] [参数],一般就是直接bcbc -l

  • -l:使用标准的数学库,例如使用内置函数时就需要要这个参数;
  • -q:直接bc会有一些欢迎信息,可以-q来去掉。
  • 进去后是交互界面,可以输入quit退出

==实例一==:bc执行计算任务的文件

创建一个task.txt文件,编辑文件内容(一个计算表达式一行)

1+2+3+4+5 12*23+45

然后执行命令bc -q task.txt就能得到整个文件的计算结果。


==内置变量==:

变量名 作用
scale 对计算结果指定精度,默认为0,即不使用小数
ibase 指定输入数字的进制,默认为十进制
obase 指定输出数字的进制,默认为十进制
last 或者. 获取最近一次计算打印结果的数字

==内置函数==(这必须要-l参数才有使用):

函数名 作用
s(x) 计算x的正弦值,x是弧度值
c(x) 计算x的余弦值,x是弧度值
a(x) 计算x的反正切值,返回弧度值
l(x) 计算x的自然对数
e(x) 求e的x次方
j(n,x) 贝塞尔函数,计算从n到x的阶数

6.5.1 交互操作

下面是进到bc操作界面(bc互动式的数学运算):

# 一般直接计算就行
10/3      # 结果会是3,因为默认精度是0,不启用小数

sclae=2   # 后面的精度都会是2了
10/3      # 3.33
10/2      # 5.00

obase=2   # 这就是把输出改成2进制
7      # 回车就会得到 111
obase=8;100      # 回车就会得到144——8进制的100

e(2)    # 使用内置函数,进入bc时一定要加-l

6.5.2 借助管道

直接进行bc的表达式计算输出:echo "expression" | bc [options]

  • "expression"表达式必须符合bc命令要求的公式;
  • "expression"表达式里面可以引用shell变量。

在shell交互界面:

echo "10/3" | bc      # 3
echo "scale=5; 10/3" | bc   # 3.33333
echo "e(3)" | bc -l       # 使用内置函数,必须加-l
echo "1+1; 2+2" | bc   # 多个式子一起

a=5   # 定义一个shell变量
echo "b=$a+1; b" | bc     # $a引用shell变量,b是bc中的变量,直接打印就好了,前面不用加$

还可以将bc结果赋值给Shell变量:

方式一:

var1=`echo "scale=2; e(2)/3" | bc -l`  # 注意这里是一对反引号,
echo $var1         # 2.46

方式二:

var2=$(echo "scale=2; e(2)/3" | bc -l)

echo $var2 # 2.46

$()与``功能一样,都是执行里面的命令,区别:

  • ``是所有linux系统支持的,兼容性较好;
  • $()不是所有linux系统支持,兼容性没那么好,但是不容易看混淆。

6.5.3 重定向到shell变量

​ 上一点那个表达式一般就写一行,当有很多行的时候,就不好用了,就可以输入多行表达式,更加清晰,语法:

方式一:

var1=`bc [options] << EOF
表达式1
表达式2
...
EOF       # EOF是比较统一的洗发==写法,是可以换成一对别的字母
`         # 别忘了这对反引号

方式二:

var1=$(bc [options] << EOF
表达式1
表达式2
...
EOF       
)         # 这就是把一对反引号``变成了$()

实例(在交互式shell界面):

var1=`bc << EOF
> 1+1
> 2+2
> 3+3
> EOF 
> `          # 一开始不要给这个,不然回车就是执行了,一定要最后给
echo $var1    # 2 4 6  这结果就是一个字符串

var2=$(bc -l << EOF
> scale=2; 10/3
> e(2);
> 1+1
> EOF
> )           # 这个括号也定要最后给
echo $var2    # 3.33 7.38 2    也是字符串

Tips:

  • 6.5.2、6.5.3都是可以把结果赋值给shell变量的,然后就可以对shell变零进行一些操作;
  • 上面的task.txt文件操作虽然也可以完成多上输入,但是是没办法把结果赋值给shell变量的;
  • 多行计算,结果赋值的shell变量就是一个字符串,每行之间的结果是用空格字符隔开的,整体就是一个字符串,注意。

七、逻辑运算符

7.1. 比较运算符

7.1.1 整数比较运算符

​ 下表列出了常用的比较运算符,注意,表达式成立返回的是0,不成立返回的是1,相当于是返回的状态码,以下的例子都是在==交互式shell==中直接执行,然后可用echo $?来获取式子是否成立,假定变量a=1,b=2(只能是整数):

运算符 说明 举例
-eq equals 检测两个数是否相等 [ $a -eq $b ]
-ne not equals 检测两个数是否不相等 [ $a -ne ​$b ]
-gt greater than 检测左边的数是否大于右边 [ $a -gt ​$b ]
-lt lower than 检测左边的数是否小于右边 [ $a -lt ​$b ]
-ge greater equals 左边的数是否大于等于右边 [ $a -ge ​$b ]
-le lower equals 左边的数是否小于等于右边 [ $a -le ​$b ]
< (($a<​$b))
<= (($a<=​$b))
> (($a>​$b))
>= 注意这边都是用的双小括号 (($a>=​$b))
== (($a==​$b))
!= (($a!=​$b))

Tips:

  • 这些运算符都只支持整数,不支持小数与字符串,除非字符串的值都是整数数字((("1"=="1"))[ "123" -eq "123" ]是可行的);

  • 一般都是在.sh脚本文件结合if来使用if [ $a -eq $b ]或是if (($a==$b));

  • 注意各自的格式,使用[ ]那变量与符号与中括号之间都必须要有空格的。

7.1.2 字符串比较运算符(空字符串及长度检测)

可以比较2个变量,变量的类型可以为数字(整数、小数)与字符串。

​ 语法:字符串比较可以使用[[]][]两种方式,假定变量a="abc",b="efg",下表列出了常用字符串运算符。

运算符 说明 举例
== 或 = 用于比较两个字符串或数字相等, [ 1.1 == 1.1 ]
[ 1.1 = 2 ]
[[ $a == $b ]]
[[ $a = $b ]]
!= 一样成立返回状态码0 [ $a != ​$b ]
[[ $a != $b ]]
< 不成立就是返回状态码1 [ $a \< $b ] 单括号的<一定要转义符
[[ $a < $b ]]
> 没有>=,用逻辑||、&&去连接两个表达式 [ "abc" \> "abc" ]
[[ $a > $b ]]
-z 检测字符串长度是否为0 [ -z "" ] 得0,成立
-n 检测字符串长度是否不为0 [ -n "" ] 得1,不成立
$ 检测字符串是否==不为空== aa="";[ $aa ];echo ​$?
得到的是1
bb=123;[ ​$bb ];echo $?
得到的时候0

Tips:

  • 单个[ ]使用<>时一定要加转义符\,不然结果可能不正确,而双[[ ]]是不需要的;
  • 特别注意[[ ]]是没有大于等于这种写法的
    • [[ "abc" < "efg" || "abc" == "abc" ]] 这就做到了<=
[[]]和[]的区别及
  • [[]]不会有word splitting,而[]会有word splitting发生

    a="a" b="a b c"
    [[ $a == $b ]]; echo $?     # 得到的是1,没问题
    [ $a == $b ]; echo $?   # 这会直接报错,说参数太多,这是因为[]会把$b通过空格分隔开就相当于3个参数了
    
  • [[]]中是不需要转义符的,而[]中部分是要转义符的。

7.1.3 总结(必看这)

  • [[ ]]:无脑使用,无论是==整数==(全是整数的话还是用(( )),[[]]里面是没有>=、<=的)、==小数==还是==字符串==都可以,判定相等有===,且不会有单词空格的分割,也不需要转义符号;
  • 针对字符串还有一个[ ],部分需要转义,有空格分割;
  • 针对整数还有一个(()),千万别用到字符串比较去了,不会报错,但结构基本都是有问题的,这个判等只有==,在这里=是赋值; 整数也还能用[ ],它就是搭配字母那种的运算符,如[ 123 -eq 123 ]这样使用。

总之:无论是整数、小数还是字符串,无脑用[[ ]]就对了。但是注意下面一节==布尔运算符==又要求只能用[ ]

7.2. 布尔运算符

运算符 说明 举例
! 非运算,取反
-o or 或运算
-a and 与运算

Tips:

  • 注意布尔运算符放在[ ]中使用(注意转义),或与test命令配合使用才有效;
  • 布尔运算常与test命令搭配使用,可看[这里](#5.5 test)。

下面是在shell交互式环境中进行:

[ 1 \> 2 ]; echo $?     # 得1,注意一定要有空格和转义符
[ ! 1 \> 2 ]; echo $?   # 得0

[ 1 \> 2 -o 1 == 1 ]    # 得 0

[ 1 \> 2 -a 1 == 1 ]    # 得 1

7.3. 逻辑运算符

运算符 说明 举例
&& 逻辑的 and [[ 表达式1 && 表达式2 ]]
|| 逻辑的 or [[ 表达式1 || 表达式2 ]]
逻辑非 [[ ! 表达式 ]]

Tips:

  • 使用&&||必须在[[ ]](( ))中才有效,否则报错;
  • !可以在[ ][[ ]]中使用,但不可以在(( ))中使用。

总之:在使用! && ||这样符号逻辑连接的时候,尽量使用[[ ]];如果要使用 -a及-o这样字母的逻辑连接时,必须使用[ ]

7.4. 文件测试运算符(重要)

文件类型介绍:

  • -:普通文件;

  • d:文件夹;

  • l:链接文件;

  • b:块设备文件,比如计算机硬盘ls -l /dev | grep sda;

  • c:字符设备文件,比如计算机的usb文件ls -l /dev | grep usb;

  • p:管道文件。

文件测试运算符用于检测文件的何种属性,如下表:

操作符 说明
==-d== file 检测文件是否是目录
==-f== file 检测文件是否是普通文件
==-r== file 检测文件是否可读
==-w== file 检测文件是否可写
==-x== file 检测文件是否可执行
==-s== file size,检测文件是否不为空(大小是否大于0)
==-e== file exists,检测文件(夹)是否存在
==file1 -nt file2== new than,file1是否比file2新
==file1 -ot file2== old than,file1是否比file2旧
-b file 是否是块设备文件
-c file 是否是字符设备文件
-g file 检测文件是否设置了SGID位
-k file 检测是否设置了粘着位(Sticky Bit)
-p file 是否是有名管道文件
-u file 检测是否设置了SUID位
-S 判断某文件是否socket
-L link,检测文件是否存在并且是一个符号链接

示例,一个.sh脚本文件:

#!/bin/bash

file1="/root/456.txt"
file2="/root/123.txt"

if [[ -e $file1 ]]         # 看到脚本里面用单括号[]的也比较多
then
	echo "文件存在"
else
	echo "文件不存在"
fi
#***********************
if [[ -s $file1 ]]
then
	echo "文件不为空"
else
	echo "文件为空"
fi
#***********************
if [ $file1 -nt $file2 ]
then
	echo "文件1比较新"
else
	echo "文件2比较新"
fi

一般我们是用[[ ]],然后[ ]也是可以的。

这是对文件检测的操作,如果对输入的数据(字符串)也有检测要求,那么就是看[这里](#7.1.2 字符串比较运算符(空字符串及长度检测))。

八、流程控制

8.1. if else语句

不多解释了,直接上例子看:

#!/bin/bash
read -p "请输入你的考试成绩:" score

if (($score < 60))
then 
	echo "不及格"
elif [ $score -ge 60 -a $score -lt 70 ]  # []里面的逻辑是要用-a -o这种连接的
then
	echo "及格"
elif [[ ( $score > 70 || $score == 70 ) && $score < 80 ]]
# 注意这里,[[]]中是没有>=、<=的,里面也也要用括号括起来
then 
	echo "良好"
elif [ $score -ge 80 -a $score -lt 90 ]
then
	echo "优秀"
elif (( $score >= 90 && $score <= 100 ))  # (())里面也是可以直接用逻辑连接的
then
	echo "完美"
else
	echo "输入的成绩不合法"
fi

​ 所以全是数字比较的话,最好还是用(( )),然后这还可以写成一行来执行,无论是在.sh脚本还是交互式shell中:if 条件; then 命令; fi,别忘了条件结束后分号,最后fi后面可跟分号也可以不跟。

例子:if ((123==123)); then echo "相等";else echo "不相等"; fi # (写成一行时才有分号)

if对状态的判断

​ linux任何命令的的执行都会有一个退出状态,无论是内置命令还是外部文件命令还是自定义的Shell函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序,这就是命令的退出状态。

​ 大多数命令状态0代表成功,非0代表失败.也有特殊的命令,比如diff命令用来比较两个文件的不同,对于“没有差别"的文件返回0,对于“找到差别"的文件返回1,对无效文件名返回2。

Shell中,有多种方式取得命令的退出状态,其中$?是最常见的一种。

​ 实例:提示输入"请输入文件全名:"和"请输入数据:"并接收文件名与数据,使用逻辑运算符判断满足2个条件:文件需要具有可写权限输入的数据长度不为0,满足以上2个条件将用户输入的数据写入到指定的文件中去:

#!/bin/bash
read -p "请输入文件绝对路径:" file_name
read -p "请输入数据:" data

# if [[ -e $file_name && -w $file_name && -n $data ]]
if [ -e $file_name -a -w $file_name -a -n $data ]
then
	echo $data >> $file_name
	echo "执行成功"
else
	echo "有问题,执行失败"
fi

Tips:

  • 上面那两种方式都是可以的,但是注意各自搭配的逻辑运算符的不同,单括号[]是不能使用&&和||的;
  • -n $data是判data字符串不为0,在[这里](#7.1.2 字符串比较运算符(空字符串及长度检测))有写;
  • 上面的条件都成立了,就会得到状态码0,if拿到0就会执行下面的成立。

8.2. case in语句

当分支较多,并且判断条件比较简单时,使用case in语句就比较方便了。

语法(这里面是没有break的):

case 值 in 匹配模式1) 命令1 命令2 ... ;; # 每个条件完了是用;; 分段的

​ 匹配模式2) # 这里的模式是支持一些简单的正则表达式的 ​ 命令1 ​ ... ​ ;;

​ *) # 这里就是代表正则表达式的任意字符匹配(做default用) ​ 命令1 ​ ... ​ ;;

esac # 注意是以这个结尾

​ 每一匹配模式必须以右括号结束,取值可以为变量或常数,匹配发现符合某一模式后,区间所有命令开始执行直至;;(类似break作用);如果无以匹配模式,使用星号*捕获该值,再执行后面的命令(相当于默认值)。

case、in和esac都是Shell关键字,esac就是case的反写,在这里就代表结束case。

简单示例:

#!/bin/bash 
read -p "请输入任意一个1-3的数字:" number
case $number in
1)
	echo "星期一"
	;;
2)
	echo "星期二"
	;;
3)
	echo "星期三"
	;;
*)
	echo "输入有误!"
	;;
esac

==shell中的正则表达式==

简单正则表达式支持如下通配符:

格式 说明
[abc15H] 表示a、b、c、1、5、H字符中的任意一个
[m-n] 表示从m到n的任意一个字符,比如[0-9]代表任意一个数字,[0-9a-zA-Z]表示字母或数字
| 表示多重选择,类似逻辑运算中的或运算,比如 abc | xyz 表示匹配字符串"abc"或者"xyz"

8.3. while语句

多行语法(多用于.sh脚本):

while 条件 do 命令1 命令2 ... continue; # 这两行是看情况给,记得后面的分号 break;

done

一行语法(多为简单的语句,交互式shell用的比较多):

while 条件; do 命令; done; # 最后的分号不是必须的

例如:

  • while ((1>0)); do echo "hello"; done # 一定注意这个条件的写法,不能直接就是while 1 > 0 ;;
  • while [[ 1 > 0 ]]; do echo "hello"; done # 双中括号[[ ]]挺不错的,但是是没有>=<=的;
  • while [ 1 -gt 0 ]; do echo "hello"; done # 单中括号[ ]大于小于都是用的字母这种。

简单示例(命令后面可跟;,也可不跟,统一都不要吧):

#!/bin/bash
read -sp "请输入一个数字:" number
echo
if (($number > 0))
then
	i=1
	while (( i <= $number ))  # 这number前可以不要$符号的,i前面也可以加上
	do
		if ((i == 3))
		then 
			echo "3这里退出了..."
			break       
		fi
		echo "hello world_${i}"
		# ((i++)) 或 ((i+=1)) 都是可以的
		let i++     # 这里结束也可以有个分号
	done
else
	echo "输入有误!"
fi

总结:

  • 对于(( ))里面引用整数变量,$可加也不可加;但是对于[[ ]]来说,$是必须要有的,不然程序运行结果就有问题;
  • 记得let、(())对整数表达式的计算,[回顾](#6.3. let (赋值推荐))。

无限循环(了解):

while : # 这一行也可以是 while true do command done

8.4. until语句

​ 这也是循环结构,until循环与while格式上一样,但是until的循环条件为false(状态码不等于0)时才执行,条件变为true时停止循环。

简单示例:

#!/bin/bash
read -p "请输入一个数字:" number
i=0
# 本来条件是成立的,取反后才不成立,才会执行
until [[ ! $i < $number ]]  # 一定要有`$`
do
	let i++
	echo "hello_${i}"
done

8.5. for语句

一共是有三种格式,一下都是在.sh脚本文件中写,在交互式shell中都是可以写成一行的形式,举个例子:

for var in {1..3}; do echo "hello_${var}"; done # done后面可加分号,也可不加;"hello_${var}"也可写作"hello_$var",但记得自己给自己的约定,有引号就加{}。

方式一:

#!/bin/bash
# 元素较少的时候,就这样直接全部写后面(可以是符号) 
for var in 1 3 5 "张三" 李四 ??    # 重点
do
	echo "1hello_$var"   # echo "1hello_${var}" 也是ok的
done

方式二:字母、数字都是ok的 (这个里面是不支持正则表达式的,只能数字这么简单使用,字母也不支持)

#!/bin/bash

# {start_num..end_num} 这是固定写法,能是两个整数或字母,中间两个点点 .. 是固定写法

for var in 2hello_{1..5}    # 注意这是用的 { }    
do
	echo ${var}   # 会取到2hello_1、2hello_2、
	echo $var     # 这两行是一个意思
done

# 经典的: 
# 下面就是循环 v01、v02、v03、v04、.... v10  v0L、v0M、v0N、...、v0R
for item in v0{1..9} v10 v0{L..R}    
do
	echo $item
done

注意:这里是不能引用变量的,就是说,如果是这么写

a=1 b=5
for var in {$a..$b}  # 那么只会把{$a..$b}当做一个字符串来处理,就只循环一次,就像方式一那种了。

方式三:

for ((i=0; i<6; i++))      # 注意这是用的 (( ))
do
	echo "3hello_${i}"
done

交互式shell中写一句:(解压当前文件夹下所有的.tgz包),注意后面写的是$file

for file in *.tgz; do tar -zxvf $file; done

8.6. select in语句

​ select in循环用来增强交互性,它可以==显示出带编号的菜单==,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能;select in是shell独有的一种循环,非常使用终端(Treminal)这种交互场景。语法:

select var in item1 item2 ...; do 命令1 ... done

注意:==select是无限循环,输入空值或者输入的值无效,都不会结束循环,只有遇到break语句或按下Ctrl+D组合键才能结束循环==(如上面的语法里没有break,就不会退出)。

这最常用的还是跟case in一起使用,示例:

#!/bin/bash

var=0
select hobby in "游戏" "旅游" "运动" ; do
    case $hobby in
        "游戏")
            echo "开始游戏"
            let var=1
            ;;
        "旅游")
            echo "去旅游吧"
            let var=2
            ;;
        "运动")
            echo "做一些运动"
            let var=3
            ;;
        *)
            echo "选择输入有误!"
            ;;
    esac
    break      # 这里必须要这个break,不然就会在这里一直选择,无法退出
done
echo "现在var值为:${var}"

九、Shell函数|重定向

函数分类:

  • 系统函数:系统自带提供的函数,可以直接使用
  • 自定义函数

9.1. 系统函数

basename系统函数

此函数主要用于获取文件名,根据给出的文件路径截取出文件名

语法:basename [string / pathname] [suffix]

  • 根据指定路径进行文件名的获取;
  • suffix:给定一个后缀格式,那获取的文件名就会把这个后缀名去掉。

简单实例(shell交互式环境中):

basename /root/456.sh      # 得到的结果就是 456.sh
basename /root/456.sh .sh      # 得到的结果就是 456

dirname系统函数

获取指定文件绝对路径中去掉文件名的目录路径

语法:dirname 文件绝对路径

简单示例(shell交互式环境中):

dirname /root/456.sh         # 得到的就是 /root

tips:

  • declare -f:可以查看所有的函数;
  • declare -F:就是查看declare定义的函数。

9.2. 自定义函数(有参|无参)

语法:

# 函数的定义
[function] fun_name () {
	要执行的命令
	[return 返回值]
}
# 函数的调用
fun_name
  • 可以是带 function fun()定义,也可以直接fun()定义,不带任何参数;
  • 参数返回:可以显示加 return 返回值;如果不加,将以最后一条命令运行结果,作为返回值(return 后跟数值0~255)。

==无参无返回值函数==:

​ vim一个1.sh文件:

#!/bin/bash
# 下面是函数的定义
demo1() {
	echo "这里执行了一些命令"
}
# 函数调用
demo1
echo "这里demo1调用后状态码:$?"    

执行:bash 1.sh,就会得到函数内执行的内容,也会的得到状态码 0 。(0代表成功)


==无参有返回值函数==:

​ vim一个2.sh文件:

#!/bin/bash
# 函数定义,function可不要
function demo2() {
	echo "demo2试求两个数的和!"
	read -p "请输入第一个数:" num1
	read -p "请输入第二个数:" num2
	return $(($num1 + $num2))      # 别忘了双括号外的$
}

demo2
echo "状态码:$?"

执行:bash 2.sh,就会得到函数执行的内容,也会得到状态码(由输入的数字决定)。


==有参函数==:

​ vim一个3.sh文件:

#!/bin/bash
# 定义函数(以下的特殊变量含义,在第一章有讲解)
demo3() {
	echo "接收的第一个参数:$1"
	echo "接收的第二个参数:$2"
	echo "接收的第十个参数:${10}"
	echo "参数总的个数:$# "
	echo "获取所有参数作为一个字符串返回:$*"
}
# 调用函数并传入参数
demo3 11 22 33 44 55 66 77 88 99 1010

执行:bash 3.sh,会得到如下结果:

接收的第一个参数:11 接收的第二个参数:22 接收的第十个参数:1010 参数总的个数:10 获取所有参数作为一个字符串返回:11 22 33 44 55 66 77 88 99 1010

9.3 ==shell输入输出重定向==

==语法介绍==:

  • 标准输入:从键盘读取用户输入的数据,然后再把数据拿到shell程序中使用;
  • 标准输出:shell程序产生的数据,这些数据一般都是呈现到显示器上供用户直接查看。

默认输入输出文件:每个Unix/Linux命令运行时都会打开三个文件,文件如下:

文件名 类型 文件描述符:fd(file description) 功能
stdin 标准输入文件 0 获取键盘的输入数据
stdout 标准输出文件 1 将正确数据输出到显示器
stderr 标准错误输出文件 2 将报错、错误信息输出到显示器上

每个文件都有一个唯一的文件描述符fd,后面会通过唯一的文件描述符fd操作对应的信息;这三个文件用于临时传输数据使用,用完即销。


==重定向输入输出介绍==:

  • 输入重定向:标准输入是数据默认从键盘流向程序,如果改变了它的方向,数据就从其它地方流入,这就是输入重定向;
  • 输出重定向:标准输出是数据默认从程序流向显示器,如果改变了它的方向,数据就流向其它地方,这就是输出重定向。

==重定向语法==:

命令 说明
命令 > file 将正确数据重定向写入到file文件中,==覆盖方式==
命令 < file 将输入重定向,从file文件中读取数据
命令 >> file 将正确数据重定向写入到file文件中,==追加方式==
命令 < file1 > file2 从file1中读取数据执行命令,并将结果写入到file2
命令 fd > file 根据指定的文件描述符fd将数据重定向到file,==覆盖==
命令 fd >> file 根据指定的文件描述符fd将数据重定向到file,==追加==
命令 > file fd1>&fd2 将fd1和fd2文件描述符合并 输出到文件file
fd1<&fd2 将fd1和fd2文件描述符合并 从文件读取输入
<<tag 读取终端输入数据,将开始标记tag和结束标记tag
之间的内容作为输入,标记名tag可以是任意的,
一般用==EOF==;

==输出重定向==:

​ 简单示例(以下是在shell交互式环境):

ls > log.txt      # 结果写到新的名为log.txt文件里
cat log.txt       # 能看到所有写入的结果

ls aaaa >> log.txt   # aaaa文件夹并不存在的话,这会执行失败,也不会把错误信息追加进log.txt
ls aaaa 2>> log.txt   # 前面fd指定为2,这就能把错误信息追加进去
ls 2>> log.txt   # 由于fd指定是2,只会把错误信息重定向追加进log.txt文件,那这个命令是正确输出,就会是标准输出,会直接输出到屏幕上

# 若是想要把正确和错误信息都要追加进log.txt,那就:
ls aaa >> log.txt 2>&1      # 注意 2>&1  要写后面

==输入重定向==:

简单示例(均是在shell交互式环境下):

  • 获取log.txt文件的所有行数:wc -l < log.txt

  • while read my_str; do echo $my_str; done # 这会一直从控制台接收输入,然后将其输出

  • while read my_str; do echo $my_str; done < log.txt # 这就会把log.txt文件一行行输出,效果类似于cat

  • rownum=1;while read my_str; do echo "第${rownum}行:${my_str}"; let rownum++; done < log.txt # 输出前面加个行号(前面的rownum=1可以前面单独一行先定义出来)

  • # 这里的EOF可以换成其它任意的tag标记
    [root@192 ~]# wc -l << EOF
    > AA
    > BB
    > CC
    > EOF
    3                
    

十、好用工具

10.1. cut

​ 使用cut可以切割提取指定列|字符|字节的数据;它是一个强大的文本处理工具,cut命令逐行读入文本,然后按列划分字段并进行提取、输出操作。

语法:cut [options] filename, 其中options参数说明:

  • -f 提取范围:获取切割后的提取对应列号的数据;
  • -d 自定义分隔符:自定义用来分割的符号,默认为制表符(后面的符号可以紧跟-d后面);
    • cut -d / -f 1 # 以==/==分隔,选取第一列的值
    • cut -d/ -f1 # 这个跟上面是一个意思,(即==选项与后面的值可要空格也可以不要空格==)
  • -c 提取范围:以字符为单位进行分割;
  • -b 提取范围:以字节为单位进行分割,这些字节位置将忽略多字节字符边界,除非也指定了-n参数;
  • -n:一般与-b参数连用,不分割多字节字符(就会保留整个汉字)。

上面提取范围的说明:

  • n-:提取指定第n列或字符或字节后面所有数据;
  • n-m:提取指定第n列或字符或字节到第m列或字符或字节中间的所有数据;
  • -m:提取第m列或字符或字节前面所有数据;
  • n1,n2,...:提起指定枚举列的所有数据(注意:枚举数据之间是不能有空格的)。

简单示例(先vim一个demo.txt文件),demo.txt:

AA helloworld 11 XX BB hello 22 XXXXXXX CC NIH 33 XXXXXXXXX DD IT 44 XXXXXXXXXX

然后在交互式shell中:

  • 获取1、3列数据:cut demo.txt -d " " -f 1,3 # 1,3之间不能有空格

  • 获取第2列以后的数据:cut demo.txt -d " " -f 2-

  • 提取第一个字符:cut demo.txt -c 1

  • 提取前5个字符:cut demo.txt -c -5

注意linux一般使用的是utf-8,一个字母占一个字节,一个汉字占3个字节:

  • echo "abc你好" | cut -b 1-3 # 得到的就是 abc
  • echo "abc你好" | cut -b 1-4 # 切到了汉字,不会连续全部保存
  • echo "abc你好" | cut -nb 1-4 # 加参数-n,结果就是 abc你
  • echo "abc你好" | cut -b 1-6 # 这也是 abc你

常用示例:

  • 找到一个文件中包含某个单词的行,并切割取出来(这一这里的行号是按照原始数据,一个空格都是一行来的): cat demo.txt | grep AA | cut -d " " -f 3

  • 查找一个进程的pid(注意行数的数取,这里连续的空格有几个就是有几列): ps -aux | grep bash | head -n 1 | cut -d " " -f 8

  • 获取本机ip(broadcast是ip地址那行独有的): ifconfig | grep broadcast | cut -d " " -f 10

10.2. sed

这也是文本处理工具,内容太多了,且暂时用不到,就没写了,后续有需要,就来看这里

当在win上写的脚本,在linux下直接运行可能会报Bash错误,运行如下类似命令就好了: sed -i "s/\r//" ./run.sh

10.3. awk

语法:awk 'pattern{action}' {filenames} # ==注意这是单引号==,千万记得

  • pattern:表示AWK在数据中查找的内容,就是匹配模式;
  • action:在找到匹配内容时所执行的一系列命令;

选项参数说明:

  • -F:指定输入文件拆分分隔符(默认是以空格进行分割);
  • -V:赋值一个用户定义变量。

awk内置变量

内置变量 含义
ARGC 命令行参数个数
ARGV 命令行参数排列
ENVIRON 支持队列中系统环境变量的使用
FILENAME awk浏览的文件名
FNR 浏览文件的记录数
FS 设置输入域分隔符,等价于命令行 -F 选项
NF 浏览记录的域的个数,根据分隔符分割后的列数
NR 已读的记录数,也就是行号
OFS 输出域分隔符
ORS 输出记录分隔符
RS 控制记录分隔符
==$n== ==$0==变量是指整条记录,==$1==表示当前行的第一个域(列)
$NF $NF是number finally,表示最后一列的信息,很变量NF是有区别的,变量NF统计的是每行列的总数

简单示例(交互式shell环境下):默认每行空格切割数据

echo "abc 123 z456" | awk '{print $2}'    # 得到123
还可以如下做一些拼接
echo "abc 123 z456" | awk '{print $1"$&aaa"$3}'  # 得到abc$&aaaz456

使用==多分隔符==进行分割

echo "abc:123/z456" | awk -F "[:/]" '{print $1"aa"$2"bb"$3}'
# 这里就是:和/都是分隔符,后面单独把三个数据都取到就是印证

  • 通过匹配模式找到demo.txt中具有==root==的行的数据:

    awk '/root/{print $0}' demo.txt    # 那两个斜杠是固定写法
    # 执行得到的结果是:
    root:x:0:0:root:/root:/bin/bash
    operator:x:11:0:operator:/root:/sbin/nologin

    因为awk默认使用空格做的分割,这里的数据分割不了,就手动指定一个:

    awk -F ":" '/root/{print $7}' demo.txt
    awk -F: '/root/{print $7}' demo.txt     # 两种写法是一样的
  • 格式化的方式加字符串:

    awk -F: '{printf("文件名:%s, 行号:%s, 列数:%s, 内容:%s\n", FILENAME, NR, NF, $0)}' demo.txt
    # 使用的printf,及awk自带的变量
  • 打印demo.txt第二行的数据:

    awk 'NR==2{print $0}' demo.txt
  • 打印demo.txt按:分割的最后一列数据:

    awk -F ":" '{print $NF}' demo.txt   # 用自定义变量时别忘了$
    # 倒数第二列
    awk -F ":" '{print $(NF-1)}' demo.txt   # $(NF-1) 使用的小括号
  • 打印demo.txt按:分割后的第10~20行的第一列的数据:

    awk -F: '{if(NR>=10 && NR <=20) {print $1}}' demo.txt
  • 在执行之前和结束时加一些文字:

    echo -e "abc\nabc\nabc" | awk 'BEGIN{print "开始处理.."} {print $0} END{print "结束处理..."}'
    # 中间打印了3行abc,开头和结束各加了一句话。
  • awk也可以使用循环,定义变量,这里就不写了,用的时候可参考这里

  • 获取ip地址:

    ifconfig | awk '/broadcast/{print $2}'
    # 通过模式匹配到有ip地址的,也唯一有broadcast的那行,每列数据之间是用空格分开的,awk默认使用空格,就取$2,要是不知是第几列,就先用$0,确定下来后再改
  • 几种获取PID的方式:

    ps -aux | awk '/python/{print $2}' | head -n 2
    ps -aux | grep python | head -n 2 | awk '{print $2}'
    ps -aux | grep python | head -n 2 | cut -d " " -f 9  # 不用

    尽量就不用cut了,因为pid长短可能不一致,那么之间的空格就存在差异,指定第9列,可能其它行的选不到。

10.3. sort

sort可以将文件内的内容进行排序,并将排序结果标准输出或重定向输出到指定文件。

还有一个核心用法:一些命令得到一些数据(就一列) | sort -u 来去重。

语法:sort (options) file

选项 说明
-n number,依照数值的大小排序
-r reverse,以相反的顺序来排序
-t 分隔符 指定排序时所用的分隔符,默认分隔符是空格
-k 指定需要排序的列
-d 排序时,处理英文字母、数字及空格,忽略其它字符
-f 排序时,将小写字母视为大写字母
-b 忽略每行前面开始出现的空格字符
-o 输出文件名 将排序后的结果存入到指定的文件
-u 意味着是唯一的(unique),输出的结果是去完重的
-m 将几个排序好的文件进行合并

file:指定待排序的文件列表。

准备一个demo.txt文件,内容如下:

张三 13 李四 9 王五 32 赵六 15 赵六 23 赵六 15 angle 35 666 100

  • 数字升序排列:sort -nk2,2 demo.txt # n代表按数字排序,k代表列数(2,2就是第2列,-nk2则就是第2列往后所有了(数字一定要紧跟在k后面));这里没有跟 -t " " 因为默认分割符就是空格,所以可以不加

  • 上面的降序再去重:sort -nrk2,2 -uk1,2 -o out.txt demo.txt # -u就是去重,-k1,2就是既按照第1列也按照第2列。

  • 要求:以“,”分割先对第一列字符串升序,再对第3列数字降序

    ​ sort -t "," -k1,1 -k3nr,3 demo1.txt # 这就是按多个条件排序,如果要去重的话,记得把-u要给到最后面。

十一、据悉面试可能会遇到

打印空行的行号

假设有10行文本,其中1、3、6行是空的,把这打印出来:

awk '/^$/{print NR}' demo.txt

# 前面是正则匹配,^是开头,$是结尾,中间是没有东西的就代空, NR是awk的内置变量就是代表行号。

求一列的和

假设一个文本,第二列全是数字,求它的和,并输出:

awk '{my_sum+=$2} END{print "求和结果:"my_sum}' 2.txt

# 定义了一个变量my_sum把第二列的数字全部加起来,最后再用END去把它打印出来,打印的时候里面是可以拼接字符串的。

判断文件是否存在

if [ -e ./123.txt ]; then echo "文件存在"; else echo "文件不存在"; fi

# 注意if用的是then,结尾是fi。

数字排序并打印

假设2.txt文本就只有一列,全是数字,且无序,对它排序打印出来并求和

sort -n 2.txt | awk '{my_sum+=$1; print $1} END{print "求和结果: "my_sum}'

# 因为只有一列,sort就直接-n就可以了,多列的去看sort;awk拿到数据后,自定义一个变量my_sum进行求和,一个{}里可以放两个命令,一定是用分号;隔开,然后这里由于只有一列,把$1改成$0也是可以的($0代表么所有列)

==搜索路径下,包含有指定内容的所有文件==

假定查找目录/root下所有的文本文件,这些文本文件中必须要包含字符"123.sh"

grep -r "123.sh" /root | cut -d ":" -f 1 | sort -u

# -r就是递归查找,查找到后,我们只要文件名不要内容,就用cut按照:分隔符切割来保留第1列文件名,然后是有重复的,就通过sort来去重。

批量生成指定数目文件,且用当前时间的"纳秒"命名

#!/bin/bash
# declare指定变量为整型后,给字符串就会把它置为0
declare -i number     # 这一行一定要定义到下一行之前
read -t 10 -p "请输入一个大于0的数字:" number

if (($number>0)); then
	# 这一句就是这个文件夹不存在,那就创建,记得这个写法
	[ ! -d "./temp/" ] && mkdir -p "./temp"
	for ((i=0; i<$number; i++)); do
		name=$(date +%N)
		name=$(date +%s)  # 当前的时间(秒记)
		touch "./temp/$name"
	done
	echo "创建成功"
else
	echo "输入有误!"
	exit 1        # 错误的话就给一个非0的状态码
fi

一种类似于os.listdir的变量获取

#!/bin/bash
# 注意下面那这种获取变量的方式
content=$(cat 123.txt)  # 获取一些东西
files=$(ls /root)
for file in $files; do
	echo $file
	echo ${file}            # 两个写法是一样的
done

==一些信息不要,输出到别的地方 /dev/null==

下面这是把错误信息,正常信息都不显示,/dev/null应该是专门这来接收的

  • 在Linux 系统代表了一个空设备,它会丢弃写入的任何内容,返回一个 EOF 字符,介绍
ls -l | grep sh > /dev/null 2>&1     # 注意这是 &
ls -l | grep sh 2> /dev/null    # 这两行是一个意思

在.sh脚本文件中,可以:

#!/bin/bash
a=zhansan
echo "$a"
[ $? -eq 0 ] && echo "上一条命令执行成功了!"

筛选长度大于3的单词

交互式shell中:

echo "I may not be able to change the past, but I can learn from it." | awk -F "[ ,.]" '{for(i=1; i<NF; i++) {if(length($i)>3) {print $i}}}'

  • 句子中有空格、逗号、句号,所以是 -F "[ ,.]" # 千万别忘了这个双引号
  • length是awk自带的函数,NF是总的列数

扫描网段内可以ping通的主机

#!/bin/bash
count=0
for i in 192.168.1.{1..254}; do
	# 2、6、4这是都是实际情况分析出来的,不是固定的
	# 反倒是一定记得这种在.sh脚本获取变量的写法
	receive=$(ping $i -c 2 | awk 'NR==6{print $4}')
	if [ $receive -gt 0 ]; then
		echo "${i}存活~"
		let count++
	fi
done
echo "共有${count}个主机可以ping通!"

十二、一些现成shell脚本

1、检测两台服务器指定目录下的文件一致性

#!/bin/bash
######################################
检测两台服务器指定目录下的文件一致性
#####################################
#通过对比两台服务器上文件的md5值,达到检测一致性的目的
dir=/data/web
b_ip=192.168.88.10
#将指定目录下的文件全部遍历出来并作为md5sum命令的参数,进而得到所有文件的md5值,并写入到指定文件中
find $dir -type f|xargs md5sum > /tmp/md5_a.txt
ssh $b_ip "find $dir -type f|xargs md5sum > /tmp/md5_b.txt"
scp $b_ip:/tmp/md5_b.txt /tmp
#将文件名作为遍历对象进行一一比对
for f in `awk '{print 2} /tmp/md5_a.txt'`do
#以a机器为标准,当b机器不存在遍历对象中的文件时直接输出不存在的结果
if grep -qw "$f" /tmp/md5_b.txt
then
md5_a=`grep -w "$f" /tmp/md5_a.txt|awk '{print 1}'`
md5_b=`grep -w "$f" /tmp/md5_b.txt|awk '{print 1}'`
#当文件存在时,如果md5值不一致则输出文件改变的结果
if [ $md5_a != $md5_b ]then
echo "$f changed."
fi
else
echo "$f deleted."
fi
done

2、定时清空文件内容,定时记录文件大小

#!/bin/bash
#################################################################
每小时执行一次脚本(任务计划),当时间为0点或12点时,将目标目录下的所有文件内#容清空,但不删除文件,其他时间则只统计各个文件的大小,一个文件一行,输出到以时#间和日期命名的文件中,需要考虑目标目录下二级、三级等子目录的文件
################################################################
logfile=/tmp/`date +%H-%F`.log
n=`date +%H`
if [ $n -eq 00 ] || [ $n -eq 12 ]
then
#通过for循环,以find命令作为遍历条件,将目标目录下的所有文件进行遍历并做相应操作
for i in `find /data/log/ -type f`
do
true > $i
done
else
for i in `find /data/log/ -type f`
do
du -sh $i >> $logfile
done
fi

3、检测网卡流量,并按规定格式记录在日志中

#!/bin/bash
#######################################################
#检测网卡流量,并按规定格式记录在日志中#规定一分钟记录一次
#日志格式如下所示:
#2019-08-12 20:40
#ens33 input: 1234bps
#ens33 output: 1235bps
######################################################3
while :
do
#设置语言为英文,保障输出结果是英文,否则会出现bug
LANG=en
logfile=/tmp/`date +%d`.log
#将下面执行的命令结果输出重定向到logfile日志中
exec >> $logfile
date +"%F %H:%M"
#sar命令统计的流量单位为kb/s,日志格式为bps,因此要*1000*8
sar -n DEV 1 59|grep Average|grep ens33|awk '{print $2,"\t","input:","\t",$5*1000*8,"bps","\n",$2,"\t","output:","\t",$6*1000*8,"bps"}'
echo "####################"
#因为执行sar命令需要59秒,因此不需要sleep
done

4、计算文档每行出现的数字个数,并计算整个文档的数字总数

#!/bin/bash
#########################################################
#计算文档每行出现的数字个数,并计算整个文档的数字总数
########################################################
#使用awk只输出文档行数(截取第一段)
n=`wc -l a.txt|awk '{print $1}'`
sum=0
#文档中每一行可能存在空格,因此不能直接用文档内容进行遍历
for i in `seq 1 $n`do
#输出的行用变量表示时,需要用双引号
line=`sed -n "$i"p a.txt`#wc -L选项,统计最长行的长度
n_n=`echo $line|sed s'/[^0-9]//'g|wc -L`
echo $n_nsum=$[$sum+$n_n]
done
echo "sum:$sum"

杀死所有脚本

#!/bin/bash
################################################################
#有一些脚本加入到了cron之中,存在脚本尚未运行完毕又有新任务需要执行的情况,
#导致系统负载升高,因此可通过编写脚本,筛选出影响负载的进程一次性全部杀死。
################################################################
ps aux|grep 指定进程名|grep -v grep|awk '{print $2}'|xargs kill -9

5、从 FTP 服务器下载文件

#!/bin/bash
if [ $# -ne 1 ]; then
    echo "Usage: $0 filename"
fi
dir=$(dirname $1)
file=$(basename $1)
ftp -n -v << EOF   # -n 自动登录
open 192.168.1.10  # ftp服务器
user admin password
binary   # 设置ftp传输模式为二进制,避免MD5值不同或.tar.gz压缩包格式错误
cd $dir
get "$file"
EOF

6、连续输入5个100以内的数字,统计和、最小和最大

#!/bin/bash
COUNT=1
SUM=0
MIN=0
MAX=100
while [ $COUNT -le 5 ]; do
    read -p "请输入1-10个整数:" INT    
    if [[ ! $INT =~ ^[0-9]+$ ]]; then
        echo "输入必须是整数!"
        exit 1
    elif [[ $INT -gt 100 ]]; then
        echo "输入必须是100以内!"
        exit 1
    fi
    SUM=$(($SUM+$INT))
    [ $MIN -lt $INT ] && MIN=$INT
    [ $MAX -gt $INT ] && MAX=$INT
    let COUNT++
    done
echo "SUM: $SUM"
echo "MIN: $MIN"
echo "MAX: $MAX

7、监测 Nginx 访问日志 502 情况,并做相应动作

假设服务器环境为 lnmp,近期访问经常出现 502 现象,且 502 错误在重启 php-fpm 服务后消失,因此需要编写监控脚本,一旦出现 502,则自动重启 php-fpm 服务。

#场景:
#1.访问日志文件的路径:/data/log/access.log
#2.脚本死循环,每10秒检测一次,10秒的日志条数为300条,出现502的比例不低于10%(30条)则需要重启php-fpm服务
#3.重启命令为:/etc/init.d/php-fpm restart
#!/bin/bash
###########################################################
#监测Nginx访问日志502情况,并做相应动作
###########################################################
log=/data/log/access.log
N=30 #设定阈值
while :do
 #查看访问日志的最新300条,并统计502的次数
    err=`tail -n 300 $log |grep -c '502" '` 
if [ $err -ge $N ] 
then
/etc/init.d/php-fpm restart 2> /dev/null 
#设定60s延迟防止脚本bug导致无限重启php-fpm服务
     sleep 60
 fi
 sleep 10
 done

8、将结果分别赋值给变量

应用场景:希望将执行结果或者位置参数赋值给变量,以便后续使用。

方法1:

for i in $(echo "4 5 6"); do
   eval a$i=$idone
echo $a4 $a5 $a6

方法2:将位置参数192.168.1.1{1,2}拆分为到每个变量

num=0
for i in $(eval echo $*);do   #eval将{1,2}分解为1 2
   let num+=1
   eval node${num}="$i"
done
echo $node1 $node2 $node3
# bash a.sh 192.168.1.1{1,2}
192.168.1.11 192.168.1.12

方法3:arr=(4 5 6)
INDEX1=$(echo ${arr[0]})
INDEX2=$(echo ${arr[1]})
INDEX3=$(echo ${arr[2]})

9、批量修改文件名

# touch article_{1..3}.html # lsarticle_1.html article_2.html article_3.html 目的:把article改为bbs

方法1:

for file in $(ls *html); do
    mv $file bbs_${file#*_}
    # mv $file $(echo $file |sed -r 's/.*(_.*)/bbs\1/')
    # mv $file $(echo $file |echo bbs_$(cut -d_ -f2)

方法2:

for file in $(find . -maxdepth 1 -name "*html"); do
     mv $file bbs_${file#*_}done

方法3:

# rename article bbs *.html
把一个文档前五行中包含字母的行删掉,同时删除6到10行包含的所有字母

1)准备测试文件,文件名为2.txt

第1行1234567不包含字母
第2行56789BBBBBB
第3行67890CCCCCCCC
第4行78asdfDDDDDDDDD
第5行123456EEEEEEEE
第6行1234567ASDF
第7行56789ASDF
第8行67890ASDF
第9行78asdfADSF
第10行123456AAAA
第11行67890ASDF
第12行78asdfADSF
第13行123456AAAA

脚本如下:

#!/bin/bash
###############################################################
把一个文档前五行中包含字母的行删掉,同时删除6到10行包含的所有字母
##############################################################
sed -n '1,5'p 2.txt |sed '/[a-zA-Z]/'d
sed -n '6,10'p 2.txt |sed s'/[a-zA-Z]//'g
sed -n '11,$'p 2.txt
#最终结果只是在屏幕上打印结果,如果想直接更改文件,可将输出结果写入临时文件中,再替换2.txt或者使用-i选项

10、统计当前目录中以.html结尾的文件总大

# find . -name "*.html" -exec du -k {} \; |awk '{sum+=$1}END{print sum}'

方法2:
```bash
for size in $(ls -l *.html |awk '{print $5}'); do
    sum=$(($sum+$size))
done
echo $sum

11、扫描主机端口状态

#!/bin/bash
HOST=$1
PORT="22 25 80 8080"
for PORT in $PORT; do
    if echo &>/dev/null > /dev/tcp/$HOST/$PORT; then
        echo "$PORT open"
    else
        echo "$PORT close"
    fi
done
用 shell 打印示例语句中字母数小于6的单词

#示例语句:
#Bash also interprets a number of multi-character options.
#!/bin/bash
##############################################################
#shell打印示例语句中字母数小于6的单词
##############################################################
for s in Bash also interprets a number of multi-character options.
do
 n=`echo $s|wc -c` 
 if [ $n -lt 6 ] 
 then
 echo $s
 fi
done

12、输入数字运行相应命令

#!/bin/bash
##############################################################
#输入数字运行相应命令
##############################################################
echo "*cmd menu* 1-date 2-ls 3-who 4-pwd 0-exit "
while :
do
#捕获用户键入值
 read -p "please input number :" n
 n1=`echo $n|sed s'/[0-9]//'g`
#空输入检测 
 if [ -z "$n" ]
 then
 continue
 fi
#非数字输入检测 
 if [ -n "$n1" ]
 then
 exit 0
 fi
 break
done
case $n in
 1)
 date
 ;;
 2)
 ls
 ;;
 3)
 who
 ;;
 4)
 pwd
 ;;
 0)
 break
 ;;
    #输入数字非1-4的提示
 *)
 echo "please input number is [1-4]"
esac

13、Expect 实现 SSH 免交互执行命令

Expect是一个自动交互式应用程序的工具,如telnet,ftp,passwd等。

需先安装expect软件包。

方法1:EOF标准输出作为expect标准输入

#!/bin/bash
USER=root
PASS=123.com
IP=192.168.1.120
expect << EOFset timeout 30spawn ssh $USER@$IP   expect {    "(yes/no)" {send "yes\r"; exp_continue}    "password:" {send "$PASS\r"}
}
expect "$USER@*"  {send "$1\r"}
expect "$USER@*"  {send "exit\r"}
expect eof
EOF

方法2:

#!/bin/bash
USER=root
PASS=123.com
IP=192.168.1.120
expect -c "
    spawn ssh $USER@$IP
    expect {
        \"(yes/no)\" {send \"yes\r\"; exp_continue}
        \"password:\" {send \"$PASS\r\"; exp_continue}
        \"$USER@*\" {send \"df -h\r exit\r\"; exp_continue}
    }"

方法3:将expect脚本独立出来

登录脚本:
# cat login.exp
#!/usr/bin/expect
set ip [lindex $argv 0]
set user [lindex $argv 1]
set passwd [lindex $argv 2]
set cmd [lindex $argv 3]
if { $argc != 4 } {
puts "Usage: expect login.exp ip user passwd"
exit 1
}
set timeout 30
spawn ssh $user@$ip
expect {    
    "(yes/no)" {send "yes\r"; exp_continue}
    "password:" {send "$passwd\r"}
}
expect "$user@*"  {send "$cmd\r"}
expect "$user@*"  {send "exit\r"}
expect eof

执行命令脚本:写个循环可以批量操作多台服务器

#!/bin/bash
HOST_INFO=user_info.txt
for ip in $(awk '{print $1}' $HOST_INFO)
do
    user=$(awk -v I="$ip" 'I==$1{print $2}' $HOST_INFO)
    pass=$(awk -v I="$ip" 'I==$1{print $3}' $HOST_INFO)
    expect login.exp $ip $user $pass $1
done
Linux主机SSH连接信息:
# cat user_info.txt
192.168.1.120 root 123456
创建10个用户,并分别设置密码,密码要求10位且包含大小写字母以及数字,最后需要把每个用户的密码存在指定文件中
```bash
#!/bin/bash
##############################################################
#创建10个用户,并分别设置密码,密码要求10位且包含大小写字母以及数字
#最后需要把每个用户的密码存在指定文件中#前提条件:安装mkpasswd命令
##############################################################
#生成10个用户的序列(00-09)
for u in `seq -w 0 09`do
 #创建用户
 useradd user_$u
 #生成密码
 p=`mkpasswd -s 0 -l 10` 
 #从标准输入中读取密码进行修改(不安全)
 echo $p|passwd --stdin user_$u
 #常规修改密码
 echo -e "$p\n$p"|passwd user_$u
 #将创建的用户及对应的密码记录到日志文件中
 echo "user_$u $p" >> /tmp/userpassworddone

14、监控 httpd 的进程数,根据监控情况做相应处理

#!/bin/bash
###############################################################################################################################
#需求:
#1.每隔10s监控httpd的进程数,若进程数大于等于500,则自动重启Apache服务,并检测服务是否重启成功
#2.若未成功则需要再次启动,若重启5次依旧没有成功,则向管理员发送告警邮件,并退出检测
#3.如果启动成功,则等待1分钟后再次检测httpd进程数,若进程数正常,则恢复正常检测(10s一次),否则放弃重启并向管理员发送告警邮件,并退出检测
###############################################################################################################################
#计数器函数
check_service()
{
 j=0
 for i in `seq 1 5` 
 do
 #重启Apache的命令
 /usr/local/apache2/bin/apachectl restart 2> /var/log/httpderr.log    
    #判断服务是否重启成功
 if [ $? -eq 0 ] then
 break
 else
 j=$[$j+1] fi
    #判断服务是否已尝试重启5次
 if [ $j -eq 5 ] then
 mail.py exit
 fi
 done }while :do
 n=`pgrep -l httpd|wc -l` 
 #判断httpd服务进程数是否超过500
 if [ $n -gt 500 ] then
 /usr/local/apache2/bin/apachectl restart 
 if [ $? -ne 0 ] 
 then
 check_service 
 else
 sleep 60
 n2=`pgrep -l httpd|wc -l` 
 #判断重启后是否依旧超过500
             if [ $n2 -gt 500 ] 
 then 
 mail.py exit
 fi
 fi
 fi
 #每隔10s检测一次
 sleep 10done

15、批量修改服务器用户密码

Linux主机SSH连接信息:旧密码

# cat old_pass.txt 
192.168.18.217  root    123456     22
192.168.18.218  root    123456     22
内容格式:IP User Password Port

SSH远程修改密码脚本:新密码随机生成
https://www.linuxprobe.com/books
#!/bin/bash
OLD_INFO=old_pass.txt
NEW_INFO=new_pass.txt
for IP in $(awk '/^[^#]/{print $1}' $OLD_INFO); do
    USER=$(awk -v I=$IP 'I==$1{print $2}' $OLD_INFO)
    PASS=$(awk -v I=$IP 'I==$1{print $3}' $OLD_INFO)
    PORT=$(awk -v I=$IP 'I==$1{print $4}' $OLD_INFO)
    NEW_PASS=$(mkpasswd -l 8)  # 随机密码
    echo "$IP   $USER   $NEW_PASS   $PORT" >> $NEW_INFO
    expect -c "
    spawn ssh -p$PORT $USER@$IP
    set timeout 2
    expect {
        \"(yes/no)\" {send \"yes\r\";exp_continue}
        \"password:\" {send \"$PASS\r\";exp_continue}
        \"$USER@*\" {send \"echo \'$NEW_PASS\' |passwd --stdin $USER\r exit\r\";exp_continue}
    }"
done
生成新密码文件:

# cat new_pass.txt 
192.168.18.217  root    n8wX3mU%      22
192.168.18.218  root    c87;ZnnL      22

16、iptables 自动屏蔽访问网站频繁的IP

场景:恶意访问,安全防范

1)屏蔽每分钟访问超过200的IP

方法1:根据访问日志(Nginx为例)

#!/bin/bash
DATE=$(date +%d/%b/%Y:%H:%M)
ABNORMAL_IP=$(tail -n5000 access.log |grep $DATE |awk '{a[$1]++}END{for(i in a)if(a[i]>100)print i}')
#先tail防止文件过大,读取慢,数字可调整每分钟最大的访问量。awk不能直接过滤日志,因为包含特殊字符。
for IP in $ABNORMAL_IP; do
    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then
        iptables -I INPUT -s $IP -j DROP    fidone

方法2:通过TCP建立的连接

#!/bin/bash
ABNORMAL_IP=$(netstat -an |awk '$4~/:80$/ && $6~/ESTABLISHED/{gsub(/:[0-9]+/,"",$5);{a[$5]++}}END{for(i in a)if(a[i]>100)print i}')
#gsub是将第五列(客户端IP)的冒号和端口去掉
for IP in $ABNORMAL_IP; do
    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then
        iptables -I INPUT -s $IP -j DROP    
        fi
done

2)屏蔽每分钟SSH尝试登录超过10次的IP

方法1:通过lastb获取登录状态:

#!/bin/bash
DATE=$(date +"%a %b %e %H:%M") #星期月天时分  %e单数字时显示7,而%d显示07
ABNORMAL_IP=$(lastb |grep "$DATE" |awk '{a[$3]++}END{for(i in a)if(a[i]>10)print i}')for IP in $ABNORMAL_IP; do
    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then
        iptables -I INPUT -s $IP -j DROP    fidone

方法2:通过日志获取登录状态

#!/bin/bash
DATE=$(date +"%b %d %H")
ABNORMAL_IP="$(tail -n10000 /var/log/auth.log |grep "$DATE" |awk '/Failed/{a[$(NF-3)]++}END{for(i in a)if(a[i]>5)print i}')"
for IP in $ABNORMAL_IP; do
    if [ $(iptables -vnL |grep -c "$IP") -eq 0 ]; then
        iptables -A INPUT -s $IP -j DROP        
        echo "$(date +"%F %T") - iptables -A INPUT -s $IP -j DROP" >>~/ssh-login-limit.log    
    fi
done

17、根据web访问日志,封禁请求量异常的IP,如IP在半小时后恢复正常,则解除封禁

#!/bin/bash
####################################################################################
#根据web访问日志,封禁请求量异常的IP,如IP在半小时后恢复正常,则解除封禁
####################################################################################
logfile=/data/log/access.log
#显示一分钟前的小时和分钟
d1=`date -d "-1 minute" +%H%M`
d2=`date +%M`
ipt=/sbin/iptables
ips=/tmp/ips.txt
block()
{ 
#将一分钟前的日志全部过滤出来并提取IP以及统计访问次数
 grep '$d1:' $logfile|awk '{print $1}'|sort -n|uniq -c|sort -n > $ips
 #利用for循环将次数超过100的IP依次遍历出来并予以封禁
 for i in `awk '$1>100 {print $2}' $ips` 
 do
 $ipt -I INPUT -p tcp --dport 80 -s $i -j REJECT 
 echo "`date +%F-%T` $i" >> /tmp/badip.log 
 done
}
unblock()
{ 
#将封禁后所产生的pkts数量小于10的IP依次遍历予以解封
 for a in `$ipt -nvL INPUT --line-numbers |grep '0.0.0.0/0'|awk '$2<10 {print $1}'|sort -nr` 
 do 
 $ipt -D INPUT $a
 done
 $ipt -Z
}
#当时间在00分以及30分时执行解封函数
if [ $d2 -eq "00" ] || [ $d2 -eq "30" ] 
 then
 #要先解再封,因为刚刚封禁时产生的pkts数量很少
 unblock
 block 
 else
 block
fi

18、判断用户输入的是否为IP地址

方法1:

#!/bin/bash
function check_ip(){
    IP=$1
    VALID_CHECK=$(echo $IP|awk -F. '$1< =255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
    if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$">/dev/null; then
        if [ $VALID_CHECK == "yes" ]; then
            echo "$IP available."
        else
            echo "$IP not available!"
        fi
    else
        echo "Format error!"
    fi
}
check_ip 192.168.1.1
check_ip 256.1.1.1

方法2:

#!/bin/bash
function check_ip(){
    IP=$1
    if [[ $IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
        FIELD1=$(echo $IP|cut -d. -f1)
        FIELD2=$(echo $IP|cut -d. -f2)
        FIELD3=$(echo $IP|cut -d. -f3)
        FIELD4=$(echo $IP|cut -d. -f4)
        if [ $FIELD1 -le 255 -a $FIELD2 -le 255 -a $FIELD3 -le 255 -a $FIELD4 -le 255 ]; then
            echo "$IP available."
        else
            echo "$IP not available!"
        fi
    else
        echo "Format error!"
    fi
}
check_ip 192.168.1.1
check_ip 256.1.1.1

增加版:

加个死循环,如果IP可用就退出,不可用提示继续输入,并使用awk判断。

#!/bin/bash
function check_ip(){
    local IP=$1
    VALID_CHECK=$(echo $IP|awk -F. '$1< =255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
    if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >/dev/null; then
        if [ $VALID_CHECK == "yes" ]; then
            return 0
        else
            echo "$IP not available!"
            return 1
        fi
    else
        echo "Format error! Please input again."
        return 1
    fi
}
while true; do
    read -p "Please enter IP: " IP
    check_ip $IP
    [ $? -eq 0 ] && break || continue
done