杨记

碎片化学习令人焦虑,系统化学习使人进步

0%

Shell

学习资料来自菜鸟教程,有增减,Shell 教程 | 菜鸟教程 (runoob.com)

黑马程序员

菜鸟

Shell脚本运行

1、作为可执行程序

1
2
chmod +x ./test.sh  #使脚本具有执行权限
./test.sh #执行脚本

注意,一定要写成 ./test.sh,而不是 test.sh,告诉系统就在当前目录找而不是去 PATH 里寻找。

2、作为解释器参数

直接运行解释器,参数就是shell脚本的文件名,如:

1
/bin/sh test.sh

Shell变量

定义变量时,变量名不加美元符号($)your_name="runoob.com"

  • 变量名和等号之间不能有空格
  • 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号

1
2
3
4
your_name="qinjx"
echo $your_name
your_name="yang" # 赋值的时候不能写$your_name 使用变量的时候才加$
echo ${your_name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界

1
2
3
for skill in Ada Coffe Action Java; do
echo "I am good at ${skill}Script"
done

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变

1
2
3
4
#!/bin/bash
myUrl="https://www.google.com"
readonly myUrl
myUrl="https://www.runoob.com"
1
/bin/sh: NAME: This variable is read only.

删除变量

unset variable_name (unset 命令不能删除只读变量)

变量类型

  • 1) 局部变量 局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。
  • 2) 环境变量 所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。
  • 3) shell变量 shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行

Shell字符串

字符串可以用单引号,也可以用双引号,也可以不用引号

单引号

1
str='this is a string'

单引号字符串的限制:

  • 单引号里的任何字符都会原样输出,单引号字符串中的变量是无效的;
  • 单引号字串中不能出现单独一个的单引号(对单引号使用转义符后也不行),但可成对出现,作为字符串拼接使用。

双引号

1
2
3
your_name="runoob"
str="Hello, I know you are \"$your_name\"! \n"
echo -e $str
1
Hello, I know you are "runoob"! 

双引号的优点:

  • 双引号里可以有变量
  • 双引号里可以出现转义字符

获取字符串长度

