看到大家都很嫌弃Windows自带的命令行工具(命令提示符和PowerShell),那么它们到底和Linux的命令行差在哪里?


差在哪?差在软体生态啊。

拿操作系统为例的话,你用的不是操作系统,你用的是操作系统之上的各种app。一个操作系统之上有各种优秀的app,那么哪怕这个操作系统设计得很烂,优化得很差,也是会有很多人用的,至于它优化差的问题可以靠疯狂堆硬体解决,我说的自然就是____系统。

说回到命令行也是一样,用户使用的,其实不是命令行的shell本身,也不是终端模拟器程序,而是,大量的基于命令行的app,以及这些app之间的搭配。

大量的基于命令行的那些Linux app形成了肌肉记忆,旁人只道我在命令行运指如飞,可不知道要达到这种状态是经过了多长时间的历史积淀。而Linux命令行的应用生态,丰富得让你可以实现操作系统中需要做到的几乎所有事情。

Windows下,没有这成百上千的基于命令行的app,没有这些让人已经习惯成自然的app,没法使用就像吃饭喝水一般自然的这些命令行app,自然,Windows命令行就没办法好用:因为基于Windows命令行的软体生态太贫瘠了。

这,其实与PowerShell还是bash并没有什么直接关系。

--

补充:有的人认为ps在设计理念上全方位超越bash。这句话粗看起来似乎有道理,然而,一个shell它首先是一个命令行,其次才是个批处理编程语言。

使用更先进编程理念的shell从来就没能在普及层面战胜目前的shell,因为他们忽略了,手工输入的方便快捷才是shell的第一需求。


Windows 在很长一段时间内,自带的终端模拟器都一言难尽:难以配置,默认情况下很丑陋,复制粘贴的逻辑很别扭等等,直到 Windows Terminal 出来才缓解了这个状况,但 Windows Terminal 到目前还不是默认的终端模拟器,单纯这个原因就让 Windows 的终端体验下降很多。

另一个问题是补全问题。虽然现在 PowerShell 支持的补全功能并不弱,但糟糕的是,没有几个第三方工具适配了 ps 补全功能,对于没有适配的工具,补全工具基本只能用来补全路径,而 bash 上补全适配状态要好的多。而且,Linux 上命令补全通常是渐进式的,每次按下 TAB 都会补全到最长的公共前缀上,并展示可能的命令,而到了 Windows 上 TAB 补全就变成了在输入处遍历可能的补全列表,按过头了还需要再循环一圈,想回退到按 TAB 前的状态还只能手动退格……也许是我习惯问题,但我觉得 Linux 上默认的的补全逻辑远远比 Windows 上更加易用。

至于 Shell 方面,cmd 更是一言难尽,我也不多说了。PowerShell 设计上很不错,但我并不认为它适合作为 Shell 来使用,而是更适合用来写脚本,冗长的命令与选项加上 Windows 的补全逻辑作为 shell 用起来简直是灾难,哪怕 wsl 里运行 pwsh 都比 Windows 上直接运行的 PowerShell 要易用的多的多。另外 PowerShell 启动速度也要慢于 cmd 慢于 bash,这个问题近期倒是改善了不少,但是用起来还是和 bash 挥之即来招之即去的流畅感有所差距。

再就是 Windows 上命令行生态和 Linux 的差距,这是 Windows 重 GUI 轻 CUI 的风格造成的。Windows 上相当一部分 CUI 应用还是移植自 *nix,由此引来的风格差异也是降低 PowerShell 易用性的重要因素之一。

顺带一提,PowerShell 用来写脚本不错,不过我更喜欢 Ammonite 一些:

Ammonite?

ammonite.io

作为一门语言来说,Ammonite 的功能远强于 PowerShell,因为它支持的是 Scala 的一个超集。与 PowerShell 能利用 .Net 生态类似,Ammonite 也能够利用整个 JVM 生态,它甚至支持从远程 Maven 仓库自由导入 package,譬如想使用阿里云的 OSS SDK,只需要这样:

import $ivy.`com.aliyun.oss:aliyun-sdk-oss:3.9.1`

然后就能导入 SDK 中的类型了:

import com.aliyun.oss.OSSClient
import com.aliyun.oss.model._

更多的功能请参见官网文档。


这个问题需要从至少 3 个不同的方面来看——首先你得把 Shell 和 Terminal 这两个概念分开,然后还要考虑的是围绕命令行界面(CLI)所构建的生态环境。很多人都把他们混在了一起,得出的结论就大不相同甚至截然相反。

Shell 就是真正解释并执行命令的那个程序,比如 cmd/PowerShell/bash,它们各自使用自己的语言,比如 cmd 使用的语言叫 Batch(批处理)。Terminal 是用来显示结果、处理输入输出的那个程序,Windows 上自带的叫 conhost,Linux 不同的桌面环境自带的 Terminal 不同。由于 bash 是大多数 Linux 发行版默认的 shell,下面就都是拿他跟 cmd/PowerShell 作比较了。


