Linux shell 编程

shell 脚本执行方式
  • bash shellscript.sh./shellscript.sh都是在使用一个新的bash环境(子进程)来执行脚本内的内容。
  • source shellscript.sh是在原父进程执行脚本内容。
shell 脚本默认变量
变量 意义
$# 指传给脚本的参数个数(不包括$0)
$0 指脚本文件本身名字
$@ 传给脚本的所有参数(不包括$0)
$$ 是脚本运行的当前进程ID号
$? 显示最后一个命令的退出状态,0表示没有错误,其他表示有错误
命令执行判断
  • cmd1; cmd2
    顺序执行
    sync; sync; shutdown -f           
    
  • cmd1 && cmd2
    • 若cmd1执行正确则执行cmd2
    • 若cmd1执行错误则不执行cmd2
    # 查看txt文件是否存在,存在就新建txt2
    ls txt && touch txt2
    
  • cmd1 || cmd2
    • 若cmd1执行正确则不执行cmd2
    • 若cmd1执行错误则执行cmd2
    # 查看txt文件是否存在,不存在就新建txt。
    ls txt || touch txt
    
  • cmd && cmd1 || cmd2
    cmd 执行成功则执行cmd1,执行失败则执行cmd2,实际上是 (cmd && cmd1) || cmd2
    # 查看txt是否存在,存在输出exit,不存在输出no exit
    test -f txt && echo 'exit' || echo 'no exit'
    
数据流重定向

在输出重定向中,>代表的是覆盖,>>代表的是追加。

  • 标准输入,使用<
  • 标准输出,使用>
  • 标准错误输出,使用2>,标准输出和错误输出重定向 2>&1

shell 脚本中的重定向写法:

类型 符号 作用
标准输出重定向 command >file 以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。
标准输出重定向 command >>file 以追加的方式,把 command 的正确输出结果输出到 file 文件中。
标准错误输出重定向 command 2>file 以覆盖的方式,把 command 的错误信息输出到 file 文件中。
标准错误输出重定向 command 2>>file 以追加的方式,把 command 的错误信息输出到 file 文件中。
正确输出和错误信息同时保存 command >file 2>&1 以覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。
正确输出和错误信息同时保存 command >>file 2>&1 以追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。
正确输出和错误信息同时保存 command >file1 2>file2 以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。
正确输出和错误信息同时保存 command >>file1 2>>file2 以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。
""''区别
  • 以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出
  • 以双引号" "包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出
根据变量状态为其赋值

TODO

