Shell 基础及实例

1 shell 编程环境

1.1 编程基础知识

1.1.1 程序编程风格

  • 过程式:以指令为中心,数据服务于指令;
  • 对象式:以数据为中心,指令服务于数据;

shell 程序,提供了编程能力,解释执行,shell 就是一解释器;

1.1.2 程序的执行方式

过程式编程的三种结构;

  • 顺序执行
  • 循环执行
  • 选择执行

1.1.3 shell 脚本

首行特定格式: #!/bin/bash

1.1.4 运行脚本的两种方式

  • a、 给予执行权限,通过具体的文件路径指定文件执行;
  • b、 直接运行解释器,将脚本作为解释器程序的参数运行;

Example

    [root@localhost test1]# vim test.sh
    [root@localhost test1]# bash test.sh
    hello,girl
    [root@localhost test1]# chmod +x test.sh
    [root@localhost test1]# ./test.sh
    hello,girl

1.2 Bash 编程

  • bash 是弱类型编程,变量默认为字符型;
  • 把所有要存储的数据统统当做字符进行存储;
  • 变量不需要事先声明,可以在调用时直接赋值使用,参与运算会自动进行隐式类型转换;
  • 不支持浮点数;

1.3 逻辑运算

  • 与:&& 同为 1 则为 1,否则为 0;
  • 或:|| 同为 0 则为 0,否则为 1;
  • 非:取反,!0 为 1,!1 为 0;
  • 短路与运算:双目运算符前面的结果为 0,则结果一定为 0,后面的不执行;
  • 短路或运算:双目运算符前面的结果为 1,则结果一定为 1,后面的不执行;

2 bash 变量类型

变量类型决定了变量的数据存储格式、存储空间大小以及变量能参与的运算种类;

2.1 强弱类型语言的区别

  • 强类型:定义变量时必须执行类型、参与运算必须符合类型要求;调用未声明变量会产生错误;
  • 弱类型:无需指定类型,默认均为字符型;参与运算会自动进行隐式类型转换;变量无需事先定义即可直接调用;

2.2 Bash 中的变量

根据变量的生效范围等标准划分:

  • 本地变量:生效范围为当前 shell 进程;对当前 shell 之外的其他 shell 进程,包括当前 shell 的子 shell 进程均无效;
  • 环境变量:生效范围为当前 shell 进程及其子进程;
  • 局部变量:生效范围为当前 shell 进程中某代码片断(通常指函数)
  • 位置变量:$1, $2, ... 来表示,用于让脚本在脚本代码中调用通过命令行传递给它的参数;
  • 特殊变量:$?, $0, $*, $@, $#
    • $$: 代表所在命令的 PID(不常用)
    • $!: 代表最后执行的后台命令的 PID(不常用)
    • $?: 上一条命令的执行状态结果
    • $0: 命令本身
    • $*: 传递给脚本的所有参数,以一对双引号给出参数列表
    • $@: 传递给脚本的所有参数,将各个参数分别加双引号返回
    • $# : 传递给脚本的参数的个数;

2.2.1 本地变量

变量赋值:name='VALUE'

a) 在赋值时,VALUE 可以使用以下引用:

【1】可以是直接字符串;name="username"
【2】变量引用:name=“$username”
【3】命令引用:name=`COMMAND`,name=$(COMMAND)

变量引用:${name}, 花括号可省略:$name

引号引用:

  • " ": 弱引用,其中的变量引用会被替换为变量值;
  • ' ': 强引用,其中的变量不会被替换为变量值,而保持原字符串;

查看所有已定义的变量: #set

销毁变量: # unset name

2.2.2 环境变量

变量声明、赋值:
   export name=VALUE
   declare -x name=VALUE
变量引用:
   $name
   ${name}
显示所有环境变量:
   export
   env
   printenv
销毁环境变量:
   unset name

Bash 中内建的环境变量:

  • PATH,SHELL,UID,HISTSIZE, HOME, PWD, OLD, HISTFILE, PS1

2.2.3 只读变量

相当于常量,变量值不可变,不能再进行赋值运算;

声明只读变量的格式:

readonly name
declare –r name

2.2.4 位置变量

在脚本代码中调用通过命令行传递给脚本的参数;

$1,$2,…. : 对应调用第 1、第 2 等参数;
$0: 命令本身;
$*: 传递给脚本的所有参数;
$@: 传递给脚本的所有参数;
$#: 传递给脚本的参数的个数;

2.3 变量特殊用法

2.3.1 将多行结果赋值给变量

将多行结果赋值给变量时使用变量时需要注意

如:ls_data=$(ls -l)

  • 在 Mac 上直接输出 echo ${ls_data} 即正常结果
  • 在 CentOS 上操作时
    • echo ${ls_data} 时为不换行内容,整体只有一行
    • echo "${ls_data}" 时内容输出正常

比如要获取 redis 的 info 信息时,不加双引号输出时,输出的内容仅仅为多行结果的最后一行,加双引号输出时可以正常输出

2.3.2 去除行结果后的特殊字符

如要去掉 ^M

echo ${variable}|tr -d '\r'

