The missing semester of your CS education--Shell工具和脚本

课程结构

01.课程概览与 shell
02.Shell 工具和脚本
03.编辑器 (Vim)
04.数据整理
05.命令行环境
06.版本控制(Git)
07.调试及性能分析
08.元编程
09.安全和密码学
10.大杂烩
11.提问&回答

本文档修改自这里,补充了一些视频中展示但配套文档中未提供的代码,以及一些注释。

Shell 脚本

大多数shell都有自己的一套脚本语言,包括变量、控制流和自己的语法。shell脚本与其他脚本语言不同之处在于,shell 脚本针对 shell 所从事的相关工作进行来优化。因此,创建命令流程(pipelines)、将结果保存到文件、从标准输入中读取输入,这些都是 shell 脚本中的原生操作,这让它比通用的脚本语言更易用.

shell变量&控制流结构&shell函数


  • 变量定义
    ~$ foo=bar # 定义变量,后面使用$foo访问该变量
    
    ~$ foo = bar # 出现空格会把后面的=和bar识别为命令foo的参数
    Command 'foo' not found, did you mean:
    command 'foot' from deb foot (1.11.0-2)
    command 'fio' from deb fio (3.28-1)
    command 'fop' from deb fop (1:2.6-2)
    command 'fox' from deb objcryst-fox (1.9.6.0-2.2)
    command 'goo' from deb goo (0.155+ds-4)
    Try: sudo apt install <deb name>
    
  • 单引号与双引号的作用
    ~$ echo '$foo' # 以''定义的字符串为原义字符串,其中的变量不会被转义
    $foo
    
    ~$ echo "$foo" # 以""定义的字符串会将变量值进行替换
    bar
    

bash也支持if, case, whilefor 这些控制流关键字,更详细的可参考这里

  • 循环结构
    for arg in [list]
    do
    command(s)...
    done
    
    while [ condition ]
    do
    command(s)...
    done
    
    until [ condition-is-true ]
    do
    command(s)...
    done
    

bash 也支持函数,它可以接受参数并基于参数进行操作。下面这个函数会创建一个文件夹并使用cd进入该文件夹。

  • 创建shell函数
    ~$ sudo apt install vim
    ~$ vim mcd.sh 
    # 创建并进入mcd.sh文件,键入"i"进入编辑模式
    # 将下面的文件内容复制进去后,按<Esc>退出编辑模式
    # 输入":",跳转到终端窗口底部,继续输入"exit"(保存并退出vim)
    
    # mcd.sh文件内容,创建shell函数mcd()
    mcd () {
        mkdir -p "$1"
        cd "$1"
    }
    
  • 执行
    ~$ source mcd.sh 
    # 在当前shell中导入mcd.sh
    # 与执行 ". mcd.sh" 效果一样
    
    ~$ mcd test 
    # mcd被当前shell识别为一个命令
    # 创建test文件夹,并进入该目录
    ~/test$ cd ..
    

bash使用了很多特殊的变量来表示参数、错误代码和相关变量,更完整的列表可以参考 这里

$0 - 脚本名
$1$9 - 脚本的参数。 $1 是第一个参数,依此类推。
$@ - 所有参数
$# - 参数个数
$? - 前一个命令的返回值
$$ - 当前脚本的进程识别码pid
!! - 完整的上一条命令,包括参数。
   常见应用:当你因为权限不足执行命令失败时,可以使用 sudo !!添加root权限再尝试一次。
$_ - 上一条命令的最后一个参数。如果你正在使用的是交互式 shell,你可以通过按下 Esc 之后键入 . 来获取这个值。
~$ mkdir /mnt/new
mkdir: cannot create directory ‘/mnt/new’: Permission denied
~$ sudo !!
sudo mkdir /mnt/new
[sudo] password for mint: 

~$ mkdir txt
~$ cd $_ # 读取并使用了上一行的参数txt
~/txt$ 

命令通常使用 STDOUT来返回输出值,使用STDERR 来返回错误及错误码,便于脚本以更加友好的方式报告错误。 返回值0表示正常执行,其他所有非0的返回值都表示有错误发生。

