Shell基础

111 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

什么是shell

1、什么是shell?“shell”的英语意思是:蛋、坚果、某些种子和某些动物的壳。注意:是xxx的壳,那么在Linux中,它就可以理解为是Linux操作系统的“外壳”。它工作于Linux内核与用户之间的解释程序(解释器)。

1642672293201

shell位于内核于用户之间,那么它更形象于是Linux内核和用户之间的“翻译官”。用户(比如:坐在电脑面前的你)想要操控Linux系统的虚拟机或者服务器,就需要用户输入请求(这个请求语言是人类的语言,电脑肯定不会懂)然后经由shell解释器去“翻译”成虚拟机可以理解的程序,最后交由内核去完成发来的请求

1642679571735

1642679619869

现在说Linux的shell也许还有些陌生,其实windows也有shell解释器:

C:\Windows\System32\cmd.exe

在Windows下快捷键:win+R,就是这个东西:

1642671756115

2、shell是一种脚本语言

脚本:本质是一个文件,文件里面存放的是特定格式的命令,在Linux中其实就是一条一条的Linux命令。系统可以使用脚本解释器(比如:shell解释器)进行翻译或解析指令并执行(它不需要编译)

shell命令解析器: 系统提供 shell命令解析器: sh、bash(这个是默认的) 查看自己linux系统的默认解析:

[root@node3 ~]# echo $SHELL
/bin/bash

可以看到shell解释器就是“bash”这个文件所提供的。bash是默认的Linuxshell解释器

登录shell

登录shell是用户登录后的第一个程序

及最常见的“Linux命令行”环境

以交互方式运行,用户每输入一个命令行,立即解释并执行

如下:

Last login: Thu Jan 20 03:29:09 2022 from 192.168.9.1     #这个是用户登录
[root@node3 ~]# ls            #这个就是用户登陆后第一个命令,可以看到输出“ls”命令,shell解释器直接解释并执行,之后返回结果
anaconda-ks.cfg

上面说到是“以交互”运行,其实shell脚本命令的工作方式由两种:

  • 交互式(Interactive):用户每输入一条命令就执行
  • 批处理(Batch):由用户实现编好一个完整的shell脚本,这个shell脚本有许多命令组成,shell会一次行执行脚本中诸多的命令

交互式 vs 非交互式(批处理)

  • 交互式

    人工干预、智能化程度高

    逐条解释执行、效率低

  • 非交互式

    需要提前设计、智能化难度大

    批量执行、效率高

    方便在后台静悄悄地运行

其中批处理(非交互式)就是下面要说的shell脚本

手动选择Shell环境

如果不想使用默认的bash 解释器,我们可以选择使用其它的比如:

常见的Shell:bash、zsh、tsch

如果使用zsh、tcsh需要安装

[root@node3 ~]# yum install -y zsh
[root@node3 ~]# yum install -y tcsh
#查看/etc/shells,会发现多个来zsh、tcsh文件
[root@node3 ~]# cat /etc/shells
/bin/sh
/bin/bash
/sbin/nologin
/usr/bin/sh
/usr/bin/bash
/usr/sbin/nologin
/bin/zsh
/bin/tcsh
/bin/csh

之后想用zsh,tcsh只需要直接输入改命令即可

[root@node3 ~]# zsh
[root@node3]~# tcsh

当我们在新开一个shell环境时,就像当于新建一个shell子环境(父--->子进程)

第一个Shell脚本程序

脚本的创建过程

如何写出自己的第一个Shell脚本?

1、整理任务---自然语言(人类可懂的语言)进行步骤拆分、顺序化整理

2、编写可执行语句---脚本语言:实现各步骤

3、完善脚本---如何使得界面友好,整洁,结构规范,代码的优化

4、赋予脚本可执行权限

5、运行脚本

比如说:现在有个需求:

新建一个名为zheng的本地用户,此用户能正常登录,并且设置其密码为:123456

我先写,先感受一遍里面的内容下面讲到

[root@node3 ~]# pwd
/root
[root@node3 ~]# vi first.sh
#!/bin/bash
#第一步:新建用户
useradd zheng
#设置用户密码
passwd zheng 
[root@node3 ~]# chmod +x first.sh          #赋予其脚本可执行权限
[root@node3 ~]# ./first.sh                 #运行其脚本
Changing password for user zheng.
New password:         #输入密码
BAD PASSWORD: The password is shorter than 8 characters
Retype new password:   #再次输出密码
passwd: all authentication tokens updated successfully.
#查看用户
[root@node3 ~]# cat /etc/passwd |grep zheng
zheng:x:1000:1000::/home/zheng:/bin/bash

可以看到脚本成功运行。

脚本语法初识

以上面的脚本为例

可以看到脚本名字为:first.sh,后缀为.sh,首先脚本名字可以随意定义,建议加上.sh后缀,以表示是个脚本文件

#!/bin/bash                   #声明使用的那种shell环境,此处是bash,它表明此行代码下的所有内容由shell解释器开始解释运行
#第一步:新建用户          #可以看到“#”,“#”代表注释,表明此行没有实际意义,便于读者理解代码
useradd zheng           #脚本内容
#设置用户密码
passwd zheng 

“chmod +x first.sh”,代表赋予此脚本可执行的权限

“./first.sh”,执行脚本

在执行脚本时,有三种执行方式,不同的写法对应的含义也不一样,如下:

三种执行脚本的方式

方法一:指定特定的Shell解释器来解释程序

[root@node3 ~]# bash first.sh      或者
[root@node3 ~]# sh first.sh     

此方法不用执行:“chmod +x first.sh”,也就是不用给刚写好的脚本赋予可执行文件

直接指明使用bash或sh解释器来执解析并执行脚本即可

方法二:

在脚本写好后执行:“chmod +x first.sh”,将脚本作为独立运行的脚本程序

之后:

[root@node3 ~]# ./first.sh 
[root@node3 ~]# /root/first.sh       
[root@node3 ~]# . first.sh

这三种都行,其中前两个是:

以脚本种#!指定的shell解释器进行解析,如果#!指定的不存在,那会使用系统默认的解释器

第三个是:

直接使用默认的解释器,不会按照#!指定的解释器,但是第一行的”#!xxx“,还是需要写的

那所以当我们使用:”. first.sh或bash first.sh “,就会创建个子shell解析脚本

调试shell脚本

在写完脚本后难免会出错,那出错我们就要排错,所以就要调试的去排错

进行脚本调试也简单

第一个就是根据运行脚本时出错的提示进行调试

第二个运行命令时加个参数即可

[root@node3 ~]# bash -x first.sh 
+ useradd zheng          #语句一
useradd: user 'zheng' already exists           #语句一输出
+ passwd zheng           #语句二
Changing password for user zheng.              #语句二输出
New password:   

有的时候在写完脚本,为避免出大错的情况下,我们可以进行预先执行脚本

即使先执行以便脚本呢,但这次不会真正的产生效果,而是为了检查代码是否有错误

也简单,加个”-n“参数即可

[root@node3 ~]# bash -n first.sh

免交互及输出处理

可以看到上面的first.sh脚本,密码任然需要用户手动输入,并且有些输出对我们无关紧要,我们也可以选择不让它显示出来,那该怎么做呢?如下:

免交互处理

在Linux命令行中有很多需要交互的操作,比如:

  • passwd改密码
  • ssh远程登录
  • vi文本编辑
  • 图形化的安装/配置过程...

first.sh脚本为例

first.sh脚本的不太友好的原因:

  • 需要用户交互输入密码
  • 输出不必要的信息
  • 错误日志输出到屏幕上,我想让其输出到某个文件中

以下是改进的脚本文件

[root@node3 ~]# vi first.sh 
#!/bin/bash
#第一步:新建用户
useradd zheng 2> /tmp/err.log         #”2>“将错误输出,写入到/tmp/err.log 文件中
#设置用户密码
echo "123456" | passwd --stdin zheng &>/dev/null         #实现自动化的参数”--stdin“即可,”&>“将标准输出到null设备文件,/dev/null设备文件,是个单向文件,只能在里面写入内容,无法从里面读出内容,俗称”黑洞“

此时查看/tmp/err.log文件的内容

[root@node3 ~]# cat /tmp/err.log 
useradd: user 'zheng' already exists         #可以看到错误输出,输入到这个文件按里去了

这样脚本文件就很友好

命令组合运用

我们在Linux命令行输入命令时,基本上都是输入个命令,然后回车执行,然后再输,在回车执行...

这样如果前一个命令运行时间很长,那我们想要运行下一个命令就需要一直等到上一个命令运行结束,那中间的时间我们需要一直坐在电脑旁看着这样就浪费时间。比如:在源码包编译时,make 和make install两条命令。再者我们想要前一个命令和后一个命令有所关联,比如:只有前面的命令成功我才执行后面的命令,负责不执行...说白了判断和查询一定要敲两次命令吗?不一定,那下面就解决

顺序分隔

使用分号:

  • 命令1 ; 命令2 ; 命令三 ...
  • 依次执行,只有先后,没有逻辑关系
[root@node3 ~]# mkdir bo ; cd bo
[root@node3 bo]# pwd
/root/bo

逻辑”与“分隔

使用&&符号:

  • 命令1 && 命令2 && 命令3 &&
  • 逻辑关系为”而且(and)”,意思就是只有前面的命令执行成功,才会执行后一个命令,比如:命令1执行成功才会执行命令2...
  • 一旦前面的命令执行失败,后面的命令不在执行

效果:

[root@node3 ~]# echo "1" && echo "2"
1
2
#典型应用
[root@node3 ~]# make && make install

逻辑“或”分隔

使用||符号

  • mingling1 || 命令2 || 命令3...
  • 逻辑关系为“或者(or)”,任何一条命令执行成功,就不再执行其它的命令
  • 只有前面的命令执行失败才会执行后面的命令

效果:

[root@node3 ~]# echo "1" || echo "2"
1
[root@node3 ~]# id bo || useradd bo       #当没有bo用户,创建bo用户

来个简单的判断操作:

组合逻辑分隔

  • 命令1 && 命令2 || 命令3
  • 当命令1执行成功,会执行命令2
  • 当命令1执行失败,会执行命令3
[root@node3 ~]# id ff &>/dev/null && echo "yes" || echo "no"      #判断ff用户是否存在
no
[root@node3 ~]# id bo &>/dev/null && echo "yes" || echo "no"      #判断bo用户是否存在
yes
#判断cpu是否支持虚拟化
[root@node3 ~]# grep -q -e 'vmx|svm' /proc/cpuinfo && echo "yes" || echo "no"
no                        #-e用来支持后面'vmx|svm'这样的表达式

管道操作

管道的作用:

将命令的屏幕输出交给另一端的命令来处理

  • 格式:命令1 | 命令2 | 命令3
  • 后续命令要能正确处理传来的文本,否则无意义

1642687862712

案例:计算/etc/目录下有多少个文件

[root@node3 ~]# find /etc/ -type f |wc -l
1676

标准输入输出

在使用计算机的过程中,用户怎样把需要处理的信息输入给操作系统?操作系统又怎样把处理之后的结果输出给用户?这个是实现“人机合一”的关键所在。

我们都知道在Linux中一切皆文件,包括:鼠标,键盘,光盘...

提到人和计算机交互,那就要提到I/O交互设备,在Linux中主要三种交互情况

  • 标准输入:从此设备中接收用户输入的数据
  • 标准输出:通过此设备向用户报告正常的命令输出结果
  • 标准错误输出:通过此设备向用户报告错误的命令输出结果
类型设备文件文件描述号默认设备
标准输入/dev/stdin0键盘
标准输出/dev/stdout1显示器
标准错误输出/dev/stderr2显示器

关于命令的执行结果:

  • 运行成功,其屏幕信息报告给stdout设备
  • 运行失败,其屏幕信息报告给stderr设备
[root@node3 ~]# ll /rootx/ /root/
ls: cannot access /rootx/: No such file or directory        #标准错误输出
/root/:
total 8
-rw-------. 1 root root 1260 Sep 17 11:28 anaconda-ks.cfg    #标准输出
drwxr-xr-x. 2 root root    6 Jan 20 08:30 bo
-rwxr-xr-x. 1 root root  139 Jan 20 08:15 first.sh

重定向操作

在执行Linux命令行的过程中需要输入的信息,会来自用户的键盘操作,而命令执行的结果也会通过显示器输出给用户。但在Shell脚本中这个交互的方式是不方便的,因此我们需要非交互式的输入输出技能操作,这就是重定向。

什么是重定向?

就是重新指定命令执行时I/O设备的方向

不在使用默认的键盘,显示器进行输入输出

改用指定的文本文件,需要输入操作时,用文件来存储内容,需要输出操作时,读取文件即可

1642690239413

重定向的类型

根据I/O方向和类别区分

类型操作符用途
重定向输入<将文本输入来源由键盘改为指定的文件
重定向输出将命令行的正常执行输出保存到文件,而不是直接显示在屏幕上
重定向输出>>与>类似,但操作是追加而不是覆盖
重定向错误输出2>将命令行执行错误的输出保存到文件,而不是直接显示在屏幕上
重定向错误输出2>>与2>类似,但操作是追加而不是覆盖
混合重定向&>相当于>和2>,覆盖到同一个文件

混合重定向案例

[root@node3 ~]# ls /rootx/ /root/ > gg 2> err.log
[root@node3 ~]# cat gg 
/root/:
anaconda-ks.cfg
bo
err.log
first.sh
gg
hh
[root@node3 ~]# cat err.log 
ls: cannot access /rootx/: No such file or directory
[root@node3 ~]# ls /rootx/ /root/ &> gg
[root@node3 ~]# cat gg            #可以看到标准输出和标准错误错误输出在同一个文件
ls: cannot access /rootx/: No such file or directory 
/root/:
anaconda-ks.cfg
bo
err.log
first.sh
gg
hh
​
#上面的"ls /rootx/ /root/ &> gg"写法相当于:"ls /rootx/ /root/ > gg 2>&1"

Shell变量

1、什么是变量?

变量就是以固定名称存放的可能会变化的值

变量的作用:

  • 提高脚本对任务需求、运行环境变化的适应能力
  • 在脚本执行中方便重复使用某个值

2、变量的格式:

  • 变量名=变量值,比如:
X=12    或者    X=13     或者      X=16

定义变量的相关注意事项:

  • 若指定的变量名已存在,相当于为此变量重新赋值
  • 等号两边不要有空格
  • 变量名由字母/数字/下划线组成,区分大小写
  • 变量名不能以数字开头,不要使用关键字和特殊字符

3、查看/引用变量

基本格式:

  • 引用变量值:$变量名
  • 查看变量值:echo 变量名、echo变量名、echo {变量名}
[root@node3 ~]# echo $x
12
[root@node3 ~]# echo $x1        #可以看到这个没有输出,我们就像在x变量后面加个“1”,那个时候shell会当成引用的是x1这个变量名,所以我们在这种情况下,需要加上{}
[root@node3 ~]# echo ${x}1      #变量名易混淆时,以{}为界定
121

4、取消变量

自定义变量的失效:

  • 退出定义变量的shell环境时,变量会自动失效
  • 在shell环境内也可手动取消:unset 变量名......
[root@node3 ~]# x=16
[root@node3 ~]# echo $x
16
[root@node3 ~]# unset x
[root@node3 ~]# echo $x         #输出为空

5、变量的分类

  • 存储类型

    在其它的编程语言中,比如:c、c++、Java,变量的存储类型有:整数型,浮点型,双精度浮点型、字符串...

    Shell不作为高级编程语言,对存储类型的要求比较松散

  • 变量的使用类型

    类型说明
    环境变量变量名通常都大写,由系统维护,用来设置工作环境,其中只有个别变量用户可以直接更改,比如:$SHELL
    位置变量由Bash内置,用来存储在执行脚本时提供的命令行参数
    预定义变量由Bash内置,一类由特殊用途的变量,可以直接调用,但不能直接赋值或修改
    自定义变量由用户自主设置、修改及使用

环境变量:

  • 设置环境变量的配置文件:

    /etc/profile               #这个文件定义的环境变量,针对Linux系统中所有的用户都起作用,当用户第一次登录时,该文件被执行,系统的公共环境变量在此设置,开始自启动的程序,一般也在这里设置
    ~/.bash_profile            #这个文件位于某个用户的家目录下,只针对当前用户的设置,登录时会自动调用,打开任意终端时也会自动调用
    
  • 列出环境变量:env、set命令

    [root@node3 ~]# env          #env命令列出所有的环境变量
    XDG_SESSION_ID=17
    HOSTNAME=node3
    SELINUX_ROLE_REQUESTED=
    TERM=vt100
    SHELL=/bin/bash
    HISTSIZE=1000
    SSH_CLIENT=192.168.9.1 53062 22
    ...
    ​
    [root@node3 ~]# set          #set命令列出所有的变量
    BASH=/bin/bash
    BASHOPTS=checkwinsize:cmdhist:expand_aliases:extquote:force_fignore:histappend:hostcomplete:interactive_comments:login_shell:progcomp:promptvars:sourcepath
    BASH_ALIASES=()
    BASH_ARGC=()
    BASH_ARGV=()
    BASH_CMDS=()
    BASH_LINENO=()
    BASH_SOURCE=()
    ...
    
  • 常见的环境变量

    PWD、PATH、USER、LOGNAME、UID、SHELL、HOME、PS1、PS2

    比如:

    [root@node3 ~]# echo $PS1             
    [\u@\h \W]$                   #列出命令行提示符
    

位置变量:

像上面这样的脚本程序只能执行一些预先定义好的功能,未免太过死板。为了让Shell脚本程序更好地满足用户的一些实时需求,以便灵活完成工作,必须要让脚本程序能够像之前执行命令时那样,接收用户输入的参数。

其实,Linux系统中的Shell脚本语言早就考虑到了这些,已经内设了用于接收参数的变量,变量之间使用空格间隔。例如:

$0对应的是当前Shell脚本程序的名称,

$#对应的是总共有几个参数,

$对应的是所有位置的参数值,

$?对应的是显示上一次命令的执行返回值,

11、2、$3……则分别对应着第N个位置的参数值,

1642734447544

由了概念之后,来实践一下:

[root@node3 ~]# vi aa.sh       #写个Shell脚本
#!/bin/bash
echo $#
echo $0
echo $*
echo $6
[root@node3 ~]# bash aa.sh 1 2 3 4 5 6
6
aa.sh
1 2 3 4 5 6
6

预定义变量:

  • 用来保存脚本程序的执行信息
  • 不能直接为这些变量赋值
变量名含义
$0当前所在的进程或脚本名
$$当前运行进程的PID号
$?命令执行后的返回状态值,0表示正常,1或其它表示异常
$#以加载的位置变量个数
$*所有位置变量的值
[root@node3 ~]# echo $$
33689
[root@node3 ~]# echo $?
0
[root@node3 ~]# echo $#
0
[root@node3 ~]# echo $*
​
[root@node3 ~]# 

变量值及范围控制

在写Shell脚本时经常需要给变量赋值,有的时候会遇见特殊情况,比如:变量值中包含空格,$,等特殊标记,再者需要在变量值中插入个换行...那么如果想要实现这些效果,仅靠前面的:变量名=变量值的写法是不行的。所以还需要学习更加高级的功能

引号在赋值中的应用

引号分为单引号和双引号

单引号'':

  • 所有的字符均视为该字符本身(无特殊含义)
  • 不允许\转义

双引号"":

  • 在双引号被允许$扩展,可调用其它变量的值
  • 出现特殊字符时,可采用\符号转义
  • 当变量不包含空格、制表符、双引号通常被省略

双引号赋值案例:

当变量值中有空格时

[root@node3 ~]# A=1
[root@node3 ~]# B=$A 2          #可以发现,当变量值有空格时,直接赋值给B会报错
-bash: 2: command not found
[root@node3 ~]# B="$A 2"        #此时加上"",就不会报错了
[root@node3 ~]# echo $B         #正确输出
1 2

当变量值中特殊字符时

[root@node3 ~]# A="a\tb\nc"
[root@node3 ~]# echo -e $A          #-e选项解析特殊转义符
a       b
c
[root@node3 ~]# fn="\""           #\可以把特殊字符转为普通字符
[root@node3 ~]# echo $fn
"

单引号赋值:

保留特殊字符

[root@node3 ~]# A=CentOS
[root@node3 ~]# B='$A Server'
[root@node3 ~]# echo $B
$A Server                         #可以看到$A并每有引用变量值,而是转为特殊字符

read读入变量值

在有些情况下,我们也需要使用交互式来获取信息,比如:用户登录需要验证的程序,创建备份文件时,备份文件名称需要用户自定义...那这时就需要个变量,来保存用户输入的信息

直接案例

[root@node3 ~]# read YOUR_NAME 
aa
[root@node3 ~]# echo $YOUR_NAME
aa
#但是上面的那种方法有点空洞,好像少点提示信息,我们可以家伙是那个-p参数来实现
[root@node3 ~]# read -p "bo login:" YOUR_AA          
bo login:aa                              #这样就很人性化
[root@node3 ~]# echo $YOUR_AA
aa
#但是呢,虽然上面的-p参数人性化了,也会发现问题,当输入信息时,它不安全,因为我们为输入的信息是现实在屏幕上的,那怎样才能不让它显示我们输入的信息呢?
#加上个-s参数即可
[root@node3 ~]# read -s -p "Password:" YOUR_PASS
Password:[root@node3 ~]# 
[root@node3 ~]# echo $YOUR_PASS
123

变量的作用范围

在Linux命令行当中,如果用户张三定义个变量,那么李四用户是否能引用呢?如果root用户在登录之后,新建一个变量它新开启的子程序里面是否也能用呢?要解决这几个问题,就需要了解变量的作用范围

变量的作用范围分为两种:

  • 局部变量

    只在定义此变量的当前Shell环境下有效

    自定义变量默认都是局部变量

    那所以张三定义的变量,李四是引用不了的

  • 全局变量

    在当前Shell及所有的子Shell环境下都有效

    子Shell中若赋值同名变量,与父Shell中的变量无关

1642743409750

现在已经知道了全局变量和局部变量了,那么我们如何来控制全局变量和局部变量呢?比如:将局部变量变为全局变量,或者说将全局变量取消到局部变量?

只需要一个命令即可:export

export 局部变量=变量值

直接定义/赋值指定的变量,作为全局变量

对已有的局部变量,只需发布不需要赋值

案例:

[root@node3 ~]# x=123              #用户自定义,属于局部变量
[root@node3 ~]# y=456              #用户自定义,属于局部变量
[root@node3 ~]# export y           #将局部变量变为全局变量
[root@node3 ~]# echo $x $y         #在原来的shell环境引用x,y变量,都能引用
123 456
[root@node3 ~]# bash               #开个子Shell
[root@node3 ~]# echo $x $y         #可以看到引用x,y变量,最终只有y变量出结果
456

既然有指定就有取消,所以取消全局变量的命令:

export -n 全局变量名

取消变量的全局属性

此操作对局部变量无实际意义

[root@node3 ~]# exit           #退回原来的子Shell
exit                           
[root@node3 ~]# export -n y    #取消y的全局属性
[root@node3 ~]# bash           #在进入子Shell
[root@node3 ~]# echo $y        #进行引用,
​
[root@node3 ~]# 

数值运算及处理

基本运算类别

四则运算:

  • 加法:num1 + num2
  • 减法:num1 - num2
  • 乘法:num1 * num2
  • 整除:num1 / num2

取余数运算

  • 求模:num1%num2

计算并获取结果

使用expr命令行工具

  • 格式:expr 整数1 运算符 整数2,运算符和数字之间需要有空格
  • 乘法操作应采用*转义,避免被作为Shell通配符
  • 计算结果直接显示在屏幕上
[root@node3 ~]# x=48 ; y=21
[root@node3 ~]# expr $x + 100 - $y * 2 / 7
142
[root@node3 ~]# expr $x / $y         #除法,取整数
2
[root@node3 ~]# expr $x % $y         #除法,取余
6

可以看到使用expr命令,有点复杂,我们可以使用$[]表达式

  • 格式:$[整数1 运算符 整数2]
  • 乘法操作无需转义,运算符两侧可以无空格,引用变量可省略$符号
  • 计算结果替换吊打是本身,结合echo命令才能显示到屏幕上
[root@node3 ~]# echo $[2**4]           #表示乘方
16
[root@node3 ~]# echo $[x+100-y*2/7]
142
[root@node3 ~]# echo $[x/y]
2
[root@node3 ~]# echo $[x%y]
6

自增表达式