李守中

Bash 2 变量操作

Table of Contents

1. 空值判断

Bash 提供四个特殊语法,跟变量的值有关,目的是保证变量不为空。

${varname:-word}

如果变量 varname 存在且不为空,则返回它的值,否则返回 word 。它的目的是 返回一个默认值,并保留原始值 (空值) 。比如 ${count:-0} 表示变量 count 不存在时返回默认值 0。

${varname:=word}

如果变量 varname 存在且不为空,则返回它的值,否则将它设为 word ,并且返回 word 。它的目的是 设置变量的默认值,并将其返回 。比如 ${count:=0} 表示变量 count 不存在时返回 0,且将 count 值设为 0。

${varname:+word}

如果变量 varname 存在且不为空,则返回 word ,否则返回空值。它的目的是 测试变量是否存在 。比如 ${count:+1} 表示变量 count 存在时返回 1 (表示 true),否则返回空值。

${varname:?message}

如果变量 varname 存在且不为空,则返回它的值,否则打印出 varname: message ,并中断脚本的执行。如果省略 message ,则输出默认的信息 parameter null or not set. 。它的目的是 防止变量未定义 。比如 ${count:?"undefined!"} 表示变量 count 未定义时就中断执行,抛出错误,返回给定的报错信息 undefined!

上面四种语法如果用在脚本中,变量名的部分可以用数字1到9,表示脚本的参数。比如:

# 1 表示脚本的第一个参数。如果该参数不存在,就退出脚本并报错。
filename=${1:?"filename missing."}

2. 变量运算

  • number: 没有任何特殊表示法的数字是十进制数。
  • 0number: 八进制数。
  • 0xnumber: 十六进制数。
  • base#number: base进制的数。

不论运算结果是否符合逻辑,只要运算结果为 0 即为运算命令执行失败。

((1-1)) 最后执行的是算数减法。运算结果为 0 即运算失败。

((var=1-1)) 最后执行的是赋值运算,即 ((var=0)) 。赋值运算总是能执行成功。

赋值语句返回等号右边的值,即赋 0 值可以正常执行,但赋 0 值的返回值为 0。比如 if ((var=0)) 等于 if [[ 0 ]] ,为 false。

2.1. 整数运算

2.1.1. ((...)) 语法

((...)) 会自动忽略内部的空格,进行 整数算术运算 。它支持:

  • +: 加法。
  • -: 减法。
  • *: 乘法。
  • /: 除法 (整除)。除法运算符的返回结果总是整数,比如 5 除以 2 得 2,不是 2.5。
  • %: 余数。
  • **: 指数。
  • ++: 自增运算 (前缀或后缀)。
  • --: 自减运算 (前缀或后缀)。

++-- 这两个运算符有前缀和后缀的区别。作为前缀,先运算后返回值。作为后缀,先返回值后运算。

赋值必须发生在 ((...)) 内部。

$ ((foo=5+5))
$ echo $foo
10

((...)) 内部可以用圆括号改变运算顺序,也可以嵌套。

$ ((var1 = (2 + 3) * 4))
$ echo var1
20
$ ((var2 = ((2 + 3)) * 4))
$ echo var2
20

((...)) 内的字符串按变量名处理。如果变量不存在,按空值 (数字 0) 处理,不报错。但如果字符串是数字,按数字字面量处理。

$ ((var1 = "hello" + 2))
$ echo $var1
2

$ ((var2 = "hello" * 2))
$ echo $var2
0

$ ((var3 = "22" + 2))
$ echo $var3
24

$ ((var4 = "22" * 2))
$ echo $var4
44

2.1.2. $((...)) 语法

如果 ((...)) 内有变量或赋值发生在 ((...)) 外,需写成 $((...))。

$ foo=$((5+5))
$ echo $foo
10
$ bar=$((foo+5))
$ echo $bar
15

$((...)) 内部可以用圆括号改变运算顺序,也可以嵌套。

$ echo $(((2 + 3) * 4))
20
$ echo $(($((2 + 3)) * 4))
20

2.2. 浮点运算

很可惜,Bash 莫得浮点运算。 echo $((5/2)) 最后得 2。

讲道理,真有这个需要,用 Python 去吧。

要非得用, sudo apt install bc 装个计算器凑合用。

value=`bc<<EOF  # 在反引号中使用here string的方式
scale=3
r=3
3.14159*r*r
EOF`

area=`echo "scale=2;r=3;3.14159*r*r"|bc`

