Mit:the missing semester 2

课程目标

学习Bash作为脚本语言的一些基础操作,以及几种最常用的shell工具

Shell脚本

大多数Shell都有自己的一套脚本语言,包括变量、控制流和语法。本次主要学习Bash脚本,因为它最流行,应用最广泛

  1. 赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    foo=bar
    #将bar的值赋给foo
    foo = bar
    #不能正确工作,会调用程序foo并将=和bar作为参数传入程序

    bar=1
    foo=bar
    echo "$foo"
    #输出1
    echo '$foo'
    #输出$foo
    #Bash中字符串通过'和"分隔符定义,'定义的字符串为原义,"定义的字符串将会替换为变量值
  2. 函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #bash也支持if、case、while和for这些控制流关键词
    #例如mcd(){
    mkdir -p "$1"
    cd "$1"
    }
    #其中$1为特殊变量,Bash中有许多特殊变量,例如:
    # $0 脚本名
    # $1 -> $9 脚本的第n个参数
    # $@ 所有参数
    # $# 参数个数
    #更多特殊变量可到官方文档查看:https://www.tldp.org/LDP/abs/html/special-chars.html
  3. 返回码

    Bash指令通常使用stdout返回输入值,stderr返回错误和错误码,返回值为0表示正常运行,返回其他任何非零的值都表示发生错误,与C/C++类似。

    退出码可以搭配&&和||使用,更好地实现运行结果的处理

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    false || echo "Oops,fail"
    #将会输出Oops,fail
    true || echo "Will not printed"
    #将不会有输出
    false && echo "will not printed"
    #将不会有输出
    true && echo "Things went well"
    #将会输出Things went well
    #因为||和&&都属于短路运算符

    #还可以用;分隔同一行的多个命令,例如
    mkdir l ; cd l
  4. 命令替换

    可以在命令中使用$(command)来执行所需的命令,命令中$(command)将会被command的输出所替换,例如:

    1
    2
    echo "Now time is $(date)"
    #$(date)将被替换为date的输出结果,即当前的日期和时间
  5. 通配

    通配符:可以分别使用 ? 和 * 来匹配一个或任意个字符。

    例如对于文件 foo,foo1,foo2,fool0和bar

    rm foo? 会删除 foo1 和 foo2

    rm foo* 会删除除bar以外的所有文件


    花括号:

    1
    2
    3
    4
    5
    convert "img.{png,jpg}"
    #等价于 convert "img.png img.jpg"

    mv *.{py,sh} folder
    #可以和通配符结合使用,将所有的.py .sh文件移动到folder文件夹中
  6. 补充

    编写Bash脚本有时候会非常别扭和反直觉,可以使用shellcheck这种工具定位脚本中的错误

    脚本不一定只有用Bash写的才能在终端中调用,例如:

    1
    2
    3
    4
    #!/usr/local/bin/python
    import sys
    for arg in reversed(sys.argv[1:])
    print(arg)

    内核知道使用python解释器来执行而不是shell,因为第一行的shebang

    还可以在shebang行中使用env命令,会利用环境变量中的程序来解析该脚本,可以提高脚本的可移植性,env会利用PATH环境变量来进行定位,例如:#!/usr/bin/env python

工具

  1. 查看命令如何使用

    课程中推荐使用man命令和TLDR pages,但我不习惯使用这些工具,我推荐善用搜索引擎Google Bing Baidu

  2. 所有类UNIX系统都会包含一个名为find的工具,是在shell中查找文件最方便的工具,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #查找所有名称为src的文件夹
    find . -name src -type d
    #查找所有文件夹路径包含test的python文件
    find . -path '*/test/*.py' -type f
    #查找所有前一天修改的文件
    find . -mtime -1
    #查找所有大小在500K到1M的tar.gz文件
    find . -size +500k -size -1M -name '*.tar.gz'

    #删除所有扩展名为.tmp的文件
    find . -name '*.tmg' -exec rm {} \;
    #查找所有png文件并转化为jpg
    find . -name '*.png' -exec convert {} {}.jpg \;

课后练习

  1. 使用ls进行以下操作

    1. 打印所有文件(包括隐藏文件)

      ls -a

    2. 文件打印以人类可以理解的格式输出 (例如,使用454M 而不是454279954)

      ls -l -h

    3. 文件以最近访问顺序排序

      ls -t

    4. 以彩色文本显示输出结果

      ls --color=auto

  2. 编写两个bash函数 marco 和 polo 执行下面的操作。 每当你执行 marco 时,当前的工作目录应当以某种形式保存,当执行 polo 时,无论现在处在什么目录下,都应当 cd 回到当时执行 marco 的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh 中,并通过 source marco.sh命令,(重新)加载函数。通过source 来加载函数,随后可以在 bash 中直接使用。

    1
    2
    3
    4
    5
    6
    7
    #!/bin/bash
    marco(){
    echo "$(pwd)" > /marco.log
    }
    polo(){
    cd "$(cat "/marco.log")"
    }
  3. 假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段bash脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。 加分项:报告脚本在失败前共运行了多少次。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #!/bin/bash
    count=1
    echo > out.log
    while true
    do
    ./bugshell.sh &>> out.log
    if [[$? -ne 0]] ; then
    cat out.log
    echo "Failed after $count attempts"
    break
    fi
    ((count++))
    done
  4. 本节课我们讲解的 find 命令中的 -exec 参数非常强大,它可以对我们查找的文件进行操作。 如果我们要对所有文件进行操作呢?例如创建一个zip压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar 则需要从参数接受输入。这里我们可以使用xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。您的任务是编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs的参数-d)

    find . -name "*.html" | xargs -d '\n' tar -czvf html.zip