3 bash 的配置文件

  • 全局配置:
    • /etc/profile
    • /etc/profile.d/*.sh
    • /etc/bashrc
  • 用户配置:
    • ~/.bash_profile
    • ~/.bashrc

4 bash 中的算术运算符

+,-,*,/,%,**

实现算术运算的方式:

(1) let var= 算术表达式
(2) var=$『算术表达式』
(3) var=$((算术表达式))
(4) var=$(expr arg1 arg2 arg3...)

乘法符号在有些场景中需要转义;

  • bash 内建随机数生成器:$RANDOM

  • 增强型赋值: +=,-=,*=,/=,%=

    let varOPERvalue:

      例如:letcount+=1
    
  • 自增,自减:

    let var+=1

      let var++
    

    let var-=1

      let var--
    

5 条件测试

判断某需求是否满足,需要有测试机制来实现;

  • Note:专用的测试表达式需要由测试命令辅助完成测试过程;

  • 测试命令:

    test EXPRESSION

[ EXPRESSION ]

[[ EXPRESSION ]]

Note: EXPRESSION 前后必须有空白字符,否则报错;

Bash 的测试类型

  • 数值测试:

    -gt : 是否大于; >

    -ge:是否大于等于; >=

    -eq:是否等于 ==

    -ne:是否不能于 !=

    -lt :是否小于 <

    -le :是否小于等于; <=

  • 字符串测试:

  • == : 是否等于;
  • > : 是否大于;
  • < : 是否小于;
  • != : 是否不等于;
  • =~ : 左侧字符串是否能够被右侧的 PATTERN 所匹配;
  • Note:此表达式一般用于 [[ ]] 中;

    -z “STRING” : 测试字符串是否为空,空则为真,不空则为假;

    -n “STRING” :测试字符串是否不空,不空则为真,空则为假;

Note:在字符串比较时用到的操作数都应该使用引号;

文件测试

  • (a) 存在性测试:

    -a FILE

    -e FILE:文件存在性测试,存在为真,否则为假;

  • (b) 存在性及类别测试

    -b FILE :是否存在且为块设备文件;

    -c FILE :是否存在且为字符设备文件;

    -d FILE :是否存在且为目录文件;

    -f FILE :是否存在且为普通文件;

    -h FILE 或 –L FILE:是否存在且为符号链接文件;

    -p FILE:是否存在且为命名管道文件;

    -S FILE :是否存在且为套接字文件;

  • (c) 文件权限测试:

    -r FILE:是否存在且可读

    -w FILE: 是否存在且可写

    -x FILE: 是否存在且可执行

  • (d) 文件特殊权限测试:

    -g FILE:是否存在且拥有 sgid 权限;

    -u FILE:是否存在且拥有 suid 权限;

    -k FILE:是否存在且拥有 sticky 权限;

  • (e) 文件大小测试:

    -s FILE:是否存在且非空;

  • (f) 文件是否打开:

    -t fd:fd 表示文件描述符是否已经打开且与某终端相关

    -N FILE:文件自从上一次被读取之后是否被修改过;

    -O FILE:当前有效用户是否为文件属主;

    -G FILE:当前有效用户是否为文件属组;

  • (g) 双目测试:

    FILE1 –ef FILE2 : FILE1 与 FILE2 是否指向同一个设备上的相同 inode;

    FILE1 –nt FILE2 :FILE 是否新于 FILE2;

    FILE1 –ot FILE2 :FILE1 是否旧于 FILE2;

  • (h) 组合测试条件:

    完成逻辑运算:

    第一种方式:

           COMMAND1 && COMMAND2
    
           COMMAND1 || COMMAND2
    
           !COMMAND
    
       eg:[ -e FILE ] && [ -r FILE ] 文件是否存在且是否有读权限;
    
 第二种方式:

         EXPRESSION1 –a EXPRESSION2

         EXPRESSION1 –o EXPRESSION2

         !EXPRESSION

 必须使用测试命令进行;
  • Example

    # [ -z "$hostName" -o "$hostName"=="localhost.localdomain" ] && hostname hostname_w
    # -z 判断 hostName 是否为空,-o 表示或者,即:hostName 为空或者值为 localhot.localdomain 的时候,使用 hostname 命令修改主机名;
    
  • Example

[root@bill ~]# [ -f /bin/cat -a -x /bin/cat ] && cat /etc/fstab
#判断文件 /bin/cat 是否存在且是否有可执行权限,&& 是短路与,如果前面执行结果为真则使用 cat 命令#查看文件;
#
# /etc/fstab
# Created by anaconda on Fri Jul 3 03:08:29 2015
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
/dev/mapper/centos-root / xfs defaults 0 0
UUID=3d04d82b-c52b-4184-8d64-1826db6e2eac /boot xfs defaults 0 0
/dev/mapper/centos-home /home xfs defaults 0 0
/dev/mapper/centos-swap swap swap defaults 0 0
  • 快速查找选项定义的方法:

    man bash --> /^[[:space:]]*-f

# [ -f /bin/cat -a -x /bin/cat ] && cat /etc/fstab
#如果文件存在且有执行权限,就用它查看文件内容;

6 bash 脚本编程之用户交互

  • read 命令:

    read [option]… [name….]

       -p “prompt” 提示符;
    
       -t TIMEOUT 用户输入超时时间;
    
  • 检测脚本中的语法错误:

       bash –n /path/to/some_script
    
  • 调试执行,查看执行流程:

       bash –x /path/to/some_script
    
  • Example

#!/bin/bash
#
#Description: Test read command's grammer.
read -t 50 -p "Enter a disk special file:" diskfile #将用户输入的内容赋值给 diskfile 变量
[ -z "$diskfile" ] && echo "Fool" && exit 1
#判断 diskfile 的值是否为空,如果为空则输出,并退出;
if fdisk -l | grep "^Disk $diskfile" &> /dev/null;then
 fdisk -l $diskfile
else
 echo "Wrong disk special file."
 exit 2
fi

7 流程控制

if 语句

if 语句:
     CONDITION:
     bash 命令:

用命令的执行状态结果:

 成功:true,即执行状态结果值为 0 时;

 失败:false,即执行状态结果为 1-255 时;

成功或失败的定义:取决于用到的命令;

  • 单分支 if:
    if CONDITION;then
       if-true(条件为真时的执行语句集合)
    fi
    
  • Example

    #!/bin/bash
    #if 单分支语句测试;
    if [ $UID -eq 0 ];then
       echo "It's amdinistrator."
    fi
    
  • 双分支 if:

    if CONDITION;then
      if-true
    else
      if-false
    fi
    
  • Example
#!/bin/bash
#
#if 双分支语句测试;
if [ $UID -eq 0 ];then
 echo "It's administrator."
else
 echo "It's Comman User."
fi
  • 多分支 if:

    if CONDITION1;then
       if-true
    elif CONDITION2;then
       if-true
    elif CONDITION3;then
       if-true
    ….
    else
       all-false
    fi
    

    逐条件进行判断,第一次遇为“真”条件时,执行其分支,而后结束;

  • Exmaple: 用户键入文件路径,脚本来判断文件类型;

#!/bin/bash
#
#if 语句多分支语句;
read -t 20 -p "Enter a file path:" filename

if [ -z "$filename" ];then #判断变量是否为空;
 echo "Usage:Enter a file path."
 exit 2
fi

if [ ! -e $filename ];then #判断用户输入的文件是否存在;
 echo "No such file."
 exit 3
fi

if [ -f $filename ];then #判断是否为普通文件;
 echo "A common file."
elif [ -d $filename ];then #判断是否为目录;
 echo "A directory"
elif [ -L $filename ];then #判断是否为链接文件;
 echo "A symbolic file."
else
 echo "Other type."
fi

for 循环

for 循环基础

循环体:要执行的代码,可能要执行 n 遍;

循环需具备进入循环的条件和退出循环的条件;

  • for 循环:

    for 变量名 in 列表;do
       循环体
    done
    
  • 执行机制:

    依次将列表中的元素赋值给“变量”;每次赋值后即执行一次循环体;直到列表中的元素耗尽,循环结束;

Example 添加 10 个用户,用户名为:user1-user10:密码同用户名

#!/bin/bash
#
#for 循环,使用列表;
#添加 10 个用户,user1-user10

if [ ! $UID -eq 0 ];then #判断执行脚本的是否为 root 用户,若不是则直接退出;
    echo "Only root can use this script."
    exit 1
fi

for i in {1..10};do
    if id user$i &> /dev/null;then    #判断用户是否已经存在;
        echo "user$i exists"
    else
        useradd user$i
        if [ $? -eq 0 ];then   #判断前一条命令是否执行成功;
            echo "user$i" | passwd --stdin user$i &> /dev/null
            #passwd 命令从标准输入获得命令,即管道前命令的执行结果;
        fi
    fi
done
  • 列表生成方式:

    (1) 直接给出列表 for i in {bill johnson rechard}

    (2) 整数列表

      (a) {start..end}
    
      (b) $(seq [start [step]] end)
    

    (3) 返回列表的命令

      $(COMMAND) --> 如:$(ls /var)
    

    (4) glob

      /etc/rc.d/rc3.d/K*
    

    (5) 变量引用

      $@ , $*  -->所有向脚本传递的参数;
    

Example 判断某路径下所有文件的类型

#!/bin/bash
#
#for 循环使用命令返回列表;

for file in $(ls /var);do #使用命令生成列表;
 if [ -f /var/$file ];then
     echo "Common file."
 elif [ -L /var/$file ];then
     echo "Symbolic file."
 elif [ -d /var/$file ];then
     echo "Directory."
 else
     echo "Other type"
 fi
done

Example 使用 for 循环统计关于 tcp 端口监听状态

#!/bin/bash
#
#使用 for 循环过滤 netstat 命令中关于 tcp 的信息;
declare -i estab=0
declare -i listen=0
declare -i other=0

for state in $( netstat -tan | grep "^tcp\>" | awk '{print $NF}');do

 if [ "$state" == 'ESTABLISHED' ];then
     let estab++
 elif [ "$state" == 'LISTEN' ];then
     let listen++
 else
     let other++
 fi
done

echo "ESTABLISHED:$estab"
echo "LISTEN:$listen"
echo "Unknow:$other"

for 循环的特殊格式

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

此种格式和 C 语言等的格式是一样一样的,只是多了一对括号;

控制变量初始化:仅在运行到循环代码段时执行一次;

控制变量的修正表达式:每轮循环结束会先进行控制变量修正运算,而后再在条件判断;

Example 求 100 以内所有正整数之和

#!/bin/bash
#
#for 循环,类似 C 语言格式,求 100 以内正整数之和;

declare -i sum=0

for ((i=1;i<=100;i++));do
    let sum+=$i
done

echo "Sum:$sum."

while 循环

while 基础

语法:

 while CONDITION;do
     循环体
 done
CONDITION:循环控制条件;进入循环之前,先做一次判断;
每一次循环之后会再次做判断;条件为“true”,则执行一次循环;
直到条件测试状态为“false”终止循环;

因此:CONDTION 一般应该有循环控制变量;
而此变量的值会在循环体不断地被修正,直到最终条件为 false,结束循环。

Example 用 while 求 100 以内所有正整数之和

#!/bin/bash
#使用 while 求 100 以内正整数之和;
declare -i sum=0
declare -i i=1
while [ $i -le 100 ];do
 let sum+=$i
 let i++
done
echo $i
echo "Summary:$sum."

Example 用 while 添加 10 个用户

#!/bin/bash
#
#使用 while 循环添加 10 个用户
declare -i i=1
declare -i users=0

while [ $i -le 10 ];do
 if ! id user$i &> /dev/null;then
     useradd user$i
     echo "Add user: user$i"
     let users++
 fi
 let i++
done
echo "Add $users users."

Example 利用 RANDOM 生成 10 个随机数字,输出这 10 个数字,并显示其中的最大者和最小者

#!/bin/bash
#
#利用 RANDOM 生成 10 个随机数,输出,并求最大值和最小值;
declare -i max=0
declare -i min=0
declare -i i=1

while [ $i -le 9 ];do
    rand=$RANDOM
    echo $rand

    if [ $i -eq 1 ];then
        max=$rand
        min=$rand
    fi

    if [ $rand -gt $max ];then
        max=$rand
    fi
    if [ $rand -lt $min ];then
        min=$rand
    fi
    let i++
done

echo "MAX:$max."
echo "MIN:$min."

创建死循环

while true;do
    循环体
done
until false;do
    循环体
done

Example 每隔 3 秒钟到系统上获取已经登录的用户信息;如果用户输入的用户名登录了,则记录于日志中,并退出

#!/bin/bash
#
#用 while 造成死循环,在系统上每隔 3 秒判断一次用户输入的用户名是否登录;
read -p "Enter a user name:" username

while true;do
    if who | grep "^$username" &> /dev/null;then
        break
    fi
    sleep 3
done

echo "$username logggen on." >> /tmp/user.log

while 循环遍历文件的每一行

    while read line;do
        循环体
    done < /PATH/FROM/SOMEFILE

依次读取 /PATH/FROM/SOMEFILE 文件中的每一行,且将该行赋值给变量 line;

另一种也很常见的用法【推荐】:

command | while read line
do
    ...
done

Example 依次读取 /etc/passwd 文件中的每一行,找出其 ID 号为偶数的所有用户,显示其用户名、ID 号及默认 shell

#!/bin/bash
#while 循环的特殊用法 读取指定文件的每一行并赋值给变量

while read line;do
    if [ $[`echo $line | cut -d: -f3` % 2] -eq 0 ];then
        echo -e -n "username:`echo $line | cut -d: -f1`\t"
        echo -e -n "uid: `echo $line | cut -d: -f3`\t"
        echo "SHELL:`echo $line | cut -d: -f7`"
    fi
done < /etc/passwd

while 与 for 的区别

行读取
  • while 循环
    • 以行读取文件
  • for 循环
    • 以空格和回车符分割读取文件,也就是碰到空格和回车,都会执行循环体,所以需要以行读取的话,就要把文件行中的空格转换成其他字符。
ssh 命令操作
  • for 循环
    • for line in $(cat $file) 在循环体中进行 ssh 命令操作可以依次执行
  • while 循环
    • 循环体内有 ssh、scp、sshpass 的时候会执行一次循环就退出的情况,解决该问题方法有如下两种
      • a、使用ssh -n "command"
      • b、将 while 循环内加入 null 重定向,如 ssh "cmd" < /dev/null 将 ssh 的输入重定向输入。

case 语句

case 变量引用 in
PAT1)
        分支 1
        ;
PAT2)
        分支 2
        ;
….
*)
        默认分支
        ;
esac

case 支持 glob 风格的通配符:

    *: 任意长度任意字符;
    ?: 任意单个字符;
    []:指定范围内的任意单个字符;
    a|b: a 或 b

Example 使用 case 语句改写前一个练习

#!/bin/bash
#
cat << EOF
cpu) show cpu information;
mem) show memory information;
disk) show disk information;
quit) quit
============================
EOF
read -p "Enter a option: " option
while [ "$option" != 'cpu' -a "$option" != 'mem' -a "$option" != 'disk' -a "$option" != 'quit' ]; do
    read -p "Wrong option, Enter again: " option
done

case "$option" in
cpu)
    lscpu
    ;;
mem)
    cat /proc/meminfo
    ;;
disk)
    fdisk -l
    ;;
*)
    echo "Quit..."
    exit 0
    ;;
esac

8 函数

函数基础

函数的作用:
过程式编程:为实现代码重用
     模块化编程
     结构化编程;
  • 语法一:

    function f_name {
      …函数体…..
    }
    
  • 语法二:

    f_name() {
      …函数….
    }
    
  • 函数调用:函数只有被调用才会执行:

    调用:给定函数名

      函数名出现的地方,会被自动替换为函数代码;
    
  • 函数的生命周期:被调用时创建,返回时终止;

    return 命令返回自定义状态结果;

      0:成功
    
      1-255:失败
    

Example 通过函数,创建 10 个用户

#!/bin/bash
#
#通过调用函数添加 10 个用户

function adduser {
    if id $username &> /dev/null;then
        echo "$username exists."
        return 1
    else
        useradd $username
        [ $? -eq 0 ] && echo "Add $username finished." && return 0
    fi
}

for i in {1..10};do
    username=myuser$i
    adduser
done

Example 编写一个服务启动关闭脚本

#!/bin/bash
# chkconfig: - 88 12
# description: test service script
prog=$(basename $0)
lockfile=/var/lock/subsys/$prog
start() {
    if [ -e $lockfile ];then
        echo "$prog is already running."
        return 0
    else
        touch $lockfile
        [ $? -eq 0 ] && echo "Starting $prog finished."
    fi
}
stop() {
    if [ -e $lockfile ];then
        rm -f $lockfile && echo "Stop $prog ok."
    else
        echo "$prog is stopped yet."
    fi
}
status() {
    if [ -e $lockfile ];then
        echo "$prog is running."
    else
        echo "$prog is stopped."
    fi
}
usage() {
    echo "Usage:$prog {start | stop | restart | status}"
}
if [ $# -lt 1 ];then
    usage
    exit 1
fi

case $1 in
start)
    start
    ;;
stop)
    stop
    ;;
restart)
    stop
    start
    ;;
status)
    status
    ;;
*)
    usage
esac

函数返回值

函数的执行结果返回值:

(1)    使用 echo 或 print 命令进行输出;

(2)    函数体中调用命令的执行结果;

函数的退出状态码:

(1)    默认取决于函数体中执行的最后一条命令的退出状态码;

(2)    自定义退出状态码;

    使用 return 关键字;

函数可以接受参数:

    传递参数给函数:调用函数时,在函数名后面以空白分隔给定参数列表即可;

例如:“testfunc arg1 arg2 …”

在函数体当中,可使用 $1,$2,…. 调用这些参数;还可以使用 $@,$*,$#等特殊变量;
#!/bin/bash
#
#使用带参数的函数添加 10 个用户

function adduser {            #一个可接受参数的函数
    if [ $# -lt 1 ];then
        return 2   # 2: no arguments
    fi

    if id $1 &> /dev/null;then
        echo "$1 exists."
        return 1;
    else
        useradd $1
        [ $? -eq 0 ] && echo "Add $1 finished." && return 0
    fi
}

while true;do        #死循环,直到输入的值为 quit 是退出;
    read -t 20 -p "Please input your username(quit to cancel):" username
    if [ $username == "quit" ];then
        echo "Quit."
        exit 0
    else
        adduser $username
    fi
done
  • 变量作用域:

    本地变量:当前 shell 进程,为了执行脚本会启动专用的 shell 进程;因此,本地变量的作用范围是当前 shell 脚本程序文件;

    局部变量:函数的生命周期:函数结束时变量被自动销毁;

      如果函数中有局部变量,其名称同本地变量无关;
    

在函数中定义局部变量的方法:

    local NAME=VALUE

函数递归:函数直接或间接调用自身;

Example 求 N 的阶乘

N!=N(N-1)(N-2)….1
    N(N-1)!=N(N-1)(N-2)!
#!/bin/bash
#
#利用函数递归求 N 的阶乘

fact() {
    if [ $1 -eq 0 -o $1 -eq 1 ];then
        echo 1
    else
        echo $[$1*$(fact $[$1-1])]
    fi
}
fact 5

9 数组

数组

  • 变量:存储单个元素的内存空间;
  • 数组:存储多个元素的连续的内存空间;
    • 索引:编号从 0 开始,属于数值索引;
    • 注意:索引页可支持使用自定义的格式,而不仅仅是数值格式;bash 的数组支持稀疏格式;

定义

在 Shell 中,用括号来表示数组,数组元素用“空格”符号分割开。定义数组的一般形式为:

array_name=(value1 ... valuen)

例如:

array_name=(value0 value1 value2 value3)

或者

array_name=(
value0
value1
value2
value3
)

还可以单独定义数组的各个分量:

array_name[0]=value0
array_name[1]=value1
array_name[2]=value2

Example 生成 10 个随机数保存于数组中,并找出其最大值和最小值

#!/bin/bash
#
#生成 10 个随机数保存于数组中;

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

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

echo "Max:$max"
echo "Min:$min"

Example 定义一个数组,数组中的元素是 /var/log 目录下所有以.log 结尾的文件;要统计其下标为偶数的文件中的行数之和

#!/bin/bash
#
#定义一个数组,数组中的元素是 /var/log 目录下所有以.log 结尾的文件;
#要统计其下标为偶数的文件中的行数之和;

declare -a files
files=(/var/log/*.log)
declare -i lines=0

for i in $(seq 0 $[${#files[*]}-1]);do
    if [ $[$i%2] -eq 0 ];then
        let lines+=$(wc -l ${files[$i]} | cut -d' ' -f1)
    fi
done

echo "Lines:$lines."

引用数组中的元素

  • 所有元素:${ARRAY[@]} , ${ARRAY[*]}

  • 数组切片:${ARRAY[@]:offset:number}

      offset: 要跳过的元素个数;
    
      number:要取出的元素个数,取偏移量之后的所有元素:${ARRAY[@]:offset};
    
  • 向数组中追加元素:ARRAY[${ARRAY[*]}]

  • 删除数组中的某元素:unset ARRAY[INDEX]

  • 关联数组: declare –A ARRAY_NAME ARRAY_NAME=([index_name1]=’val1’ [index_name2]=’val2’ ….)

[root@bill scripts]# declare -a array
[root@bill scripts]# #声明一个数组,不是必要的
[root@bill scripts]# array=(0 1 2)
[root@bill scripts]# array=([0]=0 [1]=1 [2]=v2)
[root@bill scripts]# array[0]=5
[root@bill scripts]# echo $array
5
[root@bill scripts]# #以空白作为分隔符拆分字符串为数组
[root@bill scripts]# str="1 2 3"
[root@bill scripts]# array=($str)
[root@bill scripts]# echo $array
1
[root@bill scripts]# #使用其他分隔符拆分字符串为数组,需指定 IFS
[root@bill scripts]# IFS=: array=($PATH)
[root@bill scripts]# echo $array
/usr/local/sbin
  • 引用数组元素: $array ${array} ${array[0]} #第 0 个元素

    ${array[n]} #第 n 个元素(n 从 0 开始计算)

  • 引用整个数组: ${array[*]} ${array[@]} 这两种方式等同,会把数组展开。

    ${array[*]} 表示把数组拼接在一起的整个字符串,如果作为参数传递,会把整个字符串作为一个参数。

    ${array[@]} 如果作为参数传递,表示把数组中每个元素作为一个参数,数组有多少个元素,就会展开成多少个参数。

  • 计算数组元素长度;

    ${#array[*]}

    ${#array[@]}

      不是 ${#array},
    
      因为它等同于 ${#array[0]}
    
  • 遍历数组: for i in "${array[@]}";do

      echo $i;
    

    done

10 bash 的字符串处理工具

字符串切片

  • ${var:offset:number}

    取字符串最右侧的几个字符:${var: -lengh}

    Note:冒号后面必须有一空白字符;

基于模式取子串

  • ${var#*word}:其中 word 可以是指定的任意字符;

    功能:自左而右,查找 var 变量所存储的字符串中,第一次出现的 word, 删除字符串开头至第一次出现 word 字符之间的所有字符;

  • ${var##*word}:其中 word 可以是指定的任意字符;

    功能:自左而右,查找 var 变量所存储的字符串中出现的 word,删除字符串开头至最后一次由 word 指定的字符之间的所有内容;

bash 基于模式取子串

[root@bill scripts]# file="/var/log/messages"
[root@bill scripts]# echo ${file#*/}
var/log/messages
[root@bill scripts]# echo ${file##*/}
messages
  • ${var%word*}:其中 word 可以是指定的任意字符;

    功能:自右而左,查找 var 变量所存储的字符串中,第一次出现的 word, 删除字符串最后一个字符向左至第一次出现 word 字符之间的所有字符;

  • ${var%%word*}:其中 word 可以是指定的任意字符;

    功能:自右而左,查找 var 变量所存储的字符串中出现的 word,,只不过删除字符串最右侧的字符向左至最后一次出现 word 字符之间的所有字符;

    [root@bill scripts]# file="/var/log/messages" #从右至左,匹配‘/ ’
    [root@bill scripts]# echo ${file%/*}
    /var/log
    [root@bill scripts]# echo ${file%%/*}    # 双 % 匹配并删除后为空;
    [root@bill scripts]#
    

