# 变量

# 赋值

  • 在 shell 中可以执行以下格式的命令,给变量赋值:
    var=value [var=value]...
    
    • 赋值时,如果变量不存在,则会自动创建它。
    • 创建变量时不需要声明其数据类型。
    • 赋值符号 = 的前后不能有空格,否则会被视作一条命令。如下:
      [root@CentOS ~]# A=1
      [root@CentOS ~]# A =1
      -bash: A: command not found
      [root@CentOS ~]# A= 1
      -bash: 1: command not found
      

# 取值

  • 读取变量时,变量名区分大小写。

    [root@CentOS ~]# a=1
    [root@CentOS ~]# echo $a
    1
    [root@CentOS ~]# echo $A    # 变量名不同,因此读取不到值
    
    
  • $var${var} 的格式可以读取一个变量的值。

    • 例:
      [root@CentOS ~]# a=1
      [root@CentOS ~]# echo $a        # 显示变量 a 的值
      1
      [root@CentOS ~]# echo a         # 不加取值符号 $ 则是显示字符串 a
      a
      [root@CentOS ~]# $a             # 没有 echo 则是将变量 a 的值当作一条命令去执行
      -bash: 1: command not found
      
    • 如果目标变量不存在,则读取到的值为空:
      [root@CentOS ~]# echo $a        # 这里相当于执行 echo
      
      [root@CentOS ~]# echo $a+1      # 这里相当于执行 echo +1
      +1
      
    • 比起 $var ,更推荐使用 ${var} 格式,可实现更多功能:
      [root@CentOS ~]# a=0
      [root@CentOS ~]# b=1
      [root@CentOS ~]# echo $a$b
      12
      [root@CentOS ~]# echo ${a}b       # 用花括号有利于确定变量名的边界
      1b
      
    • $var 支持在读取变量时设置默认值:
      [root@CentOS ~]# echo ${a-0}      # 如果变量存在(包括为空),则返回其值,否则返回默认值
      1
      [root@CentOS ~]# echo ${c-0}
      0
      [root@CentOS ~]# echo ${c-b}      # 默认值应该为一个常量,不会当作变量名进行取值
      b
      [root@CentOS ~]# echo ${a:-Null}  # 如果变量存在且不为空,则返回其值,否则返回默认值
      Null
      
      [root@CentOS ~]# echo ${a+True}   # 如果变量存在,则返回默认值,否则返回空
      True
      [root@CentOS ~]# echo ${a:+True}  # 如果变量存在且不为空,则返回默认值,否则返回空
      True
      
      [root@CentOS ~]# echo ${c=3}      # 如果变量存在,则返回其值。否则返回默认值,并给该变量赋值
      3
      [root@CentOS ~]# echo $c          # 该变量已被创建
      3
      [root@CentOS ~]# echo ${c:=3}     # 如果变量不存在或为空,则返回默认值,并给该变量赋值
      3
      [root@CentOS ~]# : ${PATH:=/bin}  # 在脚本中可用这种方式创建变量并采用默认值
      
      [root@CentOS ~]# a=PWD
      [root@CentOS ~]# echo ${!a}       # ${!var} 语法表示先获取变量 var 的值,再用该值作为变量名,获取另一个变量的值
      /root
      
    • Bash v4.0 开始,可通过以下语法,在读取到变量值之后,转换大小写字母:
      [root@CentOS ~]# a=hello
      [root@CentOS ~]# echo ${a^}   # 将第一个字母变为大写。后面所有字母不变,可能是大写或小写
      Hello
      [root@CentOS ~]# echo ${a^^}  # 将所有字母变为大写
      HELLO
      [root@CentOS ~]# echo ${a,,}  # 将所有字母变为小写
      hello
      [root@CentOS ~]# echo ${a~~}  # 反转所有字母的大小写
      HELLO
      
      可执行 bash --version 查看本机的 bash 版本。
  • `command`$(command) 的格式可以获取一条命令的 stdout 。

    • 例:
      [root@CentOS ~]# a=`echo 1`
      [root@CentOS ~]# echo $a
      1
      [root@CentOS ~]# a=`echo1`      # 这里获取到的 stdout 为空
      -bash: echo1: command not found
      [root@CentOS ~]# echo $a
      
      
    • 使用 $(command) 时,( ) 有利于确定命令的边界边界,从而支持嵌套使用。如下:
      [root@CentOS ~]# echo `echo `echo 1``    # 相邻的两个 ` 相互匹配,不支持嵌套
      echo 1
      [root@CentOS ~]# echo $(echo $(echo 1))
      1
      
  • $((expression))$[expression] 的格式可以获取一个表达式的运算结果。

    • 该表达式是 C 语言风格的,可以使用变量、常量,可以使用算术运算符 + - * / % 。如下:
      [root@CentOS ~]# echo $[a+2]
      3
      [root@CentOS ~]# echo $[ a + 2 ]  # 运算符之间可以插入空格
      3
      [root@CentOS ~]# echo $[(a+2)*3]
      9
      
      [root@CentOS ~]# echo $[0%2]      # 取模运算
      0
      [root@CentOS ~]# echo $[1%2]
      1
      
      [root@CentOS ~]# echo $[3+4 == 5] # 比较运算
      0
      [root@CentOS ~]# echo $[3+4 > 5]
      1
      
    • 该表达式中不支持使用小数:
      [root@CentOS ~]# echo $[1.0]
      -bash: 1.0: syntax error: invalid arithmetic operator (error token is ".0")
      
    • 如果计算结果为小数,则小数部分会直接舍去,不会四舍五入:
      [root@CentOS ~]# echo $[0/3]
      0
      [root@CentOS ~]# echo $[1/3]
      0
      [root@CentOS ~]# echo $[3/3]
      1
      
    • 还可以使用以下位运算符:
      [root@CentOS ~]# echo $[8 & 1]    # 按位与
      0
      [root@CentOS ~]# echo $[8 | 1]    # 按位或
      9
      [root@CentOS ~]# echo $[8 ^ 1]    # 按位异或
      9
      [root@CentOS ~]# echo $[~8]       # 按位非
      -9
      [root@CentOS ~]# echo $[8 << 1]   # 左移
      16
      [root@CentOS ~]# echo $[8 >> 1]   # 右移
      4
      

