Linux之shell编程

什么是shell

在Linux中,操作一些命令的时候我们是在这个命令行中执行的,这个命令行其实就可以称之为shell。
所以说这个shell其实就是用户与linux系统沟通的一个桥梁,我们想让Linux系统去做什么事情,就通过这个shell去执行对应的操作就行了。
那shell编程又是什么意思呢?
shell编程其实就是把之前在shell中执行的单个命令按照一定的逻辑和规则,组装到一个文件中,后面执行的时候就可以直接执行这个文件了,这个文件我们称之为shell脚本。
所以shell编程,最终其实就是要开发一个shell脚本。

第一个shell脚本

shell脚本的第一行内容是:#!/bin/bash
这句话相当于是一个导包语句,将shell的执行环境引入进去了。
注意了,第一行的#号可不是注释,其它行的#号才是注释。
下面我们来创建第一个shell脚本,首先创建一个新目录存放我们的脚本

[root@bigdata01 ~]# mkdir shell 
[root@bigdata01 ~]# cd shell/ 
[root@bigdata01 shell]#
[root@bigdata01 shell]# vi hello.sh 
#!/bin/bash
复制代码

下面就可以写shell命令了
我们先来一个hello world把,在这里加一个注释

[root@bigdata01 shell]# vi hello.sh 
#!/bin/bash 
# first command 
echo hello world!
复制代码

保存文件,第一个shell脚本就开发好了。

执行shell脚本

下面我们来执行一下这个shell脚本
执行shell脚本的标准写法
bash hello.sh
bash:是shell的执行程序,然后在后面指定脚本名称

[root@bigdata01 shell]# bash hello.sh 
hello world!
复制代码

还有一种写法是 sh hello.sh
其实这里不管是bash 还是sh 都是一样的,我们可以来验证一下
bash对应的是/bin目录下面的bash文件

[root@bigdata01 shell]# ll /bin/bash 
-rwxr-xr-x. 1 root root 964600 Aug 8 2019 /bin/bash
复制代码

sh是一个链接文件,指向的也是/bin目录下面的bash文件

[root@bigdata01 shell]# ll /bin/sh 
lrwxrwxrwx. 1 root root 4 Mar 28 20:54 /bin/sh -> bash
复制代码

其实bash和sh在之前对应的是两种类型的shell,不过后来统一了,我们在这也就不区分了,所以在shell脚本中的第一行引入/bin/bash,或者/bin/sh都是一样的,这块大家知道就行了。
具体是使用bash 还是sh完全看你个人喜好了。

注意了,大家在看其它资料的时候,资料中一般都会说需要先给脚本添加执行权限,然后才能执行,为什么我们在这里没有给脚本增加执行权限就能执行呢?
在这里可以看到这个脚本确实只有读写权限

[root@bigdata01 shell]# ll 
total 4 
-rw-r--r--. 1 root root 45 Apr 2 16:11 hello.sh
复制代码

主要原因是这样的,我们现在执行的时候前面指定bash或者sh,表示把hello.sh这个脚本中的内容作为参数直接传给了bash或者sh命令来执行,所以这个脚本有没有执行权限都无所谓了。

那下面我们就来给这个脚本添加执行权限

chmod u+x hello.sh

[root@bigdata01 shell]# chmod u+x hello.sh 
[root@bigdata01 shell]# ll 
total 4 
-rwxr--r--. 1 root root 45 Apr 2 16:11 hello.sh
复制代码

添加完执行权限之后,再执行的时候就可以使用简化形式了
./hello.sh
这里的.表示是当前目录,表示在当前目录下执行这个脚本

[root@bigdata01 shell]# ./hello.sh   
hello world!  
复制代码

这里指定全路径也可以执行

[root@bigdata01 shell]# /root/shell/hello.sh 
hello world!
复制代码

能不能再简化一下,前面不要带任何路径信息呢?

[root@bigdata01 shell]# hello.sh 
-bash: hello.sh: command not found
复制代码

