• SHELL 语法收集


    Bash 脚本编程的一些高级用法
    概述#
    偶然间发现 man bash 上其实详细讲解了 shell 编程的语法,包括一些很少用却很实用的高级语法。就像发现了宝藏的孩子,兴奋莫名。于是参考man bash,结合自己的理解,整理出了这篇文章。

    本文并不包含man bash所有的内容,也不会详细讲解shell编程,只会分享一些平时很少用,实际很实用的高级语法,或者是一些平时没太注意和总结的经验,建议有一定shell基础的同学进阶时可以看一看。

    当然,这只是 Bash 上适用的语法,不确定是否所有的Shell都能用,请慎用。

    shell语法#
    管道#
    有一点shell编程基础的应该都知道管道。这是一个或多个命令的序列,用字符|分隔。实际上,一个完整的管道格式是这样的:

    Copy
    [time [-p]] [ ! ] command [ | command2 ... ]
    time单独执行某一条命令非常容易理解,统计这个命令运行的时间,但管道这种多个命令的组合,他统计的是某一个命令的时间还是管道所有命令的时间呢?如果保留字 time 作为管道前缀,管道中止后将给出执行管道耗费的用户和系统时间。

    如果保留字 ! 作为管道前缀,管道的退出状态将是最后一个命令的退出状态的逻辑非值。 否则,管道的退出状态就是最后一个命令的。 shell 在返回退出状态值之前,等待管道中的所有命令返回。

    复合命令#
    我们常见的case ... in ... esac语句,if ... elif ... else语句,while .... do ... done语句,for ... in ...; do ... done,甚至函数function name() {....}都属于复合命令。

    for 语句#
    for循环常见的完整格式是:

    Copy
    for name [ in word ] ;
    do
    list ;
    done
    除此之外,其实还支持类似与C语言的for循环,

    Copy
    for (( expr1 ; expr2 ; expr3 )) ;
    do
    list ;
    done
    返回值是序列 list 中被执行的最后一个命令的返回值;或者是 false,如果任何表达式非法的话。

    case 语句#
    man bash上显示,case语句的完整格式是case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac。

    展开后应该是这样的:

    Copy
    case word in
    [(] pattern [ | pattern ])
    list
    ;;
    ...
    esac
    每一个case的分支,都是pattern,使用与路径扩展相同的匹配规则来匹配,见下面的 路径扩展 章节,且通过|支持多种匹配走同一分支。例如:

    Copy
    case ${val} in
    linux | uboot )
    ...
    ;;
    ...
    esac
    如果找到一个匹配,相应的序列将被执行。找到一个匹配之后,不会再尝试其后的匹配。

    如果没有模式可以匹配,返回值是 0。否则,返回序列中最后执行的命令的返回值。

    select 语句#
    select语句可以说用得很少,但其实在需要交互选择的场景下非常实用。它的完整格式是:

    Copy
    select name [ in word ]
    do
    list
    done
    它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。我们看一个例子:

    Copy

    !/bin/bash

    echo "What is your favourite OS?"
    select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
    do
    echo "You have selected $name"
    done
    运行结果是这样的:

    Copy
    What is your favourite OS?

    1. Linux
    2. Windows
    3. Mac OS
    4. UNIX
    5. Android

    ? 4↙

    You have selected UNIX

    ? 1↙

    You have selected Linux

    ? 9↙

    You have selected

    ? 2↙

    You have selected Windows

    ?^D

    ?用来提示用户输入菜单编号,这实际是环境变量PS3的值,可以通过改这变量来改用户提示信息。^D表示按下 Ctrl+D 组合键,它的作用是结束 select 循环。

    如果用户输入的菜单编号不在范围之内,例如上面我们输入的 9,那么就会给 name 赋一个空值;如果用户输入一个空值(什么也不输入,直接回车),会重新显示一遍菜单。

    注意,select 是无限循环(死循环),输入空值,或者输入的值无效,都不会结束循环,只有遇到 break 语句,或者按下 Ctrl+D 组合键才能结束循环。通常和 case in 一起使用,在用户输入不同的编号时可以做出不同的反应。例如

    Copy
    echo "What is your favourite OS?"
    select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
    do
    case $name in
    "Linux")
    echo "Linux是一个类UNIX操作系统,它开源免费,运行在各种服务器设备和嵌入式设备。"
    break
    ;;
    "Windows")
    echo "Windows是微软开发的个人电脑操作系统,它是闭源收费的。"
    break
    ;;
    ......
    *)
    echo "输入错误,请重新输入"
    esac
    done
    ( list ) 语句#
    ( list )会让 list 序列将在一个子 shell 中执行。变量赋值和影响 shell 环境变量的内建命令在命令结束后不会再起作用。返回值是序列的返回值。

    这个在需要临时切换目录或者改变环境变量的情况下非常使用。例如封装编译内核的命令,实现任何目录下都可以直接编译,我们总需要先cd到内核根目录,再make编译,最后再cd回原目录。例如:

    Copy
    alias mkernel='cd ~/linux ; make -j4 ; cd -'
    这样会导致,在编译过程如果Ctrl + C取消返回时,你所处在的目录就变成了~/linux。这种情况下,使用( list )就能解决这问题,甚至都不需要cd -返回原目录,直接退出即可。

    Copy
    alias mkernel='(cd ~/linux ; make -j4)'
    也例如,有某个程序比较挫,只能在程序目录执行,在其他目录,甚至上一级目录执行,都会找不到资源文件导致退出,我们可以这样解决:

    Copy
    alias xmind='(cd ~/软件/xmind/XMind_amd64 &>/dev/null && nohup ./XMind &>/dev/null) &'
    (( expression)) 语句#
    表达式 expression 将被求值。如果表达式的值非零,返回值就是 0;否则返回值是 1。这种做法和 let "expression" 等价。

    [[ expression ]] 语句#
    在 if 语句中,我们喜欢用 if [ expression ]; then ... fi的单括号的形式,但看大神们的脚本,他们更常用if [[ expression ]]; then ... fi的双括号形式。

    [ ... ]等效于test命令,而[[ ... ]]是另一种命令语法,相似功能却更高级,它除了传统的条件表达式(Eg. [ ${val} -eq 0 ])外,还支持表达式的转义,就是说可以像在其他语言中一样使用出现的比较符号,例如>,<=,&&,||等。

    举个例子,要判断变量val有值且大于4,用单括号需要这么写:

    Copy
    [ -n ${val} -a ${val} -gt 4 ]
    用双括号可以这么写:

    Copy
    [[ -n ${val} && ${val} > 4 ]]
    当使用==和!=操作符时,操作符右边的字符串被认为是一个模式,根据下面 模式匹配 章节中的规则进行匹配。如果匹配则返回值是 0,否则返回1。模式的任何部分可以被引用,强制使它作为一个字符串而被匹配。

    引用#
    这里主要讲的是$'string'特殊格式,注意的是,必须是单引号。它被扩展为string,其中的反斜杠转义字符被替换为 ANSI C 标准中规定的字符。反斜杠转义序列,如果存在的话,将做如下转换:

    转义 含义
    a alert (bell) 响铃
     backspace 回退
    e an escape character 字符 Esc
    f form feed 进纸
    new line 新行符
    carriage return 回车
    horizontal tab 水平跳格
    v vertical tab 竖直跳格
    backslash 反斜杠
    ' single quote 单引号
    nn 一个八比特字符,它的值是八进制值 nnn (一到三个数字)
    xHH 一个八比特字符,它的值是十六进制值 HH (一到两个十六进制数字)
    cx 一个 ctrl-x 字符
    例如,我希望把有换行的一段话暂存到某个变量:

    Copy
    $ var="第一行"(' '"第二行" ) echo "${var}"
    第一行
    第二行
    参数#
    数组#
    Bash 提供了一维数组变量。任何变量都可以作为一个数组;内建命令declare可以显式地定义数组。数组的大小没有上限,也没有限制在连续对成员引用和 赋值时有什么要求。数组以整数为下标,从 0 开始。

    除了```declare``定义数组外,更常用的是以下两种方式定义数组变量:

    Copy
    $ array_var=(
    "mem1"
    3
    str
    )
    $ array_var[4]="mem4"

    $ echo ({array_var[@]} mem1 3 str mem4 ) echo ${array_var[1]}
    3
    数组的使用跟C语言很像,[] + 下标数字可以访问特定某一个数组成员。花括号是必须的,以避免和路径扩展冲突。

    如果下标是 @ 或是 *,它扩展为数组的所有成员。 这两种下标只有在双引号中才不同。在双引号中,({name[*]},把所有成员当成一个词,用特殊变量 IFS 的第一个字符分隔;){name[@]} 将数组的每个成员扩展为一个词。 如果数组没有成员,${name[@]} 扩展为空串。这种不同类似于特殊参数 * 和 @ 的扩展。在作为函数参数传递的时候能很明显感受到他们的差别。

    Copy

    定义数组

    $ array=(a b c)

    定义函数

    $ function func() {

    echo first para is $1
    echo second para is $2
    echo third para is $3
    }

    双引号+'*'

    $ func "${array[*]}"
    first para is a b c
    second para is
    third para is

    双引号+‘@’

    $ func "${array[@]}"
    first para is a
    second para is b
    third para is c
    内建命令 unset 用于销毁数组。unset name[subscript] 将销毁下标是 subscript 的元素。 unset name, 这里name 是一个数组,或者 unset name[subscript], 这里subscript 是 *或者是@,将销毁整个数组。

    扩展#
    花括号扩展#
    什么是花括号扩展,举个例子就好理解了

    Copy
    mkdir /usr/local/src/bash/{old,new,dist}
    等效于

    Copy
    mkdir /usr/local/src/bash/old /usr/local/src/bash/new /usr/local/src/bash/dist
    除此之外,还支持模式匹配来批量选择,例如:

    Copy
    chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}}
    变量扩展#
    我们知道,${var}的形式可以获取变量var的值,但其实还可以有更多花式玩法。其中~表示用户根目录其实属于 波浪线扩展,这比较常见,不展开介绍了。

    下面的每种情况中,word 都要经过波浪线扩展,参数扩展,命令替换和 算术扩展。如果不进行子字符串扩展,bash 测试一个没有定义或值为空的 参数;忽略冒号的结果是只测试未定义的参数。

    大致描述下变量扩展的功能:

    扩展 功能
    ({var} 获取变量值 ){!var} 取变量var的值做新的变量名,再次获取新变量名的值
    ({!prefix* 获取prefix开头的变量名 ){#parameter} 获取变量长度
    ({parameter:-word} parameter为空时,使用wrod返回 ){parameter:+word} parameter非空时,使用word返回
    ({parameter:=word} parameter为空时,使用word返回,同时把word赋值给parameter变量 ){parameter:?word} parameter为空时,打印错误信息word
    ({parameter:offset} 从offset位置截取字符串 ){parameter:offset:length 从offset位置截取length长度的字符串
    ({parameter#word} 从头开始删除最短匹配word模式的内容后返回 ){parameter##word} 从头开始删除最长匹配word模式的内容后返回
    ({parameter%word} 从尾开始删除最短匹配word模式的内容后返回 ){parameter%%word} 从尾开始删除最长匹配word模式的内容后返回
    ({parameter/pattern/string} 最长匹配pattern的内容替换为string ){parameter//pattern/string} 所有匹配pattern的内容替换为string
    ({!var}# ){!var}是间接扩展。bash 使用以 var 的其余部分为名的变量的值作为变量的名称; 接下来新的变量被扩展,它的值用在随后的替换当中,而不是使用var自身的值。

    有点拗口,举个例子就懂了

    Copy
    $ var_name=val
    $ val="Bash expansion"
    $ echo ({!var_name} Bash expansion 所以,){!var_name}等效于${val},就是取val_name的值作为变量名,再获取新变量名的值。

    !有一种例外情况,那就是${!prefix*},下面再介绍。

    ({!prefix*}# ){!prefix*}实现扩展为名称以 prefix 开始的变量名,以特殊变量 IFS 的第一个字符分隔。换句话说,这种用法就是用于获取变量名的。例如:

    Copy

    创建3个以VAR开头的变量

    $ VAR_A=a
    $ VAR_B=b
    $ VAR_C=c

    寻找以VAR开头的变量名

    $ echo ({!VAR*} VAR_A VAR_B VAR_C ){#parameter}#
    ${#parameter}用于获取变量的长度。如果 parameter 是* 或者是 @, 替换的值是位置参数的个数。如果 parameter 是一个数组名,下标是 * 或者是 @, 替换的值是数组中元素的个数。

    ({parameter:-word}# ){parameter:-word}表示使用默认值。如果 parameter 未定义或值为空,将替换为 word 的扩展。否则,将替换为 parameter 的值。

    ({parameter:=word}# ){parameter:=word}赋默认值。如果 parameter 未定义或值为空, word 的扩展将赋予 parameter。parameter 的值将被替换。位置参数和特殊参数不能用这种方式赋值。

    ({parameter:=word}和){parameter:-word}有什么差别?还是举个例子:

    Copy

    删除var变量

    $ unset var

    确认var变量为空

    $ echo ${var}

    当var为空时,把test赋值给var,同时返回test

    $ echo ${var:=test}
    test

    可以看到,此时var已经被赋值

    $ echo ${var}
    test

    再次删除var变量,继续实验

    $ unset var

    当var为空时,返回test

    $ echo ${var:-test}
    test

    对比验证,此时var并没有赋值

    $ echo ${var}

    所以,差别在于,当parameter为空时,({parameter:=word}会比){parameter:-word}多做一步,就是把word的值赋给parameter。

    ({parameter:?word}# ){parameter:?word}主要用于当parameter为空时,显示错误信息word。shell 如果不是交互的,则将退出。

    ({parameter:+word}# 如果 parameter 未定义或非空,不会进行替换;否则将替换为 word 扩展后的值。这与){parameter:-word}完全相反。简单来说,就是当parameter非空时,才使用word。

    ${parameter:offset}#
    同 ${parameter:offset:length}

    ({parameter:offset:length}# ){parameter:offset:length}可以实现字符串的截取,从offset开始,截取length个字符。如果 offset 求值结果小于 0, 值将当作从 parameter 的值的末尾算起的偏移量。如果parameter 是 @,结果是 length 个位置参数,从 offset 开始。 如果 parameter 是一个数组名,以 @ 或 * 索引,结果是数组的 length 个成员,从 ${parameter[offset]} 开始。 子字符串的下标是从 0 开始的,除非使用位置参数时,下标从 1 开始。

    ${parameter#word}#
    参考 ${parameter##word}

    ${parameter##word}#
    word支持模式匹配,从parameter的开始位置寻找匹配,一个#的是寻找最短匹配,两个#的是寻找最长匹配,把匹配的内容删除后,把剩下的返回。例如:

    Copy
    $ str="we are testing, we are testing"
    $ echo ({str#*are} testing, we are testing ) echo ({str##*are} testing 这必须是从头开始删的,如果要删除中间的某一些字符串,可以用){parameter/pattern/string}。

    如果 parameter是一个数组变量,下标是@或者是*,模式删除将依次施用于数组中的每个成员,最后扩展为结果的列表。

    ({parameter%word}# 参考){parameter%%word}

    ${parameter%%word}#
    这也是在parameter中删除匹配的内容后返回。%与#非常类似,前者是从头开始匹配,后者是从尾部开始匹配。同样的,一个%是寻找最短匹配,两个%%是寻找最长匹配。例如:

    Copy
    $ str="we are testing, we are testing"
    $ echo ({str%are*} we are testing, we ) echo ({str%%are*} we 这必须是从末端开始删的,如果要删除中间的某一些字符串,可以用){parameter/pattern/string}。

    如果 parameter是一个数组变量,下标是@或者是*,模式删除将依次施用于数组中的每个成员,最后扩展为结果的列表。

    ({parameter/pattern/string}# 参考){parameter//pattern/string}

    ({parameter//pattern/string}# ){parameter//pattern/string}和${parameter/pattern/string},主要实现了字符串替换,当然,如果要替换的结果是空,就等效于删除。一个/,表示只有第一个匹配的被替换,两个/表示所有匹配的都替换。例如:

    Copy
    $ str="we are testing, we are testing"

    替换首次匹配

    $ echo ${str/we are/I am}
    I am testing, we are testing

    替换所有匹配

    $ echo ${str//we are/I am}
    I am testing, I am testing

    删除首次匹配

    $ echo ${str/are/}
    we testing, we are testing

    删除所有匹配

    $ echo ({str//are/} we testing, we testing 如果patten以#开始,例如){str/#we are/},则必须从头开始就匹配;以%表示,例如${str/%are testing/},必须从末端就要完全匹配。

    如果 parameter是一个数组变量,下标是@或者是*,模式删除将依次施用于数组中的每个成员,最后扩展为结果的列表。

    路径扩展#
    我们经常会这样使用路径扩展,ls ~/work,这里的就是路径匹配的一种,表示匹配包含空串的任何字符串。除了*之外,还有?和[。路径扩展其实运用了模式匹配,所以匹配规则不妨直接看模式匹配。

    模式匹配#
    任何模式中出现的字符,除了下面描述的特殊模式字符外,都匹配它本身。 模式中不能出现 NUL 字符。如果要匹配字面上的特殊模式字符,它必须被引用。

    特殊模式字符有下述意义:

    *: 匹配任何字符串包含空串。
    ?: 匹配任何单个字符。
    [...]: 匹配括号内的任意一个字符,与正则匹配一致。
    与正则的[...]一致,[!...]或者[^...]表示不匹配括号内的字符;[a-zA-Z]表示从a到z以及从A到Z的所有字符;也支持[:alinum:]这类的特殊字符。

    如果使用内建命令 shopt 启用了 shell 选项 extglob, 将识别另外几种模式匹配操作符。

    ?(pattern-list):匹配所给模式零次或一次出现
    *(pattern-list):匹配所给模式零次或多次出现
    +(pattern-list):匹配所给模式一次或多次出现
    @(pattern-list):准确匹配所给模式之一
    !(pattern-list):任何除了匹配所给模式之一的字串
    重定向#
    简单的重定向不累述了,讲一些高级用法。

    Here Documents#
    here-document 的格式是:

    Copy
    <<[-]word
    here-document
    delimiter
    这种重定向使得 shell 从当前源文件读取输入,直到遇到仅包含 word 的一行 (并且没有尾部空白,trailing blanks) 为止。直到这一点的所有行被用作 命令的标准输入。

    还是听拗口,咱们看例子:

    Copy
    $ cat <<EOF

    fist line
    second line
    third line
    EOF
    fist line
    second line
    third line
    上述的做法,把两个EOF之间的内容作为一个文件,传递给cat命令。甚至,我们还有更高级的用法,实现动态创建文件。

    Copy
    $ kernel=linux
    $ cat > ./readme.txt <<EOF

    You are using kernel ({kernel} EOF ) cat ./readme.txt
    You are using kernel linux
    Here Strings#
    here-document 的变种,形式是

    Copy
    <<<word
    word 被扩展,提供给命令作为标准输入,例如,我希望检索变量的值,有以下两种做法:

    Copy
    $ echo ({var} | grep "test" ) grep "test" <<< ${var}
    Opening File Descriptors for Reading and Writing#
    重定向操作符,[n]<>word,使得以 word 扩展结果为名的文件被打开,通过文件描述符 n 进行读写。如果没有指定 n 那么就使用文件描述符 0。如果文件不存在,它将被创建。

    这操作暂时没用过,待补充示例。

    总结#
    本文结合man bash以及自己的一些经验,总结了Shell编程的一些高级用法。还是那句话,建议有一定基础的同学学习,毕竟在跑之前要先学会走路不是?

  • 相关阅读:
    java枚举类的常见用法
    Sublime Text 3 3126 安装+注册码
    XtraFinder
    WinForm多线程+委托防止界面假死
    Win10添加简体中文美式键盘的方法
    查看sqlserver版本
    C#,PHP对应加密函数
    PHP文件缓存实现
    √GMAP.NET 地图
    JSON C# Class Generator ---由json字符串生成C#实体类的工具
  • 原文地址:https://www.cnblogs.com/sunxiuwen/p/13217678.html
Copyright © 2020-2023  润新知