请注意所有 PowerShell 指的是 pwsh7。
Shell
Shell 是个老东西了。我想,没有人会喜欢写 Bash 脚本的:
# 变量赋值:等号前后不能有空格,违反直觉name="Harusaruhi"# name = "Harusaruhi" # 这样写会报错!
# 方括号内必须有空格,否则报错if [ $name = "Harusaruhi" ]; then echo "匹配"fi# if [$name = "Harusaruhi"]; then # 错误:缺少空格# if [ $name="Harusaruhi" ]; then # 错误:等号前后不能有空格
# 字符串比较和数值比较用不同符号num1=10num2=20if [ $num1 -lt $num2 ]; then # 数值比较用 -lt echo "数值比较"fiif [ "$num1" \< "$num2" ]; then # 字符串比较用 \< (需要转义) echo "字符串比较"fi
# 数学运算的多种诡异语法result1=$((num1 + num2)) # 方式1:双括号result2=`expr $num1 + $num2` # 方式2:expr命令79 collapsed lines
result3=$(expr $num1 + $num2) # 方式3:$()包裹exprecho "结果: $result1, $result2, $result3"
# 字符串处理的反人类语法text="Hello World"echo ${text#Hello } # 删除前缀echo ${text%World} # 删除后缀echo ${text/World/Universe} # 替换
# 数组的诡异语法arr=("apple" "banana" "orange")echo ${arr[@]} # 访问所有元素 - @符号的含义不明echo ${#arr[@]} # 数组长度 - 完全无法直观理解
# 参数?echo "脚本名: $0"echo "参数个数: $#"echo "所有参数: $@"echo "上个命令退出状态: $?"
# 循环语法的不一致性
# for循环有多种奇怪的形式for i in 1 2 3; do echo "数字: $i"done
# 另一种for循环语法for ((i=1; i<=3; i++)); do echo "计数: $i"done
# while循环counter=1while [ $counter -le 3 ]; do echo "While: $counter" counter=$((counter + 1))done
# 函数定义和调用function_name() { # 函数参数通过$1, $2...访问,没有参数名 echo "函数参数1: $1" echo "函数参数2: $2" # 返回值只能是数字,且通过$?获取 return 114514}
# 调用函数function_name "hello" "world"echo "函数返回值: $?"
# 奇怪语法if [ -f "file.txt" ]; then echo "文件存在"fiif [ -d "directory" ]; then echo "目录存在"fi
# 引号echo 'Single quotes: $name will not be expanded'echo "Double quotes: $name will be expanded"echo `Command substitution with backticks`echo $(Command substitution with dollar parentheses)
# 逻辑运算符if [ $num1 -gt 5 ] && [ $num2 -lt 30 ]; then echo "逻辑与"fi
if [ $num1 -gt 5 -a $num2 -lt 30 ]; then echo "另一种逻辑与"fi
echo "当前进程ID: $$"echo "后台进程ID: $!"echo "当前选项: $-"echo "随机数: $RANDOM"
其抽象的操作符和滥用的特殊符号,可能在当时非常重要,因为存储空间和屏幕都非常有限,但是在现代只会导致代码可读性极差并且维护困难。函数设计也非常的 Shell,通过数字 index 来访问参数更是让整个逻辑的理解难上加难,没有一点信息量。
然后我们有了 PowerShell
和 Nushell
。
走进新时代:面向对象与数据驱动
微软,Microsoft,我们罪该万死的 M$,终于没有遵循可笑的 Unix 哲学,终于引入了对象,淘汰了文本流。我们现在有了:
function Process-UserData { param( [string]$Username, # 类型! [string]$Email, # 变量名! [string]$OutputFile )
# 面向对象的操作 $user = Get-User -Name $Username # 可读的 Builtin Command! $user.Email = $Email $user | Export-Csv -Path $OutputFile}
但是,遗老们就开始发力了。我不明白为什么有些人会故意的把 *sh 写的晦涩难懂,故意使用缩写而不是完整的 argument 和 option:
find . -name "*.log" -mtime +7 -exec rm {} \;grep -r "ERROR" /var/log|cut -d: -f1|sort|uniq -c|sort -nr
仅仅展开缩写就能够极大的提高可读性和可维护性:
find . --name "*.log" --mtime +7 --exec rm {} \;grep --recursive "ERROR" /var/log | cut --delimiter=: --fields=1 | sort | uniq --count | sort --numeric-reverse
真正的 Unix 用户只需要 grep、sed、awk 就能解决99%的问题,为什么要用一个臃肿的对象系统?
Unix 的管道哲学是用简单工具组合复杂功能,PowerShell 却想用一个复杂工具解决所有问题
这种设计哲学与 Unix 的极简主义背道而驰
这些都是常见的言论,我只能说 COM 设计的比 Pipeline + FileSink 好不是没有原因的。
不止 PowerShell,Nushell 也添加了基本的类型系统(尽管并不丰富):Integers、Strings、Tables、Any……一个完备可用的类型系统能够极大的提高开发和除错的效率,使得整个流程更加可控、现代化。我们必须认识到,大部分的数据都不是文本和数据流,而是有结构的:使用awk处理一个有向无环图试试?
类型系统和对象的引入使得编写的脚本更加健壮,更加结构化;IDE 也能够通过类型信息自动生成文档和提供提示。
结构化数据处理
与简单工具组合复杂功能相比,现代 Shell 可太好用了:
curl -s "https://api.github.com/repos/octocat/Hello-World" | \ grep '"name"' | \ head -1
$repoInfo = Invoke-RestMethod "https://api.github.com/repos/octocat/Hello-World"$repoInfo.name
# 其他11 collapsed lines
# 直接处理CSV$csvData = Import-Csv "employees.csv"$csvData | Where-Object {$_.Department -eq "IT"} | Select-Object Name, Salary
# 直接处理XML$xmlData = [xml](Get-Content "config.xml")$xmlData.configuration.appSettings.add | Where-Object {$_.key -eq "DatabaseUrl"}
# 直接处理Excel文件$excelData = Import-Excel "report.xlsx"$excelData | Group-Object Department | Select-Object Name, Count
http get "https://api.github.com/repos/nushell/nushell" | get name
# 其他11 collapsed lines
# 处理CSV数据open employees.csv | where department == "IT" | select name salary
# 处理YAML配置open config.yaml | get database.connections | where active == true
# 处理TOML文件open Cargo.toml | get dependencies | columns
# 处理XML数据open data.xml | get root.items.item | where status == "active"
臃肿的对象系统能够带来类型安全、性能优化以及更好的错误处理。直接加载结构化数据的能力代表了 Shell 脚本的一个重要演进方向。它不仅提高了开发效率,还大大降低了出错概率,使得复杂的数据处理任务变得简单直观。这是软件工程向更高层次抽象发展的必然结果。正如我们不再用汇编语言编写应用程序一样,在数据处理领域,我们也应该拥抱更高效、更安全的工具和方法。
Builtins:告别依赖地狱
传统的 Unix Shell 信奉”小工具组合”,结果就是每个脚本都要依赖一堆外部工具。写个处理 JSON 的脚本?你需要 curl
、jq
、head
、tail
、grep
、sed
、awk
……天知道用户的系统上有没有装这些东西,更别说版本兼容性了。
curl -s "$API_URL" | jq '.items[]' | head -10find /tmp -name "*.log" -exec gzip {} \;date -d "2024-01-01" +%s # GNU datedate -j -f "%Y-%m-%d" "2024-01-01" +%s # BSD date,完全不同的语法!
这种设计带来的问题显而易见:环境依赖、版本差异、安全风险、性能损失。每个外部命令都是一个新进程,每个新进程都是潜在的攻击面。
现代 Shell 的内置命令解决了这些问题:
$data = Invoke-RestMethod $API_URL$data.items | Select-Object -First 10
Get-ChildItem /tmp -Filter "*.log" | Compress-ArchiveGet-Date "2024-01-01" | Get-Date -UFormat %s # 跨平台一致
http get $API_URL | get items | first 10ls /tmp | where name =~ "\.log$" | each { gzip $in.name }"2024-01-01" | into datetime | format date "%s"
- 安全性提升:内置命令无法被外部劫持,
which curl
可能返回恶意替代品,但Get-Command Invoke-RestMethod
总是返回可信的内置命令。 - 性能优化:传统方式处理1000个文件需要创建3000个进程,现代内置命令在同一进程内完成所有操作。
- 一致性保证:不再需要处理 GNU coreutils 和 BSD 工具之间的差异,内置命令在所有平台上行为一致。
现代 Shell 的内置命令不是臃肿,而是必要的现代化。它们提供了类型安全、错误处理、性能优化和一致性保证,这些都是传统外部工具无法提供的。正如我们不再用汇编语言写应用程序一样,在 Shell 脚本领域,我们也应该拥抱更高效、更安全的内置工具集。
Deno Shell
deno_task_shell
是一个跨平台的 Shell 解析器,最初为 deno task
设计,但现在被许多应用嵌入使用。dax
库基于 deno_task_shell
的解析器,提供了在 Linux、Mac 和 Windows 上一致的 Shell 语法支持。
这种嵌入式 Shell 的设计理念确实很符合现代应用的需求:不是要做一个完整的 Shell,而是提供一个可预测、跨平台的 Shell 子集,让应用开发者不用再为不同操作系统的 Shell 差异而烦恼。
可移植性和可预测性比完整功能更重要。我们不需要一个功能完备的 Shell,我们需要的是一个在任何地方都能正常工作的 Shell。
新的问题
说了这么多好处,难道就没有缺点吗?有的,主要是大。Nushell
的典型 musl 分发大小在 60 MiB
左右,这使得其不能够在路由器、嵌入式上正常使用。PowerShell 更是直接分发了 .NET RT,以提供完整的CLR支持(是的你可以在 pwsh 中调用 C# 和玩反射)。相比之下,传统的 /bin/sh
可能只有几百 KB,bash 也不过几 MB。
这在软件设计中是一个永恒的矛盾。现代 Shell 为了提供类型安全、丰富的内置命令和跨平台支持,基本上不可能拥有小的体积,但是这些嵌入式应用场景,反而在一定程度上正是 Shell 的用武之地。
依旧是地狱
说了这么多,现代的 sHELL 在哪里呢?在源里。没有一个是某个操作系统的默认 shell,这使得其脚本分发变得非常困难。现代的 Shell 很难到最需要它们的地方去,我们依然在处理可能在千禧年就处理过的问题。
相关资源: