初识 Shell

序言

  在 Linux 中,我们经常需要输入一些长命令。
  比如,我们现在需要以后台模式指定内存大小运行一个 jar 文件,并且还将运行日志输出到nohup.out文件中,那么需要输入以下命令达到该效果:

1
nohup java -jar -Xms256m -Xmx256m xxx.jar &

  除此之外,我们还想看看输出后的日志信息,那么又需要执行以下命令:

1
tailf nohup.out

  这两行命令能否一起执行呢
  并且,我们知道:代码开发完成后,一般会在测试服务器上进行测试。既然需要测试,那么测试出 Bug 是再正常不过的一件事情了。
  出了 Bug 那就修呗,修好后就需要重新打 jar 包并部署到服务器,那么就又需要重新启动该应用!
  若每次部署都重复输入同样的长命令,无疑会非常耗费时间,那么有没有一种方法来保存该命令以方便后续快速运行呢
  对于以上问题的答案,均可通过 Shell 脚本解决,并且 Shell 比这更强大!

什么是脚本?

  维基百科上对于脚本是这么定义的:

脚本语言(英语:Scripting language)是为了缩短传统的“编写、编译、链接、运行”(edit-compile-link-run)过程而创建的计算机编程语言。早期的脚本语言经常被称为批处理语言或工作控制语言。一个脚本通常是解释运行而非编译。脚本语言通常都有简单、易学、易用的特性,目的就是希望能让程序员快速完成程序的编写工作。而宏语言则可视为脚本语言的分支,两者也有实质上的相同之处。[1]

  简单来讲,脚本可以将简单任务自动化处理,比如说 Java 程序,需要编写、编译、打包、部署、运行,对于后面几个动作经常是重复性的,那么我们就可以编写一个脚本来自动化处理这种过程啦!

小提示:本文讨论的均为 Shell 脚本,即对 Linux 上的一些重复性工作,进行脚本编写,便于后续使用。

基本脚本

  在命令行中,若想要一次运行多个命令,则可以通过;连接多个命令,这就是最基本的脚本啦!
  下面来看个例子:

1
data ; whoami

  运行该脚本,将输出以下结果:

1
2
Wed Jul 22 14:38:58 EDT 2020
admin

脚本文件

  序言中谈到过,对有些命令而言,可能需要频繁使用,这时我们可以将shell命令放到一个文本文件中,以后执行该脚本文件就好了。
  在创建shell脚本文件时,必须在文件的第一行指定要使用的shell,具体格式如下:

1
#!/bin/bash

  之后,在该文件中编写相关命令或判断逻辑即可。

变量使用

  运行shell脚本中的单个命令自然有用,但这有其自身的限制,比如有时需要在shell命令使用其他数据来处理信息。
  如何达到该效果?
  通过变量实现即可。
  变量允许你临时性地将信息存储在shell脚本中, 以便和脚本中的其他命令一起使用。

  具体而言,你可以在脚本的变量名称之前加上$来使用这些变量。

  变量又可以分为:

  • 环境变量:shell维护着一组环境变量,用来记录特定的系统信息
  • 用户变量:shell脚本允许在脚本中定义和使用自己的变量

环境变量

  对于环境变量,可通过set命令进行查看,下表仅列出了一部分:

系统变量 说明
USER 当前用户
UID 当前用户 ID
HOME 当前用户家目录

用户变量(常用)

  对于用户变量,可以是任何由字母、数字或下划线组成的文本字符串,长度不超过 20 个,定义后通过$引用即可。
注意哦用户变量是区分大小写滴,因此变量Var1和变量var1是不同的定义。

基本使用

  下面来看个例子吧:

1
2
3
4
5
#!/bin/bash
student="lovike"
score=100
today="2020-01-01"
echo "Today is $today,$student go to school and his math score is $score"

  运行以上脚本文件,将输出如下结果:

1
Today is 2020-01-01,lovike must go to school and his math score is 100

  另需注意的是:若一个变量引用了另一个变量值,则必须使用美元符,否则shell会将变量名解释成普通的文本字符串。

1
2
3
4
#!/bin/bash
student1="lovike"
student2=$student1 # 必须使用 $ 引用,student2 现在为 "lovike"
student2=student1 # 若不使用 $,则会输出 "student1"

