挑战:用更简洁优美的方式将《程序员修炼之道》中的 Shell 命令转为 PowerShell

2021-01-09 15:36:41 +08:00
 AndyAO

在《程序员修炼之道》的“变换式编程”章节中有这样一段 Shell 命令,其作用是列出当前目录中行数最多的 5 个文件。

$ find . -type f | xargs wc -l | sort -n | tail -6 | head -5
     470 ./debug.pml
     470 ./test_to_build.pml
     487 ./dbc.pml
     719 ./domain_languages.pml
     727 ./dry.pml

我算是 PowerShell 新手,折腾了老长时间之后终于写出来了,总感觉不如 Shell 的优雅,应该还有很大的优化空间。

(Get-ChildItem .\ | ForEach-Object {$_ | Select-Object -Property 'Name', @{label = 'Lines'; expression = {($_ | Get-Content).Length}}} |Sort-Object -Property 'Lines')|Select-Object -Last 5

这里发出自己的答案,如果你有兴趣,而且有信心,能够写出更优雅简洁实现,可以来试试看。


当然不必使用别名和缩写来缩短长度,虽然看起来更简洁了,但损失了可读性。

“实话实说”好像是微软的一贯命名风格,比如说新线程在 Linux 里面用 fork,在 Windows 里面好像就是 creatProcess,感觉是微软的那个命名方法更加具有可读性,况且 PowerShell 中还能设置别名,想要多少短都行。

4563 次点击
所在节点    程序员
38 条回复
hanxiV2EX
2021-01-09 15:40:43 +08:00
shell 的更简洁吧
zhustec
2021-01-09 17:21:18 +08:00
这么长的名称真的就优雅吗?要论命名,shell 也可以用 alias 命名出更具可读性的名字,也可以尽量使用两横的长参数。
不见得 Get-Content 就比 cat 更具可读性,也不见得 Sort-Object 就比 sort 更具可读性。可读性也是要针对有一定 shell 基础的人来说的,也不能说只有一个一点都不懂 shell 的人能看懂才叫可读。
AndyAO
2021-01-09 17:26:23 +08:00
@zhustec #2 当然不一定,因为那个词本身就没有严格的定义
felixcode
2021-01-09 17:28:52 +08:00
PowerShell 这么长的命令,就很依赖自动补全了,而且分行才能显得足够直观,至于优雅,审美比较特别才会显得优雅吧。
zhuangzhuang1988
2021-01-09 17:31:54 +08:00
还是直接 linq 把
geelaw
2021-01-09 17:33:22 +08:00
Get-ChildItem -File | Sort-Object -Descending {
$_ | Get-Content -ReadCount 1 |
Measure-Object | Select-Object -ExpandProperty Count } |
Select-Object -First 5
nightwitch
2021-01-09 20:02:51 +08:00
在 Unix 发明的那个年代,由于硬件水平和软件水平的限制,不少 C 编译器只能识别最多 6-8 个字符的标志符。那个年代的“键盘”并不是现在的结构,你需要用重重的力气才能按下一个键,甚至还要靠纸带打孔机。所以 Unix 的 API 都是能短则短。Linux 作为 Unix 兼容的系统也把这些函数名字继承过来了。

Windows 的前身 Dos 的发明也在 Unix 的十年以后了,而 Windows 本身则更迟了,无论是硬件还是软件的条件都好很多。

Reference:
https://unix.stackexchange.com/questions/214796/why-are-unix-posix-system-call-namings-so-illegible
zhuangzhuang1988
2021-01-09 21:09:00 +08:00
```C#
var dir = "./obj";
var res = (from filePath in Directory.EnumerateFiles(dir)
let lines = File.ReadAllLines(filePath).Count()
orderby lines descending
select (lines, filePath)).Take(5);
foreach (var f in res)
{
Console.WriteLine(f);
}
```
zhuangzhuang1988
2021-01-09 21:13:58 +08:00
```F#
open System.Linq
open System.IO

let dir = "./obj"

Directory.EnumerateFiles(dir)
|> Seq.map (fun filePath -> (File.ReadAllLines(filePath).Count(), filePath))
|> Seq.sortByDescending (fun (count, _) -> count)
|> Seq.take 5
|> Seq.iter (fun pair -> printfn "%A" pair)
```
都比脚本好,清楚, 不鬼画符, 而且 F#, C#也可以当脚本用
FrankHB
2021-01-10 01:36:15 +08:00
@nightwitch 那个说的是系统调用,用得没那么频繁,不用太在乎日常输入效率,反倒是不刻意习惯就容易读到一次恶心一次,强行习惯还会“没用的知识增加了”,自然要纠结 crate 还是 create 的问题。
要说终端交互式使用,就算现在的键盘按起来没那么费劲,成本也不是零,而人干这活往往不会老实严格串行傻等反馈,所以输入一长就麻烦。补全也不是解决方案,因为前缀不长到一定程度,很难预测补出来的东西会是符合预期的,结果负载一大就该纠结“我这个命令到底输入到哪再补全比较好,万一不对该怎么操作”了,严重影响操作者的乱序执行效率,或者总是会增加人脑的调度负担。
要是设计得像样就该被接受,scsh 不早该就把 POSIX shell 打得满地找牙了,而不是有自知之明“只适合写脚本”。
所以到底是要简短还是要看使用场景是否要求交互式输入(一次性代码)。你要拿个终端输入命令,那就是输入顺手的短一点比较好,看着像鬼画符也忍了;你要考虑写重复使用的脚本或者被复用的脚本模块,那就更适合用看起来罗嗦点的更正经的编程语言。
(推论:Perl 那种读起来呵呵写起来也快不到哪去的就老实凉凉吧。)
这里的界限不是严格的,但使用场景本身就不太可能混起来,所以其实各种 POSIX shell 替代品都挺尴尬的——脚本可维护性上用脚打都能干死 shell,然而实在没法在保持适合写脚本“优雅”的情况下日用起来更简洁。
FrankHB
2021-01-10 01:49:43 +08:00
也有少数情况写起来短是个优势,但并不是主要被接受的优势——短的写法可能更有可读性。典型例子是正则表达式中的最常用的语法:一看一个 * 就知道是重复 0 次或多次,换什么更长的写法来“可读”就都没个准了(大多数用户都未必知道这来自 Kleene 星号)。这个事实基于正则文法本身的简易以及这种约定的符号使用的广泛性上,以至于短的写法就是标准通用词汇,要强行展开成自动机程序搞得大脑暂存区溢出才是鬼画符。当然正则表达式(的不同方言)里面比较少用的旮旯就没那么有群众基础了(还有个问题就是符号不大够用),所以才会不得不有 [[:alpha:]] 之类明显画风不一样的叛徒……
msg7086
2021-01-10 03:04:16 +08:00
Shell 就是为了简单和强力。换句话说,如果要我写冗长的 PS 代码,为什么我不写个 Python 或者 Ruby 脚本呢。
msg7086
2021-01-10 03:19:16 +08:00
Ruby 版和#9 写的差不多。
Dir.children('./obj')
.select{|f| File.file?(f)}
.map{|f| [File.readlines(f).size, f]}
.sort_by(&:first)
.last(5)
.each{|size, f| puts "#{size} #{f}"}
autoxbc
2021-01-10 04:04:49 +08:00
能用 JS 写的最终都会用 JS 写