# 变量的作用域

# 全局变量

:global variable ,是直接通过赋值符号 = 创建的变量,只能在当前 shell 中访问。

  • 可以在赋值变量的同时执行一条命令,此时该变量的作用域仅限于该命令。
    [root@CentOS ~]# A=1 B=1 env | grep -e A= -e B=
    A=1
    B=1
    [root@CentOS ~]# echo $A $B                   # 临时赋值的变量并不会保留
    
    
    [root@CentOS ~]# export A=1 env | grep A=     # export 等命令不支持该语法
    [root@CentOS ~]# echo $A
    
    
  • 可以从文件中导入多个变量,如下:
    echo IP=127.0.0.1 >   .env
    echo PORT=80      >>  .env
    
    作为全局变量导入:
    source .env
    
    作为环境变量导入:
    export `cat .env | sed 's/#.*//g' | xargs`
    

# 局部变量

:local variable ,是通过 local 命令定义的变量,只能在函数内创建,当函数运行结束时就会被自动删除。

# 环境变量

:environment variable ,是通过 set 、export 等命令定义的变量,可以被当前 shell 及其子 shell 访问。

  • 环境变量的名称通常全部大写。

  • 退出当前 shell 之后,定义的所有局部变量、环境变量都会被删除。因此,

    • 在子 shell 中创建的环境变量,不能被父 shell 访问。
    • 子 shell 会继承父 shell 的环境变量,但不能影响父 shell 的环境变量。
  • 常用的环境变量:

    USER=root
    LOGNAME=root
    HOME=/root
    HOSTNAME=VM_16_6_CentOS
    SHELL=/bin/bash
    PWD=/root
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin  # 目录之间用冒号 : 分隔
    TERM=linux
    LANG=en_US.utf8
    
    # PROMPT_COMMAND="history -a"   # 每次显示终端提示符 $PS1 之前,先执行命令 $PROMPT_COMMAND
    PS1='[\u@\h \w]\$ '   # 终端提示符的格式。在终端每输入一行命令,就会显示一行新的终端提示符
    PS2='>'               # 输入多行命令时,终端提示符的格式
    PS3='#?'              # 在 shell 脚本中使用 select 命令时,每行显示的前缀
    PS4='+'               # set -x 模式下,每行显示的前缀
    
    • 变量 PS1、PS2 只在 shell 解释器的交互模式中存在。
  • 每次用户新建终端时会重新加载环境变量,可以采用以下方式永久配置环境变量:

    echo 'export PATH=$PATH:/root/bin'    >>  /etc/profile
    echo 'export PATH=$PATH:/usr/lib/jdk' >   /etc/profile.d/java.sh
    

# 内置变量

:shell 解释器提供了一些内置变量,可以在终端、脚本、函数中调用。

  • 常用的内置变量:

    $0      # 当前脚本启动时,使用的文件名。这取决于启动命令,可能是绝对路径或相对路径
    $1      # 当前脚本启动时,输入的第 1 个参数。类似的还有 $2、$3、$4 等
    $*      # 当前脚本启动时,输入的全部参数(作为一个字符串返回)
    $@      # 当前脚本启动时,输入的全部参数(作为多个字符串返回)
    ${@:2}  # 从第 2 个参数开始的全部参数
    $#      # 当前脚本启动时,输入参数的个数
    
    $?      # 上一条的命令的返回码
    $!      # 上一个运行的后台进程的 PID
    $$      # 当前进程的 PID
    $PPID   # 父进程的 PID
    $FUNCNAME       # 当前执行的函数名,以及调用它的每一层父函数名
    ${FUNCNAME[0]}  # 当前执行的函数名
    $RANDOM # 返回一个 0 ~ 32767 范围的整数
    
  • 例:

    [root@CentOS ~]# echo $0
    -bash
    [root@CentOS ~]# echo $1
    
    [root@CentOS ~]# echo $*
    
    [root@CentOS ~]# echo $#
    0
    [root@CentOS ~]# echo $?
    0
    [root@CentOS ~]# echo $$
    11252
    
  • 例:获取随机数

    [root@CentOS ~]# echo $RANDOM
    20383
    [root@CentOS ~]# echo $[RANDOM*10/32768]    # 返回一个 0~9 范围的随机数
    7
    

# 数据类型

# 整型

  • 整型是 int 型常数。而浮点型常数会被视作字符串。
  • 例:
    [root@CentOS ~]# a=1
    [root@CentOS ~]# a=$[a+5]   # 对变量进行算术运算
    [root@CentOS ~]# a=$[a/2]
    [root@CentOS ~]# echo $a
    3
    

