shell 编程

author : 老姚

一、shell 简介

  • Shell 是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 Shell 的本质。
  • Shell 本身并不是内核的一部分,它只是站在内核的基础上编写的一个应用程序,它和 QQ、迅雷、Firefox 等其它软件没有什么区别。
  • 然而 Shell 也有着它的特殊性,就是开机立马启动,并呈现在用户面前;用户通过 Shell 来使用 Linux,不启动 Shell 的话,用户就没办法使用 Linux
  • Shell 是如何连接用户和内核的?
    • Shell 程序本身的功能是很弱的,比如文件操作、输入输出、进程管理等都得依赖内核。我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核,只是这个过程被 Shell 隐藏了起来,它自己在背后默默进行,我们看不到而已。
    • 接口其实就是一个一个的函数,使用内核就是调用这些函数。这就是使用内核的全部内容了吗?嗯,是的!除了函数,你没有别的途径使用内核。
    • 比如,我们都知道在 Shell 中输入cat log.txt命令就可以查看 log.txt 文件中的内容,然而,log.txt 放在磁盘的哪个位置?分成了几个数据块?在哪里开始?在哪里终止?如何操作探头读取它?这些底层细节 Shell 统统不知道的,它只能去调用内核提供的 open()read() 函数,告诉内核我要读取 log.txt 文件,请帮助我,然后内核就乖乖地按照 Shell 的吩咐去读取文件了,并将读取到的文件内容交给 Shell,最后再由 Shell 呈现给用户(其实呈现到显示器上还得依赖内核)。整个过程中 Shell 就是一个“中间商”,它在用户和内核之间“倒卖”数据,只是用户不知道罢了。

二、shell 基础语法

shell 变量

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

    # variable 是变量名,value 是赋给变量的值。
    variable=value #如果不含任何空白符,可以不用引号
    variable='value' #单引号里面是什么就输出什么
    variable="value" #输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出
    
    • 注意,赋值号=的周围不能空格,这可能和你熟悉的大部分编程语言都不一样。
    • Shell 变量的命名规范和大部分编程语言都一样:
      • 变量名由数字字母下划线组成;
      • 必须以字母或者下划线开头
      • 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。
  • eg

    url=http://hzoi.com
    echo $url
    name='衡中oj'
    echo $name
    author="老姚"
    echo $author
    

使用变量

  • 使用一个定义过的变量,只要在变量名前面加美元符号 $ 即可。

  • 变量名外面的花括号{}是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界。

  • 推荐给所有变量加上花括号{},这是个良好的编程习惯。

  • eg

    author="老姚"
    skill="shell"
    echo "I am ${author},good at ${skill}Script!"
    
  • 只读变量

    • 使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能改变
  • 删除变量

    • 使用 unset 命令可以删除变量。
    • 变量被删除后不能再次使用;unset 命令不能删除只读变量
  • eg:

    url="hzoi.com"
    readonly url #设置变量url为只读变量
    unset url #错误,不能删除只读变量
    

变量的作用域

  1. Shell 脚本中定义的变量是 global 的,其作用域从被定义的地方开始,到 shell 结束或被显示删除的地方为止
  2. Shell 函数定义的变量默认是 global 的,其作用域从“函数被调用时执行变量定义的地方”开始,到 shell 结束或被显示删除处为止。
    • 函数定义的变量可以被显示定义成 local 的,其作用域局限于函数内。但请注意,函数的参数是 local 的。
    • 函数参数是local变量
  3. 如果同名,Shell函数定义的local变量会屏蔽脚本定义的global变量
#!/bin/bash
zyy(){
		x=200 #global变量,调用开始生效
		local y=100 #局部变量,函数调用结束失效
		echo ${y} #输出100
}
zyy #调用函数
echo ${x} ${y} #只会输出x的值

