【缺失的一课2】

shell工具和脚本

Posted by Kody Black on December 31, 2022

计算机教育中缺失的一课笔记

Shell脚本

单引号和双引号

foo=bar
❯ echo $foo
bar
❯ echo '$foo'
$fooecho "$foo"
bar

单引号中的所有字符都是字面含义,而双引号中定义的字符串会将变量值进行替换。

参考:https://www.gnu.org/software/bash/manual/html_node/Quoting.html

脚本参数

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

脚本实例1

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

#!/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
    # 如果模式没有找到,则grep退出状态为 1
    # 我们将标准输出流和标准错误流重定向到Null,因为我们并不关心这些信息
    if [[ $? -ne 0 ]]; then
        echo "File $file does not have any foobar, adding one"
        echo "# foobar" >> "$file"
    fi
done

#! 这部分内容叫做Shebang,在unix系统中,程序加载器会分析Shebang后面的内容,相当于会用/bin/bash执行这段脚本(对于其他脚本都适用,例如Python脚本)

使用建议:建议在Shebang中使用env,它会利用环境变量重大程序来解析该脚本,这样就可以提高脚本的可移植性。例如上述Shebang可以改为#!/usr/bin/env bash

$(CMD) 会返回CMD的输出结果

idea:如果有一个恶意文件aaa,那么它可以通过命令./$(ls aaa)执行

grep foobar "$file" > /dev/null 2> /dev/null

上面的语句用于在$file中搜索foobar,主要注意后面的重定向

重定向是把输出定向到文件或者标准流,重定向符有两个:

> 以覆盖的方式重定向输出到文件

>> 以追加的方式重定向输出到文件

/dev/null 是一类伪设备(并没有实际设备对应,用于操作系统处理一些功能)它会接受并丢弃所有输出入,并且不产生输出。

2 是文件描述符,表示标准错误输出(另外0表示标准输入,1表示标准输出),默认重定向仅仅是将标准输出重定向。

# 比如输入一个错误的指令
❯ pwdd
zsh: command not found: pwdd
# 这里重定向了标准错误输出,没有输出显示
❯ pwdd 2>/dev/null
❯ pwdd >/dev/null
zsh: command not found: pwdd

另外,这个语句还可以这么写

grep foobar "$file" > /dev/null 2> &1

最后的&1表示标准输出,2>&1就是把标准错误输出重定向到标准输出,而标准输出重定向到了/dev/null,最后的意思和grep foobar "$file" > /dev/null 2> /dev/null一样

shell语法

管道在shell中认为是用字符 分隔的一组命令,其格式为[time [-p]] [ ! ] command [ | command2 ... ]
  • time作为前缀会在管道终止后给出执行用时(-p表示将输出复合POSIX指定的格式)

  • !作为前缀表示返回值为原来命令返回状态的逻辑非值。

list(序列)是一个或多个管道,用操作符&&||分隔的序列, 并且可以选择用 ;, &或换行结束

  • &作为结束符,shell将在后台的子shell中执行这个命令,当前shell不会等待命令结束,返回状态总是0
  • ;作为结束符表示命令顺序执行,shell会等待每个命令一次结束,返回最后执行命令的返回状态
  • &&||分别代表AND和OR序列(注意它们都有短路效应)

复合命令包括以下几种情况:

  • (list)

    这时list将在一个子shell中执行,变量赋值和影响 shell 环境变量的内建命令在命令结束后不会再起作用。 返回值是序列的返回值。

  • { list; }

    我的理解是作为一个命令块

  • ((expression))

    表达式expression将会被求值

    echo $((22312/23))
    970
    
  • [[ expression]][ expression ]

    用[]包裹的命令叫做test命令,POSIX为其定义了一系列功能集,实际上它主要是计算里面的表达式;而[[]]的功能更加强大,但是[[]]实际上是bash里面的一个关键字,并不适用于原始的sh

    参考:http://mywiki.wooledge.org/BashFAQ/031

    推荐做法:追求通用性用[],但是比较麻烦;对于字符串和文件的计算用[[]];对于数值的计算用(())

  • for name [ in word ] ; do list ; done

  • for (( expr1 ; expr2 ; expr3 )) ; do list ; done

  • for (( expr1 ; expr2 ; expr3 )) ; do list ; done

  • case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac

  • if list; then list; [ elif list; then list; ] ... [ else list; ] fi

  • while list; do list; done

  • ` until list; do list; done`

  • [ function ] name () { list; }

    定义了一个名为name的函数

引用用来去掉特定字符或词的特殊意义。引用可以用来禁止对特殊字符的处理, 阻止保留字被识别,还用来阻止参数的扩展。

  • 转义字符

    \ 用于保留下一个字符的字面意义,但如果后面接换行,则表示忽略换行

  • 单引号

    将字符放在单引号之中,将保留引用中所有字符的字面意义 。单引号不能包含在单引号引用之中,即使前面加上了反斜杠。

  • 双引号

    将字符放在双引号中,同样保留所有字符的字面意义,但是存在例外情况

    注意:$`和\在双引号中仍然具有特殊意义 
    

以上大部分内容来自man bash 有时间了可以再看看

帮助命令及实用工具

帮助命令首先自然是man CMD

推荐:tldr(试了一下很nice),汉语的话还有how

搜索可以使用find或者安装fd

使用fzf也可以提供很多的便利,但是我只用到了它的历史记录功能和目录搜索显示功能,和vim结合的内容还没有研究。

课后练习

# 第一题
ls -alth

# 第二题
marco(){
        mar=$(pwd)
}

polo(){
        cd $mar
}

# 第三题
# test.sh
#!/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"
 
#runtest.sh
#!/usr/bin/env bash

touch log.txt
count=-1
while [[ $? -eq 0 ]];
do
((count = $count + 1))
bash ./test.sh >>log.txt 2>>log.txt
done

cat log.txt && rm log.txt
echo "运行了$count次"

#第四题
❯ find . -name '*.html' -type f -print0 | xargs -0 tar czf html.tar.gz
❯ tar tvf html.tar.gz
-rw-rw-r-- kody/kody       197 2022-12-31 20:44 ./b.html
-rw-rw-r-- kody/kody       197 2022-12-31 20:25 ./hello world.html
-rw-rw-r-- kody/kody       197 2022-12-31 20:23 ./bar/a.html

# 第五题
find . -type f | ls -l | sort -k 5 | head

答案理解:

第二题中,marco函数使用了export mar=$(pwd)这样的方式,相比与直接mar=$(pwd),export的方法可以将变量mar导出到子shell中,导出的变量可以被子 shell 使用,也可以被子 shell 的子 shell 使用。导出的变量会覆盖同名的环境变量。而直接赋值的变量只会在当前shell中可用。另外,如果变量在子sehll中被修改,并不会改变原shell中的值。

在第三题中bash ./test.sh >>log.txt 2>>log.txt也可以写作bash ./test.sh &>>log.txt,但是实际上后者的&>>操作符不是POSIX兼容的,例如在dash中会被当做错误语法导致脚本执行失败。

((count = $count + 1))可以直接写为((count++))

除了用while,第三题也可用for或者until循环编写

第四题还可以写成find . -name '*.html' -type f | xargs -d '\n' tar czf html.tar.gz因为find默认的间隔符为换行,在xargs中也用-d设置间隔符为换行即可。

第五题我做的不对,这样得到的结果同一天的内容会把更早的显示在前面,不符合要求,其实根本不需要sort,直接ls -tl就行了,所以初步改成find . -type f | ls -lt | head,然而这样也有问题,最后应该改成find . -type f | xargs -d '\n' ls -lt | head更好。

find . -type f | ls -lt | head
find . -type f | xargs -d '\n' ls -lt | head

上面两个语句的结果是类似的,但是会有一些细微的差别。

  • 第一个语句的结果是,在当前目录下查找文件,并使用 ls -lt 命令显示最后修改时间最近的文件。然后使用 head 命令显示前 10 行。
  • 第二个语句的结果是,在当前目录下查找文件,并使用 xargs -d '\n' 命令将结果逐行传递给 ls -lt 命令。然后使用 head 命令显示前 10 行。

在这两个语句中,第二个语句使用了 xargs -d '\n' 命令,它会将输入的每一行看作一个单独的参数。例如,如果输入的内容是 file1\nfile2\nfile3,那么 xargs -d '\n' 命令会将其转换成 file1 file2 file3。这样做的目的是避免文件名中的空格或其他特殊字符导致的问题。

在这两个语句中,如果当前目录下没有文件,或者文件名中存在空格或其他特殊字符,那么第二个语句的结果可能会更精确。注意,如果文件名中可能存在空格或其他特殊字符,那么使用 xargs -d '\n' 命令可能是一个更安全的选择。