shell脚本初探——概念篇

时间:2021-05-09 08:47:44

ForeWord

本文介绍了shell脚本的一些基础知识。

Key Point:

  • Shell概念及发展历史
  • shell执行脚本
  • shell变量
  • Shell特殊字符使用(` $() [] [[]]等)

         tips:全文阅读需8min
    

Part1:Concept&History

1. Concept

1.Shell概念

我们知道,通常计算机程序要经过编译和链接成为计算机可解读的格式,然后才能运行。如:C/C++,java,它们叫做编程语言

但还有一种未经编译就可运行的程序,通常称之为脚本程序(script)。如:shell/Python/Javascript

 所以, shell是一门弱类型解释型语言.

这里的弱类型是指没有类型划分,如:int double char 等。
解释型是不需要编译,只需要解释器解释执行。

2.shell的两种执行模式

shell有两种执行模式:交互式&批量式

交互式

Shell的作⽤是解释执⾏⽤户的命令。

比如⽤户在命令行输⼊⼀条命令,Shell就解释执⾏⼀条,这种⽅式称为交互式(Interactive),这种模式我们无时不刻在使用。

批量式

Shell还有⼀种执⾏命令的⽅式称为批量式(Batch)。

⽤户事先写⼀ 个Shell脚本(Script)其中有很多条命令,让Shell⼀次把这些命令执⾏完,⽽不必⼀条⼀条地敲命令。

    Shell脚本和编程语⾔很相似,也有变量和流程控制语句。但Shell脚本是解释执⾏的,不需要编译。Shell程序从脚本中⼀⾏⼀⾏读取并执⾏这些命令,相当于⼀个⽤户把脚本中的命令⼀⾏⼀ ⾏敲到Shell提⽰符下执⾏。

2. History

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

⽂件/etc/shells给出了系统中所有已知(不⼀定已安装)的Shell:
/bin/sh
/usr/bin/es
/usr/bin/ksh
/bin/ksh
/usr/bin/rc
/usr/bin/tcsh
/bin/tcsh
/usr/bin/esh
/bin/dash
/bin/bash
/bin/rbash
/usr/bin/screen

虽然有这么多种shell,但当前使用最广泛的当属安装在/bin目录下的bash。当然,你可以用以下命令查看和更改系统默认的shell版本:

shell脚本初探——概念篇

想要深入了解shell历史的小伙伴可以点击这里


Part2:Shell执行脚本

1. shell脚本

1.编辑

可以把这个过程当做编程语言中的写代码过程。

先来看一个shell脚本实例,vim test.sh,此时test.sh就是一个shell的执行脚本:

shell脚本初探——概念篇

1.由于shell有多种版本,且其语法会有差异。所以严谨的sh文件会在首以“#!/bin/bash”等语句指明shell脚本运行时所依赖的解释器版本。
2.Shell脚本中⽤#表⽰注释,相当于C语⾔的//注释。但如果#位于第⼀⾏开头则例外,因为它表⽰该脚本使⽤后⾯指定的解释器/bin/sh解释执⾏。

2.执行

由于shell是一门非编译型语言,所以编辑完内容后,就可以直接由解释器执行了。shell脚本有这么几种执行方式:

NO.1: chmod u+x test.sh

先把这个脚本⽂件加上可执⾏权限然后Shell会fork⼀个⼦进程并调⽤exec执⾏./test.sh这个程序:

shell脚本初探——概念篇

通常情况下,exec系统调⽤把⼦进程的代码段替换成./test.sh程序的代码段,并从它的_start开始执⾏。 然⽽test.sh是个⽂本⽂件,根本没有代码段和_start函数,怎么办呢?

     其实exec还有另外⼀种机制,如果要执⾏的是⼀个⽂本⽂件,并且第⼀⾏指定了解释器(/bin/bash),则⽤解释器程序的代码段替换当前进程,并且从解释器的_start开始执⾏。⽽这个⽂本⽂件被当作命令⾏参数传给解释器。因此,执⾏上述脚本相当于执⾏程序 。

NO.2:/bin/bash test.sh:解释器(命令)+参数(脚本文件)

这种方式通过直接指定解释器来执行脚本文件:

shell脚本初探——概念篇

还可以这样指明解释器:

shell脚本初探——概念篇

shell脚本初探——概念篇

     其实脚本语言的执行都是由解释器完成的,先加载解释器,再由解释器执行。有了这个概念,我们也可以运行Python、PHP等执行脚本,只要你有它的解释器。

2. shell执行过程

我们来深入理解一下shell脚本的执行过程:

shell脚本初探——概念篇

  1. 首先由命令行的交互Shell(bash)fork/exec⼀个⼦Shell(sh)⽤于执⾏脚本,⽗进程bash等待⼦进程sh终⽌。
  2. sh读取脚本中的cd ..命令,调⽤相应的函数执⾏内建命令,改变当前⼯作⽬录为上⼀级
    ⽬录。
  3. sh读取脚本中的ls命令,fork/exec这个程序,列出当前⼯作⽬录下的⽂件,sh等待ls终⽌。
  4. ls终⽌后,sh继续执⾏,读到脚本⽂件末尾,sh终⽌。
  5. sh终⽌后,bash继续执⾏,打印提⽰符等待⽤户输⼊。

Expand:内建命令

内建命令:一旦执行当前进程不会创建子进程,只会影响父进程(交互bash),本质:shell解释器的一个函数

先看一个例子:

shell脚本初探——概念篇

在上例中,如果将命令⾏下输⼊的命令⽤ () 括号括起来,那么命令行的交互bash就会fork出⼀个⼦Shell执⾏⼩括号中的命令。此时改变的是⼦Shell的PWD, ⽽不会影响到交互式Shell。

然⽽去掉则有不同的效果,cd ..命令是直接在交互式Shell下执⾏的,改变交互式Shell的PWD。

同样,如果用下面的⽅式执⾏Shell脚本:

shell脚本初探——概念篇

这种⽅式也不会创建⼦Shell,⽽是直接在交互式Shell下逐行执⾏脚本中的命令。

所以,source或者 . 命令是Shell的内建命令。

linux中像这样的命令有很多,比如pwd,cd ..等。有兴趣的小伙伴可以自己搜索学习。


Part3:Shell变量

Shell变量由字母加下划线组成,在变量名前加$符号,就可以取到它的值。

和C语⾔不同的是:

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

shell脚本初探——概念篇

注意,给变量赋值时=两边不能加空格,因为在shell中,只有命令才能带空格,有的小伙伴C/C++写多了就容易习惯性的加空格。这就坑了。

有两种类型的Shell变量:

环境变量

    环境变量可以从⽗进程传给⼦进程,因此Shell进程的环境变量 可以从当前Shell进程传给fork出来的⼦进程。⽤printenv命令可以显⽰当前Shell进程的环境变量。

本地变量

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

环境变量和普通(本地)变量之间的区别

    环境变量具有全局特性,可以被任何bash的子进程继承,普通变量只能在当前的bash有效

此外,变量还可以拼接打印:

shell脚本初探——概念篇


Part4:Shell特殊字符

1. 文件名代换:? * []

⽂件名代换 (Globbing )就是利用一些通配符(Wildcard)来匹配文件名中的字符,有以下3种:

通配符 含义
* 匹配0个或多个任意字符
? 匹配⼀个任意字符
[] 匹配⽅括号中任意⼀个字符的⼀次出现

具体用法见下例:

先touch100个文件用于测试:

shell脚本初探——概念篇

通配符?:

shell脚本初探——概念篇

通配符*:
shell脚本初探——概念篇

通配符[]:
shell脚本初探——概念篇

混合使用:

shell脚本初探——概念篇

最后删掉它们:

shell脚本初探——概念篇

2. 转义字符 \

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

 比如创建⼀个具有特殊⽂件名的⽂件:

shell脚本初探——概念篇

还有一个字符虽然不具有特殊含义,但是要⽤它做⽂件名也很⿇烦,就是 - 号。

如果要创建⼀个文件名以-号开头的⽂件,这样是不⾏的:

shell脚本初探——概念篇

   因为各种linux命令都把 **-** 号开头的命令⾏参数当作命令的选项,⽽不会当作⽂件名。

如果⾮要处理以-号开头的⽂件名,可以有两种办法:

shell脚本初探——概念篇

shell脚本初探——概念篇

\ 还有⼀种⽤法,在\后敲回车表⽰续⾏,Shell并不会⽴刻执⾏命令,⽽是把光标移到下⼀⾏,给出一个续⾏提⽰符>,等待⽤户继续输⼊,最后把所有的续⾏接到⼀起当作⼀个命令执⾏。例如:

shell脚本初探——概念篇

3. 命令代换:“ $()

用于命令代换的$()符号本身也是一条命令,Shell先执⾏该命令,然后将输出结果⽴刻代换到当前命令⾏中。

注意:这里的反引号和单引号是不同的:

shell脚本初探——概念篇

反引号在键盘左上角ESC键的正下方,单引号则在enter键的左边。下面是使用实例:

shell脚本初探——概念篇

我们可以看到,这两者都能达到相同的效果,此外,$()还能进行算术运算,但只能做一些+-*/的整数运算。

俗话说:“一山不容二虎”,他们必然是有区别的。区别就在这里:

shell脚本初探——概念篇

可以看到,$符号本来是取变量的值,加上转义字符\后应该失去效用才对,但为什么反引号还是输出了变量的值呢?

这是因为反引号和$()对转义字符的使用方式不同导致的。

    反引号齐本身就对/进行了转义,保留了其本身意思,如果我们想在反引号中起到/的特殊意义,我们必须使用2个/来进行表示。所以我们可以简单的想象成反引号中: // = /

shell脚本初探——概念篇

这样,就不会出错啦。

所以,虽然可以用这两种方式进行命令代换,但还是建议使用$( ),理由如下:

1、反引号很容易与单引号搞混乱,尤其对初学者来说。
2、在转义字符\的使用上,它比较直观。
3、它的弊端是,并不是所有的Linux系统都支持这种方式,但反引号是肯定支持的。
4、大神们都不用反引号。

Expand:eval命令

说道命令代换,就不得不提eval命令了,因为人家可是行业大神。它可以用于复杂变量的命令替换。具体看下例:

shell脚本初探——概念篇

可以看到,与echo相比,eval对变量txt进行了二次替换,从而打印出了文件内容。

shell脚本初探——概念篇

上例更加说明了eval的功能,先用转义字符\把设为无效,再与$#的值4拼接为参数4,最后进行二次替换打印出参数的值。

eval既然可以对复杂变量进行操作,那么简单变量也自然不在话下:
shell脚本初探——概念篇

本段参考:Linux:shell脚本之命令替换(eval,反引号和$())

4. 单引号 ”,双引号” “

和C语⾔不⼀样,Shell脚本中的单引号和双引号⼀样都是字符串的界定符,⽽不是字符的界定符。

单引号‘’ 双引号”“都可以用来表示字符串,但单引号对其内部的所有内容不做解释,直接作为字符串。而双引号对内部命令做出替换和响应。

shell脚本初探——概念篇


The End

到这里,相信大家对shell脚本已经有了初步了解了,当然,shell脚本的知识还有很多。比如它的语法,也是比较容易上手的。

点击这里,进一步学习shell的基础语法。