# 字符串

  • 关于字符串的定界符。

    • shell 中定界符分为两种:
      • 双引号
      • 单引号
    • 使用定界符可以明确字符串的界限:
      [root@CentOS ~]# echo Hello | wc -l
      1
      [root@CentOS ~]# echo "Hello |" wc -l
      Hello | wc -l
      [root@CentOS ~]# echo "Hello | wc -l"
      Hello | wc -l
      
    • 打印变量的值时,换行符 \n 会显示成空格,此时可加上字符串定界符:
      [root@CentOS ~]# stdout=`echo -e '1\n2'`
      [root@CentOS ~]# echo $stdout
      1 2
      [root@CentOS ~]# echo "$stdout"
      1
      2
      
  • 如果字符串中包含了与定界符相同的字符,会导致字符串被截断。可采用以下几种解决方法:

    • 将与定界符相同的字符声明为转义字符:
      [root@CentOS ~]# echo \"
      "
      [root@CentOS ~]# echo \'
      '
      [root@CentOS ~]# echo "\"A\'B"    # 定界符为双引号时,不支持转义 \'
      "A\'B
      [root@CentOS ~]# echo '\"'        # 定界符为单引号时,不支持转义字符
      \"
      
    • 将多个子字符串拼接在一起,依然会被视作一个字符串:
      [root@CentOS ~]# echo 'A'\"\''B'    # 每个子字符串可以使用不同的定界符
      A"'B
      
  • 定界符为单引号时:

    • 字符串中的符号 $` 、转义字符,会被视作普通字符:
      [root@CentOS ~]# echo "$A \$B"    # 这里 \$ 被视作一个转义字符
      $B
      [root@CentOS ~]# echo '$A \$B'    # 这里 \$ 被视作一个普通字符
      $A \$B
      [root@CentOS ~]# echo '`cat f1`'
      `cat f1`
      
    • $'string' 的语法可以解析转义字符:
      [root@CentOS ~]# echo $'\"Hello\''
      "Hello'
      [root@CentOS ~]# echo $'hello\n\x41'
      hello
      A
      [root@CentOS ~]# echo $'\u270B'   # 解析 Unicode 字符
  • 字符串的处理示例:

    [root@CentOS ~]# str=hello
    [root@CentOS ~]# echo ${#str}       # 获取字符串的长度
    5
    [root@CentOS ~]# echo ${str:0}      # 获取从第 0 个字符开始的所有字符
    hello
    [root@CentOS ~]# echo ${str:0:3}    # 获取从第 0 个字符开始的 3 个字符
    hel
    [root@CentOS ~]# echo ${str:4:3}
    o
    
    [root@CentOS ~]# echo ${str/l/_}    # (从左开始)寻找第一个匹配 l 的位置,执行一次字符串替换
    he_lo
    [root@CentOS ~]# echo ${str//l/_}   # 寻找所有匹配 l 的位置,执行一次字符串替换
    he__o
    [root@CentOS ~]# echo ${str#*l}     # 删掉前缀 *l
    lo
    [root@CentOS ~]# echo ${str%l*}     # 删掉后缀 l*
    hel
    

# 数组

  • shell 只支持一维数组,但不限制数组的长度。
  • 定义数组时要用小括号包住、用空格作为分隔符。
  • ${数组名[下标]} 的格式可以读取数组的某项元素,用 $数组名 的格式读取到的则是数组的第一项元素。
  • 例:
    [root@CentOS ~]# array=(1 2 3 4)
    [root@CentOS ~]# echo ${array[3]}
    4
    [root@CentOS ~]# echo ${array[4]}   # 下标无效时,返回值为空
    
    [root@CentOS ~]# echo $array
    1
    
  • 数组的处理示例:
    echo ${array[@]}     # 用 @ 作为下标,可获取数组的全部元素
    echo ${#array[@]}    # 获取数组的长度
    echo ${array[@]:0}   # 获取切片(0:]
    echo ${array[@]:0:5} # 获取切片(0:5]
    a[${#a[*]}]=5        # 在数组末尾添加元素
    

# 创建变量

# read

$ read <var>...           # 读取键盘的输入,赋值给一个或多个变量
      -p "Please input:"  # 显示提示
      -t 5                # 等待用户的输入最多 5 秒
      -n 5                # 用户最多输入 5 个字符,就会被自动结束输入
      -a                  # 将输入保存为数组类型
      -e                  # 允许在输入时按 Tab 自动补全
      -r                  # 不将反斜杠 \ 视作转义字符。默认会转义
      -s                  # 采用密文的形式输入
  • 例:
    read a
    read -p "请按下任意按键" -n 1 key
    
  • 当用户按下回车时就会结束输入。
    • 输入多个值时要以空格分隔。
    • 如果用户输入了太多值,多余的值会一起赋值给最后一个变量。如下:
      [root@CentOS ~]# read a b
      1 2 3
      [root@CentOS ~]# echo $a, $b
      1, 2 3
      

# set

$ set            # 显示当前 shell 的所有变量、函数
     -a <var>    # 将变量声明为环境变量(如果该变量不存在则无效)

     -e          # exit ,开启 e 模式,如果执行某个命令的返回码非 0 ,则立即退出当前 shell 。通常在 shell 脚本中启用,当命令执行失败时自动终止脚本
     +e          # 用减号 - 开启一个模式,用加号 + 关闭一个模式
     -u          # unset ,开启 u 模式,用到了未定义的变量时会报错
     -x          # execute ,开启 x 模式,执行每条命令之前,打印该命令到 stdout(每行开头会显示加号 + )
     -o pipefail # 通过管道符执行多个 shell 命令时,如果所有命令的返回码都为 0 ,最终返回码才为 0 。如果有多个命令的返回码非 0 ,则将最后一个非 0 的返回码作为最终返回码
  • 在一行内执行多个 shell 命令时,最后执行的一个命令的返回码,会作为最终的返回码。如下:
    [root@CentOS ~]# true | false     # 先执行 true 命令,后执行 false 命令
    [root@CentOS ~]# echo $?
    1
    
    [root@CentOS ~]# true || false    # 最后执行的是 true
    [root@CentOS ~]# echo $?
    0
    
    [root@CentOS ~]# x=`false`        # 最后执行的是 false
    
    [root@CentOS ~]# echo $?
    1
    
    [root@CentOS ~]# echo `false`     # 最后执行的是 echo
    
    [root@CentOS ~]# echo $?
    0
    
    • 通过管道符执行多个 shell 命令时,例如:
      [root@CentOS ~]# false | true
      
      • 如果处于 set -e 模式,则不会导致 shell 退出。
      • 如果处于 set -eo pipefail 模式,则会导致 shell 退出,因此能检查管道符连接的每个命令都执行成功。

# unset

$ unset <var>...  # 删除当前 shell 的指定变量

# env

$ env             # 显示当前 shell 的所有环境变量

# export

$ export                  # 显示当前 shell 的所有环境变量及其 declare 语句
        <var>...          # 将变量声明为环境变量(如果该变量不存在则创建它)
        <var>=<value>...  # 将变量声明为环境变量并赋值

# declare

$ declare        # 显示当前 shell 的所有变量、函数
         -i      # 显示所有整型变量
         -i x    # 将变量声明为整型
         -i x=0  # 将变量声明为整型并赋值
         +i x    # 用加号 + 取消声明
  • shell 中创建的变量默认为字符串类型,用 declare 命令可以声明变量的数据类型。
  • 可用的选项:
    • a :数组
    • f :函数
    • i :整型变量
    • r :只读变量
    • x :环境变量
  • 如果用字符串给整型变量赋值,则整型变量的值会变成 0 。如下:
    [root@CentOS ~]# declare -i x
    [root@CentOS ~]# echo $x
    
    [root@CentOS ~]# x=hello
    [root@CentOS ~]# echo $x
    0
    
  • 只读变量一旦创建就不能被修改、不能被删除,只能等当前 shell 退出之后被销毁。
    • 不能用 unset 命令删除,不能用 declare +x 取消只读,也不能用 declare、readonly 命令重新赋值,

# readonly

$ readonly               # 显示所有只读变量
          <var>          # 将变量声明为只读变量
          <var>=<value>  # 将变量声明为只读变量并赋值

# 数学运算

# bc

:用于打开一个简单的计算器终端。

  • 例:通过管道符输入要计算的表达式
    [root@CentOS ~]# echo "scale=2; 1/3.14" | bc    # scale 是小数点后保留的位数
    .31
    

# expr

expr <expression>   # 计算一个表达式的值,并输出到 stdout
  • 算术运算的示例:

    [root@CentOS ~]# expr 1+2       # 每个运算符、操作数之间,必须用空格分隔
    1+2
    [root@CentOS ~]# expr 1 + 2
    3
    [root@CentOS ~]# expr 1 - 2
    -1
    [root@CentOS ~]# expr \( 1 + 2 \) * 3   # shell 解释器中的特殊字符要进行转义,才能传递给 expr 命令
    9
    [root@CentOS ~]# expr 1 / 2
    0
    [root@CentOS ~]# expr 1 % 2
    1
    
  • 比较运算的示例:

    [root@CentOS ~]# expr 1 \> 2    # 返回码用 1、0 分别表示 True、False
    0
    [root@CentOS ~]# expr 1 \< 2
    1
    [root@CentOS ~]# expr 1 \>= 2
    0
    [root@CentOS ~]# expr 1 \<= 2
    1
    [root@CentOS ~]# expr 1 != 2
    1
    [root@CentOS ~]# expr 1 == 2
    0
    
  • 逻辑运算的示例:

    [root@CentOS ~]# expr 2 \& 3    # 与运算:如果两个操作数都存在且不为 0 ,则返回左操作数,否则返回 0
    2
    [root@CentOS ~]# expr 3 \& ''
    0
    
    [root@CentOS ~]# expr 2 \| 3    # 或运算:如果左操作数存在且不为 0 ,则返回它,否则返回右操作数
    2
    [root@CentOS ~]# expr 0 \| 3
    3
    
  • 支持在表达式中读取变量:

    [root@CentOS ~]# expr 1 + $a
    2
    [root@CentOS ~]# expr 1 + $a    # 如果读取一个不存在的变量,则可能导致语法错误
    expr: syntax error
    

# let

let <expression>...   # 执行一个或多个算术表达式,可以是赋值语句
  • 比 expr 命令更方便:
    • 表达式中不需要加空格。
    • 表达式中的变量不需要用 $ 取值。
    • 支持自加、自减等运算符。
  • 例:
    [root@CentOS ~]# let a=1+2
    [root@CentOS ~]# let a++
    [root@CentOS ~]# let a+=10
    [root@CentOS ~]# echo $a  b=a*2
    14 28
    
  • let 命令的返回码通常为 0 。如果最后一个表达式的值为 0 ,则返回码为 1 。
    [root@CentOS ~]# let a=0
    [root@CentOS ~]# echo $?
    1