Bash 脚本高级用法

Bash 脚本高级用法

来源 https://www.cnblogs.com/gmpy/p/13215152.html

 

概述

偶然间发现 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) 响铃
\b backspace 回退
\e an escape character 字符 Esc
\f form feed 进纸
\n new line 新行符
\r carriage return 回车
\t horizontal tab 水平跳格
\v vertical tab 竖直跳格
\\ backslash 反斜杠
\' single quote 单引号
\nnn 一个八比特字符,它的值是八进制值 nnn (一到三个数字)
\xHH 一个八比特字符,它的值是十六进制值 HH (一到两个十六进制数字)
\cx 一个 ctrl-x 字符

例如,我希望把有换行的一段话暂存到某个变量:

Copy
$ var="第一行"$'\n'"第二行"
$ 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 的扩展将赋予 parameterparameter 的值将被替换。位置参数和特殊参数不能用这种方式赋值。

${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为空时,显示错误信息wordshell 如果不是交互的,则将退出。

${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。如果文件不存在,它将被创建。

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

 

set

查看set 帮助:

bash -c "help set"

选项:

  • -e:任何命令执行失败(非0 status)直接退出
  • -x: 打印执行过程的命令行、参数
  • +e:命令执行失败不会中断退出
  • +x:不打印执行过程的命令行、参数

seq

seq: 打印数字序列

用法:seq first [incr] last

NAME
     seq -- print sequences of numbers

SYNOPSIS
     seq [-w] [-f format] [-s string] [-t string] [first [incr]] last
  blj seq 0 2
0
1
2

eval && exec

都是内建命令。

1.eval

  • bashshell中内建的一个命令,命令后面所跟的内容都认为是参数,但是会两次扫描其参数:第一次扫描会将参数中的变量进行替换;第二次扫描会将后面的参数当作一个shell中的命令组合来执行命令。
  • 实际使用中,可以将任意组合的命令赋值给一个变量,然后在需要的位置通过 eval $variable 来执行这个命令。
  • 常见用法:
    1. 直接组合命令 : eval ls -al
    2. 替换变量
    3. 可以执行任何值为命令组合的变量
    4. 变量替换赋值

2.exec

  • 也是shell内建的一个命令。类似 eval、source,不同的是exec执行后面的命令会替换当前shell进程,而前两者不会。
  • 常见用法:
    1. 用于分离执行脚本,并退出子脚本的shell进程
    2. 用于设置描述符重定向输入文件内容
    3. 用于设置描述符重定向输出内容至文件

&&和 ||

  • command1 && command2 [&& command3 ...]
    • 左边的命令返回真后,右边的命令才能够被执行
    • 只要有一个命令返回假,后面的命令就不会被执行
  • command1 || command2
    • 只有左边的命令返回假($? ==1),右边的命令才能被执行,即实现短路逻辑或操作。
    • 只要有一个命令返回真,后面的命令就不会被执行

 

:=、=、:-、-、=?、?、:+、+

变量替换和变量默认值设置是紧密相关的,至少从概念出发是如此。

参数扩张是将类似于变量的参数用它的值来替换。例如以“echo $VAR”的形式调用一个简单的变量。此外还有更多的特性可以访问。这个句法还包含一些没有扩展的特性,虽然这些特性自身很有意义。首先,这类特性执行默认变量赋值。使用这些特性时,整个表达式需要用花括号括起来。

: ${VAR:=”some default”}

这些代码开始的冒号是一个正确执行非活动任务的shell命令。在这个句法中,它仅仅扩展了行中紧随其后的所有参数。本例中,只要是要在花括号内扩展参数值。

本行ongoing冒号中的参数是最有趣的部分;它是用花括号起来的一些逻辑的参数扩展。:=句法表示VAR变量将会和“some defalut”字符串进行比较。

在这个表达式中,如果变量VAR还没有被设置,那么“:=”之后表达式的值将被赋给它,这个值可能是一个数字,一个字符串,或者是另外一个变量。

系统中的脚步可能需要将多个变量设置成默认值。程序员可以在一行中给多个变量设置默认值,而不是编码一组变量替换,这样也使得代码更加紧凑、易读。下面的例子包含了程序员需要执行的各种替换操作。第一个默认值是一个显示的串,第二个是一个显示的整数,第三个是一个已定义的变量。

: ${VAR:=”some default”} ${VAR2:=42} ${VAR3:=$LOGNAME}

这几个变量替换类型和前例中的:=句法类似。因为不同替换类型的句法都是相同的,不过它们的意义却略有不同,可能很容易混淆。在大多数情况下,代码中执行替换句法的地方,这些替换仅仅用某个值替换了变量,但是并没有设置变量,也就是说变量并没有被真正赋值。下面句法类型的定义在所有的shell联机资料中找到的,但是这些说明通常不是很清楚。

:=句法

在这种替换中,使用和前例中相同的:=句法来设置默认值。

username=””

echo “${username:=$LOGNAME}”

在使用“:=”进行比较时,username变量已经被定义了,但是它的值为空。因此,这里对echo命令使用了变量LOGNAME的值,即设置变量username的值为LOGNAME的值。

有了这个特殊的句法,只有当变量username已被定义,而且有一个实际的非空值时,变量username才会被设置为变量LOGNAME的值。

和前例的主要不同是使用活动命令(echo)而不是被动的冒号来设置变量的默认值,当活动命令被调用时,默认赋值仍然会执行,并输出显示结果。

=句法

下面的语句和:=句法非常类似,但是没有冒号。

username=””

echo “${username=$LOGNAME}”

和前面一样,变量username已经被定义,但是它的值为空。在这个句法中,命令将会输出“echo”之后语句的执行结果。因为变量username虽然为空值,但已经被定义了,所以除了一个回车不会再有其他输出。只有当username变量完全没有定义时,才会将其设置为变量LOGNAME的值。

当脚本或者函数需要依赖某些定义变量时,就要使用这种语法。它主要应用于登陆。如果一个特定环境变量还没有被定义,就可以给它赋予脚本所需要的值。

:-句法

在这个命令中,因为变量username虽然已被定义但是为空值,echo语句将使用LOGNAME变量的值。

username=””

echo “${username:-$LOGNAME}”

这里username变量的值保持不变。这个命令和使用=句语法的不同之处是,在此命令被执行前,仅仅在代码中的“${}”句法中做替换。也就是说,echo命令将输出LOGNAME变量的值,但是这个值不会被赋给username变量。

-句法

当删除上述的:-语句中的冒号,即变成-的时候,因为username变量已被定义,输出将为空。如果未定义,就会使用LOGNAME变量的值。还有一点也与:-句法相同,即username变量的值没有改变。

username=””

echo “${username-$LOGNAME}”

当脚本评价或检查系统环境的时,:-句法和-句法都可以使用。这两种检查基本上是相反的,它们用默认值替换变量,或者甚至于不依赖username变量是否已经被定义。如果脚本中急需要一组被定义的变量,也需要一些不该被定义的变量,那么在脚本执行任务之前组合这两种句法,肯定可以实现正确的设置。

:?句法

使用:?句法时,如果username变量已被定义为非空值,在echo命令中就会使用username变量的值。如果username变量已被定义但却没有一个真正的值(也就是说非空)或者完全未被定义,那么在echo命令中就会使用LOGNAME的值,并且脚本退出执行。

username=””

echo “${username:?$LOGNAME}”

如果把问号字符的参数改为某种错误字符,那这个语句就会在代码调试和查找未定义变量时变得很有用。这段代码不仅仅输出字符串,而且会显示代码在脚本中所在行的位置。

?句法

从:?句法中去掉冒号使用username变量不必一定为非空值。如果username只被设置为一个空值,那么将使用这个空值。相反的,如果username变量没有被定义,则同前所述的:?句法,执行LOGNAME替换,脚本退出运行,并显示退出时所在代码行在脚本中的位置。

username=””

echo “${username?$LOGNAME}”

在脚本调试过程中,需要检查变量是否已被定义或者是非空的是否,:?和?句法是非常有用的。这个代码最大的优点是脚本会从出错行退出,而且会显示出错误行行号。在要显示的文本中加上类似于“is undefined”或者“has a null value”信息,可以更清楚的说明脚本中的问题。

:+句法

和前面的例子相比,这个句法有相反的作用。这这是因为,只有当变量已被定义而不是未定义的时候,“${}”表达式才执行替换。

username=””

echo “${username:+$LOGNAME}”

如果这里的username变量已被定义而且非空,因此使用LOGNAME的值。如果username变量未定义,或者已定义但为空,则将使用空值。在任何情况下,username变量的值都不会改变。

+句法

如果 前例:+中的冒号,一旦变量username被定义,“${}”表达式都将使用LOGNAME的值;进行这个替换时,username变量不需要有一个实际的值(即非空值)。如

username=””

echo “${username+$LOGNAME}”

“:+”、“+”句法的用法很多是喝“:-”、“-”句法的用法相同的。最主要的区别是“:+”、“+”示例检查的是一个已定义的变量,而不是未定义的变量。这类类似于家法、减法——一枚硬币的两面。

 

 () {} [] [[]] (()) 

bash下有很多像{}、[]等一些符号命令,下面是我对一些常用的符号命令的学习笔记,若有错误或纰漏望各位兄弟指正。

一、.(source)
.(点)与source命令一样,从文件中读取并执行命令,无论该文件是否都有可执行权限都能够正确的执行。且是在当前shell下执行,而不是产生一个子shell来执行(我们通常使用“./filename.sh”去执行一个文件是在当前shell下产生一个子shell去执行的)。所以在设置bash的环境的变量时,就必须用该命令或者source命令去执行设置的环境变量才会对当前shell生效,如下:

for i in /etc/profile.d/*.sh ; do
    if [ -r "$i" ]; then
        . $i
    fi
done


二、:
: 该命令什么都不做,但执行后会返回一个正确的退出代码,即exit 0。比如在if语句中,then后面不想做任何操作,但是又不能空着,这时就可以使用“:”来解决,如下:

if [ "$i" -ne 1 ];then
    :
else
    echo "$i is not equal 1"
fi


三、()
() 将多个命令组合在一起执行,相当于一个命令组。

四、{}
{}  和()类似,也是将多个命令组合在一起。它们之间的区别是,()是在产生的子shell下执行,而{}是在当前的shell下执行。这与前面讲到是使用".  filename.sh"和"./filename.sh"的区别一样。举一个很简单的例子:
 

# A=123
 
(A=abc;echo $A);echo $A
abc
123
# { A=abc;echo $A; };echo $A
abc
abc


从上面的示例可以看出,当在()中赋值的变量,影响的只是自身的子shell,而不能将该值赋给父shell,因为“父亲不能继承儿子”。而在{}中赋值的变量,因为就在当前的shell执行的,所以就能改变原来变量的值。
注意:()里面两边可以不使用空格,{}里面两边必须使用空格,且最后一个命令也需要以“;”结尾,表示命令结束。

五、[](test)
[] 与test命令一样,用于比较值以及检查文件类型。如下:
1、[ "$A" = 123 ]:是字符串的测试,以测试 $A 是否为 1、2、3 这三个连续的"文字"。
2、[ "$A" -eq 123 ]:是整数的测试,以测试 $A 是否等于"一百二十三"。
3、[ -e "$A" ]:是关于文件的测试,以测试 123 这份"文件"是否存在。

六、[[]]
[[]]可以说是[]的“增强版”,它能够将多个test命令支持的测试组合起来,例如:

# [[ (-d "$HOME") && (-w "$HOME") ]] && echo echo "home is a writable directory"  

home is a writable directory


至于这两者的区别有位仁兄已经写的很清楚了,我将其整理一下:

数字测试: -eq -ne -lt -le -gt -ge,[[ ]]同 [ ]一致
文件测试: -r、-l、-w、-x、-f、-d、-s、-nt、-ot,[[ ]]同 [ ]一致
字符串测试: > < =(同==) != -n -z,不可使用“<=”和“>=”,[[ ]]同 [ ]
<

SPAN style="COLOR: rgb(0,1,2)">一致,但在[]中,>和<必须使用\进行转义,即\>和\<
逻辑测试: []为 -a -o ! [[ ]] 为&& || !
数学运算: [] 不可以使用 [[ ]]可以使用+ - */ %
组合: 均可用各自逻辑符号连接的数字(运算)测试、文件测试、字符测试


拿这两者对字符串的测试举一个例子,如下:

# [ a \> 1 ] && echo ture || echo false
ture
# [[ a > 1 ]] && echo ture || echo false
ture


字符串的比较是根据相应的ASCII码来比较的,所以a>1是成立的。如果有兴趣也可以思考一下为什么会出现下面的结果?

# [[ a > 1 ]] && echo ture || echo false
ture


七、(())
(())专门来做数值运算,如果表达式求值为 0,则设置退出状态为 1;如果求值为非 0 值,则设置为 0。不需要对 (( 和 )) 之间的操作符转义。算术只对整数进行。除 0 会产生错误,但不会产生溢出。可以执行 C 语言中常见的算术、逻辑和位操作。如下:

# ((i=1+99));echo $i
100


也能:

# i=99;((i++));echo $i
100


除此之外,也可以使用$(())直接进行数值运算,如下:

# echo $((2**3))
8


注意:使用 (( )) 时,不需要空格分隔各值和运算符,使用[]和[[ ]] 时需要用空格分隔各值和运算符。 

 

============== End

 

posted @   lsgxeva  阅读(2298)  评论(0编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示