Shell 学习笔记 0x06

Shell 学习笔记 0x06

RayAlto OP

1. 位置参数

Shell 中 $n 表示第 n 个参数, $# 表示位置参数的个数:

1
2
3
4
echo "argc: $#"
echo "P0: $0"
echo "P1: $1"
echo "P2: $2"

运行:

1
2
3
4
5
$ ./test.sh param1 param2 param3
argc: 4
P0: ./test
P1: param1
P2: param2

比如执行 command param1 param2 ,无论在哪里 $0 都默认是 command ,只有 $1 之后的位置参数会发生变化

1.1. 遍历所有位置参数

如果想循环遍历所有参数,这种:

1
2
3
4
while (( i < $# )); do
echo ${$i} # <- 这种写法
i=$((i+1))
done

是 Shell 不允许的, Shell 采用一种蹩脚的方式:

1
2
3
4
5
6
7
echo "0: $0"
i=1
while (( $# > 0 )); do
echo "${i}: $1"
i=$(( i + 1 ))
shift
done

shift 会把从第 1 个到第 $# 个位置参数各往前移一位,而 $0 不会被改变,同时 $# 会减一。比如:

1
command param1 param2 param3

的位置参数是:

  1. command
  2. param1
  3. param2
  4. param3

shift 之后会是这样:

  1. command
  2. param2
  3. param3

1.2. 一些特殊的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
print_params () {
echo "\$0: $0"
i=1
while (( $# > 0 )); do
echo "\$${i}: $1"
i=$(( i + 1 ))
shift
done
}

pass_params () {
echo '===== $* ====='
print_params $*
echo
echo '===== "$*" ====='
print_params "$*"
echo
echo '===== $@ ====='
print_params $@
echo
echo '===== "$@" ====='
print_params "$@"
}

pass_params "word" "words with spaces"

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
=====  $*  =====
$0: /home/rayalto/program/shell/test
$1: word
$2: words
$3: with
$4: spaces

===== "$*" =====
$0: /home/rayalto/program/shell/test
$1: word words with spaces

===== $@ =====
$0: /home/rayalto/program/shell/test
$1: word
$2: words
$3: with
$4: spaces

===== "$@" =====
$0: /home/rayalto/program/shell/test
$1: word
$2: words with spaces

$$ 会被展开为当前进程 PID :

1
2
$ echo $$
5720

$RANDOM 会被展开为 1-32767 之间的随机整数:

1
2
$ echo $RANDOM
1145

2. for

Shell 也有 for ,类似于 Python 的版本:

1
2
3
4
5
for variable [in words]; do
commands
done

for variable [in words]; do commands; done

比如:

1
2
3
4
5
$ for i in a b c d; do echo ${i}; done
a
b
c
d

Shell 还有类似 C 的版本:

1
2
3
4
5
for (( exp1; exp2; exp3 )); do
commands
done

for (( exp1; exp2; exp3 )); do commands; done

相当于:

1
2
3
4
5
(( exp1 ))
while (( exp2 )); do
commands
(( exp3 ))
done

比如:

1
2
3
for (( i = 0; i < 5; i = i + 1 )); do
echo "foo"
done

可以输出五次 foo

3. 更精细的参数展开

3.1. 空变量的展开

  • ${var:-word} :如果 var 没有定义或为空,则会被展开成 word ,变量 var 不会被改变
  • ${var:=word} :如果 var 没有定义或为空,则会被展开成 word ,变量 var 也会被定义为 word
  • ${var:?word} :如果 var 没有定义或为空,则会退出整个脚本并显示错误信息 word
  • ${var:+word} :如果 var 没有定义或为空,则会被展开为空,否则展开为 word ,变量 var 不会被改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
echo '===== :- ====='
echo ${var1:-foo}
echo ${var1:-bar}
echo
echo '===== := ====='
echo ${var2:=foo}
echo ${var2:=bar}
echo
echo '===== :+ ====='
echo ${var3:+foo}
var3=foo
echo ${var3:+bar}
echo
echo '===== :? ====='
echo ${var4:?"var4 is empty"}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
===== :- =====
foo
bar

===== := =====
foo
foo

===== :+ =====

bar

===== :? =====
./test: line 17: var4: var4 is empty

3.2. 变量名的展开

1
2
3
4
$ echo ${!XDG*}
XDG_CURRENT_DESKTOP XDG_RUNTIME_DIR XDG_SEAT XDG_SESSION_CLASS XDG_SESSION_DESKTOP XDG_SESSION_ID XDG_SESSION_TYPE XDG_VTNR
$ echo ${!XDG@}
XDG_CURRENT_DESKTOP XDG_RUNTIME_DIR XDG_SEAT XDG_SESSION_CLASS XDG_SESSION_DESKTOP XDG_SESSION_ID XDG_SESSION_TYPE XDG_VTNR

可以展开所有 XDG 开头的所有变量名

zsh 貌似没有这种展开, !str 会被展开成历史指令,比如上一次执行了 df -lh , zsh 则会把 ${!df} 展开成 ${df -lh} ,然后就会产生错误

3.3. 字符串展开

多用这些展开可以减少类似 cut, sed 之类的额外程序的使用,或许能一定程度上增加便利性,提高运行效率

3.3.1. 切片

类比 Python 的语法

1
2
3
4
foo="Bash is an sh-compatible command language interpreter"
echo "len(foo) = ${#foo}"
echo "foo[33:] = ${foo:33}"
echo "foo[33:33+8] = ${foo:33:8}"

输出:

1
2
3
len(foo)     = 53
foo[33:] = language interpreter
foo[33:33+8] = language

也有 Python 一样的负数用法,但需要在负号 - 前面加上空格,来与 ${var:-word} 进行区分:

1
2
3
foo="Bash is an sh-compatible command language interpreter"
echo "foo[-20:] = ${foo: -20}"
echo "foo[-20:-20+8] = ${foo: -20:8}"

输出:

1
2
foo[-20:]      = language interpreter
foo[-20:-20+8] = language

3.3.2. 截断

还可以取子字符串:

1
2
3
foo="unzip.exe.zip"
echo ${foo#*.}
echo ${foo##*.}

${var#glob} 会把字符串匹配 glob 的部分截掉(从头开始),但只截最短的匹配结果,而 ${var##glob} 会截掉最长的匹配结果,结果:

1
2
exe.zip
zip

${var%glob}${var%%glob} 与上面的相似,但会从末尾开始截掉文本:

1
2
3
foo="unzip.exe.zip"
echo ${foo%.*}
echo ${foo%%.*}

运行结果:

1
2
unzip.exe
unzip

3.3.3. 替换

1
2
3
4
5
foo="unzip.exe.zip"
echo ${foo/zip/rar}
echo ${foo//zip/rar}
echo ${foo/#zip/rar}
echo ${foo/%zip/rar}

运行:

1
2
3
4
unrar.exe.zip
unrar.exe.rar
unzip.exe.zip
unzip.exe.rar
  • ${var/glob/str} :替换第一个匹配项
  • ${var//glob/str} :替换所有匹配项
  • ${var/#glob/str} :替换字符串开头的匹配项
  • ${var/%glob/str} :替换字符串末尾的匹配项

str 也可以为空,来删除匹配项

3.3.4. 大小写转换

1
2
3
4
5
6
declare -u u
declare -l l
u="aBcD"
l="AbCd"
echo $u
echo $l

输出:

1
2
ABCD
abcd

declare 可以完成转换,但 declare 功能还有很多,更方便的大小写转换可以:

1
2
3
4
5
6
7
foo="abc"
echo ${foo^}
echo ${foo^^}

bar="ABC"
echo ${bar,}
echo ${bar,,}

输出:

1
2
3
4
Abc
ABC
aBC
abc

${var^} 会使首字符大写, ${var^^} 会全部大写; ${var,} 会使首字符小写, ${var,,} 会全部小写

3.4. 算术展开

1
2
$ echo $(( 114 * 1000 + 514 ))
114514

3.4.1. 进制

num 默认是十进制, 0num 为八进制, 0xnum 为十六进制, base#numbase 进制:

1
2
3
4
5
6
7
8
$ echo $((0377))       # octal
255
$ echo $((0xff)) # hexadecimal
255
$ echo $((2#11111111)) # binary
255
$ echo $((255)) # decimal
255

3.4.2. 运算符

和 C++ 一样,支持 +, -, *, /, %, =, +=, -=, *=, /=, %=, ++, --, ~, <<, >>, &, |, ^, <=, >=, <, >, ==, !=, &&, ||, ?: ,还支持类似 Python 的乘方 **

打印九九乘法表:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh

for (( i = 1; i <= 9; i++ )); do
for (( j = i; j <= 9; j++)); do
printf "%dx%d=%-2d" $i $j $(( i * j ))
if (( j != 9 )); then
printf " "
fi
done
printf "\n"
done
1
2
3
4
5
6
7
8
9
1x1=1  1x2=2  1x3=3  1x4=4  1x5=5  1x6=6  1x7=7  1x8=8  1x9=9 
2x2=4 2x3=6 2x4=8 2x5=10 2x6=12 2x7=14 2x8=16 2x9=18
3x3=9 3x4=12 3x5=15 3x6=18 3x7=21 3x8=24 3x9=27
4x4=16 4x5=20 4x6=24 4x7=28 4x8=32 4x9=36
5x5=25 5x6=30 5x7=35 5x8=40 5x9=45
6x6=36 6x7=42 6x8=48 6x9=54
7x7=49 7x8=56 7x9=63
8x8=64 8x9=72
9x9=81