Shell 语言所提供的功能角度讲,Batch 无疑是这三个当中最弱鸡的,例如没有函数(只能用 CALL 和标签模拟)、没有 switch-case 等等。PowerShell 功能最强,面向对象、并且与 .net 有良好的互操作性,甚至可以嵌入 C# 代码:

$id = get-random
$code = @"
using System;
namespace HelloWorld
{
public class Program$id
{
public static void Main(){
Console.WriteLine("Hello world!");
}
}
}
"@

Add-Type -TypeDefinition $code -Language CSharp
iex "[HelloWorld.Program$id]::Main()"

当然 Bash 也能模拟一下面向对象甚至函数式编程,但这不是语言提供的一等功能。PowerShell 也有一个相当明显的缺点,就是跟 bash 相比太啰唆。

Shell 程序所提供的功能角度讲,cmd 仍然是最弱鸡的,例如历史记录无法持久化保存、没有 profile 支持。PowerShell 目前这两点都是支持的。

因此如果有人说 cmd(Batch) 比 bash 差,这完全没有问题。但是要是拿 PowerShell 跟 bash 相比,我认为还是 PS 技高一筹。


下面说一下 Terminal:Windows 自带的那个 conhost 被 Linux 上一众 Terminals 吊打我认为毫无问题,随便就能想得到很多槽点(有一些槽点在 Windows 10 上解决了,但还是很想吐):

  • 字体受限
  • 颜色受限
  • 窗口宽度受限
  • 行选择
  • 默认是当前语言的编码
  • 不支持 ANSI terminal code
  • 功能上的缺失,比如标签页、搜索…
  • ……

由于 conhost 在兼容性上无法妥协,因此有了新的 Windows Terminal。还有很多其他的第三方 Terminal,比如 ConEmu,不过这些都不是「系统自带」的。因此如果有人说 conhost 难用的一批,请不要反驳并加入他们。


最后是围绕命令行界面所构建的生态。从系统管理与维护的角度讲,其实 Windows 与 Linux 并没有太大差距,比如 Windows ServerCore 就是不带桌面环境的 Windows Server。还有很多程序专门提供了 PowerShell Module 来对软体进行管理,例如 CosmosDB Emulator。

但在其他方面(第三方程序的丰富程度、命令行包管理器等),Linux 无疑要比 Windows 强很多,甚至 bash 不喜欢的话还可以换别的。我认为主要原因在于 Linux 的桌面环境不是必需品,用户用什么桌面环境、甚至有没有桌面环境都不一定,而 Windows API 就实实在在在那,还有相当好的兼容性,图形界面又对普通用户更友好。这样的 API 不用白不用,用了不白用,白用谁不用 _(:з」∠)_


拿一个具体例子来说,就是很多人提到的软体包管理工具 scoop。它的命令行介面就可以凸显出 Windows 的命令行和 Linux 的命令行有多少差距。

例子一、

scoop install 只能用来安装新的包,如果这个包已经存在(即使残缺不全,甚至只有一个空目录),就提示出错,只能先用 scoop uninstall 将其卸载然后重新安装。

比如我安装 curl,在下载过程中 ctrl + c 退出,再安装,就会提示这个错误:

看起来好像没什么大问题。但随便想个场景,自己用 scoop 安装一个包时,发现下载速度很慢,于是 ctrl + c 退出,挂上代理,重新安装。

而 Linux 下的各种包管理软体,都不会犯这样的低级错误,没安装上就是没安装上,下次继续安装就行。

例子二、

用 scoop install 安装一个包时,主程序会检测自身代码是否三个小时以上没更新了(写死在代码里的,不可配置),如果是,那么先强行更新自身代码,然后强行更新所有源列表,最后再给你安装。

这种做法有多荒唐我就不分析了。即使是 archlinux 那种滚动式发行版(长时间不更新源列表的话,安装大概率会出错,比如要下载的文件已经不存在了,或者本地的依赖已经无法满足了),也不会在安装之前强行把 pacman 和源列表更新到最新。

例子三、

scoop update 的功能是更新主程序和源列表(不说将二者绑定在一起是否合理了),更新过程中,会把 git log 全打出来,每个包的更新记录,不管你有没有安装它。但直到运行完,你也不知道自己已安装的包是否有更新。要想知道,你得再运行 scoop status,它会显示你哪些包有更新。如果你想全部升级,那么还得再运行一个 scoop update *。

大家可以仔细体会下这眼花缭乱的操作。对我来说,不封装是很难用的。

例子四、

scoop 会把源列表都同步到本地(一个包对应一个 json 文件),按理说搜索应该很快。但实际上很慢,因为它要依赖网路。因此我自己写了个脚本,用本地的文件来搜索,速度至少提升一数量级(从几秒到几百毫秒)。

例子五、

上边已经说到 scoop 已经把源列表同步到了本地,但用它查询指定包信息时依然很慢,而且信息不全(实际上只需要把对应 json 文件打出来就行)。所以我又写了个脚本用来查询包信息。

这耗时又差了一个数量级。

总结

如果 scoop 是一个刚出生的软体,有这些问题还好说。但它已经诞生七年多了,也有了很多用户(其中不少用户把它当神器宣传),却有著如此糟糕的命令行介面。这说明什么?说明它的多数用户根本不在乎命令行的体验,有个工具,能用,就行了。不然这些问题不会得不到解决。

所以 Windows 的命令行体验,要想提升,还是很困难的。原因很简单,就是大部分用户都不在乎。

有人会大谈 Powershell 的功能和设计,但 scoop 就是用这样「功能强大」和「设计优雅」的 Powershell 写出来的。靠 Powershell 能拯救 Windows 的命令行体验吗?

题外话

说到 Powershell,我还有些话要说。有段时间我看它的功能很心动,就想把它作为日常的 shell,适配的 alias 都写了一些,然后放弃了。下边是我遇到的问题和我尝试解决的方法:

一、启动速度太慢。可以启动后就不退出了,一个不够就启动多个,切换著使用。虽然不符合我的使用习惯,但也不是无法克服。

二、命令补全体验不好。安装 PowerTab 插件,界面可以,功能也过得去,虽然还是欠缺打磨。

三、有些内置命令不好用,比如 curl 对应的内置命令,还带弹窗的,速度也很慢。可以用外部命令代替,比如 curl.exe。

四、输出内容重定向到文件后,都成了 utf-16 编码的了,没法配置,非常不方便。到此为止我已经不大想解决了。(现在单独下载的 Core 版本已经是 utf-8 的了,但系统内置的还是 utf-16。)

五、我基本没发现迁移到 Powershell 有什么好处,只看到了代价(除了上边提到的,还涉及习惯新语法,很多用法都变麻烦了,那不是用 alias 就能简化的),于是放弃,回到 WSL 里的 zsh。

要说 Powershell 的体验怎么样?单独的问题似乎都有解决方法,即使不完美。但综合起来,就是不断做减法,减到一个很差的结果,而且 Powershell 的优势并不能将其弥补。

随便贴下我封装 scoop 的脚本,因为我是在 WSL 下用的,写的 zsh:


你要拿cmd去和bash比,那是欺负人。

但是你说powershell和bash,本质上来说PowerShell不论是设计理念还是各方面都是全方位领先的。差别主要就是生态和偏见了。

当然,powershell的版本和需要手动安装也是个麻烦事儿。

我这边只随便说一点,PowerShell有完整的dotnet支持,很多问题你要依赖某个小工具的,PowerShell可以直接调dotnet类库来解决。这并不是写个脚本或者程序,而是PowerShell本质上就是dotnet的命令行前端。

其实,有些人真的是非常完美的诠释了什么叫做偏见:

例如大家都熟悉 ls是列目录,非要来一个 dir。

事实上我估计dir比他的年龄都大,而且,你要真说是列目录的话,dir是directory的缩写,ls则是list的缩写,你说哪个更贴近列目录?而dir和ls都是pwsh的alias这种常识我都懒得说了。

更搞笑的是:

都不明白bash的精髓吗?bash的精髓不是语法,而是能将一堆Linux命令轻松的包裹在一起。语法再高级,再完备有啥用,这个世界缺高级动态语言吗?Python够不够?缺的是各种小工具。

真是不好意思,这个精髓被pwsh几乎完全的继承并且发扬光大了,那就是pwsh的管道,说发扬光大是因为pwsh的管道是强类型的。

pwsh内置的绝大多数指令都可以直接通过管道来解决。比如说我们需要递归列出所有a开头的文件,我们会写:

ls a* -r

不用怀疑pwsh是完全支持这个脚本的。

好了,我们现在确认了,我们要把这些文件删掉,怎么做?

因为pwsh管道传输的是对象,而且所有的内置指令几乎都支持管道,所以:

ls a* -r | rm

完事……你完全不用考虑ls输出的格式是什么,有没有全路径什么的,因为,管道传输的是对象!

你再看看这个精髓,pwsh这个学生,学的不错吧……

更别说,pwsh还有非常强大的where和select……

ls -r |? Name -like a* |? Length -lt 10000 |select -First 10| rm

你现在告诉我,这是bash的精髓,还是pwsh的?

我真心觉得这是欺负人……

对了,pwsh还可以呼叫帮凶dotnet:

ls |? LastWriteTime -gt ([System.DateTime]::Now.AddDays( -100 ))

数百万个基础类型的小工具够不够?

====================================================

其实大部分人说了半天就是这么一句话:

新东西是不可能学的,这辈子都不可能学的,dotnet又不会,只能用bash这样子,才能维持的了生活这样。用bash就像回家一样,指令个个都是人才,缩写又很漂亮,我超喜欢bash的……

嗯,祝好……


推荐阅读:
相关文章