1
2
string="abcd"
echo ${#string} # 输出 4

提取子字符串

1
2
string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

查找子字符串

查找字符 i 或 o 的位置(哪个字符先出现就计算哪个)

1
2
string="runoob is a great site"
echo `expr index "$string" io` # 输出 4

注意: 以上脚本中 ` 是反引号,而不是单引号

Shell注释

# 开头的行就是注释,会被解释器忽略

多行注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
:<<EOF
注释内容...
注释内容...
注释内容...
EOF

:<<'
注释内容...
注释内容...
注释内容...
'

:<<!
注释内容...
注释内容...
注释内容...
!

Shell传递参数

脚本内获取参数的格式为:$nn 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……,$0 为执行的文件名(包含文件路径)

1
2
3
4
5
6
7
8
9
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

echo "Shell 传递参数实例!";
echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";
1
2
3
4
5
6
7
$ chmod +x test.sh 
$ ./test.sh 1 2 3
Shell 传递参数实例!
执行的文件名:./test.sh
第一个参数为:1
第二个参数为:2
第三个参数为:3

其他特殊字符

1
2
3
4
5
6
7
$#	传递到脚本的参数个数
$* 以一个单字符串显示所有向脚本传递的参数。如"$*"用"括起来的情况、以"$1 $2 … $n"的形式输出所有参数。
$$ 脚本运行的当前进程ID号
$! 后台运行的最后一个进程的ID号
$@ 与$*相同,但是使用时加引号,并在引号中返回每个参数。如"$@"用"括起来的情况、以"$1" "$2" … "$n" 的形式输出所有参数。
$- 显示Shell使用的当前选项,与set命令功能相同。
$? 显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

@ 区别:

  • 相同点:都是引用所有参数。
  • 不同点:只有在双引号中体现出来。假设在脚本运行时写了三个参数 1、2、3,,则 “ * “ 等价于 “1 2 3”(传递了一个参数),而 “@” 等价于 “1” “2” “3”(传递了三个参数)。

Shell数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。

数组定义

在 Shell 中,用括号来表示数组,数组元素用”空格”符号分割开

1
2
3
4
5
6
7
8
array_name=(value1 value2 value3)

array_name=(
value0
value1
value2
value3
)

单独定义:

1
2
3
array_name[0]=value0
array_name[1]=value1
array_name[n]=valuen

可以不使用连续的下标,而且下标的范围没有限制。

数组长度

获取数组长度的方法与获取字符串长度的方法相同

1
2
3
4
5
6
# 取得数组元素的个数
length=${#array_name[@]}
# 或者
length=${#array_name[*]}
# 取得数组单个元素的长度
lengthn=${#array_name[n]}
1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
a=(1 2 3 5 7 183 37)
length=${#a[*]}
echo $length # 输出7
echo ${a[1]} # 输出2
a[10]=8 # 增加一个元素,不按连续下标
length=${#a[@]}
echo $length # 输出8
singleLength=${#a[5]}
echo $singleLength # 输出3
echo ${a[10]} # 输出8

基本运算符

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。

expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

1
2
3
4
#!/bin/bash

val=`expr 2 + 2` # 反引号不是单引号 注意空格
echo "两数之和为 : $val"

算术运算符

a=10,b=20

运算符 说明 举例
+ 加法 expr $a + $b 结果为 30。
- 减法 expr $a - $b 结果为 -10。
* 乘法 expr $a \* $b 结果为 200。
/ 除法 expr $b / $a 结果为 2。
% 取余 expr $b % $a 结果为 0。
= 赋值 a=$b 把变量 b 的值赋给 a。
== 相等。用于比较两个数字,相同则返回 true。 [ $a == $b ] 返回 false。
!= 不相等。用于比较两个数字,不相同则返回 true。 [ $a != $b ] 返回 true。

注意:条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]

乘号(*)前边必须加反斜杠()才能实现乘法运算;

关系运算符

关系运算符只支持数字,不支持字符串,除非字符串的值是数字

下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ $a -eq $b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ $a -ne $b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ $a -gt $b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ $a -lt $b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ $a -ge $b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ $a -le $b ] 返回 true。

布尔运算符

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ $a -lt 20 -a $b -gt 100 ] 返回 false。

逻辑运算符

运算符 说明 举例
&& 逻辑的 AND [[ $a -lt 100 && $b -gt 100 ]] 返回 false
\ \ 逻辑的 OR `[[ $a -lt 100 $b -gt 100 ]]` 返回 true

字符串运算符

假定变量 a 为 “abc”,变量 b 为 “efg”:

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ $a = $b ] 返回 false。
!= 检测两个字符串是否不相等,不相等返回 true。 [ $a != $b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n "$a" ] 返回 true。
$ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。

文件测试运算符

操作符 说明 举例
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false
-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true

其他检查符:

  • -S: 判断某文件是否 socket。
  • -L: 检测文件是否存在并且是一个符号链接。

echo命令

用于字符串的输出。命令格式:echo string

显示普通字符串:

1
2
3
echo "It is a test"

echo It is a test # 这里双引号省略效果一样

显示转义字符

1
2
echo "\"It is a test\""  # 双引号也可以省略
# 输出结果:"It is a test"

显示变量

read 命令从标准输入中读取一行,并把输入行的每个字段的值指定给 shell 变量

1
echo ${value_name}
1
2
3
#!/bin/sh
read name
echo "$name It is a test"
1
2
OK                     #标准输入
OK It is a test #输出

显示换行

1
2
echo -e "OK! \n" # -e 开启转义
echo "It is a test"
1
2
3
OK!

It is a test

显示不换行

1
2
3
#!/bin/sh
echo -e "OK! \c" # -e 开启转义 \c 不换行
echo "It is a test"
1
OK! It is a test

显示结果定向至文件

1
echo "It is a test" > myfile  # 文件不存在则创建文件,存在则情况再写入 两个 >> 是追加

原样输出字符串:使用单引号

1
echo '$name\"'
1
$name\"

显示命令执行结果

1
echo `date`
1
2022年 03月 19日 星期六 17:52:04 CST  

printf命令

printf 命令模仿 C 程序库(library)里的 printf() 程序

printf 由 POSIX 标准所定义,因此使用 printf 的脚本比使用 echo 移植性好。

printf 使用引用文本或空格分隔的参数,外面可以在 printf 中使用格式化字符串,还可以制定字符串的宽度、左右对齐方式等。默认的 printf 不会像 echo 自动添加换行符,我们可以手动添加\n

printf 命令的语法:

1
printf  format-string  [arguments...]

参数说明:

  • format-string: 为格式控制字符串
  • arguments: 为参数列表。
1
2
3
4
5
#!/bin/bash
printf "%-10s %-8s %-4s\n" 姓名 性别 体重kg # 双引号也可以
printf '%-10s %-8s %-4.2f\n' 郭靖 男 66.1234 # 单引号也可以
printf "%-10s %-8s %-4.2f\n" "杨过" 男 48.6555 # 要输出的字符串可以用引号
printf "%-10s %-8s %-6s\n" 郭芙 女 47.9876 # 将47.9876做字符串输出
1
2
3
4
姓名     性别   体重kg
郭靖 男 66.12
杨过 男 48.66 # 四舍五入
郭芙 女 47.9876

%s %c %d %f 都是格式替代符,%s 输出一个字符串,%d 整型输出,%c 输出一个字符,%f 输出实数,以小数形式输出。

%-10s 指一个宽度为 10 个字符(- 表示左对齐,没有则表示右对齐),任何字符都会被显示在 10 个字符宽的字符内,如果不足则自动以空格填充,超过也会将内容全部显示出来。

%-4.2f 指格式化为小数,其中 .2 指保留2位小数。

1
2
3
4
5
6
7
#!/bin/bash

# 格式只指定了一个参数,但多出的参数仍然会按照该格式输出,format-string 被重用
printf "%s\n" abc def
printf "%s %s %s\n" a b c d e f h i j
# 如果没有arguments,那么 %s 用NULL代替,%d用0代替
printf "%s and %d\n"
1
2
3
4
5
6
abc
def
a b c
d e f
h i j
and 0

转义序列

序列 说明
\a 警告字符,通常为ASCII的BEL字符
\b 后退
\c 抑制(不显示)输出结果中任何结尾的换行字符(只在%b格式指示符控制下的参数字符串中有效),而且,任何留在参数里的字符、任何接下来的参数以及任何留在格式字符串中的字符,都被忽略
\f 换页(formfeed)
\n 换行
\r 回车(Carriage return)
\t 水平制表符
\v 垂直制表符
\\ 一个字面上的反斜杠字符
\ddd 表示1到3位数八进制值的字符。仅在格式字符串中有效
\0ddd 表示1到3位的八进制值字符
1
2
3
yang@Ubuntu18:~$ printf "a string, no processing:<%b>\n" "A\nB"
a string, no processing:<A
B>

%b:开启转义字符

test命令

test 命令用于检查某个条件是否成立,它可以进行数值、字符和文件三个方面的测试

数值测试

参数 说明
-eq 等于则为真
-ne 不等于则为真
-gt 大于则为真
-ge 大于等于则为真
-lt 小于则为真
-le 小于等于则为真
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
n1=10
n2=10
n3=20

if [ $n1 == $n2 ]; then
echo "相等"
fi
if [ $n2 != $n3 ]; then
echo "不等"
fi
if test $[n1+n2] -eq $[n3]; then
echo '相等'
else
echo "不等"
fi
1
2
3
相等
不等
相等

字符串测试

参数 说明
= 等于则为真
!= 不相等则为真
-z 字符串 字符串的长度为零则为真
-n 字符串 字符串的长度不为零则为真
1
2
3
4
5
6
7
8
#!/bin/bash
n1="cainiaojiaocheng"
n2="cainaojiaocheng"
if test $n1 == $n2; then
echo "字符串相等"
elif test -n $n1; then
echo "n1=$n1,长度不为空"
fi
1
n1=cainiaojiaocheng,长度不为空

文件测试

参数 说明
-e 文件名 如果文件存在则为真
-r 文件名 如果文件存在且可读则为真
-w 文件名 如果文件存在且可写则为真
-x 文件名 如果文件存在且可执行则为真
-s 文件名 如果文件存在且至少有一个字符则为真
-d 文件名 如果文件存在且为目录则为真
-f 文件名 如果文件存在且为普通文件则为真
-c 文件名 如果文件存在且为字符型特殊文件则为真
-b 文件名 如果文件存在且为块特殊文件则为真
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash
cd /bin
if test -e ./bash
then
echo '文件已存在!'
else
echo '文件不存在!'
fi

cd /bin
if test -e ./notFile -o -e ./bash
then
echo '至少有一个文件存在!'
else
echo '两个文件都不存在'
fi
1
2
文件已存在
至少有一个文件存在!

流程控制

if

1
2
3
4
if condition
then
....
fi
1
2
3
4
5
6
if condition
then
...
else
...
fi
1
2
3
4
5
6
7
if condition
then
...
elif condition
then
...
fi

写成一行(适用于终端命令提示符):

1
if [ $(ps -ef | grep -c "ssh") -gt 1 ]; then echo "true"; fi

for

1
2
3
4
for var in 1 23 3 5
do
echo $var
done
1
2
3
4
1
23
3
5

写成一行(适用于终端命令提示符):

1
for str in a bc c d; do echo $str; done;

搭配括号扩展使用{}

1
2
3
for var in {1..10..3}; do # 从1到10,逐增3
echo $var
done
1
2
3
4
1
4
7
10
1
2
3
4
for alph in {a..z}; do
echo $alph
done
# 一行行输出26个英文字母

{}也可以枚举

1
2
3
for var in {2,a,4,fe,35}; do
echo $var
done

while

while 循环用于不断执行一系列命令,也用于从输入文件中读取数据。其语法格式为:

1
2
3
4
while condition
do
command
done
1
2
3
4
5
6
7
#!/bin/bash
int=1
while(( $int<5 )) # 一定要两个括号
do
echo $int
let "int++"
done
1
2
3
4
1
2
3
4

until

until 循环执行一系列命令直至条件为 true 时停止。

until 循环与 while 循环在处理方式上刚好相反

一般 while 循环优于 until 循环,但在某些时候—也只是极少数情况下,until 循环更加有用。

until 语法格式:

1
2
3
4
until condition
do
command
done

condition 一般为条件表达式,如果返回值为 false,则继续执行循环体内的语句,否则跳出循环。

1
2
3
4
5
6
7
#!/bin/bash
a=0
until [ ! $a -lt 10 ]
do
echo $a
a=`expr $a + 1`
done
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

case

case … esac 为多选择语句,与其他语言中的 switch … case 语句类似,是一种多分支选择结构,每个 case 分支用右圆括号开始,用两个分号 ;; 表示 break,即执行结束,跳出整个 case … esac 语句,esac(就是 case 反过来)作为结束标记

可以用 case 语句匹配一个值与一个模式,如果匹配成功,执行相匹配的命令。

case … esac 语法格式如下:

1
2
3
4
5
6
7
8
casein
模式1)
command
;;
模式2)
command
;;
esac

无一匹配模式,使用星号 *(类似default)捕获该值,再执行后面的命令

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash
echo "输入1到4之间的数字:"
echo "你输入的数字为:"
read aNum
case $aNum in
1) echo "你选择了1";;
2) echo "你选择了2";;
3) echo "你选择了3";;
4) echo "你选择了4";;
*) echo "你没有输入1到4之间的数字";;
esac
1
2
3
4
输入 1 到 4 之间的数字:
你输入的数字为:
3
你选择了 3
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
site="runoob"
case "$site" in
"runoob") echo "菜鸟教程"
;;
"google") echo "Google 搜索"
;;
"taobao") echo "淘宝网"
;;
esac
1
菜鸟教程

无限循环

1
2
3
4
while :
do
command
done
1
2
3
4
while true
do
command
done
1
for (( ; ; ))

break

break命令允许跳出当前所在循环,类似C/C++的break,但break后面加数字可以跳出外层循环,例break 2,跳出当前循环的外层循环

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
while :
do
echo -n "输入 1 到 5 之间的数字:"
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的! 游戏结束"
break
;;
esac
done
1
2
3
4
输入 1 到 5 之间的数字:3
你输入的数字为 3!
输入 1 到 5 之间的数字:7
你输入的数字不是 1 到 5 之间的! 游戏结束
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
for (( var=1 ; $var<4 ; var=$[ $var+1 ] ))
do
echo "外层循环:var=" $var
for (( i=1 ; $i<4 ; i++ ))
do
echo "内层循环:i=" $i
if [ $i == 2 ]; then
echo "内层循环结束"
break 2; # break 或 break 1 跳出的是当前循环
fi
done
done
1
2
3
4
外层循环:var= 1
内层循环:i= 1
内层循环:i= 2
内层循环结束

continue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
while :
do
echo -n "输入 1 到 5 之间的数字: "
read aNum
case $aNum in
1|2|3|4|5) echo "你输入的数字为 $aNum!"
;;
*) echo "你输入的数字不是 1 到 5 之间的!"
continue
echo "游戏结束"
;;
esac
done

运行代码发现,当输入大于5的数字时,该例中的循环不会结束,语句 echo “游戏结束” 永远不会被执行。

[]

[] 内执行基本的算术运算,如

1
2
3
4
5
6
7
#!/bin/bash
a=5nan
b=6
result=$[a+b] # 注意等号两边不能有空格
echo "result 为: $result"
result=$[6+12] # 注意等号两边不能有空格
echo "result 为: $result"
1
2
result 为: 11
result 为: 18

Shell函数

shell中函数的定义格式如下:

1
2
3
4
5
[ function ] funname [()]
{
action;
[return int;]
}

说明:

  • 1、可以带function fun() 定义,也可以直接fun() 定义,不带任何参数。
  • 2、参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值。 return后跟数值n(0-255)
1
2
3
4
5
6
7
8
9
10
11
12
13
func(){
echo "第一个参数为 $1 !"
echo "第二个参数为 $2 !"
echo "第十个参数为 $10 !"
echo "第十个参数为 ${10} !"
echo "第十一个参数为 ${11} !"
echo "参数总数有 $# 个!"
echo "作为一个字符串输出所有参数 $* !"
return $[ $1+$2 ]
}

func 1 2 3 4 5 6 7 8 9 10 11
echo "函数的返回值:$?"
1
2
3
4
5
6
7
8
第一个参数为 1 !
第二个参数为 2 !
第十个参数为 10 !
第十个参数为 10 !
第十一个参数为 11 !
参数总数有 11 个!
作为一个字符串输出所有参数 1 2 3 4 5 6 7 8 9 10 11 !
函数的返回值:3

注意,10{10}。当n>=10时,需要使用​${n}来获取参数。

shell的0表示true,其它代表false

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash

echo "Hello World !" | grep -e Hello
echo $?
echo "Hello World !" | grep -e Bye
echo $?
if echo "Hello World !" | grep -e Hello
then
echo true
else
echo false
fi

if echo "Hello World !" | grep -e Bye
then
echo true
else
echo false
fi

function demoFun1(){
return 0
}

function demoFun2(){
return 12
}

if demoFun1
then
echo true
else
echo false
fi

if demoFun2
then
echo true
else
echo false
fi
1
2
3
4
5
6
7
8
Hello World !
0
1
Hello World !
true
false
true
false

输入/输出重定向

命令 说明
command > file 将输出重定向到 file。
command < file 将输入重定向到 file。
command >> file 将输出以追加的方式重定向到 file。
n > file 将文件描述符为 n 的文件重定向到 file。
n >> file 将文件描述符为 n 的文件以追加的方式重定向到 file。
n >& m 将输出文件 m 和 n 合并。
n <& m 将输入文件 m 和 n 合并。
<< tag 将开始标记 tag 和结束标记 tag 之间的内容作为输入。

需要注意的是文件描述符 0 通常是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

1
2
3
4
5
6
7
8
9
10
yang@Ubuntu18:~$ who > users
yang@Ubuntu18:~$ cat users
yang tty2 2022-03-19 17:14
yang pts/2 2022-03-20 10:22 (10.132.6.192)
yang@Ubuntu18:~$ wc -l < users
2
yang@Ubuntu18:~$ read val < users
yang@Ubuntu18:~$ echo $val
yang tty2 2022-03-19 17:14
yang@Ubuntu18:~$

一般情况下,每个 Unix/Linux 命令运行时都会打开三个文件:

  • 标准输入文件(stdin):stdin的文件描述符为0,Unix程序默认从stdin读取数据。
  • 标准输出文件(stdout):stdout 的文件描述符为1,Unix程序默认向stdout输出数据。
  • 标准错误文件(stderr):stderr的文件描述符为2,Unix程序会向stderr流中写入错误信息。

默认情况下,command > file 将 stdout 重定向到 file,command < file 将stdin 重定向到 file。

如果希望 stderr 重定向到 file,可以这样写:

1
command 2>file

如果希望 stderr 追加到 file 文件末尾,可以这样写:

1
command 2>>file

2 表示标准错误文件(stderr)。

如果希望将 stdout 和 stderr 合并后重定向到 file,可以这样写:

1
2
3
command > file 2>&1
或者
command >> file 2>&1

如果希望对 stdin 和 stdout 都重定向,可以这样写:

1
command < file1 >file2

command 命令将 stdin 重定向到 file1,将 stdout 重定向到 file2。

Here Document

Here Document 是 Shell 中的一种特殊的重定向方式,用来将输入重定向到一个交互式 Shell 脚本或程序。

它的基本的形式如下:

1
2
3
command << delimiter
document
delimiter

它的作用是将两个 delimiter 之间的内容(document) 作为输入传递给 command。

注意:

  • 结尾的delimiter 一定要顶格写,前面不能有任何字符,后面也不能有任何字符,包括空格和 tab 缩进。
  • 开始的delimiter前后的空格会被忽略掉。

在命令行中通过 wc -l 命令计算 Here Document 的行数:

1
2
3
4
5
6
7
$ wc -l << EOF
欢迎来到
菜鸟教程
www.runoob.com
EOF
3 # 输出结果为 3 行
$

我们也可以将 Here Document 用在脚本中,例如:

1
2
3
4
5
6
7
8
9
#!/bin/bash
# author:菜鸟教程
# url:www.runoob.com

cat << EOF
欢迎来到
菜鸟教程
www.runoob.com
EOF

执行以上脚本,输出结果:

1
2
3
欢迎来到
菜鸟教程
www.runoob.com

/dev/null 文件

如果希望执行某个命令,但又不希望在屏幕上显示输出结果,那么可以将输出重定向到 /dev/null:

1
$ command > /dev/null

/dev/null 是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读不到。但是 /dev/null 文件非常有用,将命令的输出重定向到它,会起到”禁止输出”的效果。

如果希望屏蔽 stdout 和 stderr,可以这样写:

1
$ command > /dev/null 2>&1

注意:0 是标准输入(STDIN),1 是标准输出(STDOUT),2 是标准错误输出(STDERR)。

这里的 2> 之间不可以有空格,2> 是一体的时候才表示错误输出。

文件包含

和其他语言一样,Shell 也可以包含外部脚本。这样可以很方便的封装一些公用的代码作为一个独立的文件。

Shell 文件包含的语法格式如下:

1
2
3
4
5
. filename   # 注意点号(.)和文件名中间有一空格



source filename

例:创建两个shell脚本文件

include.sh

1
2
#!/bin/bash
url="http://www.runoob.com"

testInclude.sh

1
2
3
4
5
#!/bin/bash

#. ./include.sh
source ./include.sh
echo "菜鸟教程官网地址:$url"
1
菜鸟教程官网地址:http://www.runoob.com

黑马程序员

shell历史

Shell的作用是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式(Interactive),Shell还有一种执行命令的方式称为批处理(Batch),用户事先写一个Shell脚本(Script),其中有很多条命令,让Shell一次把这些命令执行完,而不必一条一条地敲命令。Shell脚本和编程语言很相似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。

由于历史原因,UNIX系统上有很多种Shell:

  1. sh(Bourne Shell):由Steve Bourne开发,各种UNIX系统都配有sh。
  2. csh(C Shell):由Bill Joy开发,随BSD UNIX发布,它的流程控制语句很像C语言,支持很多Bourne Shell所不支持的功能:作业控制,命令历史,命令行编辑。
  3. ksh(Korn Shell):由David Korn开发,向后兼容sh的功能,并且添加了csh引入的新功能,是目前很多UNIX系统标准配置的Shell,在这些系统上/bin/sh往往是指向/bin/ksh的符号链接。
  4. tcsh(TENEX C Shell):是csh的增强版本,引入了命令补全等功能,在FreeBSD、MacOS X等系统上替代了csh。
  5. bash(Bourne Again Shell):由GNU开发的Shell,主要目标是与POSIX标准保持一致,同时兼顾对sh的兼容,bash从csh和ksh借鉴了很多功能,是各种Linux发行版标准配置的Shell,在Linux系统上/bin/sh往往是指向/bin/bash的符号链接。虽然如此,bash和sh还是有很多不同的,一方面,bash扩展了一些命令和参数,另一方面,bash并不完全和sh兼容,有些行为并不一致,所以bash需要模拟sh的行为:当我们通过sh这个程序名启动bash时,bash可以假装自己是sh,不认扩展的命令,并且行为与sh保持一致。
1
2
3
4
5
6
7
itcast$ vim /etc/passwd
其中最后一列显示了用户对应的shell类型
root:x:0:0:root:/root:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
syslog:x:101:103::/home/syslog:/bin/false
itcast:x:1000:1000:itcast,,,:/home/itcast:/bin/bash
ftp:x:115:125:ftp daemon,,,:/srv/ftp:/bin/false

用户在命令行输入命令后,一般情况下Shell会fork并exec该命令,但是Shell的内建命令例外,执行内建命令相当于调用Shell进程中的一个函数,并不创建新的进程。以前学过的cd、alias、umask、exit等命令即是内建命令,凡是用which命令查不到程序文件所在位置的命令都是内建命令,内建命令没有单独的man手册,要在man手册中查看内建命令,应该执行man builtin

如export、shift、if、eval、[、for、while等等。内建命令虽然不创建新的进程,但也会有Exit Status,通常也用0表示成功非零表示失败,虽然内建命令不创建新的进程,但执行结束后也会有一个状态码,也可以用特殊变量$?读出。

执行脚本

编写一个简单的脚本test.sh:

1
2
3
#! /bin/sh
cd ..
ls

Shell脚本中用#表示注释,相当于C语言的//注释。但如果#位于第一行开头,并且是#!(称为Shebang)则例外,它表示该脚本使用后面指定的解释器/bin/sh解释执行。如果把这个脚本文件加上可执行权限然后执行:

1
2
itcast$ chmod a+x test.sh
itcast$ ./test.sh

Shell会fork一个子进程并调用exec执行./test.sh这个程序,exec系统调用应该把子进程的代码段替换成./test.sh程序的代码段,并从它的_start开始执行。然而test.sh是个文本文件,根本没有代码段和_start函数,怎么办呢?其实exec还有另外一种机制,如果要执行的是一个文本文件,并且第一行用Shebang指定了解释器,则用解释器程序的代码段替换当前进程,并且从解释器的_start开始执行,而这个文本文件被当作命令行参数传给解释器。因此,执行上述脚本相当于执行程序

1
itcast$ /bin/sh ./test.sh

以这种方式执行不需要test.sh文件具有可执行权限。

如果将命令行下输入的命令用()括号括起来,那么也会fork出一个子Shell执行小括号中的命令,一行中可以输入由分号;隔开的多个命令,比如:

1
itcast$ (cd ..;ls -l)

和上面两种方法执行Shell脚本的效果是相同的,cd ..命令改变的是子Shell的PWD,而不会影响到交互式Shell。

然而命令

1
itcast$ cd ..;ls -l

则有不同的效果,cd ..命令是直接在交互式Shell下执行的,改变交互式Shell的PWD。这种方式相当于这样执行Shell脚本:

1
2
3
itcast$ source ./test.sh

itcast$ . ./test.sh

source或者.命令是Shell的内建命令,这种方式也不会创建子Shell,而是直接在交互式Shell下逐行执行脚本中的命令。

基本语法

变量

按照惯例,Shell变量通常由字母加下划线开头,由任意长度的字母、数字、下划线组成。有两种类型的Shell变量:

1、环境变量

环境变量可以从父进程传给子进程,因此Shell进程的环境变量可以从当前Shell进程传给fork出来的子进程。用printenv命令可以显示当前Shell进程的环境变量。

2、本地变量

只存在于当前Shell进程,用set命令可以显示当前Shell进程中定义的所有变量(包括本地变量和环境变量)和函数。

环境变量是任何进程都有的概念,而本地变量是Shell特有的概念。在Shell中,环境变量和本地变量的定义和用法相似。

在Shell中定义或赋值一个变量:

1
itcast$ VARNAME=value
  • 注意等号两边都不能有空格,否则会被Shell解释成命令和命令行参数。

一个变量定义后仅存在于当前Shell进程,它是本地变量,用export命令可以把本地变量导出为环境变量,定义和导出环境变量通常可以一步完成:

1
itcast$ export VARNAME=value

也可以分两步完成:

1
2
itcast$ VARNAME=value
itcast$ export VARNAME

unset命令可以删除已定义的环境变量或本地变量:

1
itcast$ unset VARNAME

Shell变量不需要明确定义类型,事实上Shell变量的值都是字符串,比如我们定义VAR=45,其实VAR的值是字符串45而非整数。Shell变量不需要先定义后使用,如果对一个没有定义的变量取值,则值为空字符串。

文件名代换

这些用于匹配的字符称为通配符(Wildcard),如:* ? [ ] 具体如下:

1
2
3
* 匹配0个或多个任意字符
? 匹配一个任意字符
[若干字符] 匹配方括号中任意一个字符的一次出现
1
2
3
4
itcast$ ls /dev/ttyS*
itcast$ ls ch0?.doc
itcast$ ls ch0[0-2].doc
itcast$ ls ch[012] [0-9].doc

注意,Globbing所匹配的文件名是由Shell展开的,也就是说在参数还没传给程序之前已经展开了,比如上述ls ch0[012].doc命令,如果当前目录下有ch00.doc和ch02.doc,则传给ls命令的参数实际上是这两个文件名,而不是一个匹配字符串。

命令代换

由 ` 反引号括起来的也是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中。例如定义一个变量存放date命令的输出:

1
2
itcast$ DATE=`date`
itcast$ echo $DATE

命令代换也可以用$()表示:

1
itcast$ DATE=$(date)

算术代换

使用$(()),用于算术计算,(())中的Shell变量取值将转换成整数,同样含义的$[ ]等价例如:

1
2
itcast$ VAR=45
itcast$ echo $(($VAR+3)) 等价于 echo $[VAR+3]或 $[$VAR+3]

$(())中只能用+-*/()运算符,并且只能做整数运算。

$[base#n],其中base表示进制,n按照base进制解释,后面再有运算数,按十进制解释。

1
2
3
echo $[2#10+11]
echo $[8#10+11]
echo $[16#10+11]

转义字符

和C语言类似,\在Shell中被用作转义字符,用于去除紧跟其后的单个字符的特殊意义(回车除外),换句话说,紧跟其后的字符取字面值。例如:

1
2
3
4
5
6
itcast$ echo $SHELL
/bin/bash
itcast$ echo \$SHELL
$SHELL
itcast$ echo \\
\

比如创建一个文件名为的文件($间含有空格)可以这样:

1
itcast$ touch \$\ \$

还有一个字符虽然不具有特殊含义,但是要用它做文件名也很麻烦,就是-号。如果要创建一个文件名以-号开头的文件,这样是不正确的:

1
2
3
itcast$ touch -hello
touch: invalid option -- h
Try `touch --help' for more information.

即使加上\转义也还是报错:

1
2
3
itcast$ touch \-hello
touch: invalid option -- h
Try `touch --help' for more information.

因为各种UNIX命令都把-号开头的命令行参数当作命令的选项,而不会当作文件名。如果非要处理以-号开头的文件名,可以有两种办法:

1
2
3
itcast$ touch ./-hello

itcast$ touch -- -hello

\还有一种用法,在\后敲回车表示续行,Shell并不会立刻执行命令,而是把光标移到下一行,给出一个续行提示符>,等待用户继续输入,最后把所有的续行接到一起当作一个命令执行。例如

1
2
itcast$ ls \
> -l

单引号

和C语言同,Shell脚本中的单引号和双引号一样都是字符串的界定符,而不是字符的界定符。单引号用于保持引号内所有字符的字面值,即使引号内的\和回车也不例外,但是字符串中不能出现单引号。如果引号没有配对就输入回车,Shell会给出续行提示符,要求用户把引号配上对。例如:

1
2
3
4
5
6
itcast$ echo '$SHELL'
$SHELL
itcast$ echo 'ABC\(回车)
> DE'(再按一次回车结束命令)
ABC\
DE

双引号

被双引号用括住的内容,将被视为单一字串。它防止通配符扩展,但允许变量扩展。这点与单引号的处理方式不同

1
2
3
4
5
6
7
8
9
10
11
itcast$ DATE=$(date)
itcast$ echo "$DATE"
itcast$ echo '$DATE'

itcast$ VAR=200
itcast$ echo $VAR
200
itcast$ echo '$VAR'
$VAR
itcast$ echo "$VAR"
200

Shell语法

条件测试

命令test或 [ 可以测试一个条件是否成立,如果测试结果为真,则该命令的Exit Status为0,如果测试结果为假,则命令的Exit Status为1(注意与C语言的逻辑表示正好相反)。例如测试两个数的大小关系:

1
2
3
4
5
6
7
8
9
10
11
tcast@ubuntu:~$ var=2
itcast@ubuntu:~$ test $var -gt 1
itcast@ubuntu:~$ echo $?
0
itcast@ubuntu:~$ test $var -gt 3
itcast@ubuntu:~$ echo $?
1
itcast@ubuntu:~$ [ $var -gt 3 ]
itcast@ubuntu:~$ echo $?
1
itcast@ubuntu:~$

虽然看起来很奇怪,但左方括号 [ 确实是一个命令的名字,传给命令的各参数之间应该用空格隔开,比如:$VAR、-gt、3、] 是 [ 命令的四个参数,它们之间必须用空格隔开。命令test或 [ 的参数形式是相同的,只不过test命令不需要 ] 参数。以 [ 命令为例,常见的测试命令如下表所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
[ -d DIR ] 如果DIR存在并且是一个目录则为真
[ -f FILE ] 如果FILE存在且是一个普通文件则为真
[ -z STRING ] 如果STRING的长度为零则为真
[ -n STRING ] 如果STRING的长度非零则为真
[ STRING1 = STRING2 ] 如果两个字符串相同则为真
[ STRING1 != STRING2 ] 如果字符串不相同则为真
[ ARG1 OP ARG2 ] ARG1和ARG2应该是整数或者取值为整数的变量,OP是
-eq(等于)
-ne(不等于)
-lt(小于)
-le(小于等于)
-gt(大于)
-ge(大于等于)

和C语言类似,测试条件之间还可以做与、或、非逻辑运算:

1
2
3
[ ! EXPR ] EXPR可以是上表中的任意一种测试条件,!表示“逻辑反(非)”
[ EXPR1 -a EXPR2 ] EXPR1和EXPR2可以是上表中的任意一种测试条件,-a表示“逻辑与”
[ EXPR1 -o EXPR2 ] EXPR1和EXPR2可以是上表中的任意一种测试条件,-o表示“逻辑或”

例如:

1
2
3
4
$ VAR=abc
$ [ -d Desktop -a $VAR = 'abc' ]
$ echo $?
0

注意,如果上例中的$VAR变量事先没有定义,则被Shell展开为空字符串,会造成测试条件的语法错误(展开为[ -d Desktop -a = 'abc' ]),作为一种好的Shell编程习惯应该总是把变量取值放在双引号之中(展开为[ -d Desktop -a "" = 'abc' ]):

1
2
3
4
5
6
$ unset VAR
$ [ -d Desktop -a $VAR = 'abc' ]
bash: [: too many arguments
$ [ -d Desktop -a "$VAR" = 'abc' ]
$ echo $?
1

分支

if/then/fi

和C语言类似,在Shell中用if、then、elif、else、fi这几条命令实现分支控制。这种流程控制语句本质上也是由若干条Shell命令组成的,例如先前讲过的

1
2
3
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi

其实是三条命令,if [ -f ∼/.bashrc ]是第一条,then . ∼/.bashrc是第二条,fi是第三条。如果两条命令写在同一行则需要用;号隔开,一行只写一条命令就不需要写;号了,另外,then后面有换行,但这条命令没写完,Shell会自动续行,把下一行接在then后面当作一条命令处理。和[命令一样,要注意命令和各参数之间必须用空格隔开。if命令的参数组成一条子命令,如果该子命令的Exit Status为0(表示真),则执行then后面的子命令,如果Exit Status非0(表示假),则执行elif、else或者fi后面的子命令。if后面的子命令通常是测试命令,但也可以是其它命令。Shell脚本没有{}括号,所以用fi表示if语句块的结束。见下例:

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

if [ -f /bin/bash ]
then
echo "/bin/bash is a file"
else
echo "/bin/bash is NOT a file"
fi
if :; then echo "always true"; fi

:是一个特殊的命令,称为空命令,该命令不做任何事,但Exit Status总是真。此外,也可以执行/bin/true或/bin/false得到真或假的Exit Status。再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#! /bin/bash

echo "Is it morning? Please answer yes or no."
read YES_OR_NO
if [ "$YES_OR_NO" = "yes" ]; then
echo "Good morning!"
elif [ "$YES_OR_NO" = "no" ]; then
echo "Good afternoon!"
else
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1
fi
exit 0

上例中的read命令的作用是等待用户输入一行字符串,将该字符串存到一个Shell变量中。

此外,Shell还提供了&&||语法,和C语言类似,具有Short-circuit特性,很多Shell脚本喜欢写成这样:

1
test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

&&相当于if…then…,而|相当于if not…then…。&&和||用于连接两个命令,而上面讲的-a和-o仅用于在测试表达式中连接两个测试条件,要注意它们的区别,例如:

1
test "$VAR" -gt 1 -a "$VAR" -lt 3

和以下写法是等价的

1
test "$VAR" -gt 1 && test "$VAR" -lt 3
case/esac

case命令可类比C语言的switch/case语句,esac表示case语句块的结束。C语言的case只能匹配整型或字符型常量表达式,而Shell脚本的case可以匹配字符串和Wildcard,每个匹配分支可以有若干条命令,末尾必须以;;结束,执行时找到第一个匹配的分支并执行相应的命令,然后直接跳到esac之后,不需要像C语言一样用break跳出。

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

echo "Is it morning? Please answer yes or no."
read YES_OR_NO
case "$YES_OR_NO" in
yes|y|Yes|YES)
echo "Good Morning!";;
[nN]*)
echo "Good Afternoon!";;
*)
echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."
exit 1;;
esac
exit 0

使用case语句的例子可以在系统服务的脚本目录/etc/init.d中找到。这个目录下的脚本大多具有这种形式(以/etc/init.d/nfs-kernel-server为例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case "$1" in
start)
...
;;
stop)
...
;;
reload | force-reload)
...
;;
restart)
...
*)
log_success_msg"Usage: nfs-kernel-server {start|stop|status|reload|force-reload|restart}"
exit 1
;;
esac

启动nfs-kernel-server服务的命令是

1
$ sudo /etc/init.d/nfs-kernel-server start

$1是一个特殊变量,在执行脚本时自动取值为第一个命令行参数,也就是start,所以进入start)分支执行相关的命令。同理,命令行参数指定为stop、reload或restart可以进入其它分支执行停止服务、重新加载配置文件或重新启动服务的相关命令。

循环

for/do/done

Shell脚本的for循环结构和C语言很不一样,它类似于某些编程语言的foreach循环。例如:

1
2
3
4
5
#! /bin/sh

for FRUIT in apple banana pear; do
echo "I like $FRUIT"
done

FRUIT是一个循环变量,第一次循环$FRUIT的取值是apple,第二次取值是banana,第三次取值是pear。再比如,要将当前目录下的chap0、chap1、chap2等文件名改为chap0~、chap1~、chap2~等(按惯例,末尾有~字符的文件名表示临时文件),这个命令可以这样写:

1
2
3
$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done
while/do/done

while的用法和C语言类似。比如一个验证密码的脚本:

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

echo "Enter password:"
read TRY
while [ "$TRY" != "secret" ]; do
echo "Sorry, try again"
read TRY
done

下面的例子通过算术运算控制循环的次数:

1
2
3
4
5
6
7
! /bin/sh

COUNTER=1
while [ "$COUNTER" -lt 10 ]; do
echo "Here we go again"
COUNTER=$(($COUNTER+1))
done

另,Shell还有until循环,类似C语言的do…while。

break/continue

break[n]可以指定跳出几层循环;continue跳过本次循环,但不会跳出循环。

即break跳出,continue跳过。

位置参数和特殊变量

有很多特殊变量是被Shell自动赋值的,我们已经遇到了$?$1。其他常用的位置参数和特殊变量在这里总结一下:

1
2
3
4
5
6
7
$0 			相当于C语言main函数的argv[0]
$1、$2... 这些称为位置参数(Positional Parameter),相当于C语言main函数的argv[1]、argv[2]...
$# 相当于C语言main函数的argc - 1,注意这里的#后面不表示注释
$@ 表示参数列表"$1" "$2" ...,例如可以用在for循环中的in后面。
$* 表示参数列表"$1" "$2" ...,同上
$? 上一条命令的Exit Status
$$ 当前进程号

位置参数可以用shift命令左移。比如shift 3表示原来的$4现在变成$1,原来的$5现在变成$2等等,原来的$1$2$3丢弃,$0不移动。不带参数的shift命令相当于shift 1。例如:

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

echo "The program $0 is now running"
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"
shift
echo "The first parameter is $1"
echo "The second parameter is $2"
echo "The parameter list is $@"

输入输出

echo

显示文本行或变量,或者把字符串输入到文件。

1
2
3
4
5
6
7
echo [option] string
-e 解析转义字符
-n 不回车换行。默认情况echo回显的内容后面跟一个回车换行。
echo "hello\n\n"
echo -e "hello\n\n"
echo "hello"
echo -n "hello"
管道

可以通过 | 把一个命令的输出传递给另一个命令做输入。

1
2
3
4
cat myfile | more
ls -l | grep "myfile"
df -k | awk '{print $1}' | grep -v "文件系统"
df -k 查看磁盘空间,找到第一列,去除“文件系统”,并输出
tee

tee命令把结果输出到标准输出,另一个副本输出到相应文件。

1
df -k | awk '{print $1}' | grep -v "文件系统" | tee a.txt

tee -a a.txt表示追加操作。

文件重定向
1
2
3
4
5
6
7
8
cmd > file 				把标准输出重定向到新文件中
cmd >> file 追加
cmd > file 2>&1 标准出错也重定向到1所指向的file里
cmd >> file 2>&1
cmd < file1 > file2 输入输出都定向到文件里
cmd < &fd 把文件描述符fd作为标准输入
cmd > &fd 把文件描述符fd作为标准输出
cmd < &- 关闭标准输入

函数

和C语言类似,Shell中也有函数的概念,但是函数定义中没有返回值也没有参数列表。例如:

1
2
3
4
5
6
#! /bin/sh

foo(){ echo "Function foo is called";}
echo "-=start=-"
foo
echo "-=end=-"

注意函数体的左花括号 { 和后面的命令之间必须有空格或换行,如果将最后一条命令和右花括号 } 写在同一行,命令末尾必须有分号;。但,不建议将函数定义写至一行上,不利于脚本阅读。

在定义foo()函数时并不执行函数体中的命令,就像定义变量一样,只是给foo这个名一个定义,到后面调用foo函数的时候(注意Shell中的函数调用不写括号)才执行函数体中的命令。Shell脚本中的函数必须先定义后调用,一般把函数定义语句写在脚本的前面,把函数调用和其它命令写在脚本的最后(类似C语言中的main函数,这才是整个脚本实际开始执行命令的地方)。

Shell函数没有参数列表并不表示不能传参数,事实上,函数就像是迷你脚本,调用函数时可以传任意个参数,在函数内同样是用$0$1$2等变量来提取参数,函数中的位置参数相当于函数的局部变量,改变这些变量并不会影响函数外面的$0$1$2等变量。函数中可以用return命令返回,如果return后面跟一个数字则表示函数的Exit Status。

下面这个脚本可以一次创建多个目录,各目录名通过命令行参数传入,脚本逐个测试各目录是否存在,如果目录不存在,首先打印信息然后试着创建该目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#! /bin/sh

is_directory()
{
DIR_NAME=$1
if [ ! -d $DIR_NAME ]; then
return 1
else
return 0
fi
}
for DIR in "$@"; do
if is_directory "$DIR"
then :
else
echo "$DIR doesn't exist. Creating it now..."
mkdir $DIR > /dev/null 2>&1 # 标准输出指向/dev/null, 标准出错指向标准输出即/dev/null
if [ $? -ne 0 ]; then
echo "Cannot create directory $DIR"
exit 1
fi
fi
done

注意:is_directory()返回0表示真返回1表示假。

Shell调试方法

Shell提供了一些用于调试脚本的选项,如:

1
2
3
-n		读一遍脚本中的命令但不执行,用于检查脚本中的语法错误。
-v 一边执行脚本,一边将执行过的脚本命令打印到标准错误输出。
-x 提供跟踪执行信息,将执行的每一条命令和结果依次打印出来。

这些选项有三种常见的使用方法:

  1. 在命令行提供参数。如:

    1
    $ sh -x ./script.sh`
  2. 在脚本开头提供参数。如:

    1
    #! /bin/sh -x
  3. 在脚本中用set命令启用或禁用参数。如:

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

    if [ -z "$1" ]; then
    set -x
    echo "ERROR: Insufficient Args."
    exit 1
    set +x
    fi
    # set -x和set +x分别表示启用和禁用-x参数,这样可以只对脚本中的某一段进行跟踪调试。

欢迎关注我的其它发布渠道