计算机教育中缺失的一课笔记
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'
命令可能是一个更安全的选择。