Linux Shell Program - expr
Linux 下的 Shell 编程之表达式。
Difference between let, expr and $[]
命令行执行运算表达式:
$ expr 5 + 1
6
$ $(expr 5 + 1)
zsh: command not found: 6
$ $[5 + 1]
zsh: command not found: 6
$ $((5+1))
zsh: command not found: 6
如果想在命令行执行数学计算,建议使用数学计算器
bc(bash calculator)。
在 bash shell 脚本中有以下几种运算表达式的书写方式。
expr#
最开始,Bourne shell 提供了一个特别的命令用来处理数学表达式。
expr 命令允许在命令行上处理数学表达式,但是特别笨拙。
expr 命令能够识别少数的数学和字符串操作符,见表11-1。

需要注意两点:
- 操作符两侧需要空格隔开算子;
- 算子引用变量需要用美元符号;
可用 expr 表达式实现 for 循环中记录递增的索引:
综合示例2:
#!/bin/bash
$ var1=5
$ var2=1
$ x=$var1+$var2
$ echo "x=$x"
x=5+1
$ y=$(expr $var1+$var2)
$ echo "y=$y"
y=5+1
$ z=$(expr $var1 + $var2)
$ echo "z=$z"
z=6
前两种运算符与算子之间没有空格,被当成了字符串拼接。
第三种是正确的 expr 算术表达式写法,结果符合预期。
let#
let command performs arithmetic evaluation and is a shell built-in.
bash shell 内置支持的 let 表达式,直接引用变量,而无需美元符号解引用,更接近于 C 等现代编程语言里面的自然表达式。
范式:let var3=var1+var2
$ z=0
$ let z=z+3 # 等效: let z+=3
$ let "z += 3"
$ echo "z = $z"
6
$ let 'sum=10+1'
$ echo "sum = $sum"
sum = 11
综合示例:
let 表达式更自然,算子引用的变量直接采用变量名,无需添加美元符号,推荐使用。
以下用 let 表达式实现 for 循环中记录递增的索引:
$ i=0
$ let index=i+1
$ echo $i $index
0 1
$ let i++
$ echo $i
1
$ let i+=1
$ echo $i
2
$ let index=i++
$ echo $i $index
3 2
$ let index=++i
$ echo $i $index
4 4
$[]#
bash shell 为了保持跟 Bourne shell 的兼容而包含了 expr 命令,但同时提供了一种更简单的方法来执行数学表达式。
在 bash 中,在将一个数学运算结果赋给某个变量时,可以用美元符号和方括号($[operation])将数学表达式围起来。
用方括号执行shell数学运算比用expr命令方便很多。
- 操作符两侧非必须要用空格隔开算子;
- 算子可直接引用变量,可无需美元符号;
常量计算表达式:
用 $[] 表达式实现 for 循环中记录递增的索引:
变量计算表达式:
在使用方括号来计算公式时,不用担心shell会误解乘号或其他符号。
对于方括号中的星号,shell知道它执行数学中的乘法运算而不是通配符,因为它在方括号内。
$ var1=100
$ var2=50
$ var3=45
$ # var4=$[$var1 * ($var2 - $var3)]
$ var4=$[var1*(var2-var3)] # 简写
$ echo The final result is $var4
The final result is 500
无论是 expr 表达式,还是中括号运算式,bash shell 数学运算符只支持整数运算。
$ var1=100
$ var2=45
$ var3=$(expr $var1 / $var2)
$ echo The final result is $var3
2
$ var4=$[var1/var2]
$ echo The final result is $var4
2
如果需要在shell脚本中进行浮点数运算,可以考虑看看 z shell,zsh 提供了完整的浮点数算术操作。
也可将表达式重定向到 bash 内置计算器 bc 做计算,参考 REDIRECTION 相关议题。
(())#
双括号命令 (( expression )) 支持更多的数学运算符。
双括号表达式有状态返回码,当运算结果非零时,返回0;否则,返回1。
相比test命令只能使用简单的算术操作,双括号命令允许在比较过程中使用高级数学表达式。
表12-4列出了双括号命令中会用到的其他运算符:

可以在脚本中使用双括号来执行数学运算,也可以使用if判断计算结果状态。
#!/bin/bash
n=0
(( n += 1 )) #Increment
echo $? # 返回0
(( n -= 1))
echo $? # 返回1
echo "n = $n"
val1=10
if (( $val1 ** 2 > 90 ))
then (( val2 = $val1 ** 2 ))
echo "The square of $val1 is $val2"
fi
关于双括号的场景,参考bash中C语言风格的for循环格式:
注意,有些部分并没有遵循bash shell标准的for命令:
- 变量赋值可以有空格
- 条件中的变量不以美元符开头
- 迭代过程的算式未用expr命令格式。
在 Linux Command - awk control 中的格式化输出(printf)
使用 awk 对 hexdump 第一列 offset 值添加地址偏移量(baddr)以便得到 address。
对于无前缀的十六进制格式化字符串 "%08_ax\t",需先添加 0x 前缀,并使用(("0x"$1))对字符串进行数值化。
$ got_offset=$(objdump -hw a.out | awk '/.got/{print "0x"$6}')
$ got_size=$(objdump -hw a.out | awk '/.got/{print "0x"$3}')
$ hexdump -v -s $got_offset -n $got_size -e '"%08_ax\t" /8 "%016x\t" "\n"' a.out \
| awk 'BEGIN{print "Offset\t\tAddress\t\t\t\tValue"} \
{printf("%s\t", $1); printf("%016x\t", (("0x"$1))+65536); print $2}'
Offset Address Value
00000f90 0000000000010f90 0000000000000000
00000f98 0000000000010f98 0000000000000000
00000fa0 0000000000010fa0 0000000000000000
00000fa8 0000000000010fa8 00000000000005d0
00000fb0 0000000000010fb0 00000000000005d0
00000fb8 0000000000010fb8 00000000000005d0
00000fc0 0000000000010fc0 00000000000005d0
00000fc8 0000000000010fc8 00000000000005d0
00000fd0 0000000000010fd0 0000000000010da0
00000fd8 0000000000010fd8 0000000000000000
00000fe0 0000000000010fe0 0000000000000000
00000fe8 0000000000010fe8 0000000000000000
00000ff0 0000000000010ff0 0000000000000754
00000ff8 0000000000010ff8 0000000000000000
$(())#
在 dash shell、z shell 脚本中执行算术运算的正确格式是用双圆括号方法 —— $((expression))。
# man bash
Arithmetic Expansion
Arithmetic expansion allows the evaluation of an arithmetic expression and the substitu-
tion of the result. The format for arithmetic expansion is:
$((expression))
The expression is treated as if it were within double quotes, but a double quote inside
the parentheses is not treated specially. All tokens in the expression undergo parame-
ter expansion, string expansion, command substitution, and quote removal. Arithmetic
expansions may be nested.
The evaluation is performed according to the rules listed below under ARITHMETIC EVALUA-
TION. If expression is invalid, bash prints a message indicating failure and no substi-
tution occurs.
根据 Shell Check 建议,在做数学运算时,应采用 $(()) 代替 expr 和 let 表达式以及 $[ ]。
- SC2003:
expris antiquated. Consider rewriting this using$((..)),${}or[[ ]]. - SC2007: Use
$((..))instead of deprecated$[..]. - SC2219: Instead of
letexpr, prefer(( expr )).
注意:双括号中的表达式,解引用变量时可不添加美元符号。
Within double parentheses, parameter dereferencing is optional.
表达式 OPTIND=$(($OPTIND + 1)) 将被 ShellCheck 检测报错 C2004: $/${} is unnecessary on arithmetic variables. 应修改为 OPTIND=$((OPTIND + 1))。
示例:
双重括号表达式基本上和 let 表达式等效。
以下用 (( expr )) 表达式实现 for 循环中记录递增的索引:
减法计算间隔耗时:
$ time_start=1668913082
$ time_end=1668913195
$ time_cost=$(( $time_end - $time_start ))
$ echo $time_cost
113
乘法计算倍积:
双乘计算幂:
浮点数乘法,printf 可限定输出浮点位数:
整除取模运算:
取模和取余运算:
$ value1=10
$ value2=$(( $value1 / 3 ))
$ echo $value2
3
$ value3=$(( $value1 % 3 ))
$ echo $value3
1
将被除数浮点化,以便计算完整的浮点除法结果:
$ value1=10
$ value2=$(( $value1 / 3. ))
$ echo $value2
3.3333333333333335
$ printf "%.3f\n" $value2
3.333
实际案例:readelf -SW test-gdb 读取其中的 section .interp 的 Offset=0x000238, size=0x00001b。
$ readelf -SW test-gdb
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000000238 000238 00001b 00 A 0 0 1
[11] .init PROGBITS 00000000000005b8 0005b8 000018 00 AX 0 0 4
[26] .strtab STRTAB 0000000000000000 001898 000237 00 0 0 1
[27] .shstrtab STRTAB 0000000000000000 001acf 0000fa 00 0 0 1
调用 od(octal dump) 和 hd(hex dump) 工具均可打印其内容:
$ od -j 0x000238 -N 0x00001b -S 3 test-gdb
0001070 /lib/ld-linux-aarch64.so.1
$ hd -s 0x000238 -n 0x00001b test-gdb
00000238 2f 6c 69 62 2f 6c 64 2d 6c 69 6e 75 78 2d 61 61 |/lib/ld-linux-aa|
00000248 72 63 68 36 34 2e 73 6f 2e 31 00 |rch64.so.1.|
00000253
假设我们不想要 od 和 hd 开头的偏移量,只想打印纯净的 bytearray 对应的字符串(strings),可以考虑使用 head+tail+strings:
$ head -c 0x000238+0x00001b test-gdb | tail -c 0x00001b | strings
head: invalid number of bytes: ‘0x000238+0x00001b’
tail: invalid number of bytes: ‘0x00001b’
报错显示 head/tail 的 -c 选项参数不支持十六进制,只支持十进制。
可以将十六进制表达式用 $(()) 包围起来解决该问题:
$ head -c $((0x000238+0x00001b)) test-gdb | tail -c $((0x00001b)) | strings
/lib/ld-linux-aarch64.so.1
另外,我们可以使用 objdump -j .init -d test-gdb 来反汇编指定的 section .init。
另一种方式是指定地址范围 [--start-address, --stop-address),但这两个选项参数同样只认十进制。
$ objdump -d --start-address=0x5b8 --stop-address=0x5b8+0x18 test-gdb
objdump: --stop-address: bad number: 0x5b8+0x18
可以将计算结束地址的十六进制表达式用 $(()) 包围起来解决该问题:
$ objdump -d --start-address=0x5b8 --stop-address=$((0x5b8+0x18)) test-gdb
test-gdb: file format elf64-littleaarch64
Disassembly of section .init:
00000000000005b8 <_init>:
5b8: d503201f nop
5bc: a9bf7bfd stp x29, x30, [sp, #-16]!
5c0: 910003fd mov x29, sp
5c4: 9400002c bl 674 <call_weak_fn>
5c8: a8c17bfd ldp x29, x30, [sp], #16
5cc: d65f03c0 ret
Parameter Expansion#
参考 man bash - Parameter Expansion(参数扩展)章节
$ man bash
${parameter:-word}
Use Default Values. If parameter is unset or null, the expansion of word is substituted.
Otherwise, the value of parameter is substituted.
${parameter:=word}
Assign Default Values. If parameter is unset or null, the expansion of word is assigned to
parameter. The value of parameter is then substituted. Positional parameters and special
parameters may not be assigned to in this way.
${parameter:?word}
Display Error if Null or Unset. If parameter is null or unset, the expansion of word (or a
message to that effect if word is not present) is written to the standard error and the shell,
if it is not interactive, exits. Otherwise, the value of parameter is substituted.
${parameter:+word}
Use Alternate Value. If parameter is null or unset, nothing is substituted, otherwise the
expansion of word is substituted.
shell 编程::后面跟-=?+的意义
shell之变量替换::=、=、:-、-、=?、?、:+、+句法
POSIX 文档中的这张表说得很清楚:
| parameter Set and Not Null |
parameter Set But Null |
parameter Unset |
|
|---|---|---|---|
| ${parameter:-word} | substitute parameter | substitute word | substitute word |
| ${parameter-word} | substitute parameter | substitute null | substitute word |
| ${parameter:=word} | substitute parameter | assign word | assign word |
| ${parameter=word} | substitute parameter | substitute null | assign word |
| ${parameter:?word} | substitute parameter | error, exit | error, exit |
| ${parameter?word} | substitute parameter | substitute null | error, exit |
| ${parameter:+word} | substitute word | substitute null | substitute null |
| ${parameter+word} | substitute word | substitute word | substitute null |
Use Default Values#
${parameter:-word}: Use Default Values.
How variables inside braces are evaluated
Omitting the : drops the "or null" part of all these definitions.
${a:-default}: 如果变量 a 未设置或为空,则使用默认值。${a-default}: 仅当变量未设置时,才使用默认值。
This is all described in the bash(1) manpage, and in POSIX.
- 变量未定义:
# 未定义变量
~ $ unset a
# 变量未定义,返回default
~ $ echo "${a:-default}"
default
# 变量未定义,返回default
~ $ echo "${a-default}"
default
- 变量有定义,但为空值(空字符串)
# 定义变量,但赋值为空
~ $ a= # or a=''
# 变量a已定义,但值为空,返回default
~ $ echo "${a:-default}"
default
# 变量a已定义,返回a——空值
~ $ echo "${a-default}"
~ $
- 定义变量,且非空值
in /etc/zshrc: If ZDOTDIR is unset(or empty), HOME is used instead.
如果
$1存在并且不为空,则 a=$1;否则(未定义或为空),则 a=false。
Usage of :- (colon dash) in bash
${PUBLIC_INTERFACE:-eth0}: If$PUBLIC_INTERFACEexists and isn't null, return its value, otherwise return "eth0".
zsh-autosuggestions/INSTALL:如果变量 ZSH_CUSTOM 未定义或为空,则替换为 ~/.oh-my-zsh/custom。
git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
我们来看一下 How to read from a file or standard input in Bash 这个问题,优先从文件读入参数,否则从stdin接受输入。
Read either the first argument or from stdin
file=${1--}(file=${1:--},等效于 ${1:-/dev/stdin}),可理解为 [ "$1" ] && file=$1 || file="-"。
以下脚本,从文件\(1或stdin读取数据传给cat,然后输出到文件\)2或stdout。
Assign Default Values#
${parameter:=word}: Assign Default Values.
变量未定义或为空,赋默认值:
# 变量未定义,赋默认值
~ $ unset a
~ $ echo "${a:=default}"
default
~ $ echo $a
default
# 变量为空值,赋默认值
~ $ echo $a
default
~ $ a=''
~ $ echo "${a:=default}"
default
以下是一段来自生产实践中的sh脚本,基于 := 来给未定义或空值变量赋默认值兜底:
# 兜底启动角色和模式
# 当作命令执行,报错
${role:=client}
zsh: command not found: client
echo $role
client
# 在赋值表达式前添加冒号,否则设置了 set -e 会退出
: ${role:=client}
echo $role
client
echo "mode = ${mode:=debug}"
# 兜底默认服务和代理端口
: "${web_port:=8080}"
: "${proxy_port:=8010}"
Display Error if Null or Unset#
${parameter:?word}: Display Error if Null or Unset.
以下sh脚本中调用get_lan_ip函数,预期其中会定义未export的全局变量lan_ip。
由于无法确保第三方脚本中的其他函数是否定义了该变量,ShellCheck 会报引用安全警告:
SC2154: lan_ip is referenced but not assigned.
如果局域网 LAN IP 获取不到,往往意味着网络服务不可用,可以使用 :? 进行判空警告。
这样,如果 lan_ip 未定义或为空值,则直接报错中止退出(exit 1)。
Use Alternate Value#
${parameter:+word}: Use Alternate Value.
The + form might seem strange, but it is useful when constructing variables in several steps:
will add : before /blah/bin only if PATH is non-empty, which avoids having a path starting with :.
- 如果 PATH 未定义或为空,则什么也不做,第一个环境变量不用添加冒号前缀分隔符;
- 如果 PATH 有定义或非空,则相当于在现有 PATH 后面追加变量:
PATH=${PATH}:/blash/bin;