V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
AndyAO
V2EX  ›  程序员

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

  •  
  •   AndyAO · 55 天前 · 2808 次点击
    这是一个创建于 55 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在《程序员修炼之道》的“变换式编程”章节中有这样一段 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 中还能设置别名,想要多少短都行。

    第 1 条附言  ·  54 天前

    很多人喜欢的'简洁'风格,在PowerShell中可以做到,只在上面的帖子中提到了,但是没有列出来,这里补充,作为参考.

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

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

    第 2 条附言  ·  54 天前

    我终于找到了自己满意的答案! 感觉与Shell相比有过之而无不及.

    dir $testPath -file | % {
        $_ | sort {
             ($_ | gc).length 
            } 
    } | select -L 5
    

    第 3 条附言  ·  54 天前

    又发现了更简洁的写法,去除了不必要的Cmdlet,比上次更有进步了,只用了3个管道操作符,Shell用的是5个!

    dir -file | sort {($_ | gc).length} | select -L 5
    
    第 4 条附言  ·  54 天前

    对比长度之后发现还是比Shell多1个字符,这个不能忍.

    突然想到length属性其实也可以直接简写为l,那么在字符数上也最终超过了Shell写法.

    dir -file | sort {($_ | gc).l} | select -l 5
    

    只用了44个字符

    第 5 条附言  ·  54 天前
    发现最后那个 Length 是不能省略的,那个是错的,必须要写出 Length
    第 6 条附言  ·  54 天前
    修正错误,如果退回到只论字符数,目前正确的成绩好像是 PowerShell<zsh<Shell
    ![]( https://cdn.jsdelivr.net/gh/Andy-AO/GitHubPictureBed/img/20210110134710.png)
    兴奋劲儿过去了,不玩了,哈哈
    第 7 条附言  ·  54 天前

    补一下图,那个图没传成功

    第 8 条附言  ·  54 天前
    最后一条附言,应该是打错了 Shell<PowerShell<zsh

    天哪,我最近在干什么?为什么总是输入错误呢?

    o(╥﹏╥)o
    第 9 条附言  ·  12 天前

    其实我最初的那个代码段写的是很糟糕的,尤其是从可读性上来说,简直就是个灾难,我也不清楚当时为什么,我会将所有的代码都写到一行中去。

    实际上此前我对于 PowerShell 中的行延续也是支持甚少的。

    最近想全面的学习这部分内容,发现这篇文章讲得很全面很好。

    Get-PowerShellBlog: Bye Bye Backtick: Natural Line Continuations in PowerShell

    38 条回复    2021-01-13 21:44:49 +08:00
    hanxiV2EX
        1
    hanxiV2EX   55 天前 via Android
    shell 的更简洁吧
    zhustec
        2
    zhustec   55 天前
    这么长的名称真的就优雅吗?要论命名,shell 也可以用 alias 命名出更具可读性的名字,也可以尽量使用两横的长参数。
    不见得 Get-Content 就比 cat 更具可读性,也不见得 Sort-Object 就比 sort 更具可读性。可读性也是要针对有一定 shell 基础的人来说的,也不能说只有一个一点都不懂 shell 的人能看懂才叫可读。
    AndyAO
        3
    AndyAO   55 天前
    @zhustec #2 当然不一定,因为那个词本身就没有严格的定义
    felixcode
        4
    felixcode   55 天前
    PowerShell 这么长的命令,就很依赖自动补全了,而且分行才能显得足够直观,至于优雅,审美比较特别才会显得优雅吧。
    zhuangzhuang1988
        5
    zhuangzhuang1988   55 天前 via Android
    还是直接 linq 把
    geelaw
        6
    geelaw   55 天前   ❤️ 1
    Get-ChildItem -File | Sort-Object -Descending {
    $_ | Get-Content -ReadCount 1 |
    Measure-Object | Select-Object -ExpandProperty Count } |
    Select-Object -First 5
    nightwitch
        7
    nightwitch   55 天前   ❤️ 1
    在 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
        8
    zhuangzhuang1988   54 天前
    ```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
        9
    zhuangzhuang1988   54 天前   ❤️ 1
    ```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
        10
    FrankHB   54 天前
    @nightwitch 那个说的是系统调用,用得没那么频繁,不用太在乎日常输入效率,反倒是不刻意习惯就容易读到一次恶心一次,强行习惯还会“没用的知识增加了”,自然要纠结 crate 还是 create 的问题。
    要说终端交互式使用,就算现在的键盘按起来没那么费劲,成本也不是零,而人干这活往往不会老实严格串行傻等反馈,所以输入一长就麻烦。补全也不是解决方案,因为前缀不长到一定程度,很难预测补出来的东西会是符合预期的,结果负载一大就该纠结“我这个命令到底输入到哪再补全比较好,万一不对该怎么操作”了,严重影响操作者的乱序执行效率,或者总是会增加人脑的调度负担。
    要是设计得像样就该被接受,scsh 不早该就把 POSIX shell 打得满地找牙了,而不是有自知之明“只适合写脚本”。
    所以到底是要简短还是要看使用场景是否要求交互式输入(一次性代码)。你要拿个终端输入命令,那就是输入顺手的短一点比较好,看着像鬼画符也忍了;你要考虑写重复使用的脚本或者被复用的脚本模块,那就更适合用看起来罗嗦点的更正经的编程语言。
    (推论:Perl 那种读起来呵呵写起来也快不到哪去的就老实凉凉吧。)
    这里的界限不是严格的,但使用场景本身就不太可能混起来,所以其实各种 POSIX shell 替代品都挺尴尬的——脚本可维护性上用脚打都能干死 shell,然而实在没法在保持适合写脚本“优雅”的情况下日用起来更简洁。
    FrankHB
        11
    FrankHB   54 天前
    也有少数情况写起来短是个优势,但并不是主要被接受的优势——短的写法可能更有可读性。典型例子是正则表达式中的最常用的语法:一看一个 * 就知道是重复 0 次或多次,换什么更长的写法来“可读”就都没个准了(大多数用户都未必知道这来自 Kleene 星号)。这个事实基于正则文法本身的简易以及这种约定的符号使用的广泛性上,以至于短的写法就是标准通用词汇,要强行展开成自动机程序搞得大脑暂存区溢出才是鬼画符。当然正则表达式(的不同方言)里面比较少用的旮旯就没那么有群众基础了(还有个问题就是符号不大够用),所以才会不得不有 [[:alpha:]] 之类明显画风不一样的叛徒……
    msg7086
        12
    msg7086   54 天前 via Android   ❤️ 1
    Shell 就是为了简单和强力。换句话说,如果要我写冗长的 PS 代码,为什么我不写个 Python 或者 Ruby 脚本呢。
    msg7086
        13
    msg7086   54 天前   ❤️ 1
    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
        14
    autoxbc   54 天前   ❤️ 1
    能用 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
        15
    AndyAO   54 天前
    帖子是 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
        17
    AndyAO   54 天前
    @zhuangzhuang1988 #9

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

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

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

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

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

    所以我想具体的了解一下,你指的是什么.
    AndyAO
        18
    AndyAO   54 天前
    @nightwitch #7
    谢谢你提供的宝贵信息,学习了!

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

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

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

    当然这两个特性在 Windows 自带的 PowerShell 还没有或者是没有设为默认,需要新版本,而且需要在配置中自己调一下
    Jirajine
        20
    Jirajine   54 天前 via Android   ❤️ 1
    @FrankHB fish 岂不是更好,写起来简洁优雅,读起来也比 bash 清晰的多,没那些魔法。
    以及还有 elvish 、nushell 、ion 等更现代的 shell 。
    AndyAO
        21
    AndyAO   54 天前
    @geelaw
    谢谢你给出自己的答案,学到了很多.

    @autoxbc @msg7086 @FrankHB @zhuangzhuang1988 @felixcode
    嗨,伙计们,我终于想出更简洁优美的方式了,感到挺激动的.

    对于我来讲,这个写法已经比 Shell 有过之而无不及了!

    ![]( https://cdn.jsdelivr.net/gh/Andy-AO/GitHubPictureBed/img/20210110095800.png)

    dir -file | % {
    $_ | sort {
    ($_ | cat).length
    }
    } | select -l 5
    venster
        22
    venster   54 天前 via iPhone
    @AndyAO 终于上了 alias 的大杀器了
    AndyAO
        23
    AndyAO   54 天前
    @venster #22
    我发现还有容易,这是新的,哈哈哈.

    @hanxiV2EX
    这次 Shell 更繁琐了,因为 PowerShell 只需要 3 个管道操作符号.
    而且最后获取的还是对象,能够进行更多的可扩展的操作.

    dir -file | sort {($_ | gc).length} | select -L 5
    yannxia
        24
    yannxia   54 天前
    很多人熟悉了 Linux 的 Shell 了,这是先入为主的观念,如果说拿来编程那还是 Powershell 那套好一点,不过考虑到敲击的长度问题。
    msg7086
        25
    msg7086   54 天前
    一般较为复杂的操作,还是以脚本语言为主。
    Shell 命令要的是 quick and dirty and powerful 。
    但是如果要长期用,一般都会选择整整齐齐地写成脚本。
    这时候,就可以根据实际的使用环境,选择自己喜欢的语言了。
    如果你是 Python 程序员这时候可以写个 Python 小程序,没有必要拘泥于使用某一种特定的 Shell 语言。

    PowerShell 的确是太小众了,除非是很特殊的环境(例如要跑在第三方的 Windows 服务器上,并且强制要求源代码公开),否则有无数其他的方案可以用(例如用 C 或者 Go 写小程序,装脚本语言,等等)。
    如果是自己用的话,还不如安装一下然后用自己熟悉的脚本语言来写代码了。
    PowerShell 也很少用来开发其他项目,所以熟悉的人不多,毕竟相当于新学一门语言了。
    AndyAO
        26
    AndyAO   54 天前
    @yannxia #24
    我现在已经找到非常好的写法了,不管是从"长度",还是"操作次数",还是"最终结果的可扩展性"都优于 Shell !

    之所以最初我那个命令比 Shell 繁琐,是因为我是个新手,不懂怎么样写出更好的,而那本书的作者是资深程序员.

    这是最简洁优美版本,可以成功运行获取拥有最多函数的 5 个文件对象.

    dir -file | sort {($_ | gc).l} | select -l 5
    AndyAO
        27
    AndyAO   54 天前
    @msg7086 #25

    赞同你的观点,这里补充几点.

    PowerShell 也是脚本语言,是面向对象的,因为它是.NET 的类,和.NET 体系完全互通.

    从 GitHub 的数据来看,依托于 Shell 的语言,Shell 是最流行的,其次就是 PowerShell.

    考虑到之前微软的封闭战略,PowerShell 在很长一段时间内都只能用于 Windows,而这个平台中用命令行的场景很少.

    但现在 PowerShell 已经可以在其他平台上使用了,而且已经完全开源,后面的发展还是很有希望的.

    https://madnight.github.io/githut/#/pull_requests/2020/4

    目前,大多数学 PowerShell 的人应该是.NET 或者 Windows 的系统管理员(运维人员),我学的目的主要是为了将命令行上的工作自动化.
    darklowly
        28
    darklowly   54 天前
    @FrankHB 标识符和正则星号来类比在偷换概念
    aloxaf
        29
    aloxaf   54 天前   ❤️ 1
    看到这个问题我就知道肯定会引起不少争论 233 。我个人倒是挺喜欢 pwsh 的,最后的写法非常简洁,学习了

    我也来提供一个 zsh 下的写法:

    wc -l **/*(.) | sort -nr | sed -n '2,5p'

    小胜 4 个字符
    AndyAO
        30
    AndyAO   54 天前
    @aloxaf #29
    谢谢参与,本来这一篇帖子的主题,并不是争论什么 Shell/语言好,而是想征集一下有没有更好的 PowerShell 写法,看来只要是类似的,关于比较帖子都会比较有争议的.

    我有激动和急躁的毛病,最后犯了两个错误:

    首先是那个 Length 应该是不能省略的,我当时的测试出了点问题;

    其次就是字符统计也错了,我又重新尝试了一下,最长的就是那本书上的 Shell,一共是 58 个 字符,PowerShell 49 个,我看花眼了.

    然后你给出的 zsh 是 40 个,夺冠了,也让我了解了 zsh 的出色的简洁性,Thanks♪(・ω・)ノ

    ![]( https://cdn.jsdelivr.net/gh/Andy-AO/GitHubPictureBed/img/20210110134710.png)
    wellsc
        31
    wellsc   54 天前
    反 人 类
    Arnie97
        32
    Arnie97   53 天前 via Android   ❤️ 1
    可读性不错,虽然我从没学过 PowerShell,能看懂这段程序在做什么。从第 2 条附言到第 3 条的那个优化有个专门的术语:Point-free
    no1xsyzy
        33
    no1xsyzy   53 天前
    no1xsyzy
        34
    no1xsyzy   53 天前   ❤️ 1
    话说起来,fork 跟 createProcess 不一样
    fork 是起一个完全一样的进程作为自己的子进程,要完成 createProcess 类似的工作还需要 exec
    或者采用 execvp,这就更不直观了

    而且有时还会因为覆盖了信号处理方式导致子程序发生 bug

    顺便来点 golf

    # Python REPL ( iPython ), 69 bytes
    import os;sorted((len([*open(f)]),str(f)) for f in os.scandir())[-5:]
    AndyAO
        35
    AndyAO   53 天前
    @no1xsyzy #33

    没必要,因为已经找到很完美的答案了,是我自己想出来的,在附言 3 上.

    我对 fork 和 createProcess 并不了解,只是在别处看到了这个例子,直接拿来用.谢谢你补充相关的内容.
    no1xsyzy
        36
    no1xsyzy   53 天前   ❤️ 2
    @AndyAO 投到 codegolf 你会看见一堆你听都没听说过的语言用十几个字节甚至几个字节解决这件事……
    没什么意义(
    FrankHB
        37
    FrankHB   50 天前
    @AndyAO PowerShell 作为一个传统意义上的 CLI shell,本身是做不到真正即时地渲染补全结果的,它需要借助 GUI shell (具体来说典型情况下就是终端模拟器)的功能。
    而这点上,至少默认终端 /控制台的交互性不算很好。而论直观性,这方面终端模拟器也难以做出典型的 IDE 那么好的 UE 。
    其实 MS 应该明确知道这点,所以才有 PowerShell ISE 。问题是多少 shell 用户会真习惯这样来代替 cmd 的……也就是多个不太常用了选择了。
    补全列表在不太重量级的终端下这只能做成一种后验的反馈而没法总是及时即时更新,如果用户不选择是无视而使其有意义,就需要关心到底怎么获取列表信息,具体来说就是看到并且 parse 屏幕信息。(说实话 IDE 基本上也没好哪去,但是能用 IDE 的场合往往没那么“急”,即时性要求反而一般不高,而且好歹允许做得更动态……)
    非即时反馈对轻度用户来说还可以是帮助(只要性能别出大娄子明显影响到响应性),但重度负载下就会容易出来我提过的阻碍人脑“乱序执行”的问题。这里所谓的重度用户,是日常需要高频进行某一类近似但又没法确保自动化的操作的用户。(可能有人觉得这主要是运维 /DevOps,但用惯 CLI 的传统开发者这里 APM 也可以很高。)

    像学习历史记录提建议这只能说是一个帮助轻度用户习惯和适应的过程,对重度用户就不太有帮助了。特别地,不保证 100% 准确而必须让人去盯一下反馈再确认,这比起闭上眼都能键盘一把梭,差距是消不掉的。
    这些问题其实是所有 CLI shell 共通的问题。PowerShell 在这里并没法有效突破限制,又没几个人会把 PowerShell 当成年轻人的第一门语言,没比竞品强太多的情况下,特地去上手就有点鸡肋。

    我所知的 PowerShell 最擅长的还是它能调 .NET 运行时的功能比较方便,有效缓解 .NET 语言传统上缺乏成气候的 REPL 干杂活不方便的问题。(题外话,CLR 上的一些动态语言一直不温不火,C# 和 Java 的 REPL 出来也有好几年了,甚至 C++ 的都有很长历史……这些环境没普及可能更多是用户习惯而不是语言自身限制的原因。)
    就算抛开历史习惯不提,真日用起来 PowerShell 比起 sh 是没有决定性优势的(终端模拟器都是适配 sh 的更多点)。
    而只是写脚本,实际项目都不见得打得过稍微认真写的 shell 脚本——举例:安装 Flutter 的那坨 ps1 就比 sh 多写 bug 报错更加云里雾里……

    说到这里确实是有点跑题,不过我倒是不觉得闲了就原主题做点头脑体操完全没意义——只不过大多数人应该没到这个程度罢。
    FrankHB
        38
    FrankHB   50 天前
    @Arnie97 Point-free 说的是一种 style 而不是 transformation,第 3 条那个也没做到 point-free (消掉了 $testPath 这个 point,但 $_ 还是 point )。
    另外,point-free 跟可读性沾不上多少边,“尽量 point-free ”在抽象能力上倒明确是一种倒退。(虽然这个上下文中不明显。)
    https://github.com/FrankHB/pl-docs/blob/master/zh-CN/combinator-critique.md
    关于   ·   帮助文档   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2775 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 13:06 · PVG 21:06 · LAX 05:06 · JFK 08:06
    ♥ Do have faith in what you're doing.