Exampleurl=http://www.google.com:80, 分别取出协议和端口

[root@bill scripts]# url=http://www.google.com:80
[root@bill scripts]# echo ${url##*:}    #取端口号
80
[root@bill scripts]# echo ${url%%:*}   #取协议
http
[root@bill scripts]#

查找替换

${var/pattern/substi}:查找 var 所表示的字符串中,第一次被 pattern 所匹配到的字符串,以 substi 替换之;

${var//pattern/substi}: 查找 var 所表示的字符串中,所有能被 pattern 所匹配到的字符串,以 substi 替换之;

${var/#pattern/substi}:查找 var 所表示的字符串中,行首被 pattern 所匹配到的字符串,以 substi 替换之;

${var/%pattern/substi}:查找 var 所表示的字符串中,行尾被 pattern 所匹配到的字符串,以 substi 替换之;
[root@bill scripts]# var=$(head -n 1 /etc/passwd)
[root@bill scripts]# echo $var
root x 0 0 root /root /bin/bash
[root@bill scripts]# echo ${var/root/ROOT}  #替换第一次匹配到的 root 为 ROOT
ROOT x 0 0 root /root /bin/bash
[root@bill scripts]# echo ${var//root/ROOT}  #替换所有匹配到的 root 为 ROOT
ROOT x 0 0 ROOT /ROOT /bin/bash
[root@bill scripts]# useradd bash -s /bin/bash
[root@bill scripts]# cat /etc/passwd | grep "^bash.*bash$"
bash:x:1029:1029::/home/bash:/bin/bash
[root@bill scripts]# line=$(cat /etc/passwd | grep "^bash.*bash$")
[root@bill scripts]# echo $line
bash x 1029 1029  /home/bash /bin/bash
[root@bill scripts]# echo ${line/#bash/BASH}    #替换行首的 bash 为 BASH
BASH x 1029 1029  /home/bash /bin/bash
[root@bill scripts]# echo ${line/%bash/BASH}    #替换行尾的 bash 为 BASH
bash x 1029 1029  /home/bash /bin/BASH

查找并删除

${var/pattern}:查找 var 所表示的字符串中,删除第一次被 pattern 所匹配到的字符串

${var//pattern}:查找 var 所表示的字符串中,删除所有被 pattern 所匹配到的字符串;

${var/#pattern}:查找 var 所表示的字符串中,删除行首被 pattern 所匹配到的字符串;

${var/%pattern}:查找 var 所表示的字符串中,删除行尾被 pattern 所匹配到的字符串;
  • Example ``` [root@bill scripts]# line=$(tail -n 1 /etc/passwd) [root@bill scripts]# echo $line bash x 1029 1029 /home/bash /bin/bash [root@bill scripts]# echo ${line/bash} #查找并删除第一次匹配到的 bash x 1029 1029 /home/bash /bin/bash [root@bill scripts]# echo ${line//bash} #查找并删除所有匹配到的 bash x 1029 1029 /home/ /bin/ [root@bill scripts]# echo ${line/#bash} #查找并删除匹配到的行首的 bash x 1029 1029 /home/bash /bin/bash [root@bill scripts]# echo ${line/%bash} #查找并删除匹配到的行尾 bash bash x 1029 1029 /home/bash /bin/

### 字符大小写转换
    ${var^^}:把 var 中的所有小写字母转换为大写;

    ${var,,}:把 var 中的所有大写字母转换为小写;

- Example

[root@bill scripts]# line=$(tail -n 1 /etc/fstab) #将文件最后一行的值赋值给变量 [root@bill scripts]# echo ${line^^} #全部转换为大写后输出 /DEV/MAPPER/CENTOS-SWAP SWAP SWAP DEFAULTS 0 0 [root@bill scripts]# line=echo ${line^^} #将转换为大写后的值在赋值给变量; [root@bill scripts]# echo $line #确认目前变量的值全为大写字母; /DEV/MAPPER/CENTOS-SWAP SWAP SWAP DEFAULTS 0 0 [root@bill scripts]# echo ${line,,} #全部转换为大写后输出; /dev/mapper/centos-swap swap swap defaults 0 0


### 变量赋值

    ${var:-value}:如果 var 为空或未设置,那么返回 value;否则,则返回 var 的值;

    ${var:=value}:如果 var 为空或未设置,那么返回 value,并将 value 赋值给 var;否则,则返回 var 的值;

- Example

[root@bill scripts]# echo $test #变量值为空;

[root@bill scripts]# echo ${test:-helloworld} # :- 仅返回设定值,不修改; helloworld [root@bill scripts]# echo $test #变量的值依然为空;

[root@bill scripts]# echo ${test:=helloworld} # := 返回设定值,并赋值给变量; helloworld [root@bill scripts]# echo $test # 变量值已修改; helloworld

## 11 Bash 命令自动补全

### 11.1 内置补全命令

Bash 内置有两个补全命令,分别是 compgen 和 complete。compgen 命令根据不同的参数,生成匹配单词的候选补全列表,例如:

$ compgen -W 'hi hello how world' h hi hello how

compgen 最常用的选项是 -W,通过 -W 参数指定空格分隔的单词列表。h 即我们在命令行当前键入的单词,执行完后会输出候选的匹配列表,这里是以 h 开头的所有单词。

complete 命令的参数有点类似 compgen,不过它的作用是说明命令如何进行补全,例如同样使用 -W 参数指定候选的单词列表:

$ complete -W 'word1 word2 word3 hello' foo $ foo w $ foo word word1 word2 word3

我们还可以通过 -F 参数指定一个补全函数:

$ complete -F _foo foo

现在键入 foo 命令后,会调用_foo 函数来生成补全的列表,完成补全的功能,这一点正是补全脚本实现的关键所在,我们会在后面介绍。

补全相关的内置变量

除了上面的两个补全命令外,Bash 还有几个内置的变量用来辅助补全功能,这里主要介绍其中三个:

> * COMP_WORDS: 类型为数组,存放当前命令行中输入的所有单词;
> * COMP_CWORD: 类型为整数,当前光标下输入的单词位于 COMP_WORDS 数组中的索引;
> * COMPREPLY: 类型为数组,候选的补全结果;
> * COMP_WORDBREAKS: 类型为字符串,表示单词之间的分隔符;
> * COMP_LINE: 类型为字符串,表示当前的命令行输入;

例如我们定义这样一个补全函数_foo:

$ function _foo() { echo -e "\n" declare -p COMP_WORDS declare -p COMP_CWORD declare -p COMP_LINE declare -p COMP_WORDBREAKS } $ complete -F _foo foo

假设我们在命令行下输入以下内容,再按下 Tab 键补全:

$ foo b

declare -a COMP_WORDS='([0]="foo" [1]="b")' declare -- COMP_CWORD="1" declare -- COMP_LINE="foo b" declare -- COMP_WORDBREAKS=" \"'><=;|&(:"

对着上面的结果,我想应该比较容易理解这几个变量。当然正如我们之前据说,Bash-completion 包并非是必须的,补全功能是 Bash 自带的。

### 11.2 编写脚本

补全脚本分成两个部分:编写一个补全函数和使用 complete 命令应用补全函数。后者的难度几乎忽略不计,重点在如何写好补全函数。难点在,似乎网上很少与此相关的文档,但是事实上,Bash-completion 自带的补全脚本是最好的起点,可以挑几个简单的改改基本上就可以使用了。

一般补全函数(假设这里依然为_foo) 都会定义以下两个变量:

local cur prev

其中 cur 表示当前光标下的单词,而 prev 则对应上一个单词:

cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}"

#### 11.2.1 支持主选项

初始化相应的变量后,我们需要定义补全行为,即输入什么的情况下补全什么内容,例如当输入 - 开头的选项的时候,我们将所有的选项作为候选的补全结果:

local opts="-h --help -f --file -o --output"

if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 fi

不过再给 COMPREPLY 赋值之前,最好将它重置清空,避免被其它补全函数干扰。

现在完整的补全函数是这样的:

function _foo() { local cur prev opts

COMPREPLY=()

cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="-h --help -f --file -o --output"

if [[ ${cur} == -* ]] ; then
    COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
    return 0
fi

}

现在在命令行下就可以对 foo 命令进行参数补全了:

$ complete -F _foo foo $ foo - -f --file -h --help -o --output


#### 11.2.2 支持子选项
当然,似乎我们这里的例子没有用到 prev 变量。用好 prev 变量可以让补全的结果更加完整,例如当输入 --file 之后,我们希望补全特殊的文件(假设以.sh 结尾的文件):
case "${prev}" in
    -f|--file)
        COMPREPLY=( $(compgen -o filenames -W "`ls *.sh`" -- ${cur}) )
        ;;
esac
现在再执行 foo 命令,--file 参数的值也可以补全了:

$ foo --file a.sh b.sh c.sh

#### 11.2.3 安装补全脚本

如果安装了 Bash-completion 包,可以将补全脚本放在 /etc/bash_completion.d 目录下,或者放到~/.bash_completion 文件中。
如果没有安装 Bash-completion 包,可以把补全脚本放到~/.bashrc 或者其它能被 shell 加载的初始化文件中。
## 12 常用实例
### 12.1 推荐添加内容

set -u # 确保变量都被初始化 set -e # 确保捕获所有非 0 状态 set -o pipefail # 结合 -e 可以捕获管道后的异常状态

### 12.2 脚本的配置文件
-    (1) 定义文本文件,每行定义"name=value"
-    (2) 在脚本中 source 此文件即可

[root@bill scripts]# touch /tmp/config.test #创建配置文件; [root@bill scripts]# echo "name=bill" >> /tmp/config.test #在配置文件中定义变量; [root@bill scripts]# vim script_configureFile.sh #编写脚本,导入配置文件;如内容所示; [root@bill scripts]# bash script_configureFile.sh #脚本执行结果; bill [root@bill scripts]# cat script_configureFile.sh

!/bin/bash

# source /tmp/config.test #导入配置文件,脚本自身并未定义变量;

echo $name #引用的是配置文件中的变量 name

### 12.3 ssh 登录相关

> * 可以使用 sshpass 进行直接传入密码
> * 一定要加 -o StrictHostKeyChecking=no 否则如果是第一次访问对应的机器,会执行无效

ssh_option="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o NumberOfPasswordPrompts=0 -o ConnectTimeout=3 -o ConnectionAttempts=3" export WSSH="./tools/sshpass -p ${PASSWD} ssh ${ssh_option}" export WSCP="./tools/sshpass -p ${PASSWD} scp ${ssh_option}"

> * StrictHostKeyChecking=no        自动信任主机并添加到known_hosts文件
> * UserKnownHostsFile=/dev/null    跳过检查 ~/.ssh/known_hosts 中的公钥操作,比如因为远端机器重装导致无法登录问题
> * NumberOfPasswordPrompts=0       规避没有信任关系挂死的问题,当对应的机器需要输入密码时,会直接返回异常(异常返回码为 255),而不是阻塞在输入密码页面
> * ConnectTimeout=3                连接超时时间,3秒
> * ConnectionAttempts=3            连接失败后重试次数,3次
--------------------- 
作者:DemonHunter211 
来源:CSDN 
原文:https://blog.csdn.net/kwame211/article/details/79076513 
版权声明:本文为博主原创文章,转载请附上博文链接!
### 12.4 ping 文件列表中所有主机

!/bin/bash

file=$1

[[ -z $file ]] && exit 0 cat $file| while read line;do ping=ping -c 1 $line|grep loss|awk '{print $6}'|awk -F "%" '{print $1}' if [ $ping -eq 100 ];then echo ping $i fail else echo ping $i ok fi done

### 12.5 shell 模板变量替换

#### 应用场景

双引号是 json 的标准,如果一个服务的配置文件是 json 的,使用特定的配置(模板)生成对应的配置文件时。要是使用 shell,这样也可以做到:

#### 使用方式

模板文件

{ "test_ip" : ${test_ip}, "test_port" : ${test_port}, }

脚本

test_host="127.0.0.1" test_port=9099

content=$(cat ./config_tpl) content_new=$(eval "cat <<EOF $content EOF")

echo $content_new


## 13 日常使用库

f_yellow='\e[00;33m' f_red='\e[00;31m' f_green='\e[00;32m' f_reset='\e[00;0m'

function p_warn { echo -e "${f_yellow}[WRN]${f_reset} ${1}" }

function p_err { echo -e "${f_red}[ERR]${f_reset} ${1}" }

function p_ok { echo -e "${f_green}[OK ]${f_reset} ${1}" }

ROOT_PATH=S=\readlink "$0"`; [ -z "$S" ] && S=$0; dirname $S` cd ${ROOT_PATH} ```

results matching ""

    No results matching ""