但这个姿势用起来有点痛苦。

2.3. 逻辑运算

$((...)) 支持以下的逻辑运算符。

  • <: 小于。
  • >: 大于。
  • <=: 小于或相等。
  • >=: 大于或相等。
  • ==: 相等。
  • !=: 不相等。
  • &&: 逻辑与。
  • ||: 逻辑或。
  • !: 逻辑否。
  • expr1?expr2:expr3: 三元条件运算语法。若 expr1 的计算结果为非零值(算术真),则执行 expr2 ,否则执行 expr3

如果逻辑表达式为真,返回 1,否则返回 0。

$ echo $((3 < 2))
0
$ echo $((3 > 2))
1
$ echo $(((3 > 2) || (4 <= 1)))
1

三元运算符执行一个单独的逻辑测试。它用起来类似于 if/then/else 语句。

$ a=0
$ echo $((a<1 ? 1 : 0))
1
$ echo $((a>1 ? 1 : 0))
0

2.4. 位运算

$((...)) 支持以下的二进制位运算符:

  • <<: 位左移运算,数字的所有位向左移动指定的位。
  • >>: 位右移运算,数字的所有位向右移动指定的位。
  • &: 位的 运算,对两个数字的所有位执行 AND 操作。
  • |: 位的 运算,对两个数字的所有位执行 OR 操作。
  • ~: 位的 运算,对一个数字的所有位取反。
  • ^: 位的异或运算 (exclusive or),对两个数字的所有位执行异或操作。

下面是右移运算符 >> 的例子:

$ echo $((16>>2))
4

下面是左移运算符 << 的例子:

$ echo $((16<<2))
64

下面是 17 (二进制 10001) 和 3 (二进制 11) 的各种二进制运算的结果。

$ echo $((17&3))
1
$ echo $((17|3))
19
$ echo $((17^3))
18

2.5. 赋值运算

将值赋给变量的运算,如 $((a=1)) 即为赋值运算。返回值为等号右边的值。

((...)) 支持的赋值运算符有:

  • parameter = value: 简单赋值。
  • parameter += value: 等价于 parameter = parameter + value
  • parameter -= value: 等价于 parameter = parameter – value
  • parameter *= value: 等价于 parameter = parameter * value
  • parameter /= value: 等价于 parameter = parameter / value
  • parameter %= value: 等价于 parameter = parameter % value
  • parameter <<= value: 等价于 parameter = parameter << value
  • parameter >>= value: 等价于 parameter = parameter >> value
  • parameter &= value: 等价于 parameter = parameter & value
  • parameter |= value: 等价于 parameter = parameter | value
  • parameter ^= value: 等价于 parameter = parameter ^ value

2.6. 求值运算

逗号 , 在 $((...)) 内部是求值运算符,执行前后两个表达式,并返回后一个表达式的值。

$ echo ((foo = 1 + 2, 3 * 4))
12
$ echo $foo
3

上面例子中,逗号前后两个表达式都会执行,然后返回后一个表达式的值 12。

3. 字符串操作

3.1. 截取子串

返回变量 $varname 的子字符串,从位置 offset 开始 (从 0 开始计算),长度为 length

${varname:offset:length}
$ count=frogfootman
$ echo ${count:4:4}
foot

这种语法不能直接操作字符串,只能通过变量来读取字符串,并且不会改变原始字符串。

# 报错,"hello" 不是变量名
echo ${"hello":2:3}

如果省略 length 则从位置 offset 开始,一直返回到字符串的结尾。

$ count=frogfootman
$ echo ${count:4}
footman

offset 为负值表示从字符串的末尾开始算起。负数前面必须有一个空格,以防止与 ${variable:-word} 设置变量默认值的语法混淆。这时指定的 length 可以是是负值,表示排除从字符串末尾开始的 length 个字符。

$ foo="This string is long."
$ echo ${foo: -5}
long.
$ echo ${foo: -5:2}
lo
$ echo ${foo: -5:-2}
lon

3.2. 搜索和替换

不论结果如何,原变量不变。

匹配成功后删除匹配到的部分:

  • ${variable/pattern} 匹配整个串。非贪婪匹配。
  • ${variable//pattern} 匹配整个串。贪婪匹配。
  • ${variable#pattern} 从头部开始匹配。非贪婪匹配。
  • ${variable##pattern} 从头部开始匹配。贪婪匹配。
  • ${variable%pattern} 从尾部开始匹配。非贪婪匹配。
  • ${variable%%pattern} 从尾部开始匹配。贪婪匹配。

匹配成功后替换匹配到的部分 (指定开始位置的替换模式没有贪婪匹配):

  • ${variable/pattern/string} 匹配整个串。非贪婪匹配。
  • ${variable//pattern/string} 匹配整个串。贪婪匹配。
  • ${variable/#pattern/string} 从头部开始匹配。非贪婪匹配。
  • ${variable/%pattern/string} 从尾部开始匹配。非贪婪匹配。

匹配模式 pattern 可以使用 * ? [] 等通配符。

$ myPath=/home/cam/book/long.file.name

$ echo ${myPath#/*/}
cam/book/long.file.name

$ echo ${myPath##/*/}
long.file.name

下面写法可以删除文件路径的目录部分,只留下文件名。

$ path=/home/cam/book/long.file.name

$ echo ${path##*/}
long.file.name

3.3. 改变大小写

# 转为大写
${varname^^}

# 转为小写
${varname,,}

下面是一个例子。

$ foo=heLLo
$ echo ${foo^^}
HELLO
$ echo ${foo,,}
hello

3.4. Here document

Here 文档 (Here document) 是一种输入多行字符串的方法。格式如下:

<< token
text
token

它由开始标记 << token 、结束标记 token 和文本正文组成。开始标记是 << 后接 Here 文档的名称,名称可以随意取,后面必须是一个换行符。结束标记是单独一行、顶格写的 Here 文档名称,如果不是顶格,结束标记不起作用。

给个例子:

$ cat << _EOF_
<html>
<head>
    <title>
    The title of your page
    </title>
</head>

<body>
    Your page content goes here.
</body>
</html>
_EOF_

Here 文档内部:

  • 会发生变量替换。
  • 支持反斜杠转义。
  • 不支持通配符扩展。
  • 单双引号失去语法作用,变成了普通字符。
$ foo='hello world'
$ cat << _example_
$foo
"$foo"
'$foo'
_example_

hello world
"hello world"
'hello world'

如果不希望发生变量替换,可以把开始标记放在单引号之中:

$ foo='hello world'
$ cat << '_example_'
$foo
"$foo"
'$foo'
_example_

Here 文档的本质是重定向,它将字符串重定向输出给某个命令,相当于包含了 echo 命令。

command << token
  string
token

# 等同于

echo string | command

所以,Here 文档只适合接受标准输入作为参数的命令,对于其他命令无效。比如 echo 不能用 Here 文档作为参数。

此外,Here 文档也不能作为变量的值,只能用于命令的参数。

给两个例子:

  1. 重定向并覆盖 output_file.txt 文件的内容。
cat <<EOF > output_file.txt
mesg1
msg2
msg3
$var on $foo
EOF
  1. 定向并追加到 output_file.txt 文件的末尾。
cat <<EOF >> output_file.txt
mesg1
msg2
msg3
$var on $foo
EOF

3.5. Here string

Here 字符串 (Here string),作用与 Here 文档相同,都是将字符串通过标准输入,传递给命令。使用 <<< 表示。

<<< string

有些命令处理直接给定的参数,与通过标准输入接受的参数,产生的结果是不一样。所以才有了这个语法。使得将字符串通过标准输入传递给命令更方便。比如 cat 命令只接受标准输入传入的字符串。

$ cat <<< 'hi there'
# 等同于
$ echo 'hi there' | cat

md5sum 命令只能接受标准输入作为参数,直接将放在命令后面的字符串会被当作文件名,即 md5sum ddd 中的 ddd 会被解释成文件名。这时就可以用 Here 字符串,将字符串传给 md5sum 命令。

$ md5sum <<< 'ddd'
# 等同于
$ echo 'ddd' | md5sum

4. 命令

4.1. let

let 命令用于将算术运算的结果赋予变量。可以同时对多个变量赋值。

$ let x=2+3
$ echo $x
5

let命令的参数表达式如果包含空格,就需要使用引号。

$ let "v1 = 1" "v2 = v1++"
$ echo $v1,$v2
2,1

4.2. expr

expr 命令可以执行算术运算,可以不使用 ((...)) 语法,且支持变量替换。但不支持非整数参数。

$ foo=3
$ expr $foo + 2
5


Last Update: 2023-08-30 Wed 14:44

Generated by: Emacs 28.2 (Org mode 9.5.5)   Contact: [email protected]

若正文中无特殊说明,本站内容遵循: 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议