引用命令

  前面仅仅给变量中赋值了数值或字符串,其实还可以将命令赋值给变量
  由于我们经常需要从命令的输出中提取信息,因此这个特性在处理脚本数据时尤为方便。

  shell中存在两种方法可以将命令输出赋给变量:

  • 反引号字符:`
  • 美元加括号格式:$()

  下面为示例:

1
2
3
4
5
6
7
#!/bin/bash
now1=`date`
now2=$(date)

# 注:$now 在""内外均可
echo "The date and time are:" $now1
echo "The date and time are:" $now2

  再举个例子,我们可以将 Java 程序的运行信息输出到指定日期格式的日志中:

1
2
3
4
#!/bin/bash
# copy the /usr/bin directory listing to a log file
today=$(date +%Y-%m-%d)
java -jar test.jar > test-$today.log

  有时候,可能以相同方式启动多个 jar 包,那么可以编写这样的脚本:

1
2
#!/bin/bash
nohup java -Xms128M -Xmx512M -XX:PermSize=128M -jar `ls | grep jar` &

重定向

  对于某个命令的输出信息,有时我们不仅想要让它显示在显示器上,还想要保存这些信息到一个文件中。
  或者说,有时我们还需要将文本的详细信息(如行数、词数、字节数)通过命令进行展示。
  为此,shell提供了几个操作符,可以将命令的输出重定向到另一个位置(比如文件)或者将文件的信息重定向到一个命令中。

文件描述符

  在使用重定向之前,我们需要先了解下文件描述符的概念。
  当执行shell命令时,其实会默认打开 3 个文件,每个文件有对应的文件描述符来方便我们使用:

类型 文件描述符 默认情况 对应文件句柄位置
标准输入(standard input) 0 从键盘获得输入 /proc/slef/fd/0
标准输出(standard output) 1 输出到屏幕(即控制台) /proc/slef/fd/1
错误输出(error output) 2 输出到屏幕(即控制台) /proc/slef/fd/2

  什么是标准输入、标准输出呢?
  当我们在 Linux 终端输入ls时,会显示当前文件的情况:

1
2
[root@lovike /]$ ls
bin boot dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var

  对ls而言,它就是标准输入,而ls展示的内容就是标准输出。
  那么,什么是错误输出?
  当我们在 Linux 终端输入一条错误的命令,比如a

1
2
[root@lovike ~]$ a
zsh: command not found: a

  这条zsh: command not found: a信息就是错误输出。

重定向使用说明

  知道了文件描述符的概念,下面就可以结合它来使用重定向啦!
  平时我们在执行 shell 命令时,都默认是从键盘获得输入,并且将结果输出到控制台上。
  但是现在,我们可以通过更改文件描述符默认的指向,从而实现输入输出的重定向。
  比如我们将 1 指向文件,那么标准的输出就会输出到文件中。

1
ls > ls.log

  比如我们将文件信息输入到wc命令:

1
wc < info.log

  由于wc命令可以对数据中的文本进行计数,会分别计算行数、词数、字节数,现在控制台将输出以下信息:

1
4190   36315  397912

  并且,重定向输入输出可以结合使用:

1
wc < info.log > wc.log

  现在,wc.log将记录下info.log的文本信息(行数、词数、字节数)。

  下表显示了重定向结合文件描述符的示例说明:

命令 说明
command 1> filename 将标准输出写入(覆盖)指定新文件中
command > filename 将标准输出写入(覆盖)指定新文件中(简写)
command 1>> filename 将标准输出写入(追加)指定新文件中
command >> filename 将标准输出写入(追加)指定新文件中(简写)
command 2> filename 将标准错误写入(覆盖)指定新文件中
command 2>> filename 将标准错误写入(追加)指定新文件中
command 0< filename 以指定文件作为标准输入到命令中
command < filename 以指定文件作为标准输入到命令中(简写)
command << delimiter 从标准输入中读入,直到遇到delimiter分隔符

重定向实际运用

  若想要在服务器启动一个 jar 包,那么需要执行以下命令:

1
java -jar test.jar

  这个命令很不错,它可以输出当前程序的运行信息,让我们判断程序在服务器上是否还能正常运行,因此必定可以判断程序存在以下结果:

  • 运行异常,程序无法正常启动
  • 运行正常,程序正常启动

  对于结果运行异常的程序而言,一般是 jar 包中环境配置不正常,那么去更改后再重新运行校验即可。
  对于结果运行正常的程序而言,我们就要不得不考虑一些事情啦!

  • ① 我们是否希望程序在服务器一直运行?
  • ② 我们是否希望程序输出一些日志信息?

  对于问题 ① 而言,答案是肯定的,我们肯定希望程序在服务器一直运行,那么我们可以使用后台模式,在启动命令后加个&即可后台运行程序。
  对于问题 ② ,答案就不太确定了,为什么?因为 Java 程序本身是可以指定日志的输出位置的,而且其可以将不同级别的日志进行分类输出,所以此时可以不需要再次输出日志信息,不然就重复啦!
  当然,如果你使用的是别人的 jar 包,不知道别人设置的日志输出路径(其实一般会设置在执行 jar 包的目录下),或者你懒得去知道日志输出路径,那么你可以使用 Linux 提供的 nohup指令运行 jar 包,其会将程序运行的日志信息输出到当前目录的nohup.out文件中。

  因此,现在我们的命令变成了(以后台模式运行 jar 包且输出日志到nohup.out文件中):

1
nohup java -jar test.jar &

  唧唧歪歪说了这么多,其实我们最终的目的还是为了解重定向的具体使用。

清空指定文件所有信息

  前面我们使用nohup java -jar test.jar &命令启动 Java 应用,那么由于这个应用一直在运行,那么nohup.out文件中的信息会越来越多,占用服务器的磁盘空间,而我们知道服务器的资源是很宝贵的,那么有什么办法清空nohup.out中的信息呢?直接删除吗?
  若在程序运行过程中删除nohup.out,容易引发一些问题(比如 Java 程序的后续日志无法打印),其实还有一种更稳妥的解决办法,那就是使用下面的命令:

1
2
3
cat /dev/null > nohup.out
# 或者
cp /dev/null nohup.out

  以上命令可以直接清空nohup.out文件。

不输出任何信息启动应用

  我们说过,Java 程序本身可以指定日志输出位置,还可以划分日志级别,因此nohup命令此时可以不使用,但如果直接执行java -jar test.jar,程序运行信息还会输出到控制台。
  我们需要这些信息嘛?
  需要的,首次运行这些信息可以帮助我们判断程序运行情况。
  那么,判断程序运行正常之后呢?还需要吗?
  不需要。
  那么,可以丢弃这些信息吗?
  可以,以下命令可以做到:

1
java -jar test.jar > /dev/null 2>&1

  啥子鬼东西?下面便来分析一下下吧。
  > /dev/null命令的作用是将标准输出 1 重定向到/dev/null中。
  在 Linux 中,/dev/null代表空设备文件,所有往这个文件里面写入的内容都会丢失,俗称“黑洞”。
  因此,执行了> /dev/null之后,标准输出将不复存在,没有任何地方能够找到输出的内容。

  而对2>&1命令而言,采用&将两个输出(标准/错误)绑定在一起。
  因此,该命令的作用是把错误输出将和标准输出共用一个文件描述符,说人话就是:将错误输出标准输出输出到同一个地方。

  由于 Linux 在执行 Shell 命令之前,就会确定好所有的输入输出位置,并且从左到右依次执行重定向的命令,因此> /dev/null 2>&1的作用就是让标准输出重定向到/dev/null中(即丢弃标准输出),然后由于错误输出重用了标准输出的描述符,所以错误输出也会被定向到/dev/null中,那么错误输出同样也被丢弃了。

  最后总结下:> /dev/null 2>&1命令不会输出任何信息到控制台,也不会有任何信息输出到文件中 。

退出脚本

结构化命令

  shell会按照命令在脚本中出现的顺序依次进行处理,然而,并非所有程序都如此操作,因为许多程序会要求对shell脚本中的命令施加一些逻辑流程进行控制。
  有一类命令会根据条件使脚本跳过某些命令,此类命令通常称为结构化命令(structured command)。
  结构化命令允许你改变程序执行的顺序
  在shell中,存在以下结构化命令:

  • if-then
  • if-then-else

定时脚本

  通过 crontab 命令,我们可以在固定的间隔时间执行指定的系统指令或 shell script脚本。时间间隔的单位可以是分钟、小时、日、月、周及以上的任意组合。
  crontab 命令非常适合周期性的日志分析或数据备份等工作。
  crontab 命令格式如下

1
2
crontab [-u user] file
crontab [-u user] [ -e | -l | -r ]

  crontab 命令格式参数说明:

  • -u user:用来设定某个用户的 crontab 服务(若不指定用户,则表示编辑当前用户的 crontab 文件)
  • file:file是 命令文件的名字,表示将 file 做为 crontab 的任务列表文件并载入 crontab。若未指定文件,crontab 命令将接受标准输入(键盘)上键入的命令,并将它们载入 crontab
  • -e:编辑某个用户的 crontab 文件内容
  • -l:显示某个用户的 crontab 文件内容
  • -r:从/var/spool/cron目录中删除某个用户的 crontab 文件
  • -i:在删除用户的 crontab 文件时给确认提示

  下面使用 crontab file的方式来设定定时任务:

1
2
vi blogSqlBackup
00 01 * * * mysqldump -uroot blog > /tmp/mysql/blog.sql

  上面我们预设了一个任务,其每天一点可以对blog数据库备份,现在我们需要将它加入到crontab中来,以定期执行:

1
crontab blogSqlBackup

1
2
# 每天中午 12 点来检查是不是当月的最后一天,如果是,cron 将会运行该命令
00 12 * * * if [`date +%d -d tomorrow` = 01 ] ; then ; command

参考

  • [1] 维基百科——脚本语言
  • [2] Richard Blum & Christine Bresnahan. Linux 命令行与 shell 脚本编程大全(第 3 版)[M]. 人民邮电出版社,2016
0%