计算机教育中缺失的一课笔记
Shell脚本
单引号和双引号
❯ foo=bar
❯ echo $foo
bar
❯ echo '$foo'
$foo
❯ echo "$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
搜索可以使用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' 命令可能是一个更安全的选择。