这样直接执行却提示命令没找到?有没有感到疑惑?大家在看一些其它资料或视频的时候应该看到过这样直接指定脚本执行也是可以的,我们现在已经cd到这个文件所在的目录里面了,按理说是可以找到,那为什么会提示找不到呢?

下面我们来修改一下这个PATH环境变量,
注意,在这我们先直接修改,后面会详细讲解环境变量的相关内容,
打开/etc/profile文件,在最后一行添加export PATH=.:$PATH,保存文件即可 。

[root@bigdata01 shell]# vi /etc/profile 
........ 
....... 
....... 
export PATH=.:$PATH
复制代码

然后执行source /etc/profile 重新加载环境变量配置文件,这样才会立刻生效
再重新执行hello.sh
ok 成功

[root@bigdata01 shell]# hello.sh 
hello world!
复制代码

最后再讲一个小命令,shell脚本的单步执行,可以方便脚本调试。

[root@bigdata01 shell]# bash -x hello.sh 
+ echo hello 'world!' 
hello world!
复制代码

+号开头的内容表示是脚本中将要执行的命令,下面的内容是执行的结果,这样如果脚本中的命令比较多的话,看起来是比较清晰的。

shell中的变量

shell中变量的定义

学习任何编程语言都需要先学习变量,shell也不例外,但是要注意,shell中的变量不需要声明,初始化也不需要指定类型,shell是一门弱类型的语言,JAVA则是强类型的语言,需要提前声明变量,并且指定变量类型。

shell中变量的命名要求:
只能使用数字、字母和下划线,且不能以数字开头
变量赋值是通过"="进行赋值,在变量、等号和值之间不能出现空格!
下面我们来创建一些变量,执行之后提示-bash: name: command not found的都表示是错误的,执行成功的话是没有任何输出的,没有反馈就是最好的结果

[root@bigdata01 shell]# name=zs 
[root@bigdata01 shell]# name =zs 
-bash: name: command not found 
[root@bigdata01 shell]# name= zs 
-bash: zs: command not found 
[root@bigdata01 shell]# name2_=zs 
[root@bigdata01 shell]# 2name_=zs 
-bash: 2name_=zs: command not found 
[root@bigdata01 shell]# name2$=zs 
-bash: name2$=zs: command not found
复制代码

打印变量的值,通过echo命令

[root@bigdata01 shell]# echo $name 
zs
[root@bigdata01 shell]# echo ${name} 
zs
复制代码

这两种形式都可以,一个是完整写法,一个是简化写法,有什么区别吗?
如果我们想在变量的结果后面直接无缝拼接其它字符串,那就只能使用带花括号的形式

[root@bigdata01 shell]# echo $namehehe 
[root@bigdata01 shell]# echo ${name}hehe 
zshehe
复制代码

如果带空格的话就无所谓了

[root@bigdata01 shell]# echo $name hehe 
zs hehe
复制代码

shell中变量的分类

shell中的变量可以分为四种

  • 本地变量
  • 环境变量
  • 位置变量
  • 特殊变量

本地变量

首先来看本地变量
本地变量的格式是VAR_NAME=VALUE,其实就是我们刚才在shell中那样直接定义的变量,这种变量一般用于在shell脚本中定义一些临时变量,只对当前shell进程有效,关闭shell进程之后就消失了,对当前shell进程的子进程和其它shell进程无效
注意了,我们在这开启一个shell的命令行窗口其实就是开启了一个shell进程,克隆一个会话,在这两个shell进程中执行echo $name 会发现在克隆的shell进程中获取不到name的值,表示本地变量对其它shell进程无效,那这里面的对当前shell进程的子进程无效是怎么回事呢?
我们先执行pstree命令查看一下当前的进程树信息,但是会发现提示命名找不到

[root@bigdata01 shell]# pstree 
-bash: pstree: command not found
复制代码

不要着急,找不到说明没有安装这个命令,只需要安装即可,在这里我们可以使用yum快速在线安装,那在yum后面直接指定pstree可以吗?不行的,命令的软件包的名称有时候和具体的命令名称不一样,针对这种场景怎么办呢?我们可以去网上查一下,【centos7 yum 安装pstree】
然后就可以看到网上有提示说需要安装psmisc。

[root@bigdata01 shell]# yum install -y psmisc 
复制代码

再执行pstree

接下来我们来进入这个shell的子进程中,如何进入呢?很简单,直接执行bash即可。

[root@bigdata01 shell]# bash
复制代码

然后再打印name变量的值,发现也是获取不到

[root@bigdata01 shell]# echo $name
复制代码

此时再执行pstree,验证一下当前所在的shell进程是否是之前shell的子进程

最后执行exit退出子shell进程。

[root@bigdata01 shell]# exit 
exit
复制代码

环境变量

接下来看一下shell中的环境变量,这里的环境变量类似于windows中的环境变量,例如在windows中设置JAVA_HOME环境变量
它的格式为:export VAR_NAME=VALUE
它的格式是在本地变量格式的基础上添加一个export参数
环境变量的这种格式主要用于设置临时环境变量,当你关闭当前shell进程之后环境变量就消失了,还有就是对子shell进程有效,对其它shell进程无效
注意了,环境变量的生效范围和本地变量是不一样的,环境变量对子shell进程是有效的。
我们来演示一下

[root@bigdata01 shell]# export age=18 
[root@bigdata01 shell]# echo $age 
18
[root@bigdata01 shell]# bash 
[root@bigdata01 shell]# echo $age 
18
复制代码

执行exit回退到父shell进程

[root@bigdata01 shell]# exit 
exit
复制代码

注意了,在实际工作中我们设置环境变量一般都是需要让它永久生效,这种临时的并不适用,如何设置为永久的呢?
其实就是把这个临时的设置添加到指定配置文件中,以后每次开启shell进程的时候,都会去加载那个指定的配置文件中的命令,这样就可以实现永久生效了
在这里我们一般添加到/etc/profile文件中,这样可以保证对所有用户都生效

[root@bigdata01 shell]# vi /etc/profile 
........... 
........... 
........... 
export age=19
复制代码

结果发现age的值是18.并不是我们刚才在配置文件中定义的19,那也就意味着刚才的设置没有生效,为什么呢?
因为这个shell进程之前已经开启了,它在开启的时候会默认加载一次/etc/profile中的命令,
现在我们想让它重新加载/etc/profile的话需要执行 source /etc/profile

[root@bigdata01 shell]# source /etc/profile 
[root@bigdata01 shell]# echo $age 
19
复制代码

位置变量

接下来看一下位置变量
在进行shell编程的时候,有时候我们想给shell脚本动态的传递一些参数,这个时候就需要用到位置变量,类似于
$0 $1 $2
这样的,$后面的数字理论上没有什么限制,
它的格式是:
location.sh abc xyz
位置变量其实相当于java中main函数的args参数,可以在shell脚本中动态获取外部参数
这样就可以根据外部传的参数动态执行不同的业务逻辑了。
在后面的学习中大家会经常看到位置变量的使用形式
我们来演示一下
创建一个脚本文件,
location.sh
在里面打印一下这些位置变量看看到底是什么内容

[root@bigdata01 shell]# vi location.sh 
#!/bin/bash 
echo $0 
echo $1 
echo $2 
echo $3
复制代码

执行脚本
sh location.sh abc xyz

[root@bigdata01 shell]# sh location.sh abc xyz 
location.sh 
abc 
xyz
复制代码

结果发现 $0的值是这个脚本的名称
$1
是脚本后面的第一个参数
$2
是脚本后面的第二个参数
$3
为空,是因为脚本后面就只有两个参数
理论上来说,脚本后面有多少个参数,在脚本中就可以通过$和角标获取对应参数的值。
多个参数中间使用空格分隔。

特殊变量

最后来看一下shell中的特殊变量,针对特殊变量我们主要学习下面列出来的两个  
首先是 $?
它表示是上一条命令的返回状态码,状态码在0~255之间  
如果命令执行成功,这个返回状态码是0,如果失败,则是在1~255之间,不同的状态码代表着不同的错误信息,也就是说,正确的道路只有一条,失败的道路有很多。  
来演示一下  
复制代码
[root@bigdata01 shell]# ll 
total 8 
-rwxr--r--. 1 root root 45 Apr 2 16:11 hello.sh 
-rw-r--r--. 1 root root 44 Apr 3 16:23 location.sh 
[root@bigdata01 shell]# echo $?
0 
[root@bigdata01 shell]# lk 
-bash: lk: command not found [root@bigdata01 shell]#
echo $? 
127
复制代码
状态码 描述 
0 命令成功结束 
1 通用未知错误
127 没找到命令 
128 无效退出参数 
128+x Linux信号x的严重错误 
130 命令通过Ctrl+C控制码越界 
255 退出码越界
复制代码

第二个特殊变量是$#,它表示的是shell脚本所有参数的个数
我们来演示一下,先创建
paramnum.sh

[root@bigdata01 shell]# vi paramnum.sh 
#!/bin/bash
echo $#
复制代码

然后执行

[root@bigdata01 shell]# sh paramnum.sh a b c 
3 
[root@bigdata01 shell]# sh paramnum.sh a b c d 
4
复制代码

这个特殊变量的应用场景是这样的,假设我们的脚本在运行的时候需要从外面动态获取三个参数,那么在执行脚本之前就需要先判断一下脚本后面有没有指定三个参数,如果就指定了1个参数,那这个脚本就没有必要执行了,直接停止就可以了,参数个数都不够,执行是没有意义的。

变量和引号的特殊使用

前面我们学习了shell中的变量,那针对变量和引号在工作中有一些特殊的使用场景,我们来看一下
首先是单引号,’’:单引号不解析变量

[root@bigdata01 shell]# name=jack 
[root@bigdata01 shell]# echo '$name' 
$name
复制代码

然后再看一下双引号,"":双引号解析变量

[root@bigdata01 shell]# name=jack 
[root@bigdata01 shell]# echo "$name" 
jack
复制代码

还有一个特殊的引号,我们称之为反引号,在键盘左上角esc下面的那个键,在英文输入法模式下可以打出来

[root@bigdata01 shell]# name=jack 
[root@bigdata01 shell]# echo `$name` 
-bash: jack: command not found
复制代码

反引号是执行并引用命令的执行结果,在这里反引号是获取到了name变量的值,然后去执行这个值,结果发现没有找到这个命令
如果我们把name的值改为pwd,来看一下效果,这样就会执行pwd,并且把pwd执行的结果打印出来。

[root@bigdata01 shell]# name=pwd 
[root@bigdata01 shell]# echo `$name` 
/root/shell
复制代码

反引号还有另一种写法,$() 他们的效果一致,具体使用哪个就看你喜欢哪个。

最后还有一个大招 大家注意一下
有时候我们想在变量的值外面套一层引号,该怎么写呢?
echo "$name"
是不行的,最终的值是不带引号的

[root@bigdata01 shell]# echo "$name" 
pwd
复制代码

那我在外面套一层单引号呢?这样虽然值里面带双引号了,但是这个变量却没有解析

[root@bigdata01 shell]# echo '"$name"' 
"$name"
复制代码

还能怎么办呢?
看一下这个骚操作,先套一个单引号,再套一个双引号,这样就可以了。

[root@bigdata01 shell]# echo "'$name'" 
'pwd'
复制代码

shell中的相关语法

for循环

首先来看for循环,for循环本身的意义我就不再赘述了,我们直接来看一下shell中for循环的格式特点
第一种格式:和java中的for循环格式有点类似,但是也不一样

for((i=0;i<10;i++)) 
do 
循环体... 
done
复制代码

我们来演示一下,先创建 for1.sh

[root@bigdata01 shell]# vi for1.sh 
#!/bin/bash 
for((i=0;i<10;i++))
do 
echo $i 
done
复制代码

注意了,这里的do也可以和for写在一行,只是需要加一个分号;

[root@bigdata01 shell]# vi for1.sh 
#!/bin/bash 
for((i=0;i<10;i++));do 
echo $i 
done
复制代码