const temp = [];
for await( const { isFile , name } of Deno.readDir('./') )
{
if(isFile)
{
const text = await Deno.readTextFile(`./${ name }`);
temp.push([ text.match(/\n/g).length , name ]);
}
}

temp.sort( ([a],[b]) => a - b ).slice(-5).forEach( e => console.log(...e) );
AndyAO
2021-01-10 07:21:55 +08:00
帖子是 PowerShell 挑战,没想到大部分人都不讨论 PowerShell,也许是这门脚本语言太小众了.

谢谢 @zhuangzhuang1988 提供 F#和 C#版本,这两门语言和 PowerShell 是互通的,比较复杂的操作确实适合由他们,然后由 PowerShell 组合和调用.但我觉得'画鬼符'这种说法我还是要多解释几句:

利用自动完成的特性,将所有的脚本写在一行,而且都用全称,在不熟悉的人看起来好像是'画鬼符'之类的,实际上敲击的速度很快,懂语法的人也不会觉得很乱.

而且完全可以在脚本编辑器中展开来写,都用最常见的别称,那么我认为看起来会好很多:


```PowerShell
dir $testPath -File | % {
$_ | Select -P Name, @{
l = 'Lines'; e = {
($_ | cat).Length
}
}
} | Sort -P L | Select -L 5
```

```OutPut
Name Lines
---- -----
5.txt 5
6.txt 6
7.txt 7
8.txt 8
9.txt 9
```


@autoxbc 谢谢你提供 JS 版本.不过这本书将的是'变换式编程',所以最好还是用管道 /流机制.这是个带有变量的普通写法.

@msg7086 早就听说 Ruby 很优美,谢谢你提供 Ruby 版本,开眼了!
AndyAO
2021-01-10 07:23:41 +08:00
AndyAO
2021-01-10 07:33:13 +08:00
@zhuangzhuang1988 #9

还有就是将 C#/F#当做脚本用是怎么用的?

刚看到这句话的时候,我以为是用 PowerShell 调用写好的 C#代码,甚至做个 Cmdlet.但是我又突然想到,也许你的意思是直接当做脚本用.

F#没了解过,但 C#这么做应该是不行的,因为它高度的模仿 Java,而后者是公认的非脚本语言,程序必须要放在类当中,有个明确的入口,而且还是静态类型,这些和脚本语言都不沾边,那么我想 C#也是.

去网上大概的搜了一下,发现的确开始有这个倡议,但这还是 2019 年的事情.

[C# 9: 迈向支持脚本编程的第一步-InfoQ]( https://www.infoq.cn/article/a0raqtmupssflaouigih)

所以我想具体的了解一下,你指的是什么.
AndyAO
2021-01-10 07:33:44 +08:00
@nightwitch #7
谢谢你提供的宝贵信息,学习了!

阐述自己观点的同时,带有信息的来源是个很好的习惯,是普通人没有的智慧.
AndyAO
2021-01-10 07:38:42 +08:00
@FrankHB #10
> 补全也不是解决方案,因为前缀不长到一定程度,很难预测补出来的东西会是符合预期的,结果负载一大就该纠结“我这个命令到底输入到哪再补全比较好,万一不对该怎么操作”了

我说一下现在的 PowerShell 是如何解决此类问题的,目前应该有两个比较好的特性

1. 补全的时候是渲染出列表的,所以根本就不会纠结于到底要打到什么程度,因为是一目了然的
2. 根据 28 法则,大多数常用的命令,实际上只占 20%,甚至更少,PowerShell 是会学习你的历史记录给出建议的,所以很多时候打出几个字符就可以找到想要的命令,而且还可以顺便打出那些最常用的参数,这个敲一下键盘就可以完成

当然这两个特性在 Windows 自带的 PowerShell 还没有或者是没有设为默认,需要新版本,而且需要在配置中自己调一下
Jirajine
2021-01-10 09:30:39 +08:00
@FrankHB fish 岂不是更好,写起来简洁优雅,读起来也比 bash 清晰的多,没那些魔法。
以及还有 elvish 、nushell 、ion 等更现代的 shell 。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/743334

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX