当前位置 博文首页 > RtxTitanV的博客:Shell编程之变量

    RtxTitanV的博客:Shell编程之变量

    作者:[db:作者] 时间:2021-07-07 10:03

    本文主要对Shell中的变量进行简单总结,另外本文所使用的Linux环境为CentOS Linux release 8.1.1911,所使用的Shell为bash 4.4.19(1)-release

    一、变量的分类

    Shell编程中的变量一般分为三类:

    • 局部变量(自定义变量):在脚本或命令中定义,仅在当前shell中有效,其他shell启动的程序不能访问局部变量。
    • 环境变量:所有的程序,包括shell启动的程序,都能访问环境变量(这种一般是系统环境变量),有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。
    • 特殊变量:由Shell程序设置的特殊变量,这些特殊变量只能使用不能对它们赋值。常见的特殊变量有$n$#$*$@$?$$

    补充说明:$n(除了$0)又叫位置参数,$0$#$*$@$?$$这些又叫特殊参数。参数是存储值的实体。它可以是一个名字,一个数字,或者前面的#*@等特殊字符之一。

    二、自定义变量

    1.定义变量

    Shell支持以下三种定义变量的方式:

    # variable为变量名,value为变量值
    variable=value
    variable='value'
    variable="value"
    

    注意:在bash中,变量默认类型都是字符串类型,无法直接进行数值运算。

    变量定义规则:

    • 变量名由数字、字母、下划线组成,但不能以数字开头。
    • 赋值号=两侧不能有空格。
    • 变量值如果包含空白符,必须使用双引号或单引号包围起来。
    • 不能使用Shell里的关键字作为变量名。

    变量定义示例:
    1

    2.使用变量

    使用一个定义过的变量,有以下两种语法:

    $variable
    ${variable}
    

    变量名外的花括号{}是可选的,加花括号是为了帮助解释器识别变量的边界。推荐给所有变量都加上花括号{}。使用变量示例:
    2
    skill变量不加{},解释器就会把$skillScript当成一个变量,可能不是预期结果,加花括号适用于拼接字符串。

    3.修改变量的值

    已定义的变量可以被重新赋值。修改变量值的示例:
    3

    4.只读变量

    使用readonly命令可以将变量定义为只读变量,只读变量的值不能被改变。语法:

    readonly variable
    

    只读变量示例:
    4

    5.删除变量

    使用unset命令可以删除变量。变量被删除后不能再次使用,unset命令不能删除只读变量。语法:

    unset variable
    

    删除变量示例:
    5

    6.单引号与双引号的区别

    有以下示例脚本:

    #!/bin/bash
    
    name="zhang san"
    echo 'hello! ${name}'
    echo "hello! ${name}"
    

    执行结果:
    6
    可知单引号与双引号的区别:

    • 单引号包围变量的值时,引号里面的内容原样输出,即使引号里面有变量和命令也会把它们原样输出。这种方式适合需要显示纯字符串的情况,即不希望解析变量、命令等的场景。
    • 双引号包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式适合字符串中附带有变量和命令并且想将其解析后再输出的场景。

    7.命令替换

    命令替换用命令的输出取代命令本身,Shell中完成命令替换有以下两种方式:

    # commands是要执行的命令,可以只有一个命令,也可以有多个命令,多个命令之间以分号;分隔
    `command`
    $(command)
    

    命令替换支持将Shell命令的输出结果赋值给变量:

    variable=`command`
    variable=$(command)
    

    将命令的输出结果赋值给变量的示例:

    #!/bin/bash
    
    mkdir testdir
    cd testdir
    test_path=`pwd`
    touch test_path.txt
    echo "test路径:${test_path}" >> test_path.txt
    echo $(cat test_path.txt)
    

    执行结果:
    7
    如果命令的输出结果包括多行(有换行符),或者含有多个连续的空白符,那么在命令的结果赋值给变量后输出变量时应该将变量用双引号包围,如果直接输出命令的结果,应该将反引号或$()包围的命令用双引号包围,防止出现格式混乱的情况。示例如下:
    8
    反引号看起来像单引号,容易造成混乱,更推荐使用$()。命令替换可以嵌套,使用反引号形式进行嵌套时,里面的反引号需要用反斜杠转义。以下是$()嵌套的示例,计算ls命令列出的第一个文件的行数:
    9
    注意$()仅在Bash中有效,而反引号可在多种Shell中使用。

    命令替换会创建一个子Shell来执行对应的命令。如果在Shell脚本中使用命令替换,那么运行该脚本的Shell会创建一个子Shell来执行对应的命令。

    三、环境变量

    1.Shell子进程

    用于登录某个虚拟控制器终端或在GUI中运行终端仿真器时所启动的默认的交互Shell可以看成一个Shell父进程。在命令提示符后输入/bin/bash命令或其他等效的bash命令时,会创建一个新的Shell进程,这个shell进程被称为Shell子进程(child shell)。Shell子进程也有命令提示符,同样会等待命令输入。有如下示例:
    10
    第一个Shell程序是Shell父进程,进程ID为5715,进程ID5754的进程是在Shell父进程中执行的第一个ps -f命令,执行bash后创建了一个新的Shell进程,即Shell子进程,进程ID为5755,进程ID为5774的进程是在Shell子进程中执行的第二个ps -f命令。Shell子进程的父进程ID(PPID)为5715,即Shell父进程的进程ID,说明这个Shell父进程就是该Shell子进程的父进程。

    注意:这里的Shell子进程(child shell)与子Shell(subshell)是两个概念,很容易混淆。Shell子进程(child shell)本质上是当前Shell通过执行外部命令启动了新进程,而这个新进程正好是Shell进程罢了,这样的child shell进程是通过执行硬盘上的命令来生成的,只能访问其Shell父进程的环境变量;而真正的子Shell是不需要重新执行硬盘上的外部命令的,全部是内存中的操作,可以访问其父Shell的任何变量。假设当前Shell创建了一个子Shell,子Shell肯定是当前Shell的子进程,当前Shell也被称为该子Shell的父Shell,但当前Shell的子进程不一定都是子Shell,比如当前Shell创建的Shell子进程(child shell)就不是子Shell。

    Shell父进程和Shell子进程的关系如下图:
    11
    在Shell子进程中也可以继续创建Shell子进程。有如下示例:
    12
    根据PPID可以看出Shell进程是层层嵌套。Shell子进程的嵌套关系如下图:
    13
    通过exit命令可以退出Shell子进程,还能用来登出当前的虚拟控制台终端或终端仿真器软件。退出Shell子进程的示例如下:
    14

    2.环境变量介绍

    环境变量对当前Shell进程和所有生成的Shell子进程都可用,而局部变量则只对创建它们的Shell进程可用。局部变量在Shell子进程中不可用。示例如下:
    15
    新开一个Shell窗口,局部变量在新的Shell进程中不可用。示例如下:
    16
    Shell子进程中定义的局部变量,在退出Shell子进程后,该局部变量就不可用。示例如下:
    17

    以上示例说明了局部变量在除定义它的Shell之外的其他Shell中都不可用,只有在定义它的Shell中可用。

    环境变量在设定它的Shell进程及其所有Shell子进程中可用,通过export命令可以将自定义变量导出为环境变量,语法:

    # 可以同时导出多个变量
    export variable1 variable2
    # 可以在定义的同时导出为环境变量
    export variable3="abc"
    

    示例如下:
    18
    反过来在Shell子进程中设定的环境变量在Shell父进程中不可用。示例如下:
    19
    修改Shell子进程中的环境变量并不会影响到Shell父进程中该变量的值。示例如下:
    20
    Shell子进程甚至无法使用export命令改变Shell父进程中环境变量的值。示例如下:
    21
    在Shell子进程中删除一个环境变量只对Shell子进程有效,该环境变量在Shell父进程中依然可用。示例如下:
    22

    以上可总结为环境变量只能向下传递而不能向上传递,在Shell子进程中对环境变量的改动无法反映到Shell父进程中。

    环境变量在设定它的Shell进程及其所有Shell子进程中可用,并没有说它在所有的Shell进程中都可用,也就是说环境变量在与设定它的Shell进程完全没有关联的Shell进程中是不可用的。如果新开了一个Shell窗口,这个新开Shell显然不是当前Shell的Shell子进程,环境变量在这个新的Shell进程中不可用。示例如下:
    23

    3.查看环境变量

    可以使用envprintenv命令查看环境变量,以下是截取的部分环境变量:
    24
    查看个别环境变量的值可以使用printenv命令(变量前不加$),也可以使用echo(变量前加$),不要用env命令。示例如下:
    25
    通过set命令可以查看所有环境变量、自定义变量和函数。

    4.常用系统环境变量

    这里只列出一部分常用系统环境变量:

    变量描述
    BASH当前Bash实例的全路径
    BASHPID当前Bash进程的PID
    HOME当前用户的主目录
    HOSTNAME主机名
    HOSTTYPE主机的架构,如x86_64
    HISTFILE保存Shell历史记录列表的文件名
    HISTSIZE历史文件中的命令数
    LANGShell的语言环境类别
    OLDPWD之前的路径
    PATHShell查找命令的目录列表,由冒号分隔
    PWD当前的路径
    PS1Shell命令行界面的主提示符
    PS2Shell命令行界面的次提示符
    SHELLShell的全路径名
    SHLVLBash进程的嵌套层次,每启动一个新的Bash时该变量都加1

    系统环境变量基本上都是使用全大写字母,以区别于普通用户的环境变量。自己创建的局部变量一般使用小写字母。变量名区分大小写,自定义变量时使用小写字母,能够避免重新定义系统环境变量可能带来的灾难。

    5.Shell的启动方式

    Shell的启动方式有交互式、非交互式和登录、非登录两类:

    • 交互式Shell与非交互式Shell:
      • 交互式Shell:Shell的一种运行模式,Shell与用户进行交互,用户输入命令,Shell立即执行并反馈结果。
      • 非交互式Shell:Shell的另一种运行模式,Shell不与用户进行交互,运行脚本文件让所有命令批量化、一次性地执行。
    • 登录Shell与非登录Shell:
      • 登录式Shell:需要用户名、密码登录后才能进入的Shell或通过带有-l|--login参数的bash命令启动的Shell。例如系统启动,远程登录,su -切换用户,bash --login命令启动bash。
      • 非登陆式Shell:不需要输入用户名和密码即可打开的Shell。例如图形化界面终端启动,su切换用户,bash命令启动bash。

    判断Shell是否是交互式Shell

    方法1,查看变量-的值值,如果值中包含了字母i,则表示交互式。在控制台输出-的值:
    26
    包含i,为交互式。在以下脚本中输出-的值:
    27
    不包含i,执行脚本的Shell为非交互式。

    方法2,查看变量PS1的值,如果非空,则为交互式,否则为非交互式。在控制台输出PS1的值:
    28
    不为空,为交互式。在以下脚本中输出PS1的值:
    29
    为空,执行脚本的Shell为非交互式。

    判断Shell是否是登录Shell

    方法1,执行shopt login_shell,值为on为登录Shell,off为非登录Shell。登录控制台后查看,为on,是登录Shell,bash命令启动一个新的Shell后再查看,值为off,这个新启动的Shell为非登录Shell:
    30
    方法2,通过exit退出Shell,输出logout,表示之前退出的为登录Shell,输出exit,表示之前退出的为非登录Shell。先在控制台执行bash -l启动一个Shell,退出后输出logout,之前启动的为登录Shell,再执行bash启动一个Shell,退出后输出exit,之前启动的为非登录Shell:
    31

    将命令组合起来,使用echo $PS1; shopt login_shellecho $-; shopt login_shell同时判断是否交互和登录。
    使用由()包围的组命令或者命令替换进入子Shell时,子Shell会继承父Shell的交互和登录属性。
    ssh执行远程命令,但不登录时为非交互式非登录Shell。
    执行Shell脚本时(在新启动的Shell进程中执行),为非交互式非登录Shell,指定了--login时,为非交互式登录Shell。

    6.Shell配置文件的加载

    Bash Shell启动时相关的配置文件有:

    • /etc/profile
    • /etc/bashrc
    • /etc/profile.d/*.sh
    • ~/.bash_profile
    • ~/.bash_login
    • ~/.profile
    • ~/.bashrc

    Bash Shell启动时配置文件怎么加载取决于Bash Shell的启动方式。

    登录Shell加载配置文件

    登录Shell启动时配置文件的加载流程如下图:
    32

    1. 先读取执行/etc/profile文件,/etc/profiles文件中有加载/etc/profile.d/*.sh的语句,会加载/etc/profile.d/下所有可执行的sh后缀的脚本文件。
    2. 然后按下面书写的顺序寻找~/.bash_profile > ~/.bash_login > ~/.profile这三个文件,仅加载第一个找到的文件,然后该文件会加载~/.bashrc~/.bashrc中又有加载/etc/bashrc的命令,最后加载/etc/bashrc

    不同的Linux发行版附带的个人配置文件不同,有的可能只有~/.bash_profile~/.bash_login~/.profile这三个中的一个,有的可能三者都有。

    交互式非登录Shell加载配置文件

    交互式非登录Shell启动时配置文件的加载流程如下图:
    33
    不读取/etc/profile~/.bash_profile~/.bash_login~/.profile,读取~/.bashrc~/.bashrc文件还会加载 /etc/bashrc,最后再加载/etc/profile.d/*.sh

    非交互式非登录Shell加载配置文件

    执行Shell脚本时一般用的就是这种Shell。Shell启动一个非交互式非登录Shell进程时,会检查环境变量BASH_ENV来查看要执行的启动文件。如果有指定的文件,Shell会加载该文件,如果没有设置BASH_ENV,Shell脚本是通过启动一个Shell子进程来执行的,Shell子进程可以继承Shell父进程中的环境变量。对于在当前Shell进程中执行的Shell脚本,因为并没有启动一个新的Shell进程,所以执行脚本时不会加载配置文件,可以直接使用当前Shell中的变量。

    7.环境变量持久化

    通过export命令导出的环境变量只对当前Shell进程以及所有的子进程可用,如果最顶层的父进程被关闭了,那么环境变量也就失效了。只有将环境变量写入Shell配置文件中才能使该环境变量在所有Shell进程中都可用并且永久性存在,因为每次启动Shell进程都会定义这个变量。

    对于普通用户,可以将环境变量写入~/.bashrc文件,因为除了非交互式非登陆Shell设置了BASH_ENV并且没有指向~/.bashrc时,都会加载该文件。也可以将环境变量写入在/etc/profile.d目录中创建的一个以sh为后缀的文件里。不建议将新的环境变量写入/etc/profile,因为升级Linux发行版会使该文件更新,那自定义的环境变量也就没有了。这里以~/.bashrc为例,执行以下命令持久化保存一个环境变量:

    echo "export VAR1=env1" >> ~/.bashrc
    # 重载配置文件
    source ~/.bashrc
    

    在将环境变量写入~/.bashrc文件持久化保存后,该环境变量在所有Shell进程中都可用,如下图所示:
    34

    四、特殊变量

    1.$n

    $0代表shell或shell脚本的名称,通常为shell脚本文件名,n≥1时,$n代表传递给脚本或函数的参数。n是几,表示第几个参数。注意n≥10时,需要写成${n},例如${10}如果写成$10,则效果会变成$1的值拼接一个0。下面创建一个名为test1.sh的示例Shell脚本,代码如下:

    #!/bin/bash
    
    echo "执行的脚本文件名:$0"
    echo "传给脚本的第一个参数:$1"
    echo "传给脚本的第二个参数:$2"
    echo "传给脚本的第六个参数:$6"
    echo "传给脚本的第十个参数(不带{}):$10"
    echo "传给脚本的第十个参数(带{}):${10}"
    
    # 定义函数
    function func1() {
        echo "传给函数的第一个参数:$1"
        echo "传给函数的第二个参数:$2"
        echo "传给函数的第三个参数:$3"
    }
    
    # 调用函数
    func1 a b c
    

    执行脚本并附带参数:

    bash test1.sh aaa bbb ccc ddd eee fff ggg hhh iii jjj
    

    执行结果:
    35

    2.$#

    $#用于获取传递给脚本或函数的参数个数。下面创建一个名为test2.sh的示例Shell脚本,代码如下:

    #!/bin/bash
    
    echo "执行的脚本文件名:$0"
    echo "传给脚本的第一个参数:$1"
    echo "传给脚本的第二个参数:$2"
    echo "传给脚本的第三个参数:$3"
    echo "传给脚本的参数个数:$#"
    
    # 定义函数
    function func1() {
        echo "传给函数的第一个参数:$1"
        echo "传给函数的第二个参数:$2"
        echo "传给函数的参数个数:$#"
    }
    
    # 调用函数
    func1 a b c
    

    执行脚本并附带参数:

    bash test2.sh aaa bbb ccc ddd
    

    执行结果:
    36

    3.$*、$@

    $*$@都表示传递给函数或脚本的所有参数,当$*$@不被双引号""包围时,都是将接收到的每个参数看做一份数据,一般以空格分隔(以"$1" "$2""$n"的形式输出所有参数),当$*$@被双引号""包围时,"$*"会将所有的参数从整体上看做一份数据(以"$1 $2 … $n"的形式输出所有参数),而不是把每个参数都看做一份数据,"$@"仍然将每个参数都看作一份数据(以"$1" "$2""$n"的形式输出所有参数)。下面创建一个名为test3.sh的示例Shell脚本,代码如下:

    #!/bin/bash
    
    echo "执行的脚本文件名:$0"
    echo "传给脚本的所有参数(\$*):"$*
    echo "传给脚本的所有参数(\$@):"$@
    echo "传给脚本的所有参数(\"\$*\"):""$*"
    echo "传给脚本的所有参数(\"\$@\"):""$@"
    
    echo "从\$*打印传给脚本的每个参数"
    for var in $*
    do
        echo "${var}"
    done
    	
    echo "从\$@打印传给脚本的每个参数"
    for var in $@
    do
        echo "${var}"
    done
    
    echo "从\"\$*\"打印传给脚本的每个参数"
    for var in "$*"
    do
        echo "${var}"
    done
    
    echo "从\"\$@\"打印传给脚本的每个参数"
    for var in "$@"
    do
        echo "${var}"
    done
    
    # 定义函数
    function func1() {
        echo "传给函数的所有参数(\$*):"
    
    下一篇:没有了