~$ echo 'hello'
hello
~$ echo $? # 上一行运行正常,返回0,与其他编程语言的return值类似
0
~$ grep foobar mcd.sh
~$ echo $? # mcd.sh文件中没有字符串foobar,运行错误,返回1
1
~$ true
~$ echo $?
0
~$ false
~$ echo $?
1

退出码可以搭配 &&(与操作符)和 ||(或操作符)使用,用来进行条件判断,决定是否执行其他程序。它们都属于短路运算符(short-circuiting,根据左侧的返回值判断是否执行右侧的命令) 。

~$ false || echo "Oops fail"
Oops fail
~$ true || echo "will be not be printed"
~$ true && echo "Things went well"
Things went well
~$ false && echo "This will not be printed"
~$ false ; echo "This will always be printed"
This will always be printed
# "; "分号后面的空格是必要的

命令替换与进程替换


另一个常见的模式是以变量的形式获取一个命令的输出,这可以通过 命令替换(command substitution) 实现。

通过 $(CMD) 这样的方式来执行CMD 这个命令时,它的输出结果会替换掉 $(CMD) 。例如,如果执行 for file in $(ls) ,shell首先将调用ls ,然后遍历得到的这些返回值。

还有一个冷门的类似特性是 进程替换(process substitution)<(CMD) 会执行 CMD 并将结果输出到一个临时文件中,并将 <(CMD) 替换成临时文件名。这在我们希望返回值通过文件而不是STDIN传递时很有用。例如, diff <(ls foo) <(ls bar) 会显示文件夹 foobar 中文件的区别。

~$ cat <(ls) <(ls ..) # 将本目录及上一级目录的文件内容合并显示
~$ ls .. # 显示上级目录中的文件内容

下面的脚本会遍历我们提供的参数,使用grep 搜索字符串 foobar,如果没有找到,则将其作为注释追加到文件中。

  • 创建脚本example.sh
    ~$ vim example.sh # 创建文件,内容如下
    ~$ cat example.sh
    #!/bin/bash
    
    echo "Starting program at $(date)" # date会被替换成日期和时间
    
    echo "Running program $0 with $# arguments with pid $$"
    
    for file in "$@"; do
        grep foobar "$file" > /dev/null 2> /dev/null 
        # "> /dev/null"将stdout输出到"/dev/null"
        # "2> /dev/null"将stderror输出到"/dev/null"
        # 如果模式没有找到,则grep退出状态为 1
    
        if [[ $? -ne 0 ]]; then 
        # 若上面的grep命令返回值不等于0,则:
            echo "File $file does not have any foobar, adding one"
            echo "# foobar" >> "$file"
        fi
    done
    
  • 执行脚本
    ~$ source example.sh mcd.sh example.sh 
    # 把mcd.sh和example.sh作为两个参数,执行脚本example.sh
    Starting program at 2023年 03月 26日 星期日 22:50:22 CST
    Running program bash with 2 arguments with pid 11646
    File mcd.sh does not have any foobar, adding one
    
    ~$ cat mcd.sh   # 文件末尾被加上了一句"# foobar"
    mcd () {
        mkdir -p "$1"
        cd "$1"
    }
    
    # foobar
    

Bash实现了许多类似的比较操作(如上面的-ne),可以查看 test 手册

在bash中进行比较时,尽量使用双方括号 [[ ]] 而不是单方括号 [ ],这样会降低犯错的几率,尽管这样并不能兼容 sh。 更详细的说明参见这里

通配符


通配符(globbing) :当你想要利用通配符进行匹配时,你可以分别使用 ?* 来匹配一个或任意个字符。

花括号{} - 当你有一系列的指令,其中包含一段公共子串时,可以用花括号来自动展开这些命令。这在批量移动或转换文件时非常方便。

~$ mkdir test
~$ cd ./test
~/test$ touch foo{1,2,10}
~/test$ ls
foo1  foo10  foo2
~/test$ rm foo?
# "?"只匹配一个字符,所以只删除了"foo1"和"foo2"
~/test$ ls
foo10
~/test$ rm foo*
# "*"能匹配任意个字符,所以能删除"foo10"
~/test$ ls
~/test$ cd ..
~$ rmdir test
convert image.{png,jpg}
# 与下行的命令效果一致
convert image.png image.jpg

