Linux shell 编程

!/bin/bash 语句干嘛用的?

shell 脚本中的第一行 !/bin/bash 一般用来表示什么意思呢?
#!/bin/bash 是指定脚本使用 Bash 解释器来执行我们的脚本。Bash 解释器相比其他 Shell 解释器,例如sh、dash 等,提供了更多的功能和语法扩展。

当然有很多时候不规范的写法可以忽略掉这一句,执行起来好像也是ok?
这是因为在我们常用 的linux系统上默认都是执行/bin/bash来执行我们的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,比如:./xx.sh > log 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"
      
      echo "${str%?}"  # 输出 hello worl
      echo "${str%??}"  # 输出 hello wor
      echo "${str%???}"  # 输出 hello wo
      
  • 匹配替换

    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  % 以什么结尾来匹配
    

    其他见 linux shell 字符串操作详解 (长度,读取,替换,截取,连接,对比,删除,位置 )

数组

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

  • 数组定义方式

    array_test=(a b c d e f)
    declare -a array_test  # 先声明一个空的数组,后面可以根据索引来动态添加
    
  • 将命令结果直接赋值给数组

    array=(`ls`)
    
  • 数组特殊表达式

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

    #!/bin/bash
    
    array=(a b c d e f)
    for elem in  ${array[@]};
    do
      echo "${elem}"
    done
    
    或者:
    for i in "${!arr[@]}";   
    do   
      echo "$i : ${arr[$i]}"  
    done 
    
数值运算
  • 加法

    #!/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
    

    当数值小于0时的浮点数bc并不会输出前面的0,比如0.12,只会输出.12,解决办法,结合awk

    # echo "scale=2; 2/3" | bc
      .66
    # echo "scale=2; 2/3" | bc | awk '{printf "%.2f\n", $0}'
      0.66
    
if else
if [ 表达式1 ]; then
    ...
elif [ 表达式2 ]; then
    ...
else
    ...
fi
  • 条件与 if [ 表达式1 ] && [ 表达式2 ]

  • 条件或 if [ 表达式1 ] || [ 表达式2 ]

  • if 中整数比较

    if [ ${num} -eq 3 ]; then 
    
    判断式 意义
    -eq 相等
    -ne 不等
    -gt 大于
    -ge 大于等于
    -lt 小于
    -le 小于等于
  • if 中浮点数比较
    利用bc

    # echo "3.2 > 3" | bc 
      1
    # echo "3.2 > 5" | bc 
      0
    # echo "3.2 > 3.2" | bc 
      0
    # echo "3.2 >= 3.2" | bc 
      1
    
  • 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

case "$1" in
    setup)
        echo "setup"
        ;;
    unsetup)
        echo "unsetup"
        ;;
    *)
    echo "usage: $0 {setup|unsetup}"
        exit 1
esac
image.png
函数
  • 函数传参注意

    #!/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
  
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}'" # 正确写法
脚本中创建文件

脚本开发中,经常需要将内容写入文件中,采用如下:

cat > file << EOF
hello world

EOF

如果需要追加则:

cat >> file << EOF
hello world

EOF
getopts 处理选项参数

getopts是bash内嵌的一个命令。通过该命令可以获得函数的选项和参数值或者是脚本的命令行选项和参数值。语法为:getopts optstring [args]

#!/bin/bash

while getopts "n:l:t:" arg
do
    case "$arg" in
        n)
            echo "-n;count $OPTARG"
            ;;
        l)
            echo "-l length: $OPTARG"
            ;;
        t)
            echo "-t:thread num: $OPTARG"
            ;;
        ?)
            echo "not found param"
            exit 1
            ;;
    esac
done
image.png
生成随机数和随机字符串
  • 生成随机数

    • 生成 [0, 32767] 范围随机数
      # echo ${RANDOM} 
      
  • 生成随机字符串

    • UUID
      # cat /proc/sys/kernel/random/uuid
      
循环读取文件每一行
#!/bin/bash

while read line
do
  echo $line
done < txt
多任务
  • 使用后台运行&实现多任务并行

    #!/bin/bash
    
    for i in {1..254}
    do
        ip="192.168.80.$i"
        ping -c 2 $ip &> /dev/null && echo $ip is up &
    done
    
  • 使用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

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
    

    但是如果有时确实需要忽略某个错误,那么可以使用set +e,如:

    #!/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

  • 远程连接
    sshpass -p xxx ssh root@192.168.11.11
    
  • 远程执行命令
    sshpass -p xxx ssh root@192.168.11.11 "ethtool eth0"
    
shell 代码开发规范
推荐一个shell代码片段学习

项目地址:https://github.com/dylanaraps/pure-bash-bible


参考资料
1、https://zhuanlan.zhihu.com/p/123989641
2、https://zhuanlan.zhihu.com/p/46100771
3、https://www.cnblogs.com/gaochsh/p/6901809.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容

  • 从程序员的角度来看, Shell本身是一种用C语言编写的程序,从用户的角度来看,Shell是用户与Linux操作系...
    呼啦啦的爱阅读 223评论 0 1
  • linux shell 脚本攻略 [TOC] 1.基本概念与特征 基础 双引号: 引号内可以引用变量,与php相同...
    cdz620阅读 1,054评论 0 1
  • 1、什么是Shell编程? 如今很多公司在面试的时候,都会加上一条:熟悉shell编程/脚本优先。那么,什么是Sh...
    莫问以阅读 5,800评论 0 8
  • 1.什么是shell 2. 还是hello world程序 3. shell中的变量 3.1 系统变量 3.2 用...
    马小瑄阅读 149评论 0 0
  • Shell简介 Shell会执行用户输入的命令,并且在屏幕上显示执行的结果。 单从字面的意思上理解,Shell的本...
    故事狗阅读 1,728评论 2 10