Bash 脚本高级用法
Bash 脚本高级用法
来源 https://www.cnblogs.com/gmpy/p/13215152.html
概述
偶然间发现 man bash
上其实详细讲解了 shell
编程的语法,包括一些很少用却很实用的高级语法。就像发现了宝藏的孩子,兴奋莫名。于是参考man bash
,结合自己的理解,整理出了这篇文章。
本文并不包含man bash
所有的内容,也不会详细讲解shell
编程,只会分享一些平时很少用,实际很实用的高级语法,或者是一些平时没太注意和总结的经验,建议有一定shell
基础的同学进阶时可以看一看。
当然,这只是 Bash
上适用的语法,不确定是否所有的Shell
都能用,请慎用。
shell语法
管道
有一点shell
编程基础的应该都知道管道。这是一个或多个命令的序列,用字符|
分隔。实际上,一个完整的管道格式是这样的:
[time [-p]] [ ! ] command [ | command2 ... ]
time
单独执行某一条命令非常容易理解,统计这个命令运行的时间,但管道这种多个命令的组合,他统计的是某一个命令的时间还是管道所有命令的时间呢?如果保留字 time 作为管道前缀,管道中止后将给出执行管道耗费的用户和系统时间。
如果保留字 !
作为管道前缀,管道的退出状态将是最后一个命令的退出状态的逻辑非值。 否则,管道的退出状态就是最后一个命令的。 shell 在返回退出状态值之前,等待管道中的所有命令返回。
复合命令
我们常见的case ... in ... esac
语句,if ... elif ... else
语句,while .... do ... done
语句,for ... in ...; do ... done
,甚至函数function name() {....}
都属于复合命令。
for 语句
for
循环常见的完整格式是:
for name [ in word ] ;
do
list ;
done
除此之外,其实还支持类似与C语言的for循环,
for (( expr1 ; expr2 ; expr3 )) ;
do
list ;
done
返回值是序列 list 中被执行的最后一个命令的返回值;或者是 false,如果任何表达式非法的话。
case 语句
man bash
上显示,case
语句的完整格式是case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac
。
展开后应该是这样的:
case word in
[(] pattern [ | pattern ])
list
;;
...
esac
每一个case
的分支,都是pattern
,使用与路径扩展相同的匹配规则来匹配,见下面的 路径扩展 章节,且通过|
支持多种匹配走同一分支。例如:
case ${val} in
*linux* | *uboot* )
...
;;
...
esac
如果找到一个匹配,相应的序列将被执行。找到一个匹配之后,不会再尝试其后的匹配。
如果没有模式可以匹配,返回值是 0。否则,返回序列中最后执行的命令的返回值。
select 语句
select
语句可以说用得很少,但其实在需要交互选择的场景下非常实用。它的完整格式是:
select name [ in word ]
do
list
done
它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。我们看一个例子:
#!/bin/bash
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
echo "You have selected $name"
done
运行结果是这样的:
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 一起使用,在用户输入不同的编号时可以做出不同的反应。例如
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
回原目录。例如:
alias mkernel='cd ~/linux ; make -j4 ; cd -'
这样会导致,在编译过程如果Ctrl + C
取消返回时,你所处在的目录就变成了~/linux
。这种情况下,使用( list )
就能解决这问题,甚至都不需要cd -
返回原目录,直接退出即可。
alias mkernel='(cd ~/linux ; make -j4)'
也例如,有某个程序比较挫,只能在程序目录执行,在其他目录,甚至上一级目录执行,都会找不到资源文件导致退出,我们可以这样解决:
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,用单括号需要这么写:
[ -n ${val} -a ${val} -gt 4 ]
用双括号可以这么写:
[[ -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 字符 |
例如,我希望把有换行的一段话暂存到某个变量:
$ var="第一行"$'\n'"第二行"
$ echo "${var}"
第一行
第二行
参数
数组
Bash 提供了一维数组变量。任何变量都可以作为一个数组;内建命令declare
可以显式地定义数组。数组的大小没有上限,也没有限制在连续对成员引用和 赋值时有什么要求。数组以整数为下标,从 0 开始。
除了```declare``定义数组外,更常用的是以下两种方式定义数组变量:
$ 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[@]}
扩展为空串。这种不同类似于特殊参数 *
和 @
的扩展。在作为函数参数传递的时候能很明显感受到他们的差别。
#定义数组
$ 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
是 *
或者是@
,将销毁整个数组。
扩展
花括号扩展
什么是花括号扩展,举个例子就好理解了
mkdir /usr/local/src/bash/{old,new,dist}
等效于
mkdir /usr/local/src/bash/old /usr/local/src/bash/new /usr/local/src/bash/dist
除此之外,还支持模式匹配来批量选择,例如:
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
自身的值。
有点拗口,举个例子就懂了
$ var_name=val
$ val="Bash expansion"
$ echo ${!var_name}
Bash expansion
所以,${!var_name}
等效于${val}
,就是取val_name
的值作为变量名,再获取新变量名的值。
!
有一种例外情况,那就是${!prefix*}
,下面再介绍。
${!prefix*}
${!prefix*}
实现扩展为名称以 prefix 开始的变量名,以特殊变量 IFS 的第一个字符分隔。换句话说,这种用法就是用于获取变量名的。例如:
# 创建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}
有什么差别?还是举个例子:
# 删除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
的开始位置寻找匹配,一个#
的是寻找最短匹配,两个#
的是寻找最长匹配,把匹配的内容删除后,把剩下的返回。例如:
$ 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
中删除匹配的内容后返回。%
与#
非常类似,前者是从头开始匹配,后者是从尾部开始匹配。同样的,一个%
是寻找最短匹配,两个%%
是寻找最长匹配。例如:
$ 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}
,主要实现了字符串替换,当然,如果要替换的结果是空,就等效于删除。一个/
,表示只有第一个匹配的被替换,两个/
表示所有匹配的都替换。例如:
$ 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 的格式是:
<<[-]word
here-document
delimiter
这种重定向使得 shell 从当前源文件读取输入,直到遇到仅包含 word
的一行 (并且没有尾部空白,trailing blanks) 为止。直到这一点的所有行被用作 命令的标准输入。
还是听拗口,咱们看例子:
$ cat <<EOF
> fist line
> second line
> third line
> EOF
fist line
second line
third line
上述的做法,把两个EOF
之间的内容作为一个文件,传递给cat
命令。甚至,我们还有更高级的用法,实现动态创建文件。
$ kernel=linux
$ cat > ./readme.txt <<EOF
> You are using kernel ${kernel}
> EOF
$ cat ./readme.txt
You are using kernel linux
Here Strings
here-document 的变种,形式是
<<<word
word 被扩展,提供给命令作为标准输入,例如,我希望检索变量的值,有以下两种做法:
$ 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 来执行这个命令。
- 常见用法:
- 直接组合命令 : eval ls -al
- 替换变量
- 可以执行任何值为命令组合的变量
- 变量替换赋值
2.exec
- 也是shell内建的一个命令。类似 eval、source,不同的是exec执行后面的命令会替换当前shell进程,而前两者不会。
- 常见用法:
- 用于分离执行脚本,并退出子脚本的shell进程
- 用于设置描述符重定向输入文件内容
- 用于设置描述符重定向输出内容至文件
&&和 ||
- 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生效,如下:
|
二、:
: 该命令什么都不做,但执行后会返回一个正确的退出代码,即exit 0。比如在if语句中,then后面不想做任何操作,但是又不能空着,这时就可以使用“:”来解决,如下:
|
三、()
() 将多个命令组合在一起执行,相当于一个命令组。
四、{}
{} 和()类似,也是将多个命令组合在一起。它们之间的区别是,()是在产生的子shell下执行,而{}是在当前的shell下执行。这与前面讲到是使用". filename.sh"和"./filename.sh"的区别一样。举一个很简单的例子:
|
从上面的示例可以看出,当在()中赋值的变量,影响的只是自身的子shell,而不能将该值赋给父shell,因为“父亲不能继承儿子”。而在{}中赋值的变量,因为就在当前的shell执行的,所以就能改变原来变量的值。
注意:()里面两边可以不使用空格,{}里面两边必须使用空格,且最后一个命令也需要以“;”结尾,表示命令结束。
五、[](test)
[] 与test命令一样,用于比较值以及检查文件类型。如下:
1、[ "$A" = 123 ]:是字符串的测试,以测试 $A 是否为 1、2、3 这三个连续的"文字"。
2、[ "$A" -eq 123 ]:是整数的测试,以测试 $A 是否等于"一百二十三"。
3、[ -e "$A" ]:是关于文件的测试,以测试 123 这份"文件"是否存在。
六、[[]]
[[]]可以说是[]的“增强版”,它能够将多个test命令支持的测试组合起来,例如:
|
至于这两者的区别有位仁兄已经写的很清楚了,我将其整理一下:
|
SPAN style="COLOR: rgb(0,1,2)">一致 |
拿这两者对字符串的测试举一个例子,如下:
|
字符串的比较是根据相应的ASCII码来比较的,所以a>1是成立的。如果有兴趣也可以思考一下为什么会出现下面的结果?
|
七、(())
(())专门来做数值运算,如果表达式求值为 0,则设置退出状态为 1;如果求值为非 0 值,则设置为 0。不需要对 ((
和 ))
之间的操作符转义。算术只对整数进行。除 0 会产生错误,但不会产生溢出。可以执行 C 语言中常见的算术、逻辑和位操作。如下:
|
也能:
|
除此之外,也可以使用$(())直接进行数值运算,如下:
|
注意:使用 (( )) 时,不需要空格分隔各值和运算符,使用[]和[[ ]] 时需要用空格分隔各值和运算符。
============== End
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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代理技术深度解析与实战指南