cp /path/to/project/{foo,bar,baz}.sh /newpath
# 等价于
cp /path/to/project/foo.sh /path/to/project/bar.sh /path/to/project/baz.sh /newpath

mv *{.py,.sh} folder
# 移动所有 *.py 和 *.sh 文件到folder

mkdir foo bar

# 下面命令会创建foo/a, foo/b, ... foo/h, bar/a, bar/b, ... bar/h这些文件
touch {foo,bar}/{a..h}
touch foo/x bar/y
# 比较文件夹 foo 和 bar 中包含文件的不同
diff <(ls foo) <(ls bar)
# 输出
# < x
# ---
# > y

编写 bash 脚本有时候会很别扭和反直觉。例如 shellcheck 这样的工具可以帮助你定位sh/bash脚本中的错误。

注意,脚本并不一定只有用 bash 写才能在终端里调用。比如说,这是一段 Python 脚本,作用是将输入的参数倒序输出:

~$ vim script.py 
~$ cat script.py # 根据你自己的系统选择python或python3的路径
#!/usr/bin/env python3 
import sys
for arg in reversed(sys.argv[1:]):
    print(arg)
~$ chmod 777 script.py # 赋予脚本可执行权限
~$ python3 script.py a b c
c
b
a
~$ ./script.py a b c
c
b
a

shebang 行(上面脚本的第一行)中使用 env 命令(man env查询手册)是一种好的实践,它会利用环境变量中的程序来解析该脚本,这样就提高来您的脚本的可移植性。

shell函数和脚本有如下一些不同点:

  1. 函数只能与shell使用相同的语言,脚本可以使用任意语言。因此在脚本中包含 shebang 是很重要的。
  2. 函数仅在定义时被加载,脚本会在每次被执行时加载。这让函数的加载比脚本略快一些,但每次修改函数定义,都要重新加载一次。
  3. 函数会在当前的shell环境中执行,脚本会在单独的进程中执行。因此,函数可以对环境变量进行更改,比如改变当前工作目录,脚本则不行。脚本需要使用 export 将环境变量导出,并将值传递给环境变量。
  4. 与其他程序语言一样,函数可以提高代码模块性、代码复用性并创建清晰性的结构。shell脚本中往往也会包含它们自己的函数定义。

Shell 工具

查看命令如何使用


看到这里,您可能会有疑问,我们应该如何为特定的命令找到合适的标记呢?例如 ls -l, mv -imkdir -p

最常用的方法是为对应的命令行添加-h--help 标记,如ls -hls --help

另外一个更详细的方法则是使用man 命令,如man lsman 命令是手册(manual)的缩写,它提供了命令的用户手册。

在交互式的、基于字符处理的终端窗口(如vim)中,一般也可以通过 :help 命令或键入 ? 来获取帮助。

有时候手册内容太过详实,让我们难以在其中查找哪些最常用的标记和语法。 TLDR pages 是一个很不错的替代品,它提供了一些案例,可以帮助您快速找到正确的选项。

~ $ sudo apt install tldr
~ $ sudo tldr -u
# 更新数据库
~ $ sudo tldr touch 
# 查询结果中,包含示例,可以直接复制后修改使用

查找文件


find

程序员们面对的最常见的重复任务就是查找文件或目录。所有的类UNIX系统都包含一个名为 find 的工具,它是 shell 上用于查找文件的绝佳工具。find命令会递归地搜索符合条件的文件,例如:

# . 代表当前目录下的查找
# 查找所有名称为src的文件夹
find . -name src -type d

# 查找所有文件夹路径中包含test的python文件
find . -path '*/test/*.py' -type f

# 查找前一天修改的所有文件
find . -mtime -1

# 查找所有大小在500k至10M的tar.gz文件
find . -size +500k -size -10M -name '*.tar.gz'

# 删除全部扩展名为.tmp 的文件
find . -name '*.tmp' -exec rm {} \;

# 查找全部的 PNG 文件并将其转换为 JPG
find . -name '*.png' -exec convert {} {}.jpg \;

