Unix/Linux Shell

本文主要是参考书上内容并总结出来的,因此无法全面概述每一个shell的使用,具体可以man查看帮助。👀

rm

玩过linux都知道 rm 这个命令,它是用来删除一个或多个文件/目录,一般常用的操作是 rm files。不过有时为了省事,直接 rm -rf files/dirs表示强制删除文件或目录(包括子目录所有文件)。
不过有时候突然sb了,直接执行 rm -rf /* ,除非反应在0.000000009ms内否则基本就GG了,还要一种写法也可能GG,比如: rm -rf $TMP/*,如果变量$TMP 不存在或为空,那么这条命令等价于rm -rf /*。。。
总之,rm就是万恶之源,远离rm,从我做起 🤣

变量初始化空值

Linux变量初始化为空值有3种形式,效果一样。

  • var=
  • var="" (无空格)
  • var='' (无空格)

整数算数操作 $(())、$[]

基本上linux shell提供了一种叫做算术扩展的机制能够执行shell变量的整数运算。(PS: bash仅支持整数,而zsh其实支持浮点数的。以下所有命令及结果均在bash上测试)
格式: $((expression))$[expression] expression表达式首尾可无空格
$(()) 支持 +、-、、/、%、**(幂)、+=、-=、=、/=、var++、var–、++var、–var
除此之外,$(())也支持进制之间的转换(转换为10进制)。即 $((基数#转换的值))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
echo $((1+20*2))     # 41

var=100
echo $((var+=100)) # 200

echo $((var+100))
echo $(($var+100)) # $非必需

echo $((2#11111111)) # 输出 255
echo $((8#377)) # 输出 255
echo $((16#ff)) # 输出 255
echo $((10#255)) # 输出 255
echo $((3#22)) # 输出 8
var=ff
echo $((16#$var)) # 输出 255

echo $((10!=10&&var>100)) # 甚至还可以逻辑判断

除此之外,可以在if/for内进行求和,比如下面这个简短的例子,其中 : 不能去掉,因为如果没有:那么$((sum+=i))被当作一条命令去执行,而这里我们只需要求和,没有执行什么shell命令,于是bash只能报错未找到命令

1
2
3
4
5
6
#!/bin/bash
sum=
for i in {1..100};do
: $((sum+=i))
done
echo $sum

命令替换 $()

众所周知,linux通过 $()` `来执行一些命令,比如echo $(date) echo `date`,这在shell脚本中很常见,可以保存输出结果到变量中。

变量替换 ${}

所谓的变量替换也就是把变量放在用双引号""字符串而不是单引号''字符串中。这是因为单引号表示告诉shell忽略字符串中的特殊字符,比如$
比如

1
2
3
num=666
echo "number: ${num}000" # number: 666000
echo 'number: ${num}000' # number: ${num}000

参数替换

shell中参数包括:传递给程序的参数(位置参数,比如$0、$1…)、特殊的shell变量(如 $#)和普通变量(关键字参数)

参数 含义
${parameter} 替换为parameter的值
${parameter:-value} 如果parameter已设置且不为空,替换为它的值;否则,替换为 value
${parameter-value} 如果parameter已设置,替换为它的值;否则,替换为 value
${parameter:=value} 如果parameter已设置且不为空,替换为它的值;否则,替换为 value并将其赋给parameter
${parameter=value} 如果parameter已设置,替换为它的值;否则,替换为 value并将其赋给parameter
${parameter:?value} 如果parameter已设置且不为空,替换为它的值;否则,将value写入标准错误并退出。若忽略value,则向标准错误写入parameter: parameter null or not set
${parameter?value} 如果parameter已设置,替换为它的值;否则,将value写入标准错误并退出。若忽略value,则向标准错误写入parameter: parameter null or not set
${parameter:+value} 如果parameter已设置且不为空,替换为value;否则,替换为空
${parameter+value} 如果parameter已设置,替换为value;否则,替换为空
${ #parameter} 求parameter的长度
${parameter#pattern} 从parameter左边开始删除pattern的最短匹配,余下内容作为参数替换的结果
${parameter##pattern} 从parameter左边开始删除pattern的最长匹配,余下内容作为参数替换的结果
${parameter%pattern} 从parameter右边开始删除pattern的最短匹配,余下内容作为参数替换的结果
${parameter%%pattern} 从parameter右边开始删除pattern的最长匹配,余下内容作为参数替换的结果

通过 set 命令可以为位置参数重新赋值。
set a b 123,这将a赋值给$1b赋值给$2123赋值给$3
还可以将输入的每个数据依次分配到位置参数$1$2

1
2
3
4
5
6
read line
set $line
echo $#
for args; do
echo $args
done

数学等式解算器expr

expr命令没有$(())高级,功能比较局限而且也只支持整数。expr除了支持基本的 ±*/外,还支持|、&、!=、=、<=、<、>=、>逻辑判断。需要用空格分隔参数,而且乘的话需要用\转义!注意不要理所当然地用 "" 来包围表达式,这没用

1
2
3
4
5
6
7
expr 10 + 10 \* 10 / 20   # 输出 15
expr "10 + 10 * 10 / 20" # 输出 10 + 10 * 10 / 20

var=100
expr $var + 100 # 输出 200
i=`expr $var + 100` # i=200

当然expr还有其他功能,比如

1
2
3
4
5
6
7
8
9
10
# 默认返回匹配到的字符数目
expr "i love linux" : ".*" # 12
# 等同于
expr length "i love linux" # 12

# 返回某个字符在字符串中的索引(从1开始)
expr index "i love linux" o # 4

# 提取索引为8的5个字符的子字符串
expr substr "i love linux" 8 5 # linux

传递参数

可以给一个shell脚本或函数传递参数,与获取参数有关的一些变量大致如下

变量 变量
$# 参数个数 ${1}~${n} 参数1~n
$* 所有原始参数 $@ 所有参数且每个参数用""包围,如果"$1"

当然还有其他的变量没有列出。

shift

shift命令可以左移位置参数,即解开参数包,每次执行shift $#自动减1,$2赋予给$1$3赋予给$2… 当$#=0,没有参数可解时报错,具体情况根据shell的不同而不同。比如我这里bash没有显示报错信息,但$?为1,而zsh显示报错信息且$?为1。这个例子展示了如何利用shift一次解开参数包,当然也可以用${1}~${n}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in $@;do
echo "=> {$1}"
shift
done
shift # error
echo $?
echo "quit"

$ test.sh this is a test
Output
=> {this}
=> {is}
=> {a}
=> {test}
1
quit

条件测试test

test命令可以用来测试两个字符串是否相等,而且test会把测试变量当作字符串来看待,也就是说 test 100 = 100实际上是test "100" = "100"或者 test "100" = 100
由于测试的结果是上一条命令执行的退出状态码,因此结果普遍是0真,非0假。

test 字符串的其他一些操作符

操作符 说明
str1 = str2 str1 等于 str2 ?
str1 != str2 str1 不等于 str2 ?
str str 不为空?
-n str str不为空 ?
-z str str为空 ?

关于str-n str区别,举一个例子就知道了

1
2
3
4
5
6
str="    "
test $str
echo $? # 1

test -n $str
echo $? # 0

即可以把-n str看作是测试字符串长度是否大于0。

条件测试 []、[[]]、(())

除了在if中用test判断条件之外,还可以用[ ][[ ]]测试条件。
注意,[是一个命令位于 /usr/bin/[ ,而[[ ]] 是bash的一个关键字。注意[]内首尾空格。
常规写法

1
2
3
4
5
6
var=
if test "linux" = "linux" && test -z $var ;then
echo "yes"
else
echo "no"
fi

使用[ ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var=
n=100
if [ "linux" = "linux" ] && [ -z $var ] || [ $n -ge 50 ];then
echo "yes"
else
echo "no"
fi


if [ "linux" = "linux" -a -z $var -o $n -ge 50 ];then
echo "yes"
else
echo "no"
fi

上面两种写法不够简洁,因此可以使用[[ ]]

1
2
3
4
5
6
7
var=
n=100
if [[ "linux" = "linux" && -z $var || $n>=50 ]];then
echo "yes"
else
echo "no"
fi

甚至还可以使用C风格的条件判断

1
2
3
4
5
6
sum=
for i in {1..100};do
if ((i%2==0));then
: $((sum+=i))
fi
done

整数比较

操作符 说明
n1 -eq n2 n1 等于 n2 ?
n1 -ne n2 n1 不等于 n2 ?
n1 -ge n2 n1 大于等于 n2 ?
n1 -gt n2 n1 大于 n2 ?
n1 -le n2 n1 小于等于 n2 ?
n1 -lt n2 n1 小于 n2 ?
1
2
3
4
5
6
7
8
9
10
11
12
13
x1="005"
x2=" 10"
[ "$x1" = 5 ]
echo $? # 1

[ "$x1" -eq 5]
echo $? # 0

[ "$x2" = 10 ]
echo $? # 1

[ "$x2" -eq 10]
echo $? # 0

总之,在比较字符串和整数时要注意操作符

文件操作符

操作符 说明
-d file file是一个目录
-e file file存在
-f file file是一个普通文件
-s file file不是空文件
-r file file可读
-w file file可写
-x file file可执行
-L file file是一个符号链接
-g file file设置了SGID位
-u file file设置了SUID位
-p file file是一个命名管道
-S file file是一个套接字
1
2
3
4
5
6
[ ! -w /usr/bin/[ ]
echo $? # 0

if [ -d . -a -x test.sh -o -w test.sh ] ; then echo ok ;fi
# 用括号改变求值顺序
if [ -d . -a \( -x test.sh -o -w test.sh \) ] ; then echo ok ;fi

调试选项 -x

bash可以制定-x选项来跟踪执行过程,比如有下面这个shell脚本

1
2
3
4
5
6
7
8
#!/bin/bash
sum=
for i in {1..5};do
if ((i%2==0));then
: $((sum+=i))
fi
done
echo $sum

通过 bash -x test.sh 跟踪执行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ bash -x test.sh 
+ sum=
+ for i in {1..5}
+ (( i%2==0 ))
+ for i in {1..5}
+ (( i%2==0 ))
+ : 2
+ for i in {1..5}
+ (( i%2==0 ))
+ for i in {1..5}
+ (( i%2==0 ))
+ : 6
+ for i in {1..5}
+ (( i%2==0 ))
+ echo 6
6

或者set -x打开跟踪模式、set +x关闭跟踪模式

空命令 :

前面有谈到过:这个命令,它就像Python的pass,类似占位符,什么都不做。有时可以用:在if/for/while/until/case内执行一些赋值操作

.命令

. file会使Shell读取并执行指定的文件,file不一定是可执行的,只有可读就行。

参数扩展

1
2
3
var=100
var=$((var*100)) # 10000
: $((var*=100)) # 1000000

占位符、注释

1
2
: hello linux
if [ 10 -ge 5 ];then : ;else :; fi

函数名

1
2
3
4
5
:() 
{
echo xx;
}
:

比如众所周知的fork bomb :(){ :|:& };: 就是如此简单粗暴

循环

跳过/跳出循环

continue nbreak n来跳过或跳出第n层循环

后台执行循环

在循环关闭语句done后面加上&能够使循环for/while后台执行

1
2
3
for i in 1 2 3;do
sleep 2
done &

循环上的IO重定向

输出重定向

1
2
3
for i in $(ls $HOME);do
echo "=> _${i}_"
done > output 2>&1

输入重定向

1
2
3
while read line;do
echo "=> $line"
done < file.txt

printf

对于简单的信息echo足矣,但是有时候需要格式化输出信息,那么linux提供了一个类似C的printf命令来帮助完成格式化,更多内容可以去查看帮助文档😹

1
printf "%.4f %s %d\n" 10.22 $(uname -r) 100     # 10.2200 5.4.34-1-MANJARO 100

exec

exec除了会用新程序替换现有的程序,还有一下功能

重定向标准输入

exec < infile

1
2
3
4
5
#!/bin/bash
exec < infile
while read l;do
echo "=> $l"
done

重定向标准输出

exec > outfile

1
2
3
4
5
#!/bin/bash
exec > outfile
for i in $(ls);do
echo "=> $i"
done

重置标准输入/输出

  • exec > /dev/tty
  • exec < /dev/tty

命令组( … )、{ …; }

( ... )内的命令会在子shell执行,而{ ...; }内的命令则是在当前shell执行。
要注意,{ ...; }的左{后需要空格!

1
2
3
4
5
6
7
8
9
var=100
(var=200)
echo $var # 100

var=100
{ var=200;}
echo $var # 200
{ var+=300;}
echo $var # 200300

其实{ ...;}就像一个无名函数的调用…

只读变量

readonly和declare命令可以指定某个变量只读。
readonly vardeclare -r var

eval

把eval放在命令行之前,Shell会对该命令行进行二次扫描,然后执行。

1
2
pipe="|"
eval $pipe wc -l

Shell第一次扫描时把pipe变量替换为|,然后eval使得Shell重新扫描命令行,此时Shell能够识别出|是一个管道符号,因此便能成功执行该命令行
与eval搭配的字符有;|&<>引号' "

下面是一个典型的例子,展示了shell中所谓的“指针”

1
2
3
4
5
x=100
ptrx=x
eval echo \$$ptrx # 100
eval $ptrx=50
echo $x # 50

trap

trap用于信号处理,一般格式 trap commands signals
一些信号列表如下

信号 信号名称 产生原因
0 EXIT 退出SHELL
1 HUP 挂起
2 INT 中断
3 QUIT 退出
6 ABRT 中止
9 KILL "销毁"进程
14 ALAM 超时
15 TERM 软件终止信号

没有参数的trap会显示定义过或修改过的所有trap处理程序

1
2
3
4
5
6
trap "echo bye" INT
trap
for i in 1 2 3 ;do
sleep 1
echo ...
done

trap "" SIGINT 忽略SIGINT信号,这也会导致所有的子shell也忽略这个信号(无论子shell是否有自己的trap信号处理程序)
trap : SIGINT 当前shell什么也不做,子shell执行默认的信号处理程序(如果有的话)
trap SIGINT 重置中断信号处理
一、

1
2
3
4
5
6
$ trap "" SIGINT
$ bash test.sh
trap -- '' SIGINT
...
^C...
...

二、

1
2
3
4
5
6
$ trap : SIGINT
$ bash test.sh
trap -- 'echo bye' SIGINT
...
^Cbye
...

IO重定向

下表展示了重定向的一些操作(从左到右处理重定向)

符号 说明
< stdin重定向
> stdout重定向
2> stderr重定向
>&2 stdout重定向到stderr
> file 2>&1 stdout和stderr重定向到file,等价于 >file 2>>file
>&- 关闭stdout,stderr,如 exec 1>&-、exec 2>&-
<&- 关闭stdin,如 exec 0<&-

比如常见的 commands >/dev/null 2>&1

行内输入重定向 here documents

主要有以下几种形式

  • <<EOF 忽略特殊字符
  • <<\EOF 不忽略特殊字符,也就是原封不动的输出输入的内容
  • <<-EOF 删除输入内容中的前导制表符
  • <<EOF>>cat <<EOF>>outfile
  • <<EOF|teecat <<EOF|tee outfile
1
2
3
4
5
6
7
8
$ cat <<EOF
> Hello
> $SHELL
> `uname -r`
> EOF
Hello
/usr/bin/zsh
5.4.34-1-MANJARO
1
2
3
4
5
6
7
8
$ cat <<\EOF
> Hello
> $SHELL
> `uname -r`
> EOF
Hello
$SHELL
`uname -r`

删除变量和函数

unset var
unset -f func

退出函数

return n n为函数返回状态,可以用$?捕获

局部变量

使用typesetlocal定义局部变量,其中local只能用在函数中。
同时typeset还可以定义整数类型的变量-i,若赋值的不是一个整数,那么可能会提示错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typeset -i i=100 j=200
func(){
local k=300
echo $i $j $k # 100 200 300
}
func
echo $i $j $k # 100 200

typeset -i n=100
n=100*200
echo $n # 20000
typeset var=100
var=100*200
echo $var # 100*200

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arr[0]="wtf"
arr[1]=100

echo $arr[1] # wtf[1]
echo ${arr[1]} # 100
# 数组所有元素
echo ${arr[*]} # wtf 100
# 数组元素数量
echo ${#arr[*]} # 2

# 整数数组
typeset -i array
array[0]=100
array[1]=200
((array[2]=array[0]+array[1]))
array[3]=array[0]+array[1] # 只有整数数组才行
: $((array[4]=array[0]+array[1]))
echo ${array[*]} # 100 200 300 300 300

生成伪随机数

/dev/random在类UNIX系统中是一个特殊的设备文件,可以用作随机数发生器或伪随机数发生器。

/dev/random的一个副本是 /dev/urandom(“unblocked”,非阻塞的随机数发生器),它会重复使用熵池中的数据以产生伪随机数据。这表示对/dev/urandom的读取操作不会产生阻塞,但其输出的熵可能小于/dev/random的。它可以作为生成较低强度密码的伪随机数生成器,不建议用于生成高强度长期密码。

详细信息维基百科/dev/random

1
2
3
4
# 生成16位包含A-Z、a-z的随机数
head /dev/urandom |tr -dc 'A-Za-z'|head -c 16
# 生成32位包含a-z、0-9的随机数
head /dev/urandom |tr -dc '0-9a-z'|head -c 32

限制文件列宽 fold

1
2
# 每行20列 
fold -w 20 test.file

判断命令是否存在

可以用于判断linux命令是否存在的方法很多,比如 which、type、hash、command

1
2
command -V ps > /dev/null 2>&1
if [ $? -eq 0 ];then echo OK;else echo NO;fi

结尾

大概就这样吧…👴💔
以后在补充补充🍑


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!