字符串操作
  • 获取长度
    str="hello world"
    
    echo ${#str}  # 输出 11
    
  • 获得子串位置
    str="hello world hello world"
    
    expr index "${str}" "hello"   # 输出1,字符串下标从1开始
    expr index "${str}" "stu"     # 没有输出0
    
  • 截取
    • 按照索引截取
      str="hello world"
      
      echo ${str:6}      # 输出 world
      echo ${str:0:5}    # 输出 hello,截取[0, 0+5] 范围的
      echo ${str:(-5)}   # 输出world, 从左往右数, 截取长度5
      
  • 匹配替换
    str="hello world hello world"
    
    echo ${str/hello/stu}     # 输出 stu world hello world  / 替换一次    
    echo ${str//hello/stu}    # 输出 stu world stu world  // 替换所有匹配    
    echo ${str/#he/xx}        # 输出 xxllo world hello world   # 以什么开头来匹配 
    echo ${str/%ld/yy}        # 输出 hello world hello woryy  % 以什么结尾来匹配
    

参考:https://www.cnblogs.com/gaochsh/p/6901809.html

数组

shell中的数据分为2类:一类是普通数组,另一类是关联数组。

  • 数组定义方式

    array_test=(a b c d e f)
    declare -a array_test  # 先声明一个空的数组,后面可以根据索引来动态添加
    
  • 数组

    命令 解释 结果
    ${A[@]} 返回数组全部元素 a b c d e f
    ${A[0]} 返回数组第一个元素 a
    ${#A[@]} 返回数组元素总个数 4
    ${#A[3]} 返回第四个元素的长度,即def的长度 3
    A[3]=xzy 则是将第四个组数重新定义为 xyz
  • 将命令结果直接赋值给数组

    array=(`ls`)
    
数值运算
  • 加法

    #!/bin/bash
    
    service_port=10000
    port1=`expr ${service_port} + 1`      # port = 10001
    port2=`expr ${service_port} - 1`      # port = 9999
    port3=`expr ${service_port} \* 1`     # port = 10000
    port4=`expr ${service_port} / 3`      # port = 3333
    port5=`expr ${service_port} % 3`      # port = 1
    
  • 浮点运算

    #! /bin/bash
    i=90.0
    j=20.5
    c=`echo "scale=2;${i}" / "${j}" | bc`  # scale=2表示保留2位小数
    echo $c
    
if else
if [ 表达式1 ]; then
    ...
elif [ 表达式2 ]; then
    ...
else
    ...
fi

条件与 if [ 表达式1 -a 表达式2 ]
条件或 if [ 表达式1 -o 表达式2 ]

  • if 中数值比较

    if [ ${num} -eq 3 ]; then 
    
    判断式 意义
    -eq 相等
    -ne 不等
    -gt 大于
    -ge 大于等于
    -lt 小于
    -le 小于等于
  • if 中字符串比较

    if [ "$test"x = "test"x ]; then
    

    这里的关键有几点:

    1. 使用单个等号
    2. 注意到等号两边各有一个空格:这是unix shell的要求
    3. 注意到 "\$test"x 最后的x,这是特意安排的,因为当 $test 为空的时候,上面的表达式就变成了 x = testx, 显然是不相等的,而如果没有这个x,表达式执行就会报错:[: =: unary operator expected
  • 其他判断式

    判断式 意义
    -e filename 判断文件名是否存在
    -f filename 判断文件名是否存在且为文件
    -d filename 判断文件名是否存在且为目录
    -b -c -S -p -L 判断文件名是否存在且为块设备,字符设备,Socket文件,管道文件,连接文件
    -r filename 检测该文件名是否存在且具有读权限
    -w filename 检测该文件名是否存在且具有写权限
    -x filename 检测该文件名是否存在且具有执行权限
    local l_file=$1
    if [ -f "${l_file}" ]; then
        echo "found"
    else
        echo "not found"
    fi
    
for循环
# 固定循环
for var in con1 con2 con3
do 
    ...
done

# 普通的循环
for ((i=0; i<5; i++))  # 也可以使用: for i in `seq 1 1 10`
do
    ...
done

# 如果写在一行
for ((i=0; i<5; i++)); do echo "hello world"; done
while 循环
while [条件判断式]
do
    ...
done
  • 无限循环
    while :
    do
        ...  
    done
    
    #!/bin/bash
    
    kiwi_deploy_host_ip="10.1.59.21"
    max_cnt=300
    cnt=0
    
    while :
    do
        ping ${kiwi_deploy_host_ip} -c 1 > /dev/null
        if [ $? -eq 0 ]; then
            echo "success"
            break
        fi
        
        cnt=$(expr ${cnt} + 1)
        
        if [ ${cnt} -eq ${max_cnt} ]; then
            echo "timeout"
            exit 1
        fi
    done
    
case esac
case $变量名称 in
    "第一个变量内容")
        ......
    ;;        # 有2个分号
    "第二个变量内容")
        ......
     ;;        # 有2个分号
        ......
esac       
函数
  • 函数传参注意

    #!/bin/bash
    function func_param() 
    {
        local l_var=$1;
        echo "${l_var}"
    }
    
    mysql_user_info="-usheng -psheng0"
    func_param ${mysql_user_info}        # 输出 -usheng
    func_param "${mysql_user_info}"      # 输出 -usheng -psheng0
    
  • 函数内定义local 变量,防止名称污染

    var="hello"
    
    function test()
    {
        local var="world";
        echo "${var}"
    }
    
    test    # 输出 world
    
  • 函数返回字符串
    shell函数只能返回数字,不允许返回字符串,但是可以用点小技巧返回字符串。

    #! /bin/bash
    
    function return_str()
    {
        local l_str="hello world"
        echo "${l_str}"
    }
    
    str=$(return_str)
    echo ${str}    # hello world
    
  • 向函数传递数组和从函数返回数组

    #!/bin/bash
    
    function file_list() {
      array=(`ls`)
      echo "${array[@]}"
    }
    
    list=($(file_list))
    echo "len: ${#list[@]}"
    echo "${list[@]}"
    
多任务
  • 使用后台运行&实现多任务并行

    #!/bin/bash
    
    for i in {1..254};do
        ip="192.168.80.$i"
        ping -c 2 $ip &> /dev/null && echo $ip is up &
    done
    
    • 后台运行 &nohup
      https://www.jianshu.com/p/747e0d5021a2
  • 使用wait实现任务同步
    假设我们有数据库初始化脚本1.sh,辅助程序安装脚本2.sh,最后是启动主程序脚本3.sh,我们希望1.sh 和 2.sh可以并行,但3.sh需要1.sh 和 2.sh执行完毕。

    #!/bin/bash
    
    ./1.sh &
    ./2.sh &
    wait
    ./3.sh
    
注意在 < > 左边的变量不会展开
#!/bin/bash
  
ip=10.1.59.43
cmd="ping -c 2 ${ip} &> /dev/null"
${cmd}      # 这里会报错,需要用 eval ${cmd}

eval命令将首先会先扫描命令行进行所有的置换,然后再执行该命令。

ssh 执行命令配合awk时
# ssh xxx "cat /etc/passwd | grep root | awk '{print $1, $2}'"   # 这里会报错
# ssh xxx "cat /etc/passwd | grep root | awk '{print \$1, \$2}'" # 正确写法
判断一个变量是否为空
#!/bin/bash

function check_var_is_null()
{
    local l_var=$1
    if [ ! -n "${l_var}" ]; then
        echo "var is null"
    else
        echo "var is not null"
    fi
}

para1=""
para2=
check_var_is_null ${para1}  # 输出 var is null
check_var_is_null ${para2}  # 输出 var is null
check_var_is_null ${para3}  # 输出 var is null
不可逆操作引用环境变量时用${var:?"undefined 'var'"}
#!/bin/bash

rm -rf ${dir}/ # 如果dir未定义, 则删除根目录.
rm -rf ${dir:?"undefined 'dir'"} # 如果dir未定义, 报错. 
文本替换
#!/bin/bash

function global_replace() 
{
    local l_file=$1
    local l_old_word=$2
    local l_new_word=$3

    eval sed -i 's/${l_old_word}/${l_new_word}/g' ${l_file}
    if [ $? -ne 0 ]; then
        echo "error: replace word faild, exit"
        exit 1
    fi
}
定义一个彩色输出函数
#!/bin/bash

function color_echo() 
{
    if [ $1 == "green" ]; then
        echo -e "\033[32;40m$2\033[0m"
    elif [ $1 == "red" ]; then
        echo -e "\033[31;40m$2\033[0m"
    fi
}

color_echo red "test"
进制转换
  • $((N#xx)) 可以将N进制表示的xx转为十进制表示
    echo $((2#110))
     6
    
  • 将十进制转为其他进制
    利用 bc 来完成这一操作
    echo 'obase=16;15' | bc
     F
    
编写可靠shell脚本技巧

set -表示启用某些选项,set +表示关闭某些选项。

  • -x
    在执行每一个命令之前把经过的变量展开之后的命令打印出来。

  • -e
    遇到一个命令失败(返回值非0)时,立即退出。

    #!/bin/bash
    
    set -e
    
    mysql -uroot -e 'drop database xx'
    echo "hello world"
    

    假设我们数据库中并不存在xx,那么输出如下:

    #./test.sh 
      ERROR 1008 (HY000) at line 1: Can't drop database 'xx'; database doesn't exist
    

    但是如果有时确实需要忽略某个错误,那么可以使用[cmd] || true或者[cmd] || ret=$?,如:

    #!/bin/bash
    set -e
    mysql -uroot -e 'drop database xx' || echo "hello world"
    

    输出:

    # ./test.sh 
      ERROR 1008 (HY000) at line 1: Can't drop database 'xx'; database doesn't exist
      hello world
    

    更好的方式,是

  • -u
    如果shell脚本中试图使用未定义的变量,则立即退出。

    #!/bin/bash
    
    set -u
    echo ${var}
    echo "here"  #  输出:./te.sh: line 4: var: unbound variable
    
  • -o pipefail
    只要管道中的一个子命令失败,整个命令就失败。

  • timeout 限制运行时间
    有时候,需要对命令设置一个超时时间,这时可以使用timeout命令。

    timeout 600s [cmd] arg1 arg2
    

    命令在超时时间内运行结束时,返回码为 0,否则会返回一个非零返回码。

  • 使用 shellcheck 工具
    shellcheck 是一款实用的 shell脚本静态检查工具,项目地址:https://github.com/koalaman/shellcheck

    shellcheck xx.sh
    
sshpass

经常需要脚本里面执行ssh命令,需要显示写入密码而不需要键盘输入,那么可以使用sshpass 这个工具。
安装 yum -y install sshpass

shell 代码开发规范
推荐一个shell代码片段学习

https://github.com/dylanaraps/pure-bash-bible


参考资料
1、https://zhuanlan.zhihu.com/p/123989641
2、https://zhuanlan.zhihu.com/p/46100771

你可能感兴趣的:(Linux shell 编程)