尽管 find 用途广泛,它的语法却比较难以记忆。例如,为了查找满足模式 PATTERN 的文件,您需要执行 find -name '*PATTERN*' (如果您希望模式匹配时是不区分大小写,可以使用-iname选项)

fd

fd 就是一个更简单、更快速、更友好的程序。它有很多不错的默认设置,例如输出着色、默认支持正则匹配、支持unicode并且它的语法更符合直觉。以模式PATTERN 搜索的语法是 fd PATTERN

# ubuntu系统下
~$ sudo apt install fd-find
locate

如果不想每次都搜索文件而是通过编译索引或建立数据库的方式来实现更加快速地搜索,这就要靠 locate 了。

locate 使用一个由 updatedb负责更新的数据库,在大多数系统中 updatedb 都会通过 cron 每日更新。这便需要我们在速度和时效性之间作出权衡。而且,find 和类似的工具可以通过别的属性比如文件大小、修改时间或是权限来查找文件,locate则只能通过文件名。

这里有一个更详细的对比。其中一些简短的结论如下:

  • locate has only one big advantage over find : speed
  • find, though, has many advantages over locate
  • locate is better when you’re just trying to find a particular file by name, which you know exists, but you just don’t remember where it is exactly.
  • find is better when you have a focused area to examine, or when you need any of its many advantages

查找代码


一个最常见的场景是希望查找具有某种模式的全部文件,并找它们的位置。

为了实现这一点,很多类UNIX的系统都提供了grep命令,它是用于对输入文本进行匹配的通用工具。

grep 有很多选项,这也使它成为一个非常全能的工具。其中我经常使用的有 -C :获取查找结果的上下文(Context);-v 将对结果进行反选(Invert),也就是输出不匹配的结果。举例来说, grep -C 5 会输出匹配结果前后五行。当需要搜索大量文件的时候,使用 -R 会递归地进入子目录并搜索所有的文本文件。

grep也有很多替代品,包括 ack, agrg。它们都特别好用,但是功能也都差不多,我比较常用的是 ripgrep (rg) ,因为它速度快,而且用法非常符合直觉。例子如下:

# 查找所有使用了 requests 库的文件
rg -t py 'import requests'
# 查找所有没有写 shebang 的文件(包含隐藏文件)
rg -u --files-without-match "^#!"
# 查找所有的foo字符串,并打印其之后的5行
rg foo -A 5
# 打印匹配的统计信息(匹配的行和文件的数量)
rg --stats PATTERN

find/fd 一样,重要的是你要知道有些问题使用合适的工具就会迎刃而解,而具体选择哪个工具则不是那么重要。

查找 shell 命令


history命令

在shell终端,按向上的方向键会显示你使用过的上一条命令,继续按上键则会遍历整个历史记录。

history 命令会在标准输出中打印shell中的里面命令。如果我们要搜索历史记录,则可以利用管道将输出结果传递给 grep 进行模式搜索。 history | grep find 会打印包含find子串的命令。

关于history命令的更多配置,可以参考这里

# 几个与history命令相关的变量,可以在文件~/.bashrc或~/.bash_profile中设置
HISTSIZE:控制缓冲区历史记录的最大个数
HISTFILESIZE:控制历史记录文件(~/.bash_history)中的最大个数
HISTIGNORE:设置哪些命令不记录到历史记录
HISTTIMEFORMAT:设置历史命令显示的时间格式
HISTCONTROL:扩展的控制选项
对历史记录回溯搜索

对于大多数的shell来说,可以使用 Ctrl+R 对命令历史记录进行回溯搜索。敲 Ctrl+R 后您可以输入子串来进行匹配,查找历史命令行。反复按下就会在所有搜索结果中循环。实际界面可参考如下:

~ $ ls -a           
bck-i-search: ls_    

Ctrl+R 可以配合 fzf 使用。fzf 是一个通用对模糊查找工具,它可以和很多命令一起使用。这里我们可以对历史命令进行模糊查找并将结果以赏心悦目的格式输出。

基于历史记录的自动补全