执行看结果

[root@bigdata01 shell]# sh for1.sh 
0 
1 
2
……
复制代码

这一种格式适合用在迭代多次,步长一致的情况
接下来看第二种格式,这种格式针对没有规律的列表,或者是有限的几种情况进行迭代是比较方便的

for i in 1 2 3 
do 
循环体... 
done
复制代码

演示一下,

[root@bigdata01 shell]# vi for2.sh 
#!/bin/bash 
for i in 1 2 3 
do 
echo $i 
done
复制代码

执行,看结果

[root@bigdata01 shell]# sh for2.sh 
1 
2 
3
复制代码

这就是shell中for循环的用法

while循环

接下来我们来学习一下while循环
while循环主要适用于循环次数未知,或不便于使用for直接生成较大列表时
while循环的格式为:

while 测试条件  
do  
循环体...  
done  
复制代码

注意这里面的测试条件,测试条件为"真",则进入循环,测试条件为"假",则退出循环
那这个测试条件该如何定义呢?
它支持两种格式
test EXPR 或者 [ EXPR ] ,第二种形式里面中括号和表达式之间的空格不能少
这个EXPR表达式里面写的就是具体的比较逻辑,shell中的比较有一些不同之处,针对整型数据和字符串
数据是不一样的,来看一下
整型测试:-gt(大于)、-lt(小于)、-ge(大于等于)、-le(小于等于)、-eq(等于)、-ne(不等于)
针对整型数据,需要使用-gt、-lt这样的写法,而不是大于号或小于号,这个需要注意一下
还有就是字符串数据,如果判断两个字符串相等,使用=号,这里的=号不是赋值的意思,不等于就使用!=
就可以了
字符串测试:=(等于)、!=(不等于)
下面来演示一下,创建 while1.sh ,注意,这里面需要使用sleep实现休眠操作,否则程序会一直连续的

[root@bigdata01 shell]# vi while1.sh 
#!/bin/bash
while test 2 -gt 1 
do 
echo yes 
sleep 1 
done
复制代码

执行脚本,按ctrl+c可强制退出程序

[root@bigdata01 shell]# sh while1.sh 
yes 
yes 
yes ...
复制代码

把测试条件修改一下,再执行就不会打印任何内容了,因为测试条件为false

[root@bigdata01 shell]# vi while1.sh 
#!/bin/bash 
while test 2 -lt 1 
do 
echo yes 
sleep 1 
done 
[root@bigdata01 shell]# sh while1.sh
复制代码

其实我是不太喜欢这里面测试条件的格式,我喜欢使用中括号这种,看起来比较清晰
只是这种一定要注意,中括号和里面的表达式之间一定要有空格,否则就报错

[root@bigdata01 shell]# cp while1.sh while2.sh 
[root@bigdata01 shell]# vi while2.sh 
#!/bin/bash 
while [ 2 -gt 1 ] 
do 
echo yes 
sleep 1 
done 
[root@bigdata01 shell]# sh while2.sh 
yes 
yes 
yes 
...
复制代码

最后尝试一下针对字符串的测试

[root@bigdata01 shell]# cp while2.sh while3.sh 
[root@bigdata01 shell]# vi while3.sh 
#!/bin/bash 
while [ "abc" = "abc" ] 
do 
echo yes 
sleep 1 
done 
[root@bigdata01 shell]# sh while3.sh 
yes 
yes 
yes 
...
复制代码

if判断

if判断分为三种形式

  • 单分支
  • 双分支
  • 多分支

先看一下单分支,它的格式是这样的

if 测试条件 
then 
  选择分支 
fi
复制代码

这里面也用到了测试条件,所以和while中的一致
来演示一下,创建 if1.sh
在这里面我们可以动态获取一个参数,在测试条件中动态判断

[root@bigdata01 shell]# vi if1.sh 
#!/bin/bash 
flag=$1 
if [ $flag -eq 1 ] 
then 
echo one 
fi
复制代码

执行脚本