Shell 命令替换

  • Shell 命令替换是指将命令的输出结果赋值给某个变量。

  • Shell 中有两种方式可以完成命令替换,一种是反引号 ,一种是$(),使用方法如下:

    variable=`command` #第一种方式把命令用反引号(位于 Esc 键的下方)包围起来
    variable=$(command)#第二种方式把命令用$()包围起来,区分更加明显,所以推荐使用这种方式。
    # eg:
    dir=$(ls -a)
    
  • eg

    #!/bin/bash
    begin_time=`date +%s`    #开始时间,使用``替换
    sleep 20s                #休眠20秒
    finish_time=$(date +%s)  #结束时间,使用$()替换
    run_time=$((finish_time - begin_time))  #时间差
    echo "begin time: $begin_time"
    echo "finish time: $finish_time"
    echo "run time: ${run_time}s"
    LS=`ls -l`
    echo ${LS} #不用双引号包围
    echo "--------------------------"  #输出分隔符
    echo "$LS"  #使用引号包围
    
    • 第 5 行代码中的(( ))Shell 数学计算命令。
    • 如果被替换的命令的输出内容包括多行(也即有换行符),或者含有多个连续的空白符,那么在输出变量时应该将变量用双引号包围,否则系统会使用默认的空白符来填充,这会导致换行无效,以及连续的空白符被压缩成一个。
    • 原则上讲,上面提到的两种变量替换的形式是等价的,可以随意使用;
    • 反引号毕竟看起来像单引号,有时候会对查看代码造成困扰,而使用 $() 就相对清晰,能有效避免这种混乱。
    • $() 支持嵌套,反引号不行。
    • $() 仅在 Bash Shell 中有效,而反引号可在多种Shell中使用。

字符串

基本概念

  • 字符串就是一系列字符的组合。字符串是 Shell 编程中最常用的数据类型之一(除了数字和字符串,也没有其他类型了)。
  • 字符串可以由单引号' '包围,也可以由双引号" "包围,也可以不用引号。
    1. 由单引号' '包围的字符串:
      • 任何字符都会原样输出,在其中使用变量是无效的。
      • 字符串中不能出现单引号,即使对单引号进行转义也不行。
    2. 由双引号" "包围的字符串:
      • 如果其中包含了某个变量,那么该变量会被解析(得到该变量的值),而不是原样输出。
      • 字符串中可以出现双引号,只要它被转义了就行。
    3. 不被引号包围的字符串
      • 不被引号包围的字符串中出现变量时也会被解析,这一点和双引号" "包围的字符串一样。
      • 字符串中不能出现空格,否则空格后边的字符串会作为其他变量或者命令解析。

获取字符串长度

  • 在 Shell 中获取字符串长度很简单,具体方法如下:

    ${#string_name}
    #eg:
    str="hzoi.com"
    echo ${#str} #输出8
    

shell 字符串拼接

  • Shell 中你不需要使用任何运算符,将两个字符串并排放在一起就能实现拼接。

    #!/bin/bash
    name="Shell"
    url="hzoi.com"
    str1=$name$url  #中间不能有空格
    str2="$name $url"  #如果被双引号包围,那么中间可以有空格
    str3=$name": "$url  #中间可以出现别的字符串
    str4="$name: $url"  #这样写也可以
    str5="${name}Script: ${url}index.html"  #这个时候需要给变量名加上大括号
    echo $str1
    echo $str2
    echo $str3
    echo $str4
    echo $str5
    

shell 字符串截取

从指定位置开始截取

  • 这种方式需要两个参数:除了指定起始位置,还需要截取长度,才能最终确定要截取的字符串。

  • 既然需要指定起始位置,到底是从字符串左边开始计数,还是从字符串右边开始计数。答案是 Shell 同时支持两种计数方式。

    1. 从字符串边开始计数

      • 如果想从字符串的左边开始计数,那么截取字符串的具体格式如下:

        ${string: start :length} 
        #string 是要截取的字符串
        #start 是起始位置(从左边开始,从 0 开始计数)
        #length 是要截取的长度(省略的话表示直到字符串的末尾)。
        url="hzoi.com"
        url1="${url:2:5}" #从下标2开始连续截取5个字符
        url2="${url:2}" #从下标2
        
    2. 从字符串边开始计数

      • 如果想从字符串的右边开始计数,那么截取字符串的具体格式如下:

        ${string: 0-start :length}
        #eg
        url="hzoi.com"
        url1="${url:0-6:5}" #从1开始往左数6个位置开始连续截取5个字符
        url2="${url:0-6}" #从1开始往左数6个位置开始到结束
        
        • 同第 1)种格式相比,第 2)种格式仅仅多了0-,这是固定的写法,专门用来表示从字符串右边开始计数。
        • 边开始计数时,始数字是 0(这符合程序员思维);从边开始计数时,起始数字是 1(这符合常人思维)。计数方向不同,起始数字也不同。
        • 不管从哪边开始计数,截取方向都是从左到右

2.3.4.2 从指定字符串开始截取

  • 这种截取方式无法指定字符串长度,只能从指定字符(子字符串)截取到字符串末尾

  • Shell 可以截取指定字符(子字符串)右边的所有字符,也可以截取左边的所有字符。

    1. 使用 # 号截取右边字符

      • 使用 # 号可以截取指定字符(或者子字符串)右边的所有字符,具体格式如下:

        ${string#*chars}
        # string 表示要截取的字符
        # chars 是指定的字符(或者子字符串),*是通配符的一种,表示任意长度的字符串。
        # *chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars(chars 不会被截取)。
        # eg:
        url="hzoi.com"
        url1=${url#*.} #结果为:com
        url2=${url#*oi.} #结果同上
        url3=${url#*o} #遇到第一个字符o就截止,结果:i.com
        url4=${url##*o} #遇到最后一个字符o就截止,结果:m
        
    2. 使用 % 截取左边字符

    • 使用%号可以截取指定字符(或者子字符串)左边的所有字符,具体格式如下:

      ${string%chars*}
      # 请注意*的位置,因为要截取 chars 左边的字符,而忽略 chars 右边的字符
      # 同时%用法同上面#
      # eg:
      url="http://www.hbhz.net"
      url1=${url%.*} #结果为:http://www.hbhz
      url2=${url%%.*} #结果为:http://www
      

汇总

格式 说明
$ 从 string 字符串的左边第 start 个字符开始,向右截取 length 个字符。
$ 从 string 字符串的左边第 start 个字符开始截取,直到最后。
$ 从 string 字符串的右边第 start 个字符开始,向右截取 length 个字符。
$ 从 string 字符串的右边第 start 个字符开始截取,直到最后。
$ 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
$ 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 右边的所有字符。
$ 从 string 字符串第一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。
$ 从 string 字符串最后一次出现 *chars 的位置开始,截取 *chars 左边的所有字符。

shell 数组

  • Shell 并且没有限制数组的大小,理论上可以存放无限量的数据。
  • Shell 数组元素的下标也是从 0 开始计数。
  • 获取数组中的元素要使用下标[ ],下标可以是一个整数,也可以是一个结果为整数的表达式;
  • 常用的 Bash Shell 支持一维数组不支持多维数组

shell 数组的定义

  • Shell 中,用括号( )来表示数组,数组元素之间用空格来分隔。由此,定义数组的一般形式为:

    array_name=(ele1  ele2  ele3 ... elen) #注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素。
    # eg:
    nums=(29 100 13 8 91 44) #初始化长度为6的数组
    nums[6]=88 #Shell 数组的长度不是固定的,定义之后还可以增加元素。
    arr=(20 56 "http://c.biancheng.net/shell/") # Shell是弱类型的,它并不要求所有数组元素的类型必须相同
    ages=([3]=24 [5]=19 [10]=12) #无需逐个元素地给数组赋值,可以只给特定元素赋值
    

获取数组元素

  • 获取数组元素的值,一般使用下面的格式:

    ${array_name[index]} # array_name 是数组名,index 是下标。
    # eg:
    n=${nums[2]} # 表示获取 nums 数组的第二个元素,然后赋值给变量 n
    nums=(29 100 13 8 91 44)
    echo ${nums[@]}  #输出所有数组元素
    nums[10]=66  #给第10个元素赋值(此时会增加数组长度)
    echo ${nums[*]}  #输出所有数组元素
    echo ${nums[4]}  #输出第4个元素
    

获取数组长度

  • 所谓数组长度,就是数组元素的个数。

  • 利用@*,可以将数组扩展成列表,然后使用#来获取数组元素的个数,格式如下:

    ${#array_name[@]}
    ${#array_name[*]}
    # array_name 表示数组名。两种形式是等价的,选择其一即可。
    # 如果某个元素是字符串,还可以通过指定下标的方式获得该元素的长度
    ${#arr[2]} # 获取 arr 数组的第 2 个元素(假设它是字符串)的长度。
    # eg:
    nums=(29 100 13)
    echo ${#nums[*]} # 输出nums数组的长度:3
    #向数组中添加元素
    nums[10]="http://c.biancheng.net/shell/"
    echo ${#nums[@]} # 输出nums数组的长度:4
    echo ${#nums[10]} # 输出字符串nums[10]的长度:29
    unset nums[1] #删除数组元素
    echo ${#nums[*]} # 输出nums数组的长度:3
    

数组拼接

  • 所谓 Shell 数组拼接(数组合并),就是将两个数组连接成一个数组。

  • 拼接数组的思路是:先利用 @*,将数组扩展成列表,然后再合并到一起。具体格式如下:

    array_new=(${array1[@]}  ${array2[@]})
    array_new=(${array1[*]}  ${array2[*]})
    # 两种方式是等价的,选择其一即可。
    # 其中,array1 和 array2 是需要拼接的数组,array_new 是拼接后形成的新数组。
    # eg:
    array1=(23 56)
    array2=(99 "http://c.biancheng.net/shell/")
    array_new=(${array1[@]} ${array2[*]}) #拼接后重新从下标0编号
    echo ${array_new[@]}  #也可以写作 ${array_new[*]}
    

删除数组元素

  • Shell 中,使用 unset 关键字来删除数组元素,具体格式如下:

    unset array_name[index] # 删除元素
    unset array_name # 删除数组
    # array_name 表示数组名,index 表示数组下标。
    # eg:
    arr=(23 56 99 "http://c.biancheng.net/shell/") 
    unset arr[1]
    echo ${arr[@]}
    unset arr
    echo ${arr[*]}
    

关联数组

  • Bash支持关联数组,它可以使用字符串作为数组索引,有时候采用字符串索引更容易理解。

定义关联数组

declare -A assArray # declare -A 定义格式
assArray=([lucy]=beijing [yoona]=shanghai) # 利用内嵌索引-值列表的方法赋值
echo ${assArray[lucy]} # 输出:beijing
assArray[lily]=shandong # 使用独立的索引-值进行赋值
echo ${assArray[lily]} # 输出:shandong

列出数组索引

  • 每一个数组都有一个索引用于查找。使用 ${!数组名[@或者*]} 获取数组的索引列表
echo ${!assArray[*]} # 输出:lily yoona sunny lucy
echo ${!assArray[@]} # 输出:lily yoona sunny lucy

获取所有键值对

declare -A city # 声明city为关联数组
city=([capital]=beijing [economy]=shanghai [home]=hubei)
	echo "${key} come from ${city[${key}]}"
done

shell 内置命令

  • 所谓 Shell 内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。
  • 可以使用 type 来确定一个命令是否是内建命令
  • $PATH 变量包含的目录中几乎聚集了系统中绝大多数的可执行命令,它们都是外部命令
  • 内建命令会比外部命令执行得更快,执行外部命令时不但会触发磁盘 I/O,还需要 fork 出一个单独的进程来执行,执行完成后再退出。而执行内建命令相当于调用当前 Shell 进程的一个函数。

冒号(😃

  1. 空命令:空命令就是什么也不做,所以返回码永远都是0。虽说是空命令,但仍是命令,就具备一般命令应有的特征,可以象一般命令一样使用。

  2. 参数扩展:冒号引起的参数扩展,意思是将参数的值替换为新的值。一般有以下几种参数扩展用法:

    ${parameter:-word}    如果parameter没有设置或者为空,替换为word;否则替换为parameter的值
    ${parameter:+word}    如果parameter没有设置或者为空,不进行任何替换;否则替换为word。
    ${parameter:=word}    如果parameter没有设置或者为空,把word赋值给parameter。实际parameterd的值真的被替换了,这就是=号的意思。不能用这种方式指派位置参数或特殊参数的值。
    ${parameter:?word}    如果parameter没有设置或者为空,把word输出到stderr,否则替换为parameter的值。-、+、? 实际parameter的值并不被修改,扩展只是临时显示成word的值。准确的讲,扩展实际替换的是参数的显示,而不是参数的定义。只有=,才是替换参数的定义。
    ${parameter:offset}    扩展为parameter中从offset开始的子字符串。
    ${parameter:offset:length}     扩展为parameter中从offset开始的长度不超过length的字符。
    
    • word 前的“-”可以理解为“没定义,则替换成 word
    • “+”可以理解为“有定义,则替换成 word
    • word 前的“?”可以理解为“参数到底定义了没,没定义,把word当错误消息打印出来。”
  3. 三元运算符

    var=100
    (($var>100 ? var++:var--))
    

alias:别名

  • alias 用来给命令创建一个别名。若直接输入该命令且不带任何参数,则列出当前 Shell 进程中使用了哪些别名。
# 使用 alias 命令自定义别名的语法格式为:
alias new_name='command'
alias Myshutdown='shutdown -h now' #关机
# eg:
alias timestamp='date +%s' # 别名
begin=`timestamp`  # 命令替换
sleep 20s
finish=$(timestamp) # 命令替换
difference=$((finish - begin)) # 计算
echo "run time: ${difference}s"
  • unalias 命令删除别名
    • 使用 unalias 内建命令可以删除当前 Shell 进程中的别名。unalias 有两种使用方法:
      1. 第一种用法是在命令后跟上某个命令的别名,用于删除指定的别名。
      2. 第二种用法是在命令后接-a参数,删除当前 Shell 进程中所有的别名。
  • 在代码中使用 alias 命令定义的别名只能在当前 Shell 进程中使用,在子进程和其它进程中都不能使用。当前 Shell 进程结束后,别名也随之消失。
  • 要想让别名对所有的 Shell 进程都有效,就得把别名写入 Shell 配置文件。

echo 命令

  • echo 是一个 Shell 内建命令,用来在终端输出字符串,并在最后默认加上换行符

  • 如果不希望换行,可以加上-n参数

  • 默认情况下,echo 不会解析以反斜杠\开头的转义字符

  • 我们可以添加-e参数来让 echo 命令解析转义字符

    echo "读者,你好!"  #直接输出字符串
    echo $url  #输出变量
    echo "${name}的网址是:${url}"  #双引号包围的字符串中可以解析变量
    echo '${name}的网址是:${url}'  #单引号包围的字符串中不能解析变量
    echo -n "Thank you!" #输出后不换行
    echo "hello \nworld" #不会转义\n
    echo -e "hello \nworld" # 转义\n
    echo -e "hello world\c" # 转义\c 不会换行
    
posted @ 2020-08-08 07:07  hyskr  阅读(178)  评论(0)    收藏  举报