这一特性最初是由 fish shell 创建的,它可以根据最近使用过的开头相同的命令,动态地对当前对shell命令进行补全。

这一功能以及上一项的回溯搜索功能,都可在 zsh 中使用,极大的提高用户体验。

文件夹导航


之前对所有操作我们都默认一个前提,即您已经位于想要执行命令的目录下,但是如何才能高效地在目录 间随意切换呢?有很多简便的方法可以做到,比如设置alias,使用 ln -s 创建符号连接等。

  • 创建链接的方式
 - Create a symbolic link to a file or directory:
   ln -s {{/path/to/file_or_directory}} {{path/to/symlink}}

 - Overwrite an existing symbolic link to point to a different file:
   ln -sf {{/path/to/new_file}} {{path/to/symlink}}

 - Create a hard link to a file:
   ln {{/path/to/file}} {{path/to/hardlink}}

symbolic link类似于windows中的快捷方式,与之相对的是hard link硬链接必须在同一个文件系统中;一般用户权限下的硬链接只能用于文件,不能用于目录)

  • 确认一个链接是否是symbolic link
ls -l <path-to-assumed-symlink>
# 若返回"l",说明是symbolic link
  • 删除一个symbolic link(两种方法均可)
unlink <path-to-symlink>
rm <path-to-symlink>
  • 查找某一目录下的broken links并删除

broken links,即链接指向的原文件已被移动或删除,从而失效的链接。

find {{/path/to/directory}} -xtype l -delete

由于本课程的目的是尽可能对你的日常习惯进行优化。因此,我们可以使用fasdautojump 这两个工具来查找最常用或最近使用的文件和目录。

Fasd 基于 frecency 对文件和文件排序,也就是说它会同时针对频率(frequency)和时效(recency)进行排序。默认情况下,fasd使用命令 z 帮助我们快速切换到最常访问的目录。例如, 如果您经常访问/home/user/files/cool_project 目录,那么可以直接使用 z cool 跳转到该目录。对于 autojump,则使用j cool代替即可。

还有一些更复杂的工具可以用来概览目录结构,例如 tree, broot 或更加完整的文件管理器,例如 nnnranger

课后练习


  1. 阅读 man ls ,然后使用ls 命令进行如下操作:
  • 所有文件(包括隐藏文件)
  • 文件打印以人类可以理解的格式输出 (例如,使用454M 而不是 454279954)
  • 文件以最近访问顺序排序
  • 以彩色文本显示输出结果

典型输出如下:

-rw-r--r--   1 user group 1.1M Jan 14 09:53 baz
drwxr-xr-x   5 user group  160 Jan 14 09:53 .
-rw-r--r--   1 user group  514 Jan 14 06:42 bar
-rw-r--r--   1 user group 106M Jan 13 12:12 foo
drwx------+ 47 user group 1.5K Jan 12 18:08 ..
  1. 编写两个bash函数 marcopolo 执行下面的操作。 每当你执行 marco 时,当前的工作目录应当以某种形式保存,当执行 polo 时,无论现在处在什么目录下,都应当 cd 回到当时执行 marco 的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh 中,并通过 source marco.sh命令,(重新)加载函数。

  2. 假设您有一个命令,它很少出错。因此为了在出错时能够对其进行调试,需要花费大量的时间重现错误并捕获输出。 编写一段bash脚本,运行如下的脚本直到它出错,将它的标准输出和标准错误流记录到文件,并在最后输出所有内容。 加分项:报告脚本在失败前共运行了多少次。

    #!/usr/bin/env bash
    
    n=$(( RANDOM % 100 ))
    
    if [[ n -eq 42 ]]; then
    echo "Something went wrong"
    >&2 echo "The error was using magic numbers"
    exit 1
    fi
    
    echo "Everything went according to plan"
    
  3. 本节课我们讲解的 find 命令中的 -exec 参数非常强大,它可以对我们查找的文件进行操作。但是,如果我们要对所有文件进行操作呢?例如创建一个zip压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar 则需要从参数接受输入。这里我们可以使用xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。

    您的任务是编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs的参数-d,译注:MacOS 上的 xargs没有-d查看这个issue

    如果您使用的是 MacOS,请注意默认的 BSD findGNU coreutils 中的是不一样的。你可以为find添加-print0选项,并为xargs添加-0选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装

  4. (进阶)编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?