[root@bigdata01 shell]# sh if1.sh 1 
one 
[root@bigdata01 shell]# sh if1.sh 
if1.sh: line 3: [: -eq: unary operator expected
复制代码

在这里发现,如果脚本后面没有传参数的话,执行程序会抱错,错误信息看起来也不优雅,这说明我们的程序不够健壮,所以可以进行优化
先判断脚本后面参数的个数,如果参数个数不够,直接退出就行,在这里使用exit可以退出程序,并且可以在程序后面返回一个状态码,这个状态码其实就是我们之前使用$?获取到的状态码,如果这个程序不传任何参数,就会执行exit 100,结束程序,并且返回状态码100,我们来验证一下

[root@bigdata01 shell]# vi if1.sh 
#!/bin/bash 
if [ $# -lt 1 ] 
then 
	echo "not found param" 
	exit 100 
fi 
flag=$1 
if [ $flag -eq 1 ] 
then
	echo "one" 
fi 
[root@bigdata01 shell]# sh if1.sh 
not found param 
[root@bigdata01 shell]# echo $?
100
复制代码

针对这个脚本,按照正常的执行逻辑是这样的,如果传递的参数不匹配,则没有任何输出
这样也不太友好,能不能在执行错误的数据时提示用户呢?
当然可以,这样就需要使用到if的双分支了
格式如下:

if 测试条件 
then 
	选择分支1 
else 
	选择分支2 
fi
复制代码

复制一个脚本,进行修改

[root@bigdata01 shell]# cp if1.sh if2.sh 
[root@bigdata01 shell]# vi if2.sh 
#!/bin/bash 
if [ $# -lt 1 ] 
then 
echo "not found param" 
exit 100 
fi 
flag=$1 
if [ $flag -eq 1 ] 
then 
echo "one" 
else 
echo "not support" 
fi
复制代码

执行脚本

[root@bigdata01 shell]# sh if2.sh 1 
one 
[root@bigdata01 shell]# sh if2.sh 2 
not support
复制代码

现在只支持针对数字1的翻译,如果想多支持几个数字的翻译呢?
这样就需要使用if的多分支条件了
格式如下:

if 测试条件1 
then 
	选择分支1 
elif 测试条件2 
then 
	选择分支2 
  ... 
else 
	选择分支n 
fi
复制代码

复制一个脚本,进行修改

[root@bigdata01 shell]# cp if2.sh if3.sh 
[root@bigdata01 shell]# vi if3.sh
#!/bin/bash 
if [ $# -lt 1 ]
  exit 100 
fi
flag=$1 
if [ $flag -eq 1 ] 
then 
echo "one" 
elif [ $flag -eq 2 ] 
then echo "two" 
elif [ $flag -eq 3 ]
then echo "three" 
else 
echo "not support" 
fi
复制代码

执行脚本

[root@bigdata01 shell]# sh if3.sh 1 
one 
[root@bigdata01 shell]# sh if3.sh 2 
two 
[root@bigdata01 shell]# sh if3.sh 3 
three 
[root@bigdata01 shell]# sh if3.sh 4 
not support
复制代码

shell 扩展

shell脚本的后台运行
在实际工作中会遇到这种情况,针对带有while无限循环的shell脚本,我们希望它能够一直运行,不影响我在这个窗口执行其它操作
但是现在它会一直占用这个shell窗口,我们称这个脚本现在是在前台执行,不想让它一直占用shell窗口的话,需要把它放到后台执行,如何放到后台呢?很简单,在脚本后面添加一个&即可
演示一下,这样就可以了

[root@bigdata01 shell]# sh while2.sh & 
[1]2228
复制代码

但是当我们把这个窗口关闭以后会发现之前放到后台运行的shell脚本也停止了,我们是希望这个脚本能够一直在后台运行的

[root@bigdata01 ~]# ps -ef|grep while2 
root 2325 2306 0 20:48 pts/2 00:00:00 grep --color=auto while2
复制代码

如何保证关闭shell窗口而不影响放到后台的shell脚本执行呢?
也很简单,在命令前面加上nohup 即可
原理就是,默认情况下,当我们关闭shell窗口时,shell窗口会向之前通过它启动的所有shell
脚本发送停止信号,当我们加上nohup之后,就会阻断这个停止信号的发送,所以已经放到后台的shell脚本就不会停止了。
演示一下

[root@bigdata01 shell]# nohup sh while2.sh & 
[1]2326 
nohup: ignoring input and appending output to ‘nohup.out’
复制代码

注意:使用nohup之后,脚本输出的信息默认都会存储到当前目录下的一个nohup.out日志文件中,后期想要排查脚本的执行情况的话就可以看这个日志文件。
此时如果想要停止这个shell脚本的话就只能使用kill了

[root@bigdata01 shell]# ps -ef|grep while2 
root 2326 2306 0 20:51 pts/2 00:00:00 sh while2.sh 
root 3515 2306 0 21:11 pts/2 00:00:00 grep --color=auto while2 
[root@bigdata01 shell]# kill 2326 
[root@bigdata01 shell]# ps -ef|grep while2 
root 3525 2306 0 21:11 pts/2 00:00:00 grep --color=auto while2
复制代码

下面来看一下什么是标准输出、标准错误输出和重定向:
标准输出:表示是命令或者程序输出的正常信息
标准错误输出:表示是命令或者程序输出的错误信息
演示一下

[root@bigdata01 shell]# ll 
total 52 
-rw-r--r--. 1 root root 48 Apr 3 17:32 for1.sh 
-rw-r--r--. 1 root root 43 Apr 3 17:40 for2.sh 
[root@bigdata01 shell]# lk 
-bash: lk: command not found
复制代码

这里的ll执行成功了,所以下面输出的信息就是标准输出
这里的lk是一个不存在的命令,执行失败了,所以下面输出的信息就是标准错误输出
标准输出可以使用文件描述符1来表示,标准错误输出可以使用文件描述符2来表示
针对标准输出和标准错误输出,可以使用重定向操作将这些输出信息保存到文件中

[root@bigdata01 shell]# ll 1> a.txt 
[root@bigdata01 shell]# more a.txt 
total 52 
-rw-r--r--. 1 root root 0 Apr 3 21:39 a.txt 
-rw-r--r--. 1 root root 48 Apr 3 17:32 for1.sh 
-rw-r--r--. 1 root root 43 Apr 3 17:40 for2.sh 
-rwxr--r--. 1 root root 45 Apr 2 16:11 hello.sh 
-rw-r--r--. 1 root root 121 Apr 3 18:30 if1.sh 
-rw-r--r--. 1 root root 147 Apr 3 18:30 if2.sh 
-rw-r--r--. 1 root root 227 Apr 3 18:34 if3.sh 
-rw-r--r--. 1 root root 44 Apr 3 16:23 location.sh 
-rw-------. 1 root root 4692 Apr 3 21:11 nohup.out 
-rw-r--r--. 1 root root 20 Apr 3 16:48 paramnum.sh 
-rw-r--r--. 1 root root 56 Apr 3 17:59 while1.sh 
-rw-r--r--. 1 root root 55 Apr 3 18:01 while2.sh 
-rw-r--r--. 1 root root 61 Apr 3 18:03 while3.sh
复制代码

重复执行此命令会发现文件内的内容没有变化,这是因为 > 会覆盖掉之前的内容

[root@bigdata01 shell]# ll 1> a.txt 
[root@bigdata01 shell]# more a.txt 
total 52 
-rw-r--r--. 1 root root 0 Apr 3 21:39 a.txt 
-rw-r--r--. 1 root root 48 Apr 3 17:32 for1.sh 
-rw-r--r--. 1 root root 43 Apr 3 17:40 for2.sh 
-rwxr--r--. 1 root root 45 Apr 2 16:11 hello.sh 
-rw-r--r--. 1 root root 121 Apr 3 18:30 if1.sh 
-rw-r--r--. 1 root root 147 Apr 3 18:30 if2.sh 
-rw-r--r--. 1 root root 227 Apr 3 18:34 if3.sh 
-rw-r--r--. 1 root root 44 Apr 3 16:23 location.sh 
-rw-------. 1 root root 4692 Apr 3 21:11 nohup.out 
-rw-r--r--. 1 root root 20 Apr 3 16:48 paramnum.sh 
-rw-r--r--. 1 root root 56 Apr 3 17:59 while1.sh 
-rw-r--r--. 1 root root 55 Apr 3 18:01 while2.sh 
-rw-r--r--. 1 root root 61 Apr 3 18:03 while3.sh
复制代码

如果想要追加的话需要使用 >>

[root@bigdata01 shell]# ll 1>> a.txt
复制代码

注意,这里的1可以省略,因为默认情况下不写也是1。
标准错误输出的用法和这个一样,标准错误输出需要使用2,使用1是无法把这个错误输出信息重定向到文件中的
下面这个写法是错误的。

[root@bigdata01 shell]# lk 1> b.txt 
-bash: lk: command not found
复制代码

正确的写法是这样的。

[root@bigdata01 shell]# lk 2> b.txt 
[root@bigdata01 shell]# more b.txt 
-bash: lk: command not found
复制代码

ok,这就是标准输出、标准错误输出、和重定向的用法
最后来看一个综合案例
nohup hello.sh >/dev/null 2>&1 &
我们来解释一下

nohup和&:可以让程序一直在后台运行 
/dev/null:是linux中的黑洞,任何数据扔进去都找不到了 
>/dev/null:把标准输出重定向到黑洞中,表示脚本的输出信息不需要存储 
2>&1 :表示是把标准错误输出重定向到标准输出中
复制代码

定时器crontab

查看crontab服务状态: systemctrl status crond
启动/停止crontab服务: systemctrl start/stop crond
添加定时任务: vi /etc/crontab

假设我们有一个需求,每隔1分钟打印一次当前时间,时间格式为年月日 时分秒

[root@bigdata01 shell]# vi showTime.sh 
#!/bin/bash 
showTime=`date "+%Y-%m-%d %H:%M:%S"` 
echo $showTime
复制代码

然后在/etc/crontab文件中配置

每1分钟执行一次,其实是最简单的写法,前面都是*号就行,表示都匹配
最终的效果就是这样的
* * * * * root sh /root/shell/showTime.sh

这样验证脚本可以正常执行以后就可以保存配置文件了,但是还有一个问题
现在这种情况脚本执行之后的结果我们是没有保存的,如果让crontab定时去调度执行,我们压根就看不到执行的结果信息,所以需要把脚本执行的结果重定向到一个文件中,
需要使用追加重定向
* * * * * root sh /root/shell/showTime.sh >> /root/shell/showTime.log
保存配置文件即可,等待执行。
我们来看查看一下结果文件,确认一下是否正常执行,可以使用tail -f 监控一会

[root@bigdata01 shell]# tail -f /root/shell/showTime.log 
2026-04-06 21:14:01 
2026-04-06 21:15:01 
2026-04-06 21:16:01 
......
复制代码

如果我们执行的脚本确实不会产生任何输出信息,那么我们如何确认脚本是否被成功调度了呢?
这个时候可以通过查看crontab的日志来确认
crontab的日志在/var/log/cron文件中,使用tail -f命令实时监控

[root@bigdata01 shell]# tail -f /var/log/cron 
......... 
Apr 6 21:14:01 bigdata01 CROND[1577]: (root) CMD (sh /root/shell/showTime.sh 
Apr 6   21:15:01 bigdata01 CROND[1584]: (root) CMD (sh /root/shell/showTime.sh 
Apr 6 21:16:01 bigdata01 CROND[1591]: (root) CMD (sh /root/shell/showTime.sh 
Apr 6 21:17:01 bigdata01 CROND[1597]: (root) CMD (sh /root/shell/showTime.sh 
Apr 6 21:18:01 bigdata01 CROND[1603]: (root) CMD (sh /root/shell/showTime.sh
复制代码

小技巧:

(crontab -l;echo -e "*/10 * * * * echo 'ok'>/home/admin/crontab.log") |crontab
复制代码

列出所有定时任务并且追加