资料来源:《Linux命令行与shell脚本编程大全》
使用的是CentOS的bash shell。
shell是什么
用户与linux内核之间的一个“代理”,让用户能高效、安全、低成本地使用 Linux 内核。
除了作为中介,shell还能进行编程。也可将shell看做一种编程语言,shell程序本身就是编译器。
shell就是脚本语言,一边执行一边翻译,不会生成任何可执行文件。
命令
shell的命令可分为内置命令和外部命令。
内置命令可视为函数,外部命令可视为另一个程序。
一个例子:
getsum -s 1 -e 100
输入了一个命令getsum
,其后跟着4个参数,每个参数用空格分隔。
内置命令
shell本身自带的命令,可以看做是shell程序的内部函数。与shell编译成一体,在shell启动时内置命令也被加载到内存中。
外部命令
也称为系统文件,不属于bash shell的一部分。
通常位于/bin
、/usr/bin
、/sbin
、/usr/sbin
中。
外部命令执行时,会创建出一个子进程,因此运行效率比内置命令低。
自己的理解:外部命令就是bash shell之外额外安装的应用程序,只是通过bash shell将其启动而已,除此以外没有任何联系。自然需要创建子进程才能运行。
相关命令
which
应该是通过查找$PATH
来获取外部命令的位置,用于查找内建命令则返回no exit in $PATH
。
type
判断一个命令是外部命令还是内建命令。
部分命令同时拥有外部命令和内建命令2个版本,可用type -a
查看。(显示有路径的就是外部命令)
calhost ~]# type -a echo
echo is a shell builtin
echo is /usr/bin/echo
echo is /bin/echo
[root@localhost ~]# type -a exit
exit is a shell builtin
修改命令提示符
Shell 通过PS1
和PS2
这两个环境变量来控制提示符的格式,修改PS1
和PS2
的值就能修改命令提示符的格式。
[root@client0 ~]# echo $PS1
[\u@\h \W]\$
[root@client0 ~]# echo $PS2
>
通配符的含义:
字符 | 描述 |
---|---|
\a | 铃声字符 |
\d | 格式为“日 月 年”的日期 |
\e | ASCII 转义字符 |
\h | 本地主机名 |
\H | 完全合格的限定域主机名 |
\j | shell 当前管理的作业数 |
\1 | shell 终端设备名的基本名称 |
\n | ASCII 换行字符 |
\r | ASCII 回车 |
\s | shell 的名称 |
\t | 格式为“小时:分钟:秒”的24小时制的当前时间 |
\T | 格式为“小时:分钟:秒”的12小时制的当前时间 |
@ | 格式为 am/pm 的12小时制的当前时间 |
\u | 当前用户的用户名 |
\v | bash shell 的版本 |
\V | bash shell 的发布级别 |
\w | 当前工作目录 |
\W | 当前工作目录的基本名称 |
! | 该命令的 bash shell 历史数 |
# | 该命令的命令数量 |
$ | 如果是普通用户,则为美元符号$ ;如果超级用户(root 用户),则为井号# 。 |
\nnn | 对应于八进制值 nnn 的字符 |
\ | 斜杠 |
[ | 控制码序列的开头 |
] | 控制码序列的结尾 |
注意,所有的特殊字符均以反斜杠\
开头。
修改
如果值中含有空格,则必须要用单引号或双引号括起来,不然shell会将空格后的字符识别为新命令。
[root@client0 ~]# PS1='[\t][\u@\h \W]\$'
[04:41:41][root@client0 ~]# // 多了时间
本质上就是修改变量,所以可以随便赋值(虽然这么做没意义)。
[04:41:41][root@client0 ~]#PS1='asdqwe[\t][\u@\h \W]\$'
asdqwe[04:42:32][root@client0 ~]#
通过对变量直接赋值的方式只是暂时修改,仅在当前shell会话期间有效。要永久修改得修改bash shell配置文件。
shell的一系列括号
可以在终端输入多个命令,命令之间用(;)分隔
[root@localhost ~]# echo hello; echo world; sleep 1; echo wake
hello
world
wake
组命令
组命令,就是将多个命令划分为一组,或者看成一个整体。
写法有两种:花括号/括号包围,分号分隔(命令之间有没有空格无所谓)
# 花括号和命令之间必须有一个空格,并且最后一个命令必须用一个分号或者一个换行符结束。
{ command1; command2; command3; . . . }
(command1; command2; command3;. . . )
a
进程列表 ()
格式相对于{}更自由,命令间不需要用空格分开,也不需要以分号
;
结尾。
用括号括起来的一系列命令形成进程列表。生成了一个子shell来执行这些命令。
普通地输入多个命令:
[root@localhost ~]# echo -n "hello "; echo world; sleep 5 &
hello world
[2] 3693
[1] Done ( echo -n "hello "; echo world; sleep 5 )
[root@localhost ~]# ps
PID TTY TIME CMD
3199 pts/0 00:00:00 bash
3693 pts/0 00:00:00 sleep
3700 pts/0 00:00:00 ps
进程列表:
[root@localhost ~]# (echo -n "hello "; echo world; sleep 5) &
[1] 3670
hello world
[root@localhost ~]# ps
PID TTY TIME CMD
3199 pts/0 00:00:00 bash
3670 pts/0 00:00:00 bash
3672 pts/0 00:00:00 sleep
3678 pts/0 00:00:00 ps
进程分组 {}
将一系列命令放入花括号({})中。不会生成子shell。
注意:所有命令(包括最后一个)结尾都要加上分号(;),且花括号与命令之间必须留有一个空格。
**疑问:**书中和网上都表示进程分组不会像进程列表那样生成子shell,只是给命令进行了分组,但如下所示,用解决:直接使用ps
仍能看到有2个bash进程存在。ps
命令显式的结果无法作为“是否生成了子shell”
的判断依据,得参考变量$BASH_SUBSHELL
。
[root@localhost ~]# { echo -n "hello "; echo world; sleep 5; } &
[1] 3764
hello world
[root@localhost ~]# ps
PID TTY TIME CMD
3199 pts/0 00:00:00 bash
3764 pts/0 00:00:00 bash
3766 pts/0 00:00:00 sleep
3772 pts/0 00:00:00 ps
关于{}的注意事项:
**1、**为什么要有空格?
[root@client0 asd]# {ll; echo "hello"; cat record.txt}
-bash: {ll: command not found
hello
cat: record.txt}: No such file or directory
答:即使没有空格,夹在中间的命令仍会执行,但第一个指令、最后一个指令在做判断时会将分号也作为指令的一部分来解析。所以导致错误。
**2、**为什么要有;
结尾?
[root@client0 asd]# { ll; echo "hello"; cat record.txt }
>
[root@client0 asd]# { ll;echo "hello";cat record.txt }
> ;
-bash: syntax error near unexpected token `;'
没用;
结尾的指令会呈现待输入的状态。
关于二者是否生成子shell的验证
[root@localhost ~]# echo $BASH_SUBSHELL
0
不能直接执行
((echo $BASH_SUBSHELL))
,所以需要一个pwd
命令占位。
[root@localhost ~]# (pwd; echo $BASH_SUBSHELL)
/root
1
[root@localhost ~]# (pwd; (echo $BASH_SUBSHELL))
/root
2
[root@localhost ~]# { pwd; echo $BASH_SUBSHELL; }
/root
0
[root@localhost ~]# { pwd; { echo $BASH_SUBSHELL; } }
/root
0
${}
变量。
将${}内的元素视为一个变量,能帮助编译器识别变量的边界。
skill="Java"
echo "I am good at ${skill}Script"
# 如果没加花括号:
# echo "I am good at $skillScript"
# 解释器会把$skillScript当做一个变量(值为空)。
在数组中的应用:
[root@localhost ~]# echo ${arry[*]}
a b c d e
[root@localhost ~]# echo arry[1]
arry[1]
[root@localhost ~]# echo $arry[1]
a[1]
[root@localhost ~]# echo ${arry[1]}
b
$() ``
命令替换。
内部不需要空格隔开
取得命令的执行结果,可以为变量赋值,或是作为另一个命令的参数。
oot@localhost ~]# myPath=$(pwd)
[root@localhost ~]# echo $myPath
/root
[root@localhost ~]# ll $(pwd)
total 3664
-rw-------. 1 root root 1526 Apr 29 07:25 anaconda-ks.cfg
drwxr-xr-x. 7 root root 4096 Jul 27 16:59 coding
drwxr-xr-x. 2 root root 6 May 5 08:46 Desktop
drwxr-xr-x. 2 root root 6 May 5 08:46 Documents
...
二者区别:
1、$()
仅在bash shell中有效,反引号``在多个shell中都有效。
2、$()
可以嵌套,反引号不行。
$[]
数学运算。
执行数学运算。内部不需要空格隔开。
[root@localhost ~]# echo $[ 1 + 1 ]
2
[root@localhost ~]# echo $[1 + 1]
2
[root@localhost ~]# echo $[1+1]
2
[ ]
等价于test命令。
在if-then
语句中替代test
。if与[],[ ]内部必须要有空格分开。
if [ condition ]
then
commands
fi
(())
可用于数学运算、逻辑运算、流程控制。
双括号内使用变量可以不加$
。
对空格没有严格限制,下面的例子使用空格只是为了排版美观。
一、if 判断条件中的数学运算
(( expression ))
test命令只能使用-eq
等命令进行简单的算术操作,双括号命令提供了更多的数学符号。
可以在双括号内直接使用数学运算符,不需要使用(\)进行转义。
if (( $val * 2 > 10 ))
...
二、C语音风格for
除了双括号外,for循环的条件设置与C语言for几乎完全一致。(shell里用于迭代的变量不需要声明类型,直接用)
// 尽管变量i是第一次出现
for (( i = 0; i < 10; ++i ))
三、执行数学运算
[root@localhost ~]# a=1
[root@localhost ~]# b=10
[root@localhost ~]# (( c = a + b + 3 ))
[root@localhost ~]# echo $c
14
补充
逻辑运算
echo $((a > 1 ? 8 : 9))
用,
分隔多个表达式
带“$”: 将表达式的值赋予变量a
a=$((a+1,b++,c++));
[[ ]]
可以视为[ ]
的扩展版。
expression与括号前后都必须有空格
[[ expression ]]
使用了test命令中采用的标准字符串比较,还额外提供模式匹配功能。
[[ ]]
内使用变量不需要对$变量
加双引号,使用<、>也不需要转义。
[[ ]]
内可以直接使用&&
、||
和 !
三种逻辑运算符。
提供模式匹配功能,可以使用正则表达式来匹配字符串值。
# 匹配所有以'r'开头的字符串
if [[ $USER == r* ]]
then
...
else
...
fi
一个简单的shell脚本
#!/bin/bash
echo "What is your name?"
read PERSON
echo "Hello, $PERSON"
#!
:一个约定标记,告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell;后面的/bin/bash
指明了解释器的具体位置。
echo
:一个命令,向标准输出文件输出文本。
#
:以#
开头的是注释行。除了#!
以外,以#
开头的都是注释。
read
:从标准输入文件读取用户输入数据。
脚本功能:输出一段问候,然后从标准输入读取数据,赋值给PERSON
变量,然后读出该变量。
执行shell脚本
用另一个进程执行
方法一:脚本文件中第一行有#!/bin/bash
可在终端直接调用(先要赋予执行权限)
chmod +x
[root@client0 test]#chmod +x test.sh
[root@client0 test]#./test.sh
What is your name?
qwe
Hello, qwe
赋予权限时使用的是相对路径,执行的时候因为没有将路径记录到PATH
变量中,所以必须用绝对路径执行该文件。
./
表示当前目录。
方法二:将含有shell代码的文件作为参数传给bash程序
[root@localhost ~]# vim file
# 仅一行,没有指定编译器
echo hello world
减少file文件的权限:
[root@localhost ~]# chmod -w file
[root@localhost ~]# ll file
-r--r--r--. 1 root root 17 Sep 6 21:40 file
成功运行。
[root@localhost ~]# bash file
hello world
文件file
,可以是一个可执行的shell脚本,也可以是一个只含有代码的只读文件。
在当前进程中执行shell
使用source
命令或者简写为.
。
source
是shell内置命令,读取脚本文件中的代码,并依次执行所有语句。忽略文件权限。
[mozhiyan@localhost demo]$ source ./test.sh
[mozhiyan@localhost demo]$ source test.sh
[mozhiyan@localhost demo]$ . ./test.sh
[mozhiyan@localhost demo]$ . test.sh
变量
bash shell中,每一个变量的值都是字符串,无论赋值时是否使用了引号。
定义、赋值
variable=value
variable='value'
variable="value"
三者等价。如果value
中带有空白符(空格、缩进等),就必须使用引号。
注意!=
左右不能有空格。
变量命名规范和大部分编程语言相同。
!使用变量时用花括号修饰变量
在输出变量时可以加上花括号{}
,帮助解释器识别变量的边界。
skill="Java"
echo "I am good at ${skill}Script"
# 如果没加花括号:
# echo "I am good at $skillScript"
# 解释器会把$skillScript当做一个变量(值为空)。
修改
像赋值那样直接variable=value
就行了。只有在使用变量时才在变量名前加$
。
操作变量时不需要加
$
,使用变量时需要加$
。
变量用单双引号赋值的区别
[root@client0 asd]# vim test0
#!/bin/bash
value=123qwe
str1='str1: $value'
str2="str2: $value"
echo $str1
echo $str2
[root@client0 asd]# test0
str1: $value
str2: 123qwe
使用vim编辑时,也能看到单引号
''
内的变量也是红色的,与一般字符串相同;而双引号""
内的变量是紫色的。
以单引号' '
包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。
以双引号" "
包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。
!? 变量的“环境”
脚本之间的变量交互。还没弄清楚。
假设从脚本A中调用脚本B,脚本B运行时,似乎能共享脚本A中的变量。
caller.sh
:
#!/bin/bash
# 定义了循环的条件变量i和n,但不对i执行操作
i=0
n=3
while (( i < n ))
do
echo "caller.sh: i=$i"
echo "j before calling looper.sh: $j"
. ./looper.sh
echo "j after calling looper.sh: $j"
echo "------------------------------------------"
done
looper.sh
:
#!/bin/bash
# 让变量i递增,定义变量j
j=111
(( i++ ))
echo "looper.sh: i=$i"
运行效果:(等价于直接将looper.sh
的代码粘贴到caller.sh
中调用它的位置)
1、caller.sh
通过调用looper.sh
来递增控制变量i
;
2、caller.sh
在调用looper.sh
后可以访问在looper.sh
中定义的变量j
。
[root@localhost ~]# ./caller.sh
caller.sh: i=0
j before calling looper.sh:
looper.sh: i=1
j after calling looper.sh: 111
------------------------------------------
caller.sh: i=1
j before calling looper.sh: 111
looper.sh: i=2
j after calling looper.sh: 111
------------------------------------------
caller.sh: i=2
j before calling looper.sh: 111
looper.sh: i=3
j after calling looper.sh: 111
------------------------------------------
变量的作用域
Shell 变量的作用域可以分为三种:
- 有的变量只能在函数内部使用,这叫做局部变量(local variable);
- 有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
- 而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)。
局部变量:local
shell中定义的变量默认是全局变量,即使是在函数内定义的变量。
#!/bin/bash
#定义函数
function func(){
a=99
}
#调用函数,定义全局变量a
func
#输出变量a的值
echo $a
运行脚本:
[root@client0 asd]# test0
99
设置为局部变量:local
修饰变量
#定义函数
function func(){
local a=99
}
运行脚本得到的结果为空。
全局变量
注意!:一个shell进程不等于一个shell脚本文件。
打开一个 Shell 窗口就创建了一个 Shell 进程,打开多个 Shell 窗口就创建了多个 Shell 进程,每个 Shell 进程都是独立的,拥有不同的进程 ID。
例子:2个文件sh1
、sh2
sh1:
#!/bin/bash
a=123
b=456
echo $a
echo $b
sh2:
#!/bin/bash
echo $a
echo $b
运行:
[root@localhost ~]# ./sh1
123
456
[root@localhost ~]# ./sh2
在一个 Shell 进程中可以使用 source
命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。
[root@localhost ~]# . ./sh1
123
456
[root@localhost ~]# . ./sh2
123
456
环境变量:export
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效,子进程中定义的全局变量也不会影响到父进程。
[root@localhost ~]# var=111
[root@localhost ~]# echo $var
111
# --------------------------------------
[root@localhost ~]# bash
[root@localhost ~]# echo $var
[root@localhost ~]# var2=222
[root@localhost ~]# echo $var2
222
[root@localhost ~]# exit
exit
# --------------------------------------
[root@localhost ~]# echo $var2
如果使用export
命令将全局变量导出,那么它就在所有的子进程中也有效,这称为“环境变量”。
环境变量只能从父进程传递给子进程。
子进程对环境变量的修改不会影响到父进程。
创建 Shell 子进程最简单的方式是运行 bash 命令。能通过exit命令一层层退出shell。
[root@client0 asd]# var=10
[root@client0 asd]# echo $var
10
[root@client0 asd]# bash
[root@client0 asd]# echo $var
[root@client0 asd]# export var # 也可以在定义的同时导出为环境变量:export var=10
[root@client0 asd]# bash
[root@client0 asd]# echo $var
10
环境变量持久化
将变量存入bash shell的配置文件中。
可以放入/etc/profile
文件中,但不推荐,因为这个文件在系统升级时会随之更新。
推荐:
在/etc/profile.d
目录下创建一个以.sh
结尾的文件,将新的/修改的全局环境变量设置放入其中。
大多数linux发行版中,使用$HOME/.bashrc
文件存储个人用户的永久性bash shell变量。
Shell数组
shell支持数组,下标从0开始。
获取数组中的元素要使用下标[ ]
,下标可以是一个整数,也可以是一个结果为整数的表达式;当然,下标必须大于等于 0。
常用的 Bash Shell 只支持一维数组,不支持多维数组。
定义 数组名=(e1 e2 ...)
[root@localhost ~]# arry=(a b c d e)
获取 ${数组名[下标]}
(下标从0开始)
# 直接取数组名只能获得第一个元素
[root@localhost ~]# echo $arry
a
# 正确的操作
[root@localhost ~]# echo ${arry[0]}
a
[root@localhost ~]# echo ${arry[1]}
b
[root@localhost ~]# echo ${arry[2]}
c
[root@localhost ~]# echo ${arry[3]}
d
[root@localhost ~]# echo ${arry[4]}
e
[root@localhost ~]# echo ${arry[5]}
oot@localhost ~]# echo ${arry[*]}
a b c d e
删除 uset 数组名[下标]
[root@localhost ~]# echo ${arry[*]}
a b c d e
[root@localhost ~]# unset arry[2]
[root@localhost ~]# echo ${arry[*]}
a b d e
效果像vector容器的删除,后面的元素会往前填补空缺的位置。
补充:将数组转换为一串用空格分隔参数的字符串
$(echo ${arry[*]})
,可以视为标准操作吧。
arry=(1 2 3 4 5)
str_arry=$(echo ${arry[*]})
创建和赋值
在 Shell 中,用括号( )
来表示数组,数组元素之间用空格来分隔。定义数组的一般形式:
array_name=(ele1 ele2 ele3 ... elen)
赋值号
=
两边不能有空格。
Shell 是弱类型的,它并不要求所有数组元素的类型必须相同,例如:(实际上都记为字符串了)
arr=(20 5.6 "http://c.biancheng.net/shell/")
1、Shell 数组的长度不固定,定义之后还可以增加元素。例如,对于上面的 nums 数组,它的长度是 6,使用下面的代码会在最后增加一个元素,使其长度扩展到 7:
nums[6]=88
2、无需逐个元素给数组赋值**,下面的代码就是只给特定元素赋值:
ages=([3]=24 [5]=19 [10]=12)
以上代码就只给第 3、5、10 个元素赋值,所以数组长度是 3。
!!:
综合上述两条,shell的数组与C/C++那样的连续存储的数组应该是不一样的。例如下方代码:
[root@client0 asd]# arr=(1 5.6 asdqwe) [root@client0 asd]# echo ${arr[0]} 1 [root@client0 asd]# echo ${arr[1]} 5.6 [root@client0 asd]# echo ${arr[2]} asdqwe [root@client0 asd]# echo ${arr[3]} [root@client0 asd]# arr[10]=987 [root@client0 asd]# echo ${arr[10]} 987
与其将
arr
看做是个数组,不如说是个变量集合;下标arr[10]=987
与其说是为数组的第11个元素赋值
,不如说是在arr变量集合中用下标10登记了一个新变量,变量名为“arr[10]”
。
获取数组元素
必须有{}
,否则shell不会将其识别为数组,只会输出第一个元素。
${array_name[index]}
使用*
或@
可以获取数组中所有元素:
[root@client0 asd]# echo ${arr[*]}
1 5.6 asdqwe 987
[root@client0 asd]# echo ${arr[@]}
1 5.6 asdqwe 987
获取数组长度
数组长度,就是数组元素的个数。
利用@
或*
,将数组扩展成列表,然后使用#
来获取数组元素的个数。
${#array_name[@]}
${#array_name[*]}
也可以通过#
来获取特定元素的长度:
[root@client0 ~]# arr[10]="qwe"
[root@client0 ~]# echo ${arr[10]}
qwe
[root@client0 ~]# echo ${#arr[10]}
3
数组合并
将两个数组连接成一个数组。
思路:先利用@
或*
,将数组扩展成列表,然后再合并到一起。具体格式如下:
array_new=(${array1[@]} ${array2[@]})
array_new=(${array1[*]} ${array2[*]})
# 注意2个数组间的空格
删除数组元素
unset array_name[index]
不写下标,而是写成下面的形式:
unset array_name
那么就是删除整个数组,所有元素都会消失。
例子:
[root@client0 ~]# echo ${arr3[*]}
1 2 3 4 5 qwe 1.1 2.2 3.3
[root@client0 ~]# unset arr3[5]
[root@client0 ~]# echo ${arr3[*]}
1 2 3 4 5 1.1 2.2 3.3
[root@client0 ~]# unset arr3
[root@client0 ~]# echo ${arr3[*]}
[root@client0 ~]#
关联数组(数组的下标为字符串)
declare -A 数组名
关联数组也称为“键值对(key-value)”数组:
键(key)——字符串形式的数组下标,
值(value)——元素值。
例子:
# 先定义,再赋值
declare -A color
color["red"]="#ff0000"
color["green"]="#00ff00"
color["blue"]="#0000ff"
# 定义的同时赋值(用逗号“,”分隔)
declare -A color=(["red"]="#ff0000", ["green"]="#00ff00", ["blue"]="#0000ff")
赋值、删除、获取所有元素、获取元素个数的方法与普通函数一样。
**特殊的方法:**获取关联数组的所有下标值:
[root@client0 ~]# echo ${!colors[@]}
red blue green
Shell的字符串
3种定义方式:
str1='C语言中文网'
str2="shell script"
str3=c.biancheng.net
区别:
1) 由单引号' '
包围的字符串:
- 任何字符都会原样输出,在其中使用变量是无效的。
- 字符串中不能出现单引号,即使对单引号进行转义也不行。
2) 由双引号" "
包围的字符串:
- 如果其中包含了某个变量,那么该变量会被解析(得到该变量的值),而不是原样输出。
- 字符串中可以出现双引号,只要它被转义了就行(\)。
3) 不被引号包围的字符串
- 不被引号包围的字符串中变量也会被解析,这一点和双引号
" "
包围的字符串一样。 - 字符串中不能出现空格,否则空格后边的字符串会作为其他变量或者命令解析。
例子:
#!/bin/bash
n=74
str1=c.biancheng.net$n
str2="shell \"script\" $n"
str3='C语言中文网 $n'
echo $str1
echo $str2
echo $str3
[root@client0 asd]# exe1
c.biancheng.net74
shell "script" 74
C语言中文网 $n
获取字符串长度
${}
和#
。
${#string_name}
string_name 表示字符串名字。
字符串拼接
在脚本语言中,字符串的拼接(也称字符串连接或者字符串合并)往往都非常简单。
在 Shell 中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接,非常简单粗暴:
[root@client0 asd]# str1="/root"
[root@client0 asd]# str2="/temp"
[root@client0 asd]# str3="/test.exe"
[root@client0 asd]# str4=$str1$str2${str3}_123
[root@client0 asd]# echo $str4
/root/temp/test.exe_123
# 注意,当“变量”与“字符串常量”连接时,要加上{}将二者区分,例如“${str3}_123”
截取字符串 !
2种方式:从指定位置开始;从指定字符开始。
从指定位置开始
需要2个参数:起始位置、截取长度。
1)从字符串左边开始计数
${string: start :length}
string 是要截取的字符串,start 是起始位置(从左边开始,从 0 开始计数),length 是要截取的长度(省略的话表示直到字符串的末尾)。
[root@client0 asd]# url="c.biancheng.net"
[root@client0 asd]# echo ${url: 2: 9} # 不加空格也行
biancheng
[root@client0 asd]# echo ${url: 2}
biancheng.net
2)从字符串右边开始计数
${string: 0-start :length}
- 从左边开始计数时,起始数字是 0(这符合程序员思维);从右边开始计数时,起始数字是 1(这符合常人思维)。计数方向不同,起始数字也不同。
- 不管从哪边开始计数,截取方向都是从左到右(省略第2个参数都表示截到末尾)。
[root@client0 asd]# echo ${url: 0-13: 9}
biancheng
从指定字符开始截取
这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾。
Shell 可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。
1)使用 # 截取右侧子串
${string#sub}
sub
必须是string
的前缀才能生效,否则仍会输出完整字符串:
[root@client0 asd]# url="https://www/gamersky/com/"
# 失败,"tt"不是url的前缀
[root@client0 asd]# echo ${url#tt}
https://www/gamersky/com/
# 成功,"htt"是url的前缀
[root@client0 asd]# echo ${url#htt}
ps://www/gamersky/com/
有时候不关心或不知道字符串的完整前缀,只想通过字符串中间的特定子串截取字符串,这时候可以使用通配符*
:
${string#*chars}
*chars
意思为:字符串string
中以chars
作为结尾的前缀。
例子:
#:通配符匹配到第一个chars
[root@client0 asd]# echo ${url#*/}
/www/gamersky/com/
# 忽略了第一个“/”及左边的所有字符(*/)
##:通配符匹配到最后一个chars
使用##
指定到最后一个匹配字符才结束匹配:
[root@client0 asd]# url="https://www/gamersky/com/123"
[root@client0 asd]# echo ${url##*/}
123
# 忽略了最后一个“/”及左边的所有字符(*/),只剩下“123”
2)使用 % 截取左侧子串
${string%sub}
同样,sub
必须是string
的后缀才会有效果。
搭配通配符使用:
${string%chars*}
*
的位置在匹配字符右侧。
规则基本和#
一样:
%:匹配到右侧第一个chars
%%:匹配到右侧最后一个一个chars
[root@client0 asd]# url="https://www/gamersky/com/"
# 删除第一个“/”及右侧字符—— %
[root@client0 asd]# echo ${url%/*}
https://www/gamersky/com
# 删除最后一个“/”及右侧字符 —— %%
[root@client0 asd]# echo ${url%%/*}
https:
退出状态
每一条 Shell 命令,不管是 Bash 内置命令(例如 cd、echo)、还是外部 Linux 命令(例如 ls、awk)、还是自定义 Shell 函数,当它退出(运行结束)时,都会返回一个比较小的整数值给调用(使用)它的程序,这就是命令的退出状态(exit statu)。
按照惯例来说,退出状态为 0 表示“成功”;也就是说,程序执行完成并且没有遇到任何问题。除 0 以外的其它任何退出状态都为“失败”。
之所以说这是“惯例”而非“规定”,是因为也会有例外,比如 diff 命令用来比较两个文件的不同,对于“没有差别”的文件返回 0,对于“找到差别”的文件返回 1,对无效文件名返回 2。
在 Shell 中,有多种方式取得命令的退出状态,其中变量$?
是最常见的一种。
结构化命令
if-then
最简单的用法就是只使用 if 语句。
if condition
then
statement(s)
fi
condition
在使用test
命令或[ ]
时可以视为像C/C++那样的判断条件。但实际上在shell里,condition
是一个命令。
shell会运行condition
命令,当命令正常退出(退出状态码为0)时,条件成立,then部分会被执行。所以对于if
语句来说,0就相当于true
。
除了0以外的任何退出状态码都会导致“false判断”。
如果将then
与if
写在同一行,必须加上分号:
if condition; then
statement(s)
fi
!test ( [ ] )
操作符 [] 与 test 等价。
if
的condition
无法测试命令退出状态码之外的条件!!所以shell提供了test
命令,使得if
语句可以对变量进行测试。
如果test
命令中列出的条件成立,则test命令就会返回0。
if test condition
then
statement(s)
fi
这时可以理解为使用了
test
之后的if
有了C++的if
行为。
test
可以进行数值比较、字符串比较、文件比较。
1) 数值比较
比较 | 描述 |
---|---|
n1 -eq n2 | ( equal ) n1 与 n2 是否相等 |
n1 -ge n2 | ( great equal ) n1 是否大于等于 n2 |
n1 -gt n2 | ( great than ) n1 是否**大于 **n2 |
n1 -le n2 | ( less equal ) n1 是否小于等于 n2 |
n1 -lt n2 | ( less than) n1 是否小于 n2 |
n1 -ne n2 | ( not equal ) n1 是否不等于 n2 |
可以用于数字和变量。只能处理整数。变量需要加上$
。
2) 字符串比较
比较 | 描述 |
---|---|
str1 = str2(与== 等价) | str1 是否与 str2 相同 |
str1 != str2 | str1 是否与 str2 不同 |
str1 \< str2 | str1 是否小于 str2 |
str1 \> str2 | str1 是否大于 str2 |
-n str | str1 长度是否非0 |
-z str | str1 长度是否为0 |
str1 -ef str2 | 判断 str1 是否和 str2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法 |
1、大于号( >
)和小于号( <
)必须转义( \<
、\>
),否则shell会把它们视为重定向符号,把字符串值当做文件名。
if [ $var1 \> $var2 ]
2、字符串比较中,大写字母
小于小写字母
,而sort命令视大写字母
大于小写字母
。
字符串比较使用了标准的ASCII排序,而sort命令使用的是系统的本地化语言设置中定义的顺序。
3、不太让人适应的一点:test命令使用数学比较符号来表示字符串比较,而用文本代码来表示数值比较,很容易记混。
如果对数值使用数学运算符(=
、!=
、<
、>
),shell会将它们视为字符串值,从而得到错误的结果。
4、未定义的字符串,被赋值为''
的字符串(str=''
)会使-z
为true。
!补充!
如果在使用-n
、-z
没有为变量加上双引号,会导致错误。
==、!=、<、>暂时还没遇到这种问题。
#!/bin/bash
val1=testing
val2=''
if [ -n $val1 ]
then
echo "val1 is not empty"
else
echo "val1 is empty"
fi
if [ -n $val2 ]
then
echo "val2 is not empty"
else
echo "val2 is empty"
fi
if [ -n $val3 ]
then
echo "val3 is not empty"
else
echo "val3 is empty"
fi
3个判断都为true
,将操作符换为-z
结果也是一样。
[root@localhost ~]# ./test
val1 is not empty
val2 is not empty
val3 is not empty
在做判断时为变量加上双引号后才能正确运行:
...
if [ -n "$val1" ]
...
if [ -n "$val2" ]
...
if [ -n "$val3" ]
...
3) 文件比较
比较 | 描述 |
---|---|
-d file | ( dir ) file 是否存在且是一个目录 |
-e file | ( exist ) file 是否存在 |
-f file | ( file ) file 是否存在且是一个文件 |
-r file | ( read ) file 是否存在并可读 |
-s file | ( ) file 是否存在并非空 |
-w file | ( write ) file 是否存在并可写 |
-x file | ( execute ) file 是否存在并可执行 |
-u file | ( user ) file 是否存在并且拥有 SUID 权限 |
-g file | ( group ) file 是否存在并且拥有 SGID 权限 |
-k file | file 是否存在并且拥有 SBIT 权限 |
-O file | ( Own ) file 是否存在并属于当前用户 |
-G file | ( Group ) file 是否存在并且默认组与当前用户相同 |
file1 -nt file2 | ( newer than )file1 是否比 file2 新 |
file1 -ot file2 | ( older than )file1 是否比 file2 旧 |
-b file | ( block ) file 是否存在并非空,且是否为块设备文件 |
-c file | ( char ) file 是否存在并非空,且是否为字符设备文件 |
-L file | ( Link ) file 是否存在并非空,且是否为符号链接文件 |
-p file | ( pipe ) file 是否存在并非空,且是否为管道文件 |
-S file | ( Socket ) file 是否存在并非空,且是否为套接字文件 |
4) 逻辑运算
选 项 | 作 用 |
---|---|
exp1-a exp2 | 逻辑与(&& ),表达式 exp1 和 exp2 都成立,最终的结果才是成立的。 |
exp1-o exp2 | 逻辑或(|| ),表达式 exp1 和 exp2有一个成立,最终的结果就成立。 |
!exp | 逻辑非,对 exp 进行取反。 |
补充及对字符串部分的解释
一个命令上对应一个程序或函数,而程序本身都有一个入口函数,所以本质上还是函数。
命令后附带的选项和参数最终都会作为实参传递给函数。
假设 test 命令对应的函数是 func()
,使用test -z $str1
命令时,会先将变量 $str1 替换成字符串:
- 如果 $str1 是一个正常的字符串,比如 abc123,那么替换后的效果就是
test -z abc123
,调用func()
函数的形式就是func("-z abc123")
。test 命令后面附带的所有选项和参数会被看成一个整体,并作为实参传递进函数。 - 如果 $str1 是一个空字符串,那么替换后的效果就是
test -z
,调用func()
函数的形式就是func("-z ")
,这就比较奇怪了,因为-z
选项没有和参数成对出现,func()
在分析时就会出错。
复合测试条件
使用&&
、||
、!
3个运算符将多个退出状态组合起来,运算规则和C/C++一样。
// 条件之间不加空格也可以
[ condition1 ] && [ condition2 ]
[ condition1 ] || [ condition2 ]
使用高级的数学表达式 ( (( )) )
看上面。
针对字符串的高级特性( [[ ]] )
看上面。
if-else
如果有两个分支,就可以使用 if else 语句,它的格式为:
if condition
then
statement1
else
statement2
fi
除了condition
,其结构及行为与C/C++一致。
例子:
#!/bin/bash
read a
read b
if (( $a == $b ))
then
echo "a和b相等"
else
echo "a和b不相等,输入错误"
fi
if elif else 语句
当分支比较多时,可以使用 if elif else 结构:
if condition1
then
statement1
elif condition2
then
statement2
elif condition3
then
statement3
……
else
statementn
fi
if 和 elif 后边都得跟着 then。
case in
当分支较多,并且判断条件比较简单时,使用 case in 语句就比较方便了。
case expression in
pattern1)
statement1
;;
pattern2)
statement2
;;
pattern3 | pattern4 | pattern5)
statement3
;;
……
*)
statementn
esac
可以将多个匹配模式放入1个分支,用(
|
)隔开。
expression
既可以是一个变量、一个数字、一个字符串,还可以是一个数学计算表达式,或者是命令的执行结果,只要能够得到 expression 的值就可以。pattern
可以是一个数字、一个字符串,一个简单的正则表达式。
如果 expression 和某个模式(比如 pattern2)匹配成功,就会执行这模式(比如 pattern2)后面对应的所有语句(该语句可以有一条,也可以有多条),直到遇见双分号;;
才停止;然后 case 语句执行结束,执行 esac 后面的其它语句。
如果 expression 没有匹配到任何一个模式,那么就执行*)
后面的语句。
实际上
*
与其他的pattern
是等价的,那是个正则表达式,表示匹配任意长度的任意字符。
例子:
#!/bin/bash
printf "Input integer number: "
read num
case $num in
1)
echo "Monday"
;;
2)
echo "Tuesday"
;;
3)
echo "Wednesday"
;;
4)
echo "Thursday"
;;
5)
echo "Friday"
;;
6)
echo "Saturday"
;;
7)
echo "Sunday"
;;
*)
echo "error"
esac
for
for variable in list
do
statements
done
in value_list
部分可以省略,省略后的效果相当于in $@
。
每次循环都会从 value_list 中取出一个值赋给变量 variable,然后进入循环体(do 和 done 之间的部分),执行循环体中的 statements。直到取完 value_list 中的所有值,循环就结束了。
例子:
#!/bin/bash
sum=0
for n in 1 2 3 4 5 6
do
echo $n
((sum+=n))
done
echo "The sum is "$sum
对 list 的说明
value_list
是个能被IFS
分割成多个元素的字符串。
可以直接给出具体的值、可以给出一个范围、可以使用命令产生的结果、可以是通配符、可以是特殊变量。
1、具体的值
直接给出具体的值,用空格分隔
#!/bin/bash
for str in "C语言中文网" "http://c.biancheng.net/" "成立7年了" "日IP数万"
do
echo $str
done
2、范围
花括号,标明下限、上限,用2个句号.
分隔。
{start..end}
#!/bin/bash
sum=0
for n in {1..100}
do
((sum+=n))
done
echo $sum
#!/bin/bash
# 输出的是从ASCII-A到ASCII-z之间的所有字符
for c in {A..z}
do
echo $c
done
3、命令结果
$()
或``。
#!/bin/bash
for filename in $(ls *.sh)
do
echo $filename
done
4、通配符
Shell 通配符可以认为是一种精简化的正则表达式,通常用来匹配目录或者文件,而不是文本。
有了 Shell 通配符,不使用 ls 命令也能显示当前目录下的所有脚本文件:
bash shell 的通配符不等于正则表达式,只有部分相通。
*:代表任意多字符 0~n
?:代表一个字符
^:取反
[数字或字母]
[root@client0 asd]# ll exe[12]
-rwxr--r--. 1 root root 72 Mar 21 05:42 exe1
-rwxr--r--. 1 root root 29 Mar 17 00:15 exe2
[root@client0 asd]# ll exe[12]*
-rwxr--r--. 1 root root 72 Mar 21 05:42 exe1
-rw-r--r--. 1 root root 0 Mar 24 00:05 exe1qwe
-rwxr--r--. 1 root root 29 Mar 17 00:15 exe2
使用
#!/bin/bash
for filename in *.sh
do
echo $filename
done
5、特殊变量
Shell 中有多个特殊的变量,例如 $#、$*、$@、$?、$$
等,在 value_list 中就可以使用它们。
#!/bin/bash
function func(){
for str in $@
do
echo $str
done
}
func C++ Java Python C#
IFS
环境变量IFS
是内部字段分隔符,bash shell根据它的值来从list
中划分元素进行遍历。
修改IFS
IFS=$'\n':;"
// 一次指定多个IFS字符,同时将 换行符($'\n')、冒号(:)、分号(;)、双引号(")作为字段分隔符。
注意:IFS=\n
、IFS='\n'
、IFS=$'\n'
的区别
#!/bin/bash
echo "------ cat file ------"
cat file
echo "------ IFS=\n ------"
IFS=\n
for word in $(cat file)
do
echo $word
done
echo "------ IFS='\n' ------"
IFS='\n'
for word in $(cat file)
do
echo $word
done
echo "------ IFS=$'\n' ------"
IFS=$'\n'
for word in $(cat file)
do
echo $word
done
运行效果
[root@localhost ~]# ./test
------ cat file ------
123'456'789
ewq\dsa\cxz
321n654n987
------ IFS=\n ------
123'456'789
ewq\dsa\cxz
321
654
987
------ IFS='\n' ------
123'456'789
ewq
dsa
cxz
321
654
987
------ IFS=$'\n' ------
123'456'789
ewq\dsa\cxz
321n654n987
IFS=\n
分隔符被设置为 n
。\
被视为转义字符。如果修改为 IFS=\\n
,则结果与IFS='\n'
等价(暂限于本例)。
IFS=’\n’
分隔符被设置为 \
和 n
。
IFS=$’\n’
分隔符被设置为 \n
。
https://qastack.cn/unix/184863/what-is-the-meaning-of-ifs-n-in-bash-scripting
$'string'
形式的单词经过特殊处理。该单词扩展为“字符串”,并按ANSI C标准的规定替换反斜杠转义字符。\n
是换行符的转义序列,因此IFS
最终被设置为单个换行符。
C语音风格的for命令
使用双圆括号(())
,括号内的语法与C语言一致。
for((exp1; exp2; exp3))
do
statements
done
exp1/2/3
关键字也都能省略。
while
while condition
do
statements
done
condition
是一个test命令
,表示判断条件,statements
表示要执行的语句(可以只有一条,也可以有多条),do
和done
都是 Shell 中的关键字。
当condition
是多条命令时,只有最后一个执行的命令的退出状态码会影响while语句的判断。
while 语句和 if else 语句中的 condition 用法都是一样的,可以使用 test 或 [ ] 命令,也可以使用 (()) 或 [[ ]]。
例子1:
#!/bin/bash
i=1
sum=0
while ((i <= 100))
do
((sum += i))
((i++))
done
echo "The sum is: $sum"
例子2:实现一个简单的加法计算器,用户每行输入一个数字,计算所有数字的和。
#!/bin/bash
sum=0
echo "请输入您要计算的数字,按 Ctrl+D 组合键结束读取"
while read n
do
((sum += n))
done
echo "The sum is: $sum"
按下 Ctrl+D 组合键表示读取到文件流的末尾,此时 read 就会读取失败,得到一个非 0 值的退出状态,从而导致判断条件不成立,结束循环。
until
unti 循环和 while 循环恰好相反,当判断条件不成立时才进行循环,一旦判断条件成立,就终止循环。
until 的使用场景很少,一般使用 while 即可。
until condition
do
statements
done
condition
表示判断条件,statements
表示要执行的语句(可以只有一条,也可以有多条),do
和done
都是 Shell 中的关键字。
until行为和while基本一致,除了判断条件相反。
select in
select in 循环用来增强交互性,它可以显示出带编号的菜单,用户输入不同的编号就可以选择不同的菜单,并执行不同的功能。
select in 是 Shell 独有的一种循环,非常适合终端(Terminal)这样的交互场景。
select variable in value_list
do
statements
done
结构和
for
一样
例子:
#!/bin/bash
echo "What is your favourite OS?"
select name in "Linux" "Windows" "Mac OS" "UNIX" "Android"
do
echo $name
done
echo "You have selected $name"
效果:
[root@client0 asd]# exe1
What is your favourite OS?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#? 1
Linux
#? 2
Windows
#? 3
Mac OS
#? 4
UNIX
#? 5
Android
#? 6
#?
1) Linux
2) Windows
3) Mac OS
4) UNIX
5) Android
#?
用来提示用户输入菜单编号;^D
表示按下 Ctrl+D 组合键,它的作用是结束 select in 循环。
运行到 select 语句后,取值列表 value_list 中的内容会以菜单的形式显示出来,用户输入菜单编号,就表示选中了某个值,这个值就会赋给变量 variable,然后再执行循环体中的 statements(do 和 done 之间的部分)。
如果用户输入的菜单编号不在范围之内,例如上面输入的 6,那么就会给 variable 赋一个空值;如果用户输入一个空值(什么也不输入,直接回车),会重新显示一遍菜单。
select只有遇到break语句,或者按下 Ctrl+D 组合键才能结束循环。
break和continue跳出循环
在大部分编程语言中,break 和 continue 只能跳出当前层次的循环,内层循环中的 break 和 continue 对外层循环不起作用;但是 Shell 中的 break 和 continue 却能够跳出多层循环。
break
break n
n 表示跳出循环的层数,如果省略 n,则表示跳出当前的循环。
例:
#!/bin/bash
i=0
while ((++i)); do #外层循环
j=0;
while ((++j)); do #内层循环
if((i>4)); then
break 2 #跳出内外两层循环
fi
if((j>4)); then
break #跳出内层循环
fi
printf "%-4d" $((i*j))
done
printf "\n"
done
continue
continue n
如果带上 n,比如 n 的值为 2,那么 continue 对内层和外层循环语句都有效,不但内层会跳过本次循环,外层也会跳过本次循环,其效果相当于内层循环和外层循环同时执行了不带 n 的 continue。
例子:
#!/bin/bash
for((i=1; i<=5; i++)); do
for((j=1; j<=5; j++)); do
if((i*j==12)); then
continue 2
fi
printf "%d*%d=%-4d" $i $j $((i*j))
done
printf "\n"
done
运行结果:一共3行,因为i=2
、i=3
的情况都遇到了 continue 2
导致没有输出后续的换行符。
1*1=1 1*2=2 1*3=3 1*4=4 1*5=5
2*1=2 2*2=4 2*3=6 2*4=8 2*5=10
3*1=3 3*2=6 3*3=9 4*1=4 4*2=8 5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
函数
函数可以直接视为“小型脚本”。可通过重复定义函数来修改函数代码。
Shell 函数的本质是一段可以重复使用的脚本代码,这段代码被提前编写好了,放在了指定的位置,使用时直接调取即可。
function name() {
statements
[return value]
}
function
是 Shell 中的关键字,专门用来定义函数;name
是函数名;statements
是函数要执行的代码,也就是一组语句;return value
表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写。
可以直接在命令行上定义函数。
如果在一行内定义,则各个命令结尾必须加上分号(;),就像C++程序那样。
[root@localhost ~]# function add() { echo $[ $1 + $2]; }
[root@localhost ~]# add 123 654
777
如果是多行定义,则不需要分号
[root@localhost ~]# function add() {
> echo $[ $1 + $2 ]
> }
可以在.bashrc
或.bash_profile
文件中定义函数,这些函数可以像命令一样随时在shell终端中使用。
简化写法
如果嫌麻烦,函数定义时也可以不写 function 关键字:
name() {
statements
[return value]
}
如果写了 function 关键字,也可以省略函数名后面的小括号:
function name {
statements
[return value]
}
建议使用标准的写法,这样能够做到“见名知意”,一看就懂。
函数调用
函数可以视为小型脚本,调用函数的方式与调用命令的方式基本相同。
调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可:
name
如果传递参数,那么多个参数之间以空格分隔:
name param1 param2 param3
不管是哪种形式,函数名字后面都不需要带括号。
和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。
Shell 也不限制定义和调用的顺序,你可以将定义放在调用的前面,也可以反过来,将定义放在调用的后面。
函数的参数
再次强调:函数就是小型脚本。
所以向函数传递参数的方法与向命令传入参数的方法完全一致。但在脚本中指定函数时,必须将参数和函数放在同一行。
同时,函数也有自己的$#、$1、$2、……
等变量,与脚本的不通用,如果要使用脚本的参数,就必须在调用函数时手动传入数据。例如:
value=$(foo $1 $2)
函数与变量
如果函数内用到了全局变量,那么在调用该函数前,所有用到的全局变量必须都已定义。
脚本变量都默认是全局变量,在函数内定义的全局变量在函数运行结束(前提是函数正常运行)后仍能使用。
可以在函数内使用local
将变量设置为局部变量。
例子:
#!/bin/bash
foo()
{
local result=$[ $result * 3]
echo $result
fvar=123
}
result=5
foo
echo "result: $result"
echo $fvar
[root@localhost ~]# ./test
15
result: 5
123
数组变量与函数
输入
如果直接向函数通过$数组名
的方式传递数组,则只会传入数组的第一个值。
#!/bin/bash
foo()
{
echo "Parameters: $@"
m_arry=$1
echo "m_arry: ${m_arry[*]}"
}
arry=(1 2 3 4 5)
foo $arry
[root@localhost ~]# ./test
Parameters: 1
m_arry: 1
向函数传递数组的正确方式:
1、拆开数组,将元素一个个传入;(将数组
转换为字符串
形式作为函数的参数
)
2、在函数内重新将元素组合成一个新数组。
#!/bin/bash
foo()
{
local m_arry
m_arry=($(echo "$@")) # 2
echo "m_arry: ${m_arry[*]}"
}
arry=(1 2 3 4 5)
foo ${arry[*]} # 1
[root@localhost ~]# ./test
m_arry: 1 2 3 4 5
1、拆开数组
foo ${arry[*]}
2、重组元素建立新数组
m_arry=($(echo "$@"))
# 解释:
m_arry=( ... ) # 建立数组的标准格式
$(echo "$@") # 用echo输出函数收到的所有参数,然后将输出作为建立数组的参数
输出
也是同样的步骤。
1、函数内用echo输出所有元素;
2、函数外将函数输出的结果作为构建数组的参数。
#!/bin/bash
foo()
{
local m_arry
m_arry=( $(echo "$@") )
...
echo ${m_arry[*]} # 1
}
arry=(1 2 3 4 5)
str=$(echo ${arry[*]})
newarry=($(foo $str)) # 2
[root@localhost ~]# ./test
1 2 3 4 5
使用变量str
是为了方便理解,不然代码看起来会很麻烦:
newarry=($(foo ${arry[*]}))
函数返回值
Shell 中的返回值表示的是函数的退出状态:返回值为 0 表示函数执行成功了,返回值为非 0 表示函数执行失败(出错)了。if、while、for 等语句都是根据函数的退出状态来判断条件是否成立。
可通过return
命令指定返回值。
Shell 函数的返回值只能是一个介于 0~255 之间的整数,其中只有 0 表示成功,其它值都表示失败。
如果函数体中没有 return 语句,那么使用默认的退出状态,也就是最后一条命令的退出状态。等价于:
return $?
$?
是一个特殊变量,用来获取上一个命令的退出状态,
获取函数的处理结果
两种解决方案:
- 一种是借助全局变量,将得到的结果赋值给全局变量;
- 一种是在函数内部使用 echo、printf 命令将结果输出,在函数外部使用
$()
或者``捕获结果。
如果调用函数时通过$()
或``将函数赋值给一个变量,那么函数内的echo命令就不会将数据输出到STDOUT上,而是被读入变量中。
例子1:将函数结果赋值给变量
#!/bin/bash
function m2 ()
{
echo hello
read -p "Enter a value: " value
echo $[ $value * 2 ]
echo world
}
result=$(m2)
echo "New value: $result"
使用了3次echo
命令,它们都作为结果返回给变量$result
。
[root@localhost ~]# ./test
Enter a value: 123
New value: hello
246
world
例子2:不获取函数结果
#!/bin/bash
function m2 ()
{
echo hello
read -p "Enter a value: " value
echo $[ $value * 2 ]
echo world
}
# 直接调用函数,不赋值给变量
m2
函数内的echo命令直接将数据输出到终端屏幕
[root@localhost ~]# ./test
hello
Enter a value: 123
246
world
创建库
如果要在多个脚本中使用同一段代码,就能使用库文件,在多个脚本中引用该库文件。
库文件就是个含有脚本函数定义的文本文件,不需要设置#!/bin/bash
,也不需要赋予执行权限。虽然都设置了也没关系。
使用库
使用source命令。快捷方式:点操作符(.)。
以下两行代码等价:
source ./func . ./func
库文件:
function addem
{
echo $[ $1 + $2 ]
}
脚本文件:
#!/bin/bash
source ./func # 假设了库文件与脚本文件在同一目录
echo $(addem 3 5)
脚本
**使用脚本的必要性:**通过(;)做分隔可以一次执行多个命令,但每次都得在命令提示符中输入所有的命令。可以将这些命令放入一个文件中,然后为该文件赋予x权限。需要执行命令时执行这个文件就行了。
第一行必须指定要使用的shell。
#!/bin/bash
运行一个脚本与执行一个命令一样,要么将其路径添加进$PATH中,要么使用绝对路径、相对路径执行。
脚本中可以用转义字符(\),作用和C++中的一样。
慎用空格
在定义、赋值变量时,变量、等号、值之间不能有空格。
source命令
可用点操作符(.)替代。
source命令会在当前shell上下文中执行命令,而不是创建一个新的shell。
重定向
注意!:重定向的箭头方向与C++完全相反。
从逻辑上理解,箭头所指方向都是数据流向的方向(不论是shell还是C++),就是在叫法上出现了冲突。
输出重定向 > >>
command > outputfile
如果outputfile
不存在会创建。> 会覆盖文件内容。
>> 追加模式,不会清空文件原有内容。
输入重定向 <
command < inputfile
将inputfile
文件的内容作为命令的参数。
内联输入重定向(Here Document)
无需使用文件就能重定向。
command << marker
data
marker
marker
是文本标记字符,可以是任意字符串,必须前后一致。
例子:文本标记字符设置为“EOP”
[root@localhost ~]# wc << EOP
> string 1
> test string 2
> test string 3
> EOP
3 8 37
注意!
- 文本标记字符的结尾必须在行首位置。
- data是什么样的,输出的信息就是什么样。不用画蛇添足地加上
\n
。
while read no name age sex
do
cat >> $outfile << EOF
Passenger $no
Name : $name
Age : $age
Sex : $sex
EOF
done < ${1}
忽略命令替换
command <<'END'
document
END
[mozhiyan@localhost ~]$ name=C语言中文网
[mozhiyan@localhost ~]$ url=http://c.biancheng.net
[mozhiyan@localhost ~]$ age=7
[mozhiyan@localhost ~]$ cat <<END
> ${name}已经${age}岁了,它的网址是 ${url}
> END
C语言中文网已经7岁了,它的网址是 http://c.biancheng.net
可以将分界符用单引号或者双引号包围起来使 Shell 替换失效:
[mozhiyan@localhost ~]$ name=C语言中文网
[mozhiyan@localhost ~]$ url=http://c.biancheng.net
[mozhiyan@localhost ~]$ age=7
[mozhiyan@localhost ~]$ cat <<'END' #使用单引号包围
> ${name}已经${age}岁了,它的网址是 ${url}
> END
${name}已经${age}岁了,它的网址是 ${url}
忽略制表符
command <<-END
document
END
#!/bin/bash
cat <<END
Shell教程
http://c.biancheng.net/shell/
已经进行了三次改版
END
运行结果:
Shell教程
http://c.biancheng.net/shell/
已经进行了三次改版
#!/bin/bash
#增加了减号-
cat <<-END
Shell教程
http://c.biancheng.net/shell/
已经进行了三次改版
END
运行结果:
Shell教程
http://c.biancheng.net/shell/
已经进行了三次改版
Here String
Here Document的一个变种,用法如下:
command <<< string
command 是 Shell 命令,string 是一个普通的字符串。
这种写法告诉 Shell 把 string 部分作为命令需要处理的数据。
例子:
[mozhiyan@localhost ~]$ tr a-z A-Z <<< one
ONE
对于这种发送较短的数据到进程是非常方便的。
[root@client0 asd]# cat "qwe"
cat: qwe: No such file or directory
[root@client0 asd]# cat <<<"qwe"
qwe
与vim中解析字符串一样,简单的单个词不需要引号。如果字符串中有空格,则必须有单/双引号。单引号不接自变量,双引号解析。
重定向与脚本!
linux
启动后会默认打开STDIN、STDOUT、STDERR
这3个文件,重定向都是紧紧围绕这3个文件的文件描述符0、1、2展开的。
一个shell中最多可以有9个打开的文件描述符。
列出文件描述符的命令:lsof
,P.322。
脚本外重定向
经常使用的:(0<
在脚本内重定向使用)
输出
1> :输出重定向(STDOUT)
2> :错误重定向(STDERR)
&> :同时重定向输出和错误。错误的优先级较高,会先于输出存入文件。
输入
< filename :将filename文件的内容作为命令的输入
文件描述符(左侧)得紧贴重定向符号,右侧若直接使用文件名则无所谓。
一些例子:
重定向输出(看一看格式就行了)
命令的输出重定向到file2,命令的错误重定向到file1。
[root@localhost ~]# ls -al test test2 2> file1 1> file2
脚本内重定向!
自己的理解:
1、某种程度上可以把用于重定向的文件描述符视为C++中的指针一类的东西。
2、基于上一条的理解,
&
操作符的效果可视为C++中的解引用(*p
)操作。3、大多数命令都会使用0、1、2这3个文件描述符作为自己的输入、输出,脚本内的重定向也是针对这3个文件描述符的操作来进行的。(看例子就清楚了)
脚本内,如果重定向符号前不加描述符,则:>
默认为STDOUT,<
默认为STDIN
补充
在使用&
获取文件描述符指向的文件时,&
必须与重定向符号连在一起。
command >&n
临时重定向
仅进行重定向的那一行有用。
#!/bin/bash
echo "hello" >file
echo "world" >&2
[root@localhost ~]# ./test
world
[root@localhost ~]# cat file
hello
>file
:将STDOUT重定向到file文件。
>&2
:将STDOUT重定向到STDERR,通常在重定向了STDERR后才有用。
- 从上述例子中看不出
>&2
的效果,因为linux默认STDERR导向了STDOUT。 &2
意为获取文件描述符“2”所指向的文件!!
永久重定向
相关命令:exec
exec n> filename
或者应该理解为:
exec 文件描述符-重定向符号 重定向目标文件
重定向符号的取值有3种:<、>、<>(还有输出的附加版本)
同理,如果输出重定向不是附加模式,执行exec 3>file
时就会情况file文件的内容。
重定向输出
#!/bin/bash
echo "a output message before redirect"
echo "a error message before redirect"
exec 1> outfile
exec 2> errfile
echo "a output message"
echo "a error message" > &2
# “> &2”会导致错误!正确写法是“>&2”
// ---------------------------------------
[root@localhost ~]# ./test
a output message before redirect
a error message before redirect
[root@localhost ~]# cat outfile
a output message
[root@localhost ~]# cat errfile
./test: line 10: syntax error near unexpected token `&'
./test: line 10: `echo "a error message" > &2'
// ---------------将错误修改后--------------
a output message before redirect
a error message before redirect
[root@localhost ~]# cat errfile
a error message
重定向的解析:
之前的理解:大多数命令都会使用0、1、2这3个文件描述符作为自己的输入、输出。
在重定向前的数据流向:
echo "a output message before redirect"
echo --> 1(STDOUT)
重定向后的数据流向:
1、
exec 1> outfile
echo "a output message"
数据流向:
echo --> 1(outfile)
2、
exec 2> errfile
echo "a error message" > &2
数据流向:
echo --> 1(2(errfile)) 等价于 echo --> 1(errfile)
重定向输入
exec 0< testfile
while read line
do
...
done
重定向后,read
命令从testfile文件中获取输入。
– 自定义重定向
**1、**提前设置好文件描述符的重定向
可以让一些文件描述符提前重定向到文件,在需要时将输出重定向到对应的文件描述符。
exec 3> file1
exec 4> file2
...
echo "normal message 1"
echo "redirect message 1" >&3
echo "redirect message 2" >&4
**2、**恢复STD的重定向
使用文件描述符“3”暂时替“1”保存STDOUT文件的指向。
exec 3>&1
exec 1> file
...
exec 1>&3
输入重定向同理,就是换了个箭头的方向。
exec 4<&0 exec 0< file ... exec 0<&4
**3、**同时重定向输入输出
shell维护一个内部指针,指明在文件中的当前位置。
read命令读取了第一行后,将指针指向第二行的第一个字符,然后用字符串“This is a test line”
覆盖了原有内容。(This is the second line.
比This is a test line
多4个字符,刚好对应剩下的ine.
)
#!/bin/bash
exec 3<> file
read line <&3
echo "Read: $line"
echo "This is a test line" >&3
// ---------------------------------------
[root@localhost ~]# cat file
This is the first line.
This is the second line.
This is the third line.
[root@localhost ~]# ./test
Read: This is the first line.
[root@localhost ~]# cat file
This is the first line.
This is a test line
ine.
This is the third line.
交换2行echo的顺序,得到的结果仍然相同。
因为read只读取了file文件中的一行,在echo执行前就已经将指针定位好了。
关闭文件描述符
重定向到&-
exec 3>&-
如果要组织命令的任何输出,可将其重定向到/dev/null
文件中。
----------------------------
Linux的文件描述符
Linux中一切都是文件,文件描述符就是一个整数编号,用于标识已打开的文件。
每个进程都有一个文件描述符
;打开文件表
和i-node表
整个系统都只有一个。
- 同一个进程的不同文件描述符可以指向同一个文件;
- 不同进程可以拥有相同的文件描述符;
- 不同进程的同一个文件描述符可以指向不同的文件(一般也是这样,除了 0、1、2 这三个特殊的文件);
- 不同进程的不同文件描述符也可以指向同一个文件。
Linux 系统每次读写文件的时候,都从文件描述符下手,通过文件描述符找到文件指针,然后进入打开文件表和 i-node 表,这两个表里面才真正保存了与打开文件相关的各种信息。
文件指针只不过是一个内存地址,修改它是轻而易举的事情。文件指针是文件描述符和真实文件之间最关键的“纽带”,然而这条纽带却非常脆弱,很容易被修改。
Linux 系统提供的函数可以修改文件指针,比如 dup()、dup2();Shell 也能修改文件指针,输入输出重定向就是这么干的。
输入输出重定向就是通过修改文件指针实现的。
发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,整体上的资源开销相当低。
结合资料自己的理解
将文件标识符看做指针:
n>&m
表示将n所指目标重定位到m所指的目标;n>&-
关闭文件描述符相当于p == nullptr
将该指针置空。
Shell 文件描述符操作方法一览表:
分类 | 用法 | 说明 |
---|---|---|
输出 | n>filename | 以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。 |
n>&m | 用文件描述符 m 修改文件描述符 n,或者说用文件描述符 m 的内容覆盖文件描述符 n,结果就是 n 和 m 都代表了同一个文件,因为 n 和 m 的文件指针都指向了同一个文件。 因为使用的是> ,所以 n 和 m 只能用作命令的输出文件。n 可以不写,默认为 1。 | |
n>&- | 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 1。 | |
&>filename | 将正确输出结果和错误信息全部重定向到 filename。 | |
输入 | n<filename | 以输入的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 0,也即标准输入文件。 |
n<&m | 类似于 n>&m,但是因为使用的是< ,所以 n 和 m 只能用作命令的输入文件。n 可以不写,默认为 0。 | |
n<&- | 关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 0。 | |
输入和输出 | n<>filename | 同时以输入和输出的方式打开文件 filename,并绑定到文件描述符 n,相当于 n>filename 和 n<filename 的总和。。n 可以不写,默认为 0。 |
例子:前面的文章中提到了下面这种用法:
command >file 2>&1
等价于
command 1>file 2>&1
这个语句可以分成两步:
1、先执行1>file
,让文件描述符 1 指向 file;
2、再执行2>&1
,自己理解:让文件描述符2
指向文件描述符1的引用
,结果就是2个文件描述符都指向同一个文件,也就是 file。
要注意执行顺序,多个操作符在一起会从左往右依次执行。
command 2>&1 1>file
Shell 会先执行2>&1
,这样 1 和 2 都指向了标准错误输出文件,也即显示器;接着执行1>file
,这样 1 就指向了 file 文件,但是 2 依然指向显示器。
一个较奇葩、能帮助理解的例子:
echo "C语言中文网" 10>log.txt >&10
1、先执行10>log.txt
,打开 log.txt,并给它分配文件描述符 10;
2、接着执行>&10
,将文件描述符 1
来重定位到文件描述符 10
上
3、最后echo "C语言中文网"
的结果输出给文件标识符1
,而它已被重定位了,最后指令结果保存在 log.txt
文件中。
文件描述符 10 只用了一次,我们在末尾最好将它关闭,这是一个好习惯。
echo "C语言中文网" 10>log.txt >&10 10>&-
10>&-
可看成p == nullptr
。
----------------------------
Linux的回收站:/dev/null
如果既不想把命令的输出结果保存到文件,也不想把命令的输出结果显示到屏幕上,干扰命令的执行,那么可以把命令的所有结果重定向到 /dev/null 文件中。
ll &>/dev/null
任何放入垃圾箱的数据都会被丢弃,不能恢复。
tee
将输出同时发往显示器和日志文件。
tee filename
[root@localhost ~]# date | tee file
Thu Aug 5 16:16:31 CST 2021
[root@localhost ~]# cat file
Thu Aug 5 16:16:31 CST 2021
实例 !
下方的内联输入重定向不要被“箭头的方向就是数据的流向”这一理解误导,把重定向符号看做普通的操作符就行了。
脚本
#!/bin/bash
outfile='outfile'
while read no name age sex
do
cat >> $outfile << EOF
Passenger $no
Name : $name
Age : $age
Sex : $sex
EOF
done < ${1}
数据源
[root@localhost ~]# cat file
1 Tom 45 m
2 John 23 m
3 Dave 35 m
4 Grace 29 f
5 Jenny 42 f
运行结果
[root@localhost ~]# ./test file
[root@localhost ~]# cat outfile
Passenger 1
Name : Tom
Age : 45
Sex : m
Passenger 2
Name : John
Age : 23
Sex : m
Passenger 3
Name : Dave
Age : 35
Sex : m
Passenger 4
Name : Grace
Age : 29
Sex : f
Passenger 5
Name : Jenny
Age : 42
Sex : f
解析
用到了3个重定向操作
1:cat >> $outfile
将cat的输出重定向到文件$outfile
。
2:<< EOF ... EOF
没什么好说的。
3:while ... do ... done < ${1}
将脚本的第一个参数(文件名)作为while
循环中read
命令的输入。
管道
command1 | command2
linux系统会同时运行这2个命令,在系统内部将它们连接起来,第1个命令一产生输出,就会送给第2个命令,中间没有用到任何中间文件或缓冲区。
command1 必须有正确输出,而 command2 必须可以处理 command2 的输出结果;
而且 command2 只能处理 command1 的正确输出结果,不能处理 command1 的错误信息。
管道与重定向的区别
重定向操作符>连接命令与文件,用文件来接收命令的输出;
command > file
管道符|连接命令与命令,用第二个命令来接收第一个命令的输出。
command1 | command1
如果想用管道符替代重定向操作符,可能会产生严重的后果。例如以root身份执行如下命令:
cd /usr/bin
ls > less
第一条命令将当前目录切换到了大多数程序所存放的目录;
第二条命令是告诉 Shell 用 ls 命令的输出重写文件 less。如果 /usr/bin 目录刚好有名称为 less(less 程序)的文件,第二条命令用 ls 输出的文本重写了 less 程序,因此破坏了文件系统中的 less 程序。
管道能与重定向配合使用
command1 < input.txt | command2
command1 < input.txt | command2 -option | command3
例:
[c.biancheng.net]$ cat os.txt
redhat
suse
centos
ubuntu
solaris
hp-ux
fedora
centos
redhat
hp-ux
[c.biancheng.net]$ tr a-z A-Z <os.txt | sort
CENTOS
CENTOS
FEDORA
HP-UX
HP-UX
REDHAT
REDHAT
SOLARIS
SUSE
UBUNTU
数学运算
shell不能直接进行算术运算,必须使用数学计算命令。
(())
expr
操作数与运算符之间必须有空格。只支持整数运算。
[root@localhost ~]# expr 1 + 5
6
[root@localhost ~]# expr 1+5
1+5
[root@localhost ~]# expr 1 - 4
-3
[root@localhost ~]# expr 1 = 4
0
$[]
只支持整数运算。
bc
一个交互式程序,可用于计算浮点数。
可以结合内联输入重定向来使用。
varible=$(bc << EOF
options
statements
expressions
EOF
)
var1=10.46
var2=43.67
var3=33.2
var4=71
var5=$(bc << EOF
scale = 4
a1 = ($var1 + $var2)
b1 = ($var3 * $var4)
a1 + b1
EOF)
# 变量a1、b1只能在bc计算器中使用。
bc 甚至可以称得上是一种编程语言了,它支持变量、数组、输入输出、分支结构、循环结构、函数等基本的编程元素。
在终端输入bc
命令,然后回车即可进入 bc 进行交互式的数学计算。
bc 命令还有一些选项:
选项 | 说明 |
---|---|
-h | --help | 帮助信息 |
-v | --version | 显示命令版本信息 |
-l | --mathlib | 使用标准数学库 |
-i | --interactive | 强制交互 |
-w | --warn | 显示 POSIX 的警告信息 |
-s | --standard | 使用 POSIX 标准来处理 |
-q | --quiet | 不显示欢迎信息 |
使用 bc 进行数学计算是非常容易的,像平常一样输入数学表达式,然后按下回车键就可以看到结果。
[root@client0 asd]# bc -q
n = 10
n++
10
++n
12
除了变量,bc 还支持函数、循环结构、分支结构等常见的编程元素,它们和其它编程语言的语法类似。
[root@client0 asd]# bc -q
define foo(x)
{
if(x <=1)
return 1;
return x * foo(x-1);
}
foo(5)
120
内置变量
bc 有四个内置变量,我们在计算时会经常用到,如下表所示:
变量名 | 作 用 |
---|---|
scale | 指定精度,也即小数点后的位数;默认为 0,也即不使用小数部分。 |
ibase | 指定输入的数字的进制,默认为十进制。 |
obase | 指定输出的数字的进制,默认为十进制。 |
last 或者 . | 表示最近打印的数字 |
[root@client0 asd]# bc -q
n=10
obase=2
n # 输入n后按回车,就出现下方结果
1010
scale=5
10/3
3.33333
**注意!:**设置ibase
和 obase
时, obase
要尽量放在 ibase
前面,因为 ibase
设置后,后面的数字都是以 ibase
的进制来换算的。
内置函数
除了内置变量,bc 还有一些内置函数,如下表所示:
函数名 | 作用 |
---|---|
s(x) | 计算 x 的正弦值,x 是弧度值。 |
c(x) | 计算 x 的余弦值,x 是弧度值。 |
a(x) | 计算 x 的反正切值,返回弧度值。 |
l(x) | 计算 x 的自然对数。 |
e(x) | 求 e 的 x 次方。 |
j(n, x) | 贝塞尔函数,计算从 n 到 x 的阶数。 |
要想使用这些数学函数,在启动 bc 时需要使用-l
选项,表示启用数学库。
一行中使用多个表达式
之前的例子都是一行一个表达式,不需要任何符号结尾。可以将多个表达式放在一行,只要用分号;
隔开就行。
x=5; y=6; z=x+y; z
11
在Shell中使用bc计算器
在 Shell 脚本中,我们可以借助管道或者输入重定向来使用 bc 计算器。
借助管道使用 bc 计算器
如果读者希望直接输出 bc 的计算结果,那么可以使用下面的形式:
echo "expression" | bc
expression
就是希望计算的数学表达式,它必须符合 bc 的语法。在 expression 中,还可以使用 Shell 脚本中的变量。
最简单的例子:
[c.biancheng.net]$ echo "3*8"|bc
24
[c.biancheng.net]$ ret=$(echo "4+9"|bc)
[c.biancheng.net]$ echo $ret
13
使用 bc 中的变量:
[c.biancheng.net]$ echo "scale=4;3*8/7"|bc
3.4285
[c.biancheng.net]$ echo "scale=4;3*8/7;last*5"|bc
3.4285
17.1425
使用 Shell 脚本中的变量:
[c.biancheng.net]$ x=4
[c.biancheng.net]$ echo "scale=5;n=$x+2;e(n)"|bc -l
403.42879
$x
表示使用第一条 Shell 命令中定义的变量,n
是在 bc 中定义的新变量,它和 Shell 脚本是没关系的。
进制转换:
#十进制转十六进制
[mozhiyan@localhost ~]$ m=31
[mozhiyan@localhost ~]$ n=$(echo "obase=16;$m"|bc)
[mozhiyan@localhost ~]$ echo $n
1F
#十六进制转十进制
[mozhiyan@localhost ~]$ m=1E
[mozhiyan@localhost ~]$ n=$(echo "obase=10;ibase=16;$m"|bc)
[mozhiyan@localhost ~]$ echo $n
30
借助输入重定向使用 bc 计算器
可以使用下面的形式将 bc 的计算结果赋值给 Shell 变量:
variable=$(bc << EOF
expressions
EOF
)
variable
是 Shell 变量名,express
是要计算的数学表达式(可以换行,和进入 bc 以后的书写形式一样),EOF
是数学表达式的开始和结束标识(你也可以换成其它的名字,比如 aaa、bbb 等)。
[c.biancheng.net]$ m=1E
[c.biancheng.net]$ n=$(bc << EOF
> obase=10;
> ibase=16;
> print $m
> EOF
> )
[c.biancheng.net]$ echo $n
30
如果有大量的数学计算,那么使用输入重定向就比较方便,因为数学表达式可以换行,写起来更加清晰明了。
进程替换
和命令替换非常相似。命令替换是把一个命令的输出结果赋值给另一个变量,而进程替换则是把一个命令的输出结果传递给另一个(组)命令。
进程替换的必要性的例子:
echo "http://c.biancheng.net/shell/" | read
echo $REPLY
以上代码输出结果总是为空。echo 命令在父进程中运行,read 命令在子进程中运行,读取的数据也保存在子进程的 REPLY 变量中,echo 命令和 REPLY 变量不在一个进程中,而子进程的环境变量对父进程是不可见的,所以读取失败。
管道中的命令总是在子 Shell 中执行的,任何给变量赋值的命令都会遭遇到这个问题。
Shell 进程替换有两种写法,一种用来产生标准输出,借助输入重定向,它的输出结果可以作为另一个命令的输入:
<(commands)
另一种用来接受标准输入,借助输出重定向,它可以接收另一个命令的输出结果:
>(commands)
commands 是一组命令列表,多个命令之间以分号;
分隔。注意,<
或>
与圆括号之间是没有空格的。
例子:
[root@client0 asd]# echo $REPLY
[root@client0 asd]# read < <(echo "hello world")
[root@client0 asd]# echo $REPLY
hello world
$REPLY
是read命令默认保存结果的变量。
命令的结构与普通的脚本外重定向语法一致,只是将重定向的文件名改为了进程替换命令。
第一个<
表示输入重定向,第二个<
和()
连在一起表示进程替换。
原理
为了能够在不同进程之间传递数据,实际上进程替换会跟系统中的文件关联起来,这个文件的名字为/dev/fd/n
(n 是一个整数)。该文件会作为参数传递给()
中的命令,()
中的命令对该文件是读取还是写入取决于进程替换格式是<
还是>
:
- 如果是
>()
,那么该文件会给()
中的命令提供输入;借助输出重定向,要输入的内容可以从其它命令而来。 - 如果是
<()
,那么该文件会接收()
中命令的输出结果;借助输入重定向,可以将该文件的内容作为其它命令的输入。
使用 echo 命令可以查看进程替换对应的文件名:
[root@client0 asd]# echo <(true)
/dev/fd/63
[root@client0 asd]# echo >(true)
/dev/fd/63
[root@client0 asd]# echo <(true) >(true)
/dev/fd/63 /dev/fd/62
/dev/fd/
目录下有很多序号文件,进程替换一般用的是 63 号文件,该文件是系统内部文件,一般查看不到。
例子:
[root@client0 asd]# echo "shellscript" > >(read; echo "hello, $REPLY")
[root@client0 asd]# hello, shellscript
- 第一个
>
表示输出重定向,它把第一个 echo 命令的输出结果重定向到/dev/fd/63
文件中。 >()
中的第一个命令是 read,它需要从标准输入中读取数据,此时就用/dev/fd/63
作为输入文件,把该文件的内容交给 read 命令,接着使用 echo 命令输出 read 读取到的内容。
脚本的用户输入
读取参数
参数也就是程序名后跟的额外输入,相当于C++程序的argv[]
。
参数默认使用空格分隔,含有空格的参数要加上单引号/双引号。
读取各个参数 $n ${n}
$0 :程序名
$1 :第一个参数
$2 :第二个参数,以此类推
...
$9 :第九个参数
${10} :第十个及以后的参数要加上花括号{}标明变量范围,否则$10会被识别为“$1”和“0”
一些特殊变量 $# $@ $*
$# :参数的个数,相当于C++中main函数的argc,无参数时为0
$@ :将所有参数当做一个字符串中的多个独立的单词(可作为循环的list使用,使用时必须套上"")
$* :将所有参数作为一个单词整体
区分$@
和$*
的例子:
如果第二个for中使用的list是
$*
而不是"$*"
,则2个for的输出相同。自己的理解:
$@
是以空格为分隔,一个个地取得参数,$*
是一次性取得所有参数,如果不加""
,默认的IFS仍会根据空格将$*
划分开。
for var in "$@"
do
echo $@
done
echo
for var in "$*"
do
echo $*
done
[root@localhost ~]# ./test 1 2 3
1 2 3
1 2 3
1 2 3
1 2 3
(花括号内不能使用美元符)
花括号内的美元符( { $ } )要用感叹号( { ! } )替代。
例如以下脚本输出所有参数:
#!/bin/bash
for ((i = 0; i <= $#; ++i))
do
echo ${!i} // 如果是${$i},会报语法错误。
done
移动变量
shift命令。
**效果:**将所有变量左移一位,覆盖左侧的参数,$0
保持不变,$1
被舍弃。
**用途:**在不清楚参数数量时使用,可以只操作第一个参数。
例子
while [ -n "$1" ] // 如果没有双引号,会陷入死循环
do
echo -$1
shift
done
shift n // 一次移动n个位置
实现选项输入
在“输入”这一范围内,选项和参数是一样的。需要通过case
语句识别,执行分支。
1.简单选项
./test -a -b -c
仅输入选项,无参数。
while [ -n "$1" ]
do
case "$1" in
-a) echo "found the -a option" ;;
-b) echo "found the -b option" ;;
-c) echo "found the -c option" ;;
*) echo "$1 is not an option" ;;
esac
shift
done
[root@localhost ~]# ./test -c -a -b -d -e
found the -c option
found the -a option
found the -b option
-d is not an option
-e is not an option
2.分离参数和选项
./test -a -b -c -- qwe asd zxc
同时输入选项和参数,使用特殊字符(通常为双破折线(–))将选项和参数分隔开。
下方代码暂时算作范例参考吧。
while [ -n "$1" ]
do
case "$1" in
-a) echo "found the -a option" ;;
-b) echo "found the -b option" ;;
-c) echo "found the -c option" ;;
--) shift; break ;; // break之前先shift
*) echo "$1 is not an option" ;;
esac
shift
done
count=1
for param in "$@"
do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done
[root@localhost ~]# ./test -a -b -e -- qwe asd zxc
Found the -a option
Found the -b option
-e is not an option
Parameter #1: qwe
Parameter #2: asd
Parameter #3: zxc
3.带值的选项
./test -a -b qwe -c
选项后跟上一个参数。
处理方法(完整代码懒得上了):在2的基础上,-b
选项作如下修改:
-b)
param="$2"
echo "Found the -b option, with parameter value $param"
shift
;;
-b
选项后跟1个参数,占用了2个参数位,所以要额外shift一次。
!将输入规范化的工具 getopt 和 set
getopt
getopt optstring parameters
参数说明:
getopt ab:cd -a -b test1 -dc test2 test3
ab:cd
指明了abcd
都属于选项,在需要参数的选项字母后面加上一个冒号(:)。(optstring
部分以冒号开头表示忽略错误输出)
命令返回规范化后的结果。
例子:
[root@localhost ~]# getopt ab:cd -a -b test1 -dc test2 test3
-a -b test1 -d -c -- test2 test3
紧跟在-b
后的字符串就是b
选项的参数。
加深理解:
# -b后紧跟的字符串是"-dc",所以-dc被识别为-b的参数
[root@localhost ~]# getopt ab:cd -a -b -dc test2 test3
-a -b -dc -- test2 test3
# 同上,-a被识别为-b的参数
[root@localhost ~]# getopt ab:cd -b -a test1 -dc test2 test3
-b -a -d -c -- test1 test2 test3
# 未定义的参数字母"c"
[root@localhost ~]# getopt ab -c test1 -ab
getopt: invalid option -- 'c'
-a -b -- test1
在
optstring
部分定义好选项字母后,parameters
部分的选项先后顺序,组合/单独使用都可以。
应用
set
set
的"--"
选项能将命令行参数替换成getopt
命令的返回值。
set -- $(getopt -q ab:c "$@")
echo "param list1: "
for par in "$@"
do
echo -n "$par "
done
echo
echo
# 同样的for循环代码,先执行set命令
set -- $(getopt -q ab:c "$@")
echo "after set: "
for par in "$@"
do
echo -n "$par "
done
echo
[root@localhost ~]# ./test p1 p2 -a -b p3 -c
param list1:
p1 p2 -a -b p3 -c
after set:
-a -b 'p3' -c -- 'p1' 'p2'
getopts
具体原理书中没提,已知的行为:在while中调用该命令,它会获取脚本参数并解析。
getopts optstring value
-
value
会依次被赋值为选项字母(没有减号(-))。 -
getopts
有2个环境变量$OPTARG
和$OPTIND
:
**$OPTARG
**用于记录选项的参数,没有设置参数的选项为空;
**$OPTIND
**用于记录下一个选项的位置。
- 未定义的选项被输出成问号(?)
- 识别完所有选项后,最后的
$OPTIND
的值就是第一个参数的位置。
例子:
while getopts :ab:c opt
do
echo "-----------"
echo " opt : $opt"
echo " OPTARG : $OPTARG"
echo " OPTIND : $OPTIND"
echo "\${OPTIN} : ${!OPTIND}"
echo "-----------"
done
[root@localhost ~]# ./test -a -b qwe -c -e param1 param2
-----------
opt : a
OPTARG :
OPTIND : 2
${OPTIN} : -b
-----------
-----------
opt : b
OPTARG : qwe // 仅有选项b定义了参数
OPTIND : 4 // 位置3被参数qwe占用
${OPTIN} : -c
-----------
-----------
opt : c
OPTARG :
OPTIND : 5
${OPTIN} : -e
-----------
-----------
opt : ? // 未定义选项e
OPTARG : e // e被识别为未定义选项的参数
OPTIND : 6
${OPTIN} : param1 // 第一个参数
-----------
后续的参数的处理:
与2.分离参数和选项
部分一样,变量$OPTIND
与shift
命令配合将带参数的选项部分消除,只剩下参数。
shift $[ $OPTIND - 1 ]
echo
count=1
for param in "$@"
do
echo "Parameter #$count: $param"
count=$[ $count + 1 ]
done
交互获取参数 read
对应C语言scanf
。
read [var1 var2 ...]
-p:额外输出提示。
-t:定时器,过期后返回非零值。
-n:指定参数,输入一定字符后立刻返回。
-s:避免将read
中输入的数据出现在显示器上(通过将文本色设置成背景色实现)。
如果变量数量不够,剩下的数据都塞给最后一个变量。
如果没有指定变量,则会将数据存入环境变量$REPLY
中。
read -p "input message\n: " p1 p2 p3
echo $p1
echo $p2
echo $p3
[root@localhost ~]# ./test
input message: 12 qwe ads gf3w wfe
12
qwe
ads gf3w wfe
读取文本内容
cat file | while read line
do
# do something with $line
done
脚本控制(信号)
刚接触shell时运行的脚本、命令基本上都是以实时模式在命令行界面上直接运行。这并非唯一的运行方式,并且有很多方法能控制shell的运行。
按 Ctrl+C 组合键来从命令行终结一个命令的执行,就使用了信号。
Linux 系统中,信号被用于进程间的通信。信号是一个发送到某个进程或同一进程中的特定线程的异步通知,用于通知发生的一个事件。
当一个事件发生时,会产生一个信号,然后内核会将事件传递到接收的进程。
运行在用户模式下的进程会接收信号。如果接收的进程正运行在内核模式,那么信号的执行只有在该进程返回到用户模式时才会开始。
如果一个在不可中断休眠状态的进程收到了一个信号,那么内核会拖延此信号,直到该事件完成为止。
当一个进程执行信号处理时,如果还有其他信号到达,那么新的信号会被阻断直到处理器返冋为止。
当进程收到一个信号时,可能会发生以下 3 种情况:
- 进程可能会忽略此信号。有些信号不能被忽略,而有些没有默认行为的信号,默认会被忽略。
- 进程可能会捕获此信号,并执行一个被称为信号处理器的特殊函数。
- 进程可能会执行信号的默认行为。例如,信号 15(SIGTERM) 的默认行为是结束进程。
信号的名称和值
每个信号都有以SIG
开头的名称,并定义为唯一的正整数。在 Shell 命令行提示符 下,输入kill -l
命令,将显示所有信号的信号值和相应的信号名,类似如下所示:
[root@client0 asd]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
一些信号的含义
信 号 | 默认行为 | 描 述 | 信号值 |
---|---|---|---|
SIGABRT | 生成 core 文件然后终止进程 | 这个信号告诉进程终止操作。ABRT 通常由进程本身发送,即当进程调用 abort() 函数发出一个非正常终止信号时 | 6 |
SIGALRM | 终止 | 警告时钟 | 14 |
SIGBUS | 生成 core 文件然后终止进程 | 当进程引起一个总线错误时,BUS 信号将被发送到进程。例如,访问了一部分未定义的内存对象 | 10 |
SIGCHLD | 忽略 | 当了进程结束、被中断或是在被中断之后重新恢复时,CHLD 信号会被发送到进程 | 20 |
SIGCONT | 继续进程 | CONT 信号指不操作系统重新开始先前被 STOP 或 TSTP 暂停的进程 | 19 |
SIGFPE | 生成 core 文件然后终止进程 | 当一个进程执行一个错误的算术运算时,FPE 信号会被发送到进程 | 8 |
SIGHUP | 终止 | 当进程的控制终端关闭时,HUP 信号会被发送到进程 | 1 |
SIGILL | 生成 core 文件然后终止进程 | 当一个进程尝试执行一个非法指令时,ILL 信号会被发送到进程 | 4 |
SIGINT | 终止 | 当用户想要中断进程时,INT 信号被进程的控制终端发送到进程 | 2 |
SIGKILL | 终止 | 发送到进程的 KILL 信号会使进程立即终止。KILL 信号不能被捕获或忽略 | 9 |
SIGPIPE | 终止 | 当一个进程尝试向一个没有连接到其他目标的管道写入时,PIPE 信号会被发送到进程 | 13 |
SIGQUIT | 终止 | 当用户要求进程执行 core dump 时,QUIT 信号由进程的控制终端发送到进程 | 3 |
SIGSEGV | 生成 core 文件然后终止进程 | 当进程生成了一个无效的内存引用时,SEGV 信号会被发送到进程 | 11 |
SIGSTOP | 停止进程 | STOP 信号指示操作系统停止进程的执行 | 17 |
SIGTERM | 终止 | 发送到进程的 TERM 信号用于要求进程终止 | 15 |
SIGTSTP | 停止进程 | TSTP 信号由进程的控制终端发送到进程来要求它立即终止 | 18 |
SIGTTIN | 停止进程 | 后台进程尝试读取时,TTIN 信号会被发送到进程 | 21 |
SIGTTOU | 停止进程 | 后台进程尝试输出时,TTOU 信号会被发送到进程 | 22 |
SIGUSR1 | 终止 | 发送到进程的 USR1 信号用于指示用户定义的条件 | 30 |
SIGUSR2 | 终止 | 同上 | 31 |
SIGPOLL | 终止 | 当一个异步输入/输出时间事件发生时,POLL 信号会被发送到进程 | 23 |
SIGPROF | 终止 | 当仿形计时器过期时,PROF 信号会被发送到进程 | 27 |
SIGSYS | 生成 core 文件然后终止进程 | 发生有错的系统调用时,SYS 信号会被发送到进程 | 12 |
SIGTRAP | 生成 core 文件然后终止进程 | 追踪捕获/断点捕获时,会产生 TRAP 信号。 | 5 |
SIGURG | 忽略 | 当侖一个 socket 有紧急的或是带外数据可被读取时,URG 信号会被发送到进程 | 16 |
SIGVTALRM | 终止 | 当进程使用的虚拟计时器过期时,VTALRM 信号会被发送到进程 | 26 |
SIGXCPU | 终止 | 当进程使用的 CPU 时间超出限制时,XCPU 信号会被发送到进程 | 24 |
SIGXFSZ | 生成 core 文件然后终止进程 | 当文件大小超过限制时,会产生 XFSZ 信号 | 25 |
作业
shell中运行的每个进程。
使用后台方式运行时,方括号中显示的是shell分配的作业号。
[root@localhost ~]# sleep 100 &
[1] 2477
Shell向进程发送信号
可以使用键盘或 pkill 命令、kill 命令和 killall 命令向进程发送各种信号。
能通过键盘发送的信号
组合键 | 含 义 |
---|---|
Ctrl+C | 中断信号,发送 SIGINT 信号到运行在前台的进程。 |
Ctrl+Y | 延时挂起信号,使运行的进程在尝试从终端读取输入时停止。控制权返回给 Shell,使用户可以将进程放在前台或后台,或杀掉该进程。 |
Ctrl+Z | 挂起信号,发送 SIGTSTP 信号到运行的进程,由此将其停止,并将控制权返回给 Shell。 |
kill 命令发送信号
大多数主流的 Shell,包括 Bash,都有内置的 kill 命令。Linux 系统中,也有 kill 命令,即 /usr/bin/kill
,一个可执行文件。
当准备杀掉一个进程或一连串的进程时,我们的常识是从尝试发送最安全的信号开始,即 SIGTERM 信号。以这种方式,关心正常停止运行的程序,当它收到 SIGTERM 信号时,有机会按照已经设计好的流程执行,比如,清理和关闭打开的文件。
如果发送一个 SIGKILL 信号到进程,你将消除进程先清理而后关闭的机会,而这可能会导致不幸的结果。但如果一个有序地终结不管用,那么发送 SIGINT 或 SIGKILL 信号就可能是唯一的方法了。例如,当一个前台进程使用 Ctrl+C 组合键杀不掉时,那最好就使用命令“kill -9 PID” 了。
killall 命令发送信号
killall 命令会发送信号到运行任何指定命令的所有进程。所以,当一个进程启动了多个实例时,使用 killall 命令来杀掉这些进程会更方便些。
如果没有指定信号名,killall 命令会默认发送 SIGTERM 信号。例如,使用 killall 命令杀掉所有 firefox 进程:
killall firefox
发送 KILL 信号到 firefox 的进程:
killall -s SIGKILL firefox
pkill 命令发送信号
使用 pkill 命令,可以通过指定进程名、用户名、组名、终端、UID、EUID 和 GID 等属性来杀掉相应的进程。pkill 命令默认也是发送 SIGTERM 信号到进程。
实例1
使用 pkill 命令杀掉所有用户的 firefox 进程。
pkill firefox
实例2
强制杀掉用户 mozhiyan 的 firefox 进程。
pkill -KILL -u mozhiyan firefox
实例3
让 sshd 守护进程重新加载它的配置文件。
pkill -HUP sshd
捕获信号
trap命令
trap commands signals
捕获signals
列出的命令,然后执行commands
。
例子:
#!/bin/bash
trap "echo hello" SIGINT
count=1
while [ $count -le 5 ]
do
echo "Loop #$count"
count=$[ $count + 1 ]
sleep 1
done
[root@localhost ~]# ./test
Loop #1
Loop #2
^Chello
Loop #3
Loop #4
Loop #5
注意!如果不使用while而是直接
sleep 10
,则ctrl+c
会输出hello后结束脚本。理解:
ctrl+c
先是触发了trap
命令输出“hello”
,然后结束了sleep 10
进程,脚本运行结束,因此停止。
脚本也有自己的执行顺序,当在脚本中捕获了同一个信号的多个不同的处理方法,则根据信号到达的时机不同,会有不同的结果。
修改信号捕获
重新使用带有新选项的trap命令。
删除信号捕获
使用单/双破折号作为选项。
trap - SIGINT
trap -- SIGINT
捕获退出
捕获EXIT信号,脚本在正常退出、提前退出都会发出EXIT信号。
trap "...." EXIT
后台运行
在命令后加个(&)就行了。
后台运行的程序仍会使用STDOUT和STDERR,最好将2者重定向。
nohup
nohup
命令会解除终端与进程的关联,并将STDOUT和STDERR以追加模式重定向到nohup.out
文件中。
位于同一个目录的多个命令使用nohup要注意,所有命令的输出都会堆放在
nohup.out
文件中。
[root@localhost ~]# ./test
#1: Fri Aug 6 13:05:50 CST 2021
#2: Fri Aug 6 13:05:51 CST 2021
#3: Fri Aug 6 13:05:52 CST 2021
[root@localhost ~]# nohup ./test
nohup: ignoring input and appending output to ‘nohup.out’
[root@localhost ~]# cat nohup.out
#1: Fri Aug 6 13:03:05 CST 2021
#2: Fri Aug 6 13:03:06 CST 2021
#3: Fri Aug 6 13:03:07 CST 2021
#1: Fri Aug 6 13:06:02 CST 2021
#2: Fri Aug 6 13:06:03 CST 2021
#3: Fri Aug 6 13:06:04 CST 2021
作业控制
启动、停止、终止、恢复 作业的功能统称为作业控制。
jobs命令
[root@localhost ~]# sleep 100 &
[1] 3014
[root@localhost ~]# sleep 200 &
[2] 3015
[root@localhost ~]# sleep 300 &
[3] 3016
[root@localhost ~]# jobs
[1] Running sleep 100 &
[2]- Running sleep 200 &
[3]+ Running sleep 300 &
带加号+
的为默认作业,使用作业控制命令若没有指定作业号,就会操作默认作业。带减号-
的为下一个默认作业。
重启停止的作业
**bg命令:**以后台模式重启。
bg n
n为作业号,如果不带参数n,则重启默认作业。
**fg命令:**以前台模式重启。
谦让度(优先级)
–nice
**nice命令:**运行在运行程序时指定进程的优先级。
nice -n 优先级 执行程序
或者不用n参数:
nice -优先级 执行程序
优先级为整数值,从-20(最高优先级)到+19(最低优先级)。默认值为0。
按谦让度理解,越“好相处”的进程越容易受欺负,也就是优先级最低;反之越“不好相处”的进程优先级越高。
–renice
改变已运行的进程的优先级。
renice -n 优先级 -p 进程号
计划作业
at
at是个命令,atd是个守护进程。
/var/spool/at
使用at命令添加计划任务后,会在/var/spool/at
目录中生成一个可执行文件,atd每60s检查一次该目录,如果作业设置的运行时间与当前时间匹配,就会运行该作业。
at [-f filename] time
使用-f选项从指定文件中读取命令。
filename文件只是个普通的文本文件,存有命令就行了,不需要编辑成一个脚本。
书中只展示了使用文件的方式设置计划作业。
使用at命令后,作业会被提交到作业队列中。作业队列由26个等级,用字母标识。z为最高优先级。
at会通过邮件的方式发送消息。
**atq命令:**查看系统中有哪些计划作业。
**atrm命令:**移除计划任务。
例子:
命令文件内容
[root@localhost ~]# vim schedule
echo "a test for at"
执行
[root@localhost ~]# at -f schedule 14:00
job 9 at Sat Aug 7 14:00:00 2021
[root@localhost ~]# atq
9 Sat Aug 7 14:00:00 2021 a root
添加计划作业后,/var/spool/at
目录下多了1个可执行文件
[root@localhost ~]# ls /var/spool/at
a00009019e1a28 spool
可以直接执行该文件
[root@localhost ~]# /var/spool/at/a00009019e1a28
a test for at
生成的可执行文件设置了很多环境变量,不可草率地认为只执行了命令而已。
cron
at命令适合用于设置未来特定时间执行的作业,如果需要周期性地运行脚本,就需使用cron命令。
具体的使用看书P.349。
相关的文件:
1、/var/spool/cron/$USER
:存放cron时间表的文件。
用于创建cron时间表的命令crontab -e
就等价于vim /var/spool/cron/$USER
。
2、如果对于执行时间的精度要求不高,目录/etc/
中有4个基本目录:cron.hourly、cron.daily、cron.monthly、cron.weekly。
如果要脚本每天运行一次,就将脚本放入/etc/cron.daily
目录中。其他目录效果也一样。
anacron
具体参数P.351。
cron假定系统不关机,如果在关机时错过了cron时间表的安排时间,则开机不会再执行那些错过的脚本。
为了解决这问题就有了anacron。
他通过时间戳来决定作业是否在正确的时间运行了。时间戳文件位于作业目录/var/spool/anacron
。
[root@localhost ~]# ls /var/spool/anacron
cron.daily cron.monthly cron.weekly
anacron不处理时间需求小于1天的脚本。
时间表文件是:/etc/anacrontab
。
bash shell
Bash Shell在启动时总要配置运行环境,例如初始化环境变量、设置命令提示符、指定系统命令路径等。这个过程是通过加载一系列配置文件完成的,这些配置文件其实就是 Shell 脚本文件。
shell的启动文件
登录式shell有5个启动文件(配置文件)。
- /etc/profile
/etc/profile
是bash shell的启动文件,每个用户登录都会执行这个文件。这就意味着对/etc/profile
文件的修改会影响到所有使用该系统的用户。在这个文件中,会使用循环语句迭代执行
/etc/profild.d
目录下的所有文件。
- $HOME/.bash_profile
- $HOME/.bashrc
- $HOME/.bash_login
- $HOME/.profile
这4个启动文件可针对用户需求自行修改。仅影响用户本身。
自己的系统中只有前3个文件。
[root@localhost ~]# ls -a | grep bash
.bash_history
.bash_logout
.bash_profile
.bashrc
登录式
shell会按如下顺序,运行第一个找到的文件。
- $HOME/.bash_profile
- $HOME/.bash_login
- $HOME/.profile
$HOME/.bashrc
文件会在$HOME/.bash_profile
执行时被执行。
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
...
非登录式
直接读取 ~/.bashrc
对于普通用户来说,也许 ~/.bashrc
才是最重要的文件,因为不管是否登录都会加载该文件。
可以根据自己的需求对$HOME/.bashrc
文件进行修改,以设置bash shell的运行环境。例如在文件中定义变量、函数、别名等。
shell的运行方式
交互式/非交互式、登录式/非登录式。两两组合,4种运行方式:
- 交互式的登录 Shell;
- 交互式的非登录 Shell;
- 非交互式的登录 Shell;
- 非交互式的非登录 Shell。
交互式/非交互:
一个个输入命令并查看结果,全程和shell互动,就是交互式;
让所有命令批量化、一次性执行,就是非交互式。
判断
一、查看变量-
的值:如果包含字母i
,表示为交互式;否则为非交互式;
直接在终端输入命令:
[root@localhost ~]# echo $-
himBH
编写脚本,在脚本里查看命令:
#!/bin/bash
echo $-
echo $PS1
[root@localhost ~]# ./test
hB
二、查看变量PS1
的值,如果非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。
交互式shell
如果bash shell不是系统登录时启动的(例如通过命令bash
启动),那么启动的shell就属于交互式shell。
交互式shell不会访问/etc/profile
文件,只会检查$HOME/.bashrc
文件。
在终端上执行,有交互界面。
非交互式shell
系统执行shell脚本是就是使用了非交互式shell。
bash shell通过$BASH_ENV
指定非交互式shell运行时要启动的文件。通过执行这些文件来进行shell脚本变量设置。
通常$BASH_ENV
为空,非交互式shell通过继承父shell的全局变量来获得运行的环境变量。
登录式/非登录式shell
登录式:需要输入用户名、密码才能启动的shell。能用exit
或logout
退出。
非登录式反之。只能用exit
退出。
查看:
[root@localhost ~]# shopt login_shell
login_shell on
on
表示为登录式,off
反之。
子Shell和子进程的区别
linux中有2种方式创建子进程:
1、只执行fork()函数:子进程和父进程几乎一样,父进程的函数、变量、别名等都能在子进程中使用。
2、使用fork()和exec()函数:子进程根据exec()函数加载的程序重建进程,除了“父子关系”外,与父进程没有任何联系。
组命令、命令替换、管道这几种语法都是用第一种方式创建进程,所以子进程可以使用父进程的全局变量、局部变量、别名等。我们将这种子进程称为子 Shell(sub shell)。
如果子 Shell 对数据做了修改,比如修改了全局变量,那么这种修改只能停留在子 Shell,无法传递给父 Shell。子进程同理。
检测子shell和子进程
$ 变量可以获取当前进程的 ID,但 $ 变量在子 Shell 中无效。在普通的子进程中,$ 确实被展开为子进程的 ID;但是在子 Shell 中,$ 却被展开成父进程的 ID。
另外两个环境变量——SHLVL 和 BASH_SUBSHELL,用它们来检测子 Shell 非常方便。(初始值都为0)
SHLVL
是记录多个 Bash 进程实例嵌套深度的累加器,每次进入一层普通的子进程,SHLVL 的值就加 1。
BASH_SUBSHELL
是记录一个 Bash 进程实例中多个子 Shell(sub shell)嵌套深度的累加器,每次进入一层子 Shell,BASH_SUBSHELL 的值就加 1。
[root@client0 asd]# echo "$SHLVL $BASH_SUBSHELL"
1 0
[root@client0 asd]# test.sh
2 0
[root@client0 asd]# { echo "$SHLVL $BASH_SUBSHELL"; }
1 0
[root@client0 asd]# (echo "$SHLVL $BASH_SUBSHELL")
1 1
文件的权限
umask
默认为0022,第一个0表示是八进制。
umask的值就是文件/目录的全权限减去的值。减去后就得到了新创建的文件/目录的默认权限。
文件的全权限:666
目录的全权限:777
3个特殊的权限
SUID
- 只对可执行文件有效。
- 用户(以及文件所有者)必须对该文件拥有x权限。
- 用户执行拥有s权限的文件时,他会暂时获得文件所有者的权限。
- 命令执行结束后撤销用户的权限。
例子:
普通用户没有权限修改记录密码的文件/etc/shadow
,但仍能通过passwd
命令修改自己的密码,正是因为passwd
设置了SUID权限,使得普通用户在passwd
进程运行期间暂时拥有了root的权限。
而普通用户使用cat
命令则无法查看/etc/shadow
文件的内容。
[root@localhost ~]# ll /usr/bin/passwd
-rwsr-xr-x. 1 root root 27856 Apr 1 2020 /usr/bin/passwd
[root@localhost ~]# ll /usr/bin/cat
-rwxr-xr-x. 1 root root 54080 Aug 20 2019 /usr/bin/cat
补充:如果
ll
显示的S为大写,说明连文件所属者都没有该文件的x权限,SUID位设置无效。
SGID
作用于文件时,基本上和SUID一样,不过s是作用于用户组部分的x权限上。
- 只对可执行文件有效。
- 用户必须对该文件拥有x权限。
- 用户执行拥有s权限的文件时,他会暂时获得文件所属组的权限。
- 命令执行结束后撤销用户的权限。
-rwx--s--x. 1 root slocate 40520 Apr 11 2018 /usr/bin/locate
作用于目录:
一个目录被赋予 SGID 权限后,进入此目录的普通用户,其所属组会变为该目录的所属组。
这就使得用户在创建文件(或目录)时,该文件(或目录)的所属组都属于目录的所属组。
SBIT
仅对目录有效。
目录设定了 SBIT 权限后,用户在此目录下创建的文件或目录,就只有自己和 root 才有权利修改或删除该文件。
一些应用的小技巧
一天一个日志文件
date命令的参数:date +(%~)
,具体参数参照man date
today=$(date +%y%m%d) // 获得日志文件后缀
ls /usr/bin -al > log.$today // 将数据存入日志
ps命令的使用
例子:
[root@localhost ~]# ps -p 2254 -o pid,ppid,ni,cmd
PID PPID NI CMD
2254 2246 0 -bash
-o
后的参数指定了要显示哪些列。