习题解答


  1. 阅读 man ls ,然后使用ls 命令进行如下操作:
  • 所有文件(包括隐藏文件) :-a

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

  • 文件以最近访问顺序排序:-t

  • 以彩色文本显示输出结果--color=auto

    ~$ man ls
    # 对应题目要求的各个相关参数摘录如下:
    # -a, --all : do not ignore entries starting with .
    # -h, --human-readable : with -l and -s, print sizes like 1K 234M 2G etc.
    # -t : sort by time, newest first; see --time
    # --color[=WHEN] : colorize  the output; WHEN can be 'always' (default if omitted), 'auto', or 'never'; more info below
    ~$ ls -l -a -h -t --color
    
  1. 编写两个bash函数 marco 和 polo 执行下面的操作。 每当你执行 marco 时,当前的工作目录应当以某种形式保存,当执行 polo 时,无论现在处在什么目录下,都应当 cd 回到当时执行 marco 的目录。 为了方便debug,你可以把代码写在单独的文件 marco.sh 中,并通过 source marco.sh命令,(重新)加载函数。通过source 来加载函数,随后可以在 bash 中直接使用。

    #!/bin/bash
    marco(){
        echo "$(pwd)" > $HOME/marco_history.log
        # 将当前目录保存在绝对路径下的一个文件
        echo "save pwd $(pwd)"
    }
    polo(){
        cd "$(cat "$HOME/marco_history.log")"
    }  
    

    或者

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

    #!/usr/bin/env bash
    
    n=$(( RANDOM % 100 ))
    
    if [[ n -eq 42 ]]; then
        echo "Something went wrong"
        >&2 echo "The error was using magic numbers"
        exit 1
    fi
    
    echo "Everything went according to plan"
    

    将这个脚本写入~/missing/buggy.sh文件中

  • 使用 while 循环完成

    count=1
    
    while true
    do
        ./buggy.sh &>> out.log
        # ./buggy.sh 遇到 exit1 时,不会退出当前命令行窗口
        # 本行命令等价于./buggy.sh >> out.log 2>&1
        if [[ $? -ne 0 ]]; then
            cat out.log
            echo "failed after $count times"
            break
        fi
        ((count++))
    
    done
    
  • 使用 for 循环完成

    for ((count=1;;count++))
    do
        ./buggy.sh &>> out.log
        if [[ $? -ne 0 ]]; then
            cat out.log
            echo "failed after $count times"
            break
    
        echo "$count try"
        fi
    done
    
  • 使用 until 完成(注意:该方法需要先运行一次./buggy.sh,若这次运行刚好出错,那该方法将失效)

    #!/usr/bin/env bash
    count=0
    until [[ "$?" -ne 0 ]];
    do
        count=$((count+1))
        ./buggy.sh &>> out.log
    done
    
    cat out.log
    echo "failed after $count times"
    

    执行测试脚本

    ~/missing$ vim ./debug.sh
    ~/missing$ ./debug.sh
    failde after 34 times
    Something went wrong
    The error was using magic numbers
    
  • 另一种方法

    ~$ vim buggy.sh
    ~$ cat buggy.sh
    #!/usr/bin/bash
    
    n=$((RANDOM % 100))
    
    if [[ n -eq 42 ]]; then
        echo "Something went wrong"
        >&2 echo "The error was using magic numbers"
        # ">&2" : stderror标准错误输出,&无实质意义; >1为stdout标准输出
        exit 1
    fi
    
    echo "Everything went according to plan"
    ~$ chmod 777 buggy.sh
    # 添加可执行权限
    

    测试buggy.sh的输出

    ~$ ./buggy.sh >> record.log 2>&1
    # "2>&1" 将stderror重定向为stdout输出,使得buggy.sh中的所有echo语句都能输出到record.log
    ~$ cat record.log
    Everything went according to plan
    ~$ rm record.log
    

    设计测试脚本并执行

    ~$ vim debug.sh 
    ~$ cat debug.sh 
    #!/usr/bin/bash
    let count=0
    
    while  ./buggy.sh >> record.log  2>&1 ;
    do let count=count+1;
    done;	
    
    echo "The script $0 had run successfully for $count times before it crashed."
    ~$ source debug.sh
    The script buggy.sh had run successfully for 30 times before it crashed.
    ~$ vim record.log
    # 通过查看vim窗口下方显示的文件行数,可以验证debug.sh的正确性
    
  1. 本节课我们讲解的 find 命令中的 -exec 参数非常强大,它可以对我们查找的文件进行操作。 如果我们要对所有文件进行操作呢?例如创建一个zip压缩文件?我们已经知道,命令行可以从参数或标准输入接受输入。在用管道连接命令时,我们将标准输出和标准输入连接起来,但是有些命令,例如tar 则需要从参数接受输入。这里我们可以使用xargs 命令,它可以使用标准输入中的内容作为参数。 例如 ls | xargs rm 会删除当前目录中的所有文件。您的任务是编写一个命令,它可以递归地查找文件夹中所有的HTML文件,并将它们压缩成zip文件。注意,即使文件名中包含空格,您的命令也应该能够正确执行(提示:查看 xargs的参数-d)译注:MacOS 上的 xargs没有-d,查看这个issue

    如果您使用的是 MacOS,请注意默认的 BSD find 与GNU coreutils 中的是不一样的。你可以为find添加-print0选项,并为xargs添加-0选项。作为 Mac 用户,您需要注意 mac 系统自带的命令行工具和 GNU 中对应的工具是有区别的;如果你想使用 GNU 版本的工具,也可以使用 brew 来安装。

    1. 首先创建所需的文件

      ~$ mkdir -p ./tmp/html_root
      ~$ cd ./tmp/html_root
      ~/tmp/html_root$ touch {1..10}.html
      ~/tmp/html_root$ mkdir html
      ~/tmp/html_root$ cd html
      ~/tmp/html_root/html$ touch xxxx.html
      ~/tmp/html_root/html$ cd ~/tmp 
      ~/tmp$ tree
      .
      └── html_root
          ├── 10.html
          ├── 1.html
          ├── 2.html
          ├── 3.html
          ├── 4.html
          ├── 5.html
          ├── 6.html
          ├── 7.html
          ├── 8.html
          ├── 9.html
          └── html
              └── xxx.html
      
      2 directories, 11 files
      
    2. 执行 find 命令

      #for MacOS
      find html_root -name "*.html" -print0 | xargs -0 tar vcf html.zip
      
      #for Linux
      ~/tmp$ find . -type f -name "*.html" | xargs -d '\n'  tar -cvzf html.zip
      ./html_root/3.html
      ./html_root/8.html
      ./html_root/6.html
      ./html_root/10.html
      ./html_root/4.html
      ./html_root/html/xxx.html
      ./html_root/1.html
      ./html_root/9.html
      ./html_root/7.html
      ./html_root/5.html
      ./html_root/2.html
      
  2. (进阶) 编写一个命令或脚本递归的查找文件夹中最近使用的文件。更通用的做法,你可以按照最近的使用时间列出文件吗?

    ~/tmp $ find . -type f -print0 | xargs -0 ls -lt | head   
    -rw-rw-r-- 1 laihj laihj 230  427 23:30 ./html.zip
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/html/xxx.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/10.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/1.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/2.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/3.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/4.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/5.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/6.html
    -rw-rw-r-- 1 laihj laihj   0  427 23:26 ./html_root/7.html
    

    使用head参数,显示文件夹最近使用的文件

    ~/tmp $ find . -type f -print0 | xargs -0 ls -lt | head -1
    -rw-rw-r-- 1 laihj laihj 230  427 23:30 ./html.zip
    

    当文件数量较多时,上面的解答会得出错误结果,解决办法是增加 -mmin 条件(仅在多少分钟内修改的文件中查找),先将最近修改的文件进行初步筛选再交给ls进行排序显示,例如

    ~$ find . -type f -mmin -60 -print0 | xargs -0 ls -lt | head -10
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值