Linux之Shell编程从初学到精通

Linux之Shell编程

什么是shell

我们常用的linux命令行其实就是shell
我们可以通过shell来让linux系统完成我们的指令

那shell编程又是什么意思呢?
shell编程其实就是把之前在shell中执行的单个命令按照一定的逻辑和规则,组装到一个文件中,后面执行的时候就可以直接执行这个文件了,这个文件我们称之为shell脚本。

我的第一个shell脚本

shell脚本的后缀倒没有那么严格的要求,只是建议大家以.sh结尾,这算是一个约定,大家都遵守这个约定,后期只要看到.sh结尾的文件就知道这个是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到这个文件所在的目录里面了,按理说是可以找到,那为什么会提示找不到呢?

注意了,在这我们就详细分析一下,避免大家在使用的时候一知半解,迷迷糊糊
主要原因是这样的,因为在这里我们直接指定的文件名称,前面没有带任何路径信息,那么按照linux的查找规则,它会到PATH这个环境变量中指定的路径里面查找,这个时候PATH环境变量中都有哪些路径呢,我们来看一下

[root@bigdata01 shell]# echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

所以说到这些路径里面去找肯定是找不到的,那怎么办呢?如果大家在windows中配置过JAVA的PATH环境变量的话就比较容易理解了,在这里我们只需要在PATH中加一个.即可,.表示当前目录,这样在执行的时候会自动到当前目录查找。

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

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

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

[root@bigdata01 shell]# source /etc/profile

此时再查看PATH变量的值,会发现里面包含了.

[root@bigdata01 shell]# echo $PATH         
.:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

再重新执行 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中的变量不需要声明,初始化也不需要指定类型,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中的变量可以分为四种,

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

这些变量都有什么区别呢?

本地变量

首先来看本地变量
本地变量的格式是VAR_NAME=VALUE,其实就是我们刚才在shell中那样直接定义的变量,这种变量一般用于在shell脚本中定义一些临时变量,只对当前shell进程有效,关闭shell进程之后就消失了,对当前shell进程的子进程和其它shell进程无效,
注意了,我们在这开启一个shell的命令行窗口其实就是开启了一个shell进程。

环境变量

接下来看一下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

添加好了以后执行echo命令获取age的值

[root@bigdata01 shell]# echo $age
18

结果发现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就不需要再执行source命令了。

用完以后我们就把这个age变量去掉,这个是没有意义的,仅供测试时使用
后期我们在工作中会在这个配置文件中添加JAVA以及其它大数据框架的环境变量

位置变量

接下来看一下位置变量
在进行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

这里的127状态码表示是没有找到命令。
具体的状态码信息也可以在网上查到,搜索【linux $? 状态码】

状态码	描述
0	命令成功结束
1	通用未知错误  
2	误用Shell命令
126	命令不可执行
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

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

[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中的循环和判断

前面我们掌握了shell脚本中单独命令的操作,下面我们就希望能在shell脚本中增加一些逻辑判断,这样就可以解决一些复杂的工作需求了
在这里我们主要学习for循环、while循环和if判断

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
3
4
5
6
7
8
9

这一种格式适合用在迭代多次,步长一致的情况

接下来看第二种格式,这种格式针对没有规律的列表,或者是有限的几种情况进行迭代是比较方便的

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循环主要适用于循环次数未知,或不便于使用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,shell脚本才真正有了灵魂

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

针对这个脚本,按照正常的执行逻辑是这样的,如果传递的参数不匹配,则没有任何输出

[root@bigdata01 shell]# sh if1.sh 1
one
[root@bigdata01 shell]# sh if1.sh 2

这样也不太友好,能不能在执行错误的数据时提示用户呢?

当然可以,这样就需要使用到if的双分支了
格式如下:

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

复制一个脚本,进行修改

[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 ]
then
  echo "not found param"
  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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值