美文网首页
shell学习笔记

shell学习笔记

作者: dounine | 来源:发表于2022-02-10 11:18 被阅读0次

    title: shell学习笔记
    date: 2021-04-25 15:59:05


    [TOC]

    0. 前言

    鉴于实习中经常使用 shell 语言,因此趁此机会好好学习一下“强大”的 shell 语言。

    参考:

    1. 简介

    shell 直译为壳。在操作系统(Linux)中,被称为外壳,通常与 kernel 内核相对立。在没有图形界面的时代,shell 是用户与操作系统交互的接口

    shell 本身是一个程序。在操作系统实验课程中,老师曾让我们实现一个微型 shell ,代码见:https://github.com/99MyCql/OS_pratice。它包括 shell 的许多基本功能:命令提示符运行命令/程序(如:cat、echo、ls等)、重定向输入输出管道。完成实验时,成就感满满,几乎与真实的 shell 无疑。

    但,当时的我可能忽略了 shell 的另一个“身份”——解释器。shell 解释器可运行 shell 脚本语言,它支持变量、条件判断、循环等等语法。这让 shell 具备了可编程性,而失去这一大功能的 shell 只能称为 mini shell 。

    关于解释型语言的定义,以及与编译型语言的区别,就不在此赘述了。

    shell 分为很多种,包括:Bourne Shell(sh)、Bourne Again shell(bash)、Z Shell(zsh)等等,它们的本质基本相同,本文将主要基于 Bash Shell

    接触过 python 的hxd应该知道,python 既可以在解释器中一行一行地敲,也可以写在一个文件中再运行文件。

    shell 也是同理,既可以运行在命令行,也可以写入脚本文件再执行。shell 脚本语言的代码文件以 .sh 结尾:

    • 可以通过执行解释器执行:bash test.sh
    • 或者直接使用当前命令行的解释器执行:相对路径执行 ./test.sh 或绝对路径 /home/test/test.sh(需为脚本添加可执行权限 chmod +x test.sh

    在 shell 脚本文件中,需要在第一行添加如下内容,指定解释器,如下指定 bash shell 为解释器:

    #!/bin/bash
    # 或者, env 命令(这个命令总是在 /usr/bin 目录),返回 Bash 可执行文件的位置
    #!/usr/bin/env bash
    

    shell 中注释为 #

    2. 使用命令

    在 shell 命令行中,可以输入cat、echo、grep等命令(这些命令本质是一个可执行文件),去执行对应的程序。

    同样,在 shell 脚本中,也可以使用命令,包括内部命令外部命令(在我看来 shell 语言中的命令就相当于其他语言中的库函数)。比如:

    #!/bin/bash
    echo "hello world"
    

    同时还包括:

    • 管道 |
    • 重定向 < >
    • 命令结束符 ; 。使用:Command1 ; Command2 允许单行多个命令,第二个命令总是接着第一个命令执行,不管第一个命令执行成功或失败。
    • 命令组合符 && || 。使用:Command1 && Command2 第一个命令运行成功,才继续运行第二个命令;Command1 || Command2 第一个命令运行失败,才继续运行第二个命令。

    更多 Linux 命令可以参见我的另一篇笔记:linux命令学习笔记

    3. 变量

    定义变量

    var=value
    var='value'
    var="value"
    

    变量名:由字母、数字和下划线字符组成;首字符必须是字母下划线,不能是数字。

    变量值:没有数据类型的概念,都是字符串,如果值中包含空格,需使用引号包围。

    注意:赋值符号附近不能有空格。

    同时,可以将命令执行的结果赋给变量

    var=`command`
    var=$(command) # 更推荐这种表示方式
    

    也可以将运算结果赋给变量

    var=$((5 * 7))
    

    使用变量

    两种方式:

    $var
    ${var} # 更推荐这种表示方式
    

    花括号 {} 用于区分变量边界。比如:在如下代码中,不使用花括号,会把 varScript 当成变量。

    var="Java"
    echo "I am good at $varScript" # 错误
    

    如果变量值包含连续空格或制表符,使用变量时应用双引号 "" 包围起来,因为 Shell 会将多个空格合为一个:

    var="1      2  3"
    echo $var   # 1 2 3
    echo "$var" # 1      2  3
    

    注意:当使用单引号 '' 将包围变量时,变量将不会解析,而是会被当成普通字符串

    var="1      2  3"
    echo $var   # 1 2 3
    echo "$var" # 1      2  3
    echo '$var' # $var
    

    修改变量值

    重新赋值即可:

    var="hello world" # hello world
    echo $var
    var="hello world!!!" # hello world!!!
    echo $var
    

    删除变量

    var="hello world"
    echo $var # hello world
    unset var
    echo $var #
    

    shell 中不存在的变量一律等于空字符串,所以即使unset命令删除了变量,还是可以读取这个变量(值为空字符串)。而且,被删除的变量可再次使用。

    环境变量

    用户创建的变量仅用于当前 Shell,子 Shell (在当前shell中运行的shell)默认读取不到父 Shell 定义的变量。

    使用 export 命令可以设置变量为环境变量,使子 shell 可以读取该变量。

    测试脚本 test.sh 如下:

    #!/bin/bash
    echo $test_export
    echo $test_noexport
    export test_export2="export"
    

    运行:

    $ export test_export="export" # 设为环境变量,子 shell 可读
    $ test_noexport="no export"
    $ ./test.sh
    export
    
    
    $ echo $test_export2 # 显然,父 shell 也读不到子 shell export 的变量
    
    
    

    注意:子 Shell 如果修改环境变量,不会影响父 Shell 。

    常用的环境变量有:

    • HOME:用户的主目录。
    • HOST:当前主机的名称。
    • PATH:由冒号分开的目录列表,当输入可执行程序名后,会搜索这个目录列表。
    • PWD:当前工作目录。
    • USER:当前用户的用户名。
    • LINENO:返回它在脚本中的行号。
    • FUNCNAME:返回一个数组,内容是当前的函数调用堆栈。该数组的0号成员是当前调用的函数,1号成员是调用当前函数的函数。
    • BASH_SOURCE:返回一个数组,内容是当前的脚本调用堆栈。该数组的0号成员是当前执行的脚本,1号成员是调用当前脚本的脚本

    只读变量

    readonly 命令指示变量只读,不可修改。

    readonly var="hello"
    var="hello world"   # var: readonly variable
    echo $var           # hello
    

    变量默认值

    • ${var:-word} 如果变量 var 为空或已被删除(unset),那么返回 word,但不改变 var 的值。
    • ${var:=word} 如果变量 var 为空或已被删除(unset),那么返回 word,并将 var 的值设置为 word。
    • ${var:?message} 如果变量 var 为空或已被删除(unset),那么将消息 message 送到标准错误输出,并将脚本停止运行,可以用来检测变量 var 是否可以被正常赋值。
    • ${var:+word} 如果变量 var 被定义,那么返回 word,但不改变 var 的值。

    特殊变量

    • $0 当前脚本的文件名。
    • $n 传递给脚本或函数的参数,$1 表示第一个参数,$2 表示第二个参数。
    • $# 传递给脚本或函数的参数个数。
    • $* 传递给脚本或函数的所有参数。
    • $@ 传递给脚本或函数的所有参数。被双引号 "" 包含时,$* 会将所有参数作为一个整体,而 $@ 会分开。
    • $? 上个命令的退出状态,或函数的返回值。
    • $$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

    4. 字符串

    字符串是 shell 最基本的数据类型。

    拼接字符串(推荐使用 {} ):

    str="world"
    echo "hello $str!"    # hello world!
    echo "hello ${str}!"  # hello world!
    

    获取字符串长度(变量使用必须要加 {} ):

    str="hello"
    echo ${#str} # 5
    

    提取字符串(offset 默认为0,length 默认到结尾):

    ${str:offset:length}
    

    字符转义:

    • 经典的转义字符 \n \t 转义;
    • 对于 shell 中的特殊字符,如 $ * & 等,需要转义;
    • 使用单引号 '' 时,转义字符都会被当成普通字符串

    字符串匹配并删除:

    • ${str#pattern} 从字符串首字符开始,删除最短匹配的部分,返回剩余字符串。pattern 支持 *?[] 等通配符。

    • ${str##pattern} 从字符串首字符开始,删除最长匹配(贪婪匹配)的部分,返回剩余字符串。

      str=/home/root/shell/study
      str=${str#/*/}    # root/shell/study
      echo $str
      echo ${str##/*/}  # root/shell/study
      str=/home/root/shell/study
      echo ${str##/*/}  # study
      
    • ${str%pattern} 从字符串尾字符开始,删除最短匹配的部分,返回剩余字符串。

    • ${str%%pattern} 从字符串尾字符开始,删除最长匹配的部分,返回剩余字符串。

    更高级的匹配建议使用:grepawk

    5. 数组

    定义数组

    arr=(value0 value1 value2 value3)
    

    arr=(
      value0
      value1
      value2
      value3
    )
    

    或单独定义(可以不使用连续的下标,而且下标的范围没有限制):

    arr[0]=value0
    arr[1]=value1
    arr[3]=value3
    

    追加

    使用 += 可追加元素:

    arr=(a b c d)
    echo $arr       # a b c d
    arr+=(e f)
    echo ${arr[@]}  # a b c d e f
    

    使用数组

    单个元素:

    value=${arr[i]}
    

    全部元素:

    ${arr[*]}
    ${arr[@]}
    

    注意

    • 默认 ${arr} = ${arr[0]} 而非全部元素。
    • ${arr[@]} "${arr[@]}" ${arr[*]} "${arr[*]}" 有不同效果,详情见:读取所有成员推荐使用 "${arr[@]}"

    多个元素:

    ${arr[@]:offset:length}
    

    获取数组长度:

    ${#arr[*]}
    ${#arr[@]}
    

    获知数组哪个位置上有值,即获取数组中存在值的元素的索引(提取数组索引):

    unset arr
    arr[1]=a
    arr[3]=b
    echo ${!arr[@]} # 1 3
    

    6. 运算表达式

    语法:使用 (( )) 包裹,或者使用 expr 命令。更推荐前一种。

    获取表达式的结果:$(( ))

    在表达式中可以使用变量,且不需要加$。若变量为空,则当作 0 。

    在表达式中,可以使用进制:默认十进制、0num 八进制、0xnum 十六进制、base#num base进制

    算术运算

    • + 加法
    • - 减法
    • * 乘法
    • / 除法(整除)
    • % 余数
    • ** 指数
    • ++ 自增运算(前缀或后缀)
    • -- 自减运算(前缀或后缀)
    i=0
    echo $((++i))       # 1
    echo $(((1+2) * 3)) # 9
    

    位运算

    与 C 语言一致:

    • << 左移
    • >> 右移
    • &
    • |
    • ~ 按位取反
    • ^ 异或
    echo $((16>>2)) # 4
    

    逻辑运算

    与 C 语言一致:

    • < 小于
    • > 大于
    • <= 小于或相等
    • >= 大于或相等
    • == 相等
    • != 不相等
    • && 逻辑与
    • || 逻辑或
    • ! 逻辑否
    • expr1 ? expr2 : expr3 三元条件运算

    如果逻辑表达式为真,返回1,否则返回0:

    echo $((3 > 2)) # 1
    

    赋值运算

    支持直接赋值 = ,也支持 += *= |= 等等。

    i=1
    echo $((i+=1)) # 2
    

    7. 条件判断 if

    if commands
    then
      commands
    [elif commands
    then
      commands...]
    [else
      commands]
    fi
    
    # 或
    
    if commands; then
      commands
    [elif commands; then
      commands...]
    [else
      commands]
    fi
    

    if 后面所接的判断条件是一个命令,命令返回成功(0)则为真,返回失败(非0)则为假。

    test

    if 判断条件通常使用 test 命令,它是一个用于判断的命令,它有三种形式:

    # 写法一
    test expr
    
    # 写法二
    [ expr ]
    
    # 写法三
    [[ expr ]]
    

    注意

    • 中括号 [ ] 与表达式之间必须包含空格
    • 第二种形式与第三种形式,在某些场景(比如逻辑判断)有所不同,详情参考:bash中 [ ] 与 [[ ]] 的区别

    由于 test 是一个命令,它支持很多选项:

    1) 文件判断

    • [ -a $file ]:如果 file 存在,则为true。
    • [ -b $file ]:如果 file 存在并且是一个块(设备)文件,则为true。
    • [ -c $file ]:如果 file 存在并且是一个字符(设备)文件,则为true。
    • [ -d $file ]:如果 file 存在并且是一个目录,则为true。
    • [ -e $file ]:如果 file 存在,则为true。
    • [ -f $file ]:如果 file 存在并且是一个普通文件,则为true。

    更多见:条件判断

    2) 字符串判断

    • [ $str ]:如果str不为空(长度大于0),则判断为真。
    • [ -n $str ]:如果字符串str的长度大于零,则判断为真。
    • [ -z $str ]:如果字符串str的长度为零,则判断为真。
    • [ $str1 = $str2 ]:如果str1和str2相同,则判断为真。
    • [ $str1 == $str2 ]:等同于[ str1 =str2 ]。
    • [ $str1 != $str2 ]:如果str1和str2不相同,则判断为真。
    • [ $str1 '>' $str2 ]:如果按照字典顺序str1排列在str2之后,则判断为真。
    • [ $str1 '<' $str2 ]:如果按照字典顺序str1排列在str2之前,则判断为真。

    注意:test命令内部的><,必须用引号括起来(或者是用反斜杠转义),否则它们会被 shell 解释为重定向操作符。

    3) 整数判断

    由于 > < 会被误解为重定向操作法,所以有专门的整数判断指令。

    • [ $int1 -eq $int2 ]:如果int1等于int2,则为true。
    • [ $int1 -ne $int2 ]:如果int1不等于int2,则为true。
    • [ $int1 -le $int2 ]:如果int1小于或等于int2,则为true。
    • [ $int1 -lt $int2 ]:如果int1小于int2,则为true。
    • [ $int1 -ge $int2 ]:如果int1大于或等于int2,则为true。
    • [ $int1 -gt $int2 ]:如果int1大于int2,则为true。

    4) 逻辑判断

    • [[ $expr1 && $expr1 ]] / [ $expr1 ] && [ $expr1 ]
    • [[ $expr1 || $expr1 ]] / [ $expr1 ] || [ $expr1 ]
    • [ ! $expr1 ] / [ ! \( $expr1 && $expr2 \) ]

    注意:test命令内部使用()必须使用引号或转义。

    运算表达式

    if 判断条件也可以使用运算表达式 (( ))

    但注意:运算表达式返回非0 ((1)) 表示真,返回0 ((0)) 表示假

    echo $((2 > 1)) # 1
    if ((2>1)); then
        echo true # true
    else
        echo false
    fi
    

    普通命令

    if 判断条件可以直接使用命令,命令返回成功(0)则为真,返回失败(非0)则为假。

    当然,也可以使用管道、重定向、命令结束符;、命令组合符&& ||等。

    比如:

    if mkdir temp && cd temp; then
      echo "enter in temp/"
    fi
    

    8. case

    case expression in
      pattern )
        commands ;;
      pattern )
        commands ;;
      ...
    esac
    

    pattern 支持基本的模式匹配,比如:

    echo -n "输入一个字母或数字 > "
    read character
    case $character in
      [[:lower:]] | [[:upper:]] ) echo "输入了字母 $character"
                                  ;;
      [0-9] )                     echo "输入了数字 $character"
                                  ;;
      * )                         echo "输入不符合要求"
    esac
    

    9. 循环

    while

    while commands; do
      commands
    done
    

    判断条件与 if 一样。

    until

    until commands; do
      commands
    done
    

    for

    遍历列表每一项:

    for variable in ${arr[@]}; do
      commands
    done
    

    或:

    for (( expr1; expr2; expr3 )); do
      commands
    done
    

    比如:

    for ((i=0; i<10; i++)); do
      echo $i
    done
    
    for i in $(seq 0 9); do
      echo $i
    done
    

    continue

    提前终止本轮循环,进行下一轮循环。

    10. 函数

    定义:

    # 第一种
    func() {
    }
    
    # 第二种
    function func() {
    }
    

    调用:

    func # 直接调用无参数
    func param1 param2 # 传入参数
    

    参数

    $1~$9:函数的第1个到第9个的参数。
    $0:函数所在的脚本名。
    $#:函数的参数总数。
    $@:函数的全部参数,参数之间使用空格分隔。
    $*:函数的全部参数,参数之间使用变量$IFS值的第一个字符分隔,默认为空格,但是可以自定义。

    return

    函数返回,可指定返回值,调用者通过 $? 获取。

    local 局部变量

    shell 中定义变量属于全局变量,在函数中声明局部变量需使用 local ,比如:

    func1() {
      foo1=1
      echo $foo1  # 1
    }
    func1
    echo $foo1    # 1
    
    func2() {
      local foo2
      foo2=2
      echo $foo2  # 2
    }
    func2
    echo $foo2    # 空
    

    其它

    主要介绍在脚本中使用较多,而在命令行中使用较少的命令。

    set

    命令行下不带任何参数,直接运行 set ,会显示所有的环境变量和 Shell 函数

    常用选项:

    • set -u 遇到不存在的变量则报错(默认会跳过)
    • set -x 在运行命令前,先输出该命令,常用于调试。set -x 开启,set +x 关闭。
    • set -e 遇到错误则终止执行(默认命令执行出错会忽略)。set -e 有一个例外情况,就是不适用于管道命令(多个子命令通过管道符组合,Bash 会把最后一个子命令的返回值,作为整个命令的返回值)。
    • set -o pipefail 用来解决这种情况,只要一个子命令失败,整个管道命令就失败,脚本就会终止执行。

    使用示例:

    # set -x # 调试时再开启
    set -euo pipefail
    

    注意

    • 使用 set -e 后,如果调用函数,函数返回了非零值,程序也会退出

    read

    read [-options] [var...]
    

    输入由回车结束,用户输入将被保存到变量 var ,多个输入项通过空格区分。

    若未提供变量名,环境变量 REPLY 会包含用户输入的一整行数据。

    若提供的输入项少于变量数目,则剩余变量为空。

    常用选项:

    • p 指定提示信息。
    read -p "Enter your input:"
    echo $REPLY
    
    • a 把用户的输入赋值给一个数组,从零号位置开始。
    read -a arr
    echo ${arr[@]}
    
    • n 指定只读取若干个字符作为变量值,而不是整行读取。
    read -n 3 var
    echo $var
    

    read 还可用于读文件:

    filename='xxx'
    while read line; do
      echo "$line"
    done < $filename
    

    exit

    用于退出当前执行的 Shell ,并返回一个值,返回 0 代表成功,返回 非0 代表失败。

    source

    用于执行一个脚本文件,但不同于直接执行(会新建子 shell ),source 会在当前 shell 执行。

    类似于加载外部库。

    相关文章

      网友评论

          本文标题:shell学习笔记

          本文链接:https://www.haomeiwen.com/subject/easnkrtx.html