Loading...
墨滴

张春成

2021/06/09  阅读:86  主题:默认主题

从 PowerShell 理解 OOP 理念

从 PowerShell 理解 OOP 理念

写在前面, 本文是之前写过的东西的整合,之所以重新发布是出于以下原因

  1. 将同样内容的文章进行合并,有益于知识的整理;
  2. 由于之前文章使用 OneNote 进行排版,但使用一段时间之后,发现与公众号的写法兼容性太差
    • 标题格式不兼容;
    • 缩进长度不兼容;
    • 图像和表格不兼容;
    • 公式编写不兼容;
  3. 如何解决这个问题呢?我找到了墨滴社区[1]
  4. Markdown YYDS,正经话就是,将内容与形式分开才是正路,“所见即所得”是邪教;
    • 由于内容与形式分开,我们可以使用 markdown 语言,专注于内容的编辑和分发;
    • 墨滴社区的作用是对内容进行排版,并格式化成“娇贵”的微信公众号所认识的格式;
    • 从而使大家能够看到良好排版的文章;
  5. 由于使用了 Markdown 写作方式,大家可以在我的 Github 网站[2]找到全部内容,欢迎各种形式的转载和抄录。

吐槽完毕,本文将包含以下内容。

PowerShell 与 LinuxShell 之不同

PowerShell[3] 是微软自带的交互软件,与类 Linux 系统的 Shell 具有一定的平行替代关系。 但使用起来,二者却给人以完全不同的感觉。

当然,PowerShell (PS)令人智熄的蓝色界面有一定影响,这却不是主要原因。 其根源在于 Windows 系统(W)与类 Linux 系统(L)的不同构建哲学。 一句话来说,L 是以文件为基础而构建的系统,W 是以抽象为基础而构建的系统。 在 L 系统中,用户所看见和所操作的都是具体的文件,所谓“一切皆文件”; 而在 W 系统中,用户始终处于抽象构建的类结构中,所见的都是一望无际的抽象概念。 孰优孰劣我们不去评价,具体和抽象的区别却是在实际使用中产生直观的感受。


先务虚

举个栗子:

  1. 在 L 系统中,几乎所有的命令都将终端当作一张白纸在用,比如常用命令 ls 是将当前目录下的文件以各种各样的方式“打印”到屏幕上,一般来说,每个文件或目录对应单独的一行输出,根据命令参数的不同,可以包括名称、创建时间、权限等一系列信息;
  2. 而在 W 系统中,如果你使用的是 PS,那么你能发现一个神奇的现象,就是你可以找一个变量来接收 ls 的输出结果,并且这个变量是一个类似列表的玩意儿,你甚至可以单独操作其中的某个“元素”,同时,不难发现,这些“元素”对应着当前目录下的文件或目录,或者更正经一点的说法,把“目录”看是“树”,这些“元素”就是“树叉”或“叶子”,分别代表“子目录”和“文件”;
  3. 这些“元素”具有各自的“属性”,包括名称、创建时间、权限等一系列信息。

有点计算机科学基础的同学不难理解,在 PS 的环境下,一切都可以说是类似计算机语言中“类”或“结构”的东西,换句话说,一切都是操作对象的抽象。

再举一个栗子:

  1. 在 L 系统中,键盘、鼠标、硬盘等所有计算机能够操作的外设都是文件,只不过这些文件是动态变化的,比如 /dev/input/mouse0 代表鼠标, /dev/sda# 代表硬盘等等,系统通过检测这些动态变化的文件来响应它们产生的事件、或对它们进行操作;
  2. 而在 W 系统中,同样这些设备的存在就变成了抽象的设备类和它们的接口,就是你能从“设备管理器”里看到的东西。

从这个角度来看,我个人更喜欢 L 系统,因为它更加看得见摸得着;但对于 W 系统只有膜拜,因为它更加贴近于一个理想中的抽象的计算机世界;如果有“MATRIX”那样的世界,我相它更加可能是 W 那样的;如果我要自己实现一个小功能的小系统,我会采用 L 的理念,因为它可控,而且看上去比较优雅。

再务实

先说 L 系统

在 L 系统中鼎鼎大名的三剑客 Grep、Sed、Awk 都是文本处理的高手,一个是高效的单行文本探索器、一个是高效的单行文本编辑器、一个是面向单行文本的编程语言。 它们在 L 系统中横行天下的原因是 L 系统是基于文件的操作系统,在系统设计过程中,高效的单行文本传递几乎是系统的命脉。 而它们显然的短板自然地是面向多行文本时的无力感,将这三个神器使用得炉火纯青的大神与小白之间的区别正是在面对多行文本处理的情况时,所使用的奇技淫巧,我非常尊重前辈在这方面所作的努力,但这些工作并不是开拓性和创新性的工作,而是带着镣铐跳舞的无奈之举。

举个栗子:

  • 在 L 系统中,我们有时会关心设备的情况,要用到 lspci 这个命令,列出系统 PCI 总线上挂载的东东;
  • 由于输出太长,我们使用 lspci |grep -e [PAT] 来找到由 "PAT" 字符串所指定的内容,事情往往没完,当我们想看上下文时,需要加上 -C# 参数,其中的 # 代表要看上下文的几行;
  • 剩下的事情基本就听天由命了,因为要找的东西不见得就一定在指定的范围内,范围太小会丢东西,太大又没意义,况且你也不能让后面接收这个信息的程序自动判断哪些是有用的信息;
  • 这就是 L 系统的短板。

再说 W 系统

在习惯了 L 系统的文件操作之后,在 W 系统中的 PS 一开始看上去就是一场彻头彻尾的灾难。因为它看上去太“反 人 类”了; 所有显示在 PS 里的东西都成了抽象的“类”,我们习惯的文件消失了。

举个栗子:

  • 当你在上网,看到一张图,有了它的 URL 就可以用 wget 命令下载下来,在 L 系统中,它会乖乖地以文件的形式出现在的目录下;
  • 而在 PS 环境下,它弄出来一个叫 Invoke-WebRequest 的替代,它的 alias 名称正是 wget,你用同样的命令试图下载,它会给你返回一个类,这个类基本上长这样

在 PS 中的一个标准的网络请求响应

StatusCode        : 200
StatusDescription : OK
Content           : {255, 216, 255, 224...}
RawContent        : HTTP/1.1 200 OK
                    X-Timestamp: 1458043266.82517
                    Age: 42
                    X-Cache: cp4021 miss, cp4024 hit/2
                    X-Cache-Status: hit-front
                    Server-Timing: cache;desc="hit-front", host;desc="cp4024"
                    Strict-Transport-Secu...
Headers           : {[X-Timestamp, 1458043266.82517], [Age, 42], [X-Cache, cp4021 miss, cp4024 hit/2], [X-Cache-Status,
                     hit-front]...}
RawContentLength  : 98440

如果你下载的是一张图,恭喜你,你的图现在是躺在Content成员里的一列数,它的长度大概是 98K,标记在了RawContentLength里,你还能看到网络连接时的全部信息,存在Headers里。 也就是说,你的图,现在是一个抽象“类”,你除了看不见它,它的全部信息都对你的透明的,是不是很开心? 而如果你想看图,你需要在执行 Invoke-WebRequest 时带上 -OutFile 参数,那么你的图才会出现在那里。

当然,你的图并没有消失,它只在以编码数字的形式静静地躺在Content字段里,你可以把它存储在一个文件里,然后用“Python”或其他软件对它进行解析

使用 Python 解析 Content 的例子

from io import BytesIO
from PIL import Image
raw = open(fname, 'rb').read()
numbers = np.array([int(e) for e in raw.decode('utf-16').split()])
parsedBytes = ''.join([chr(e) for e in numbers]).encode('latin1')
img1 = BytesIO(parsedBytes)
Image.open(img1)

到此为止,我们已经论述了 L 和 W 两者的不同,下面的文章将着重对 PS 的功能进行学习和探索。

为什么是 PS ? 我们可以从后一个例子来看,它其实是比 L 系统更进一步地把计算机系统里的五脏六腑都以抽象“类”的良好包装形式呈现给用户,用户才能看到这些信息,了解以前自以为了解的,然而并没有真正了解的东西。

用 PowerShell 寻找你找不到的文件

继续说 PowerShell (PS)的事情,通过本文档,希望你能够获得一个在 Windows (W)系统中高效找到你想要的文件的方法。能够开始使用 W 系统中提供的 PS 终端,并且习惯用键盘打字的方式而不是鼠标乱点的方式操作你的电脑。 当然,本文档的目的还是深入理解 PS 的抽象类模式,并提供一个非常工程化的视角来阐述这一特性。


找文件

当用户使用电脑时,很常见的一个情况就是要找到某个文件,却忘了它在哪。 在 L 系统中,可以使用如下语句来找寻想要的文件。

#!/bin/sh
find -name [fname]

理想很丰满,但现实很骨感,要是用户记得文件名,还需要费那么大劲找吗? 另一更现实的问题是,在输入过程中,神奇的自动补全 TAB 键完全派不上用场。

这里啰嗦一下神奇的 TAB 键的用法,它其实就是一个自动补全的呼出方式,假设要查看一个文件叫做 abcdefg.docx,其实不必输入文件全名

#!/bin/sh
cat abcdefg.docx

而是可以转而使用 TAB 键进行补全

#!/bin/sh
cat abcd<TAB>

系统会自动把文件的全名补全出来。 这样来说,假设要找一个 WORD 文件,由于 TAB 键补全功能的缺失,就不得不把正则表达式 xxxx.docx 完整地打在到屏幕上。 你知道的,优秀程序员往往是按敲击键盘次数计价的,所以这么做就赔大发了。

把 TAB 补全用上

如果能够将 TAB[4] 补全用到找寻文件上,操作将会简化很多,也比较不容易出错。 事实上,更常见的情况是,用户并不完全清晰地记得自己要找的文件后缀,最好系统能够把能够找的选项列出来,并且适当地允许用户使用 TAB 键进行补全。 或者更进一步地,最好系统能够快速、全面地生成一份报告,告诉用户有哪些文件是可选的。

目前的情况

这样的工作很难吗?不见得。现在有这样的工具吗?并没有。

  • 至少 L 系统的 Terminal 实现起来比较麻烦,普通用户一般不会弄出来,(当然也没有普通用户会没事就用这个环境);
  • W 系统的资源管理器在这个方面是个废物,搜索功能在右上角,但每次点进去找文件时我的血压都会有点高,我总能看到一堆莫名其妙的文件,当我点开其中一个但发现不是想要的,而需要返回的时候,它会再费时费力地搜索一遍,留我在屏幕前思考人生(搜索时间越长,思考得就越深刻);
  • MacOS 的 Finder 直接一脸劝退的态度,它很漂亮,还允许你进入目录、浏览或选择文件,其他真的就没有了;
  • 其他类似的工具,如 Everything 等,虽然补足了各个操作系统的搜索 GUI 的不足,但使用起来也没有办法将 TAB 补全加入操作流程。

功能抽象

那么这个不难,还实用,却没有的功能是什么呢?我们不妨抽象一下。 就拿扩展名为例,我想我们需要这样一个工具,它能够快速并自动对当前目录及子目录的文件扩展名进行识别和统计,并在用户输入过程中,可以使用在 TAB 键进行智能补全。这样做有几个好处

  • 用户可以通过自动补全功能减少输入量,比如输入.d<TAB>,系统会自动在 .docx, .doc, .data等等可选扩展名中切换,等待用户确认;
  • 允许用户浏览当前目录下的所有可选文件扩展名,用户往往需要看看当前目录下到底有哪些种类的文件;
  • 能够根据用户输入的扩展名,列出所有待选文件。 在列出所有待选文件后,用户可以比较容易地找到目标文件(如果存在这个文件的话)。

功能实现

下面,我们将使用简单的 PS 代码实现这一功能。首先是基础功能。

function Get-FilesByExtension {
    # Binding Parameters
    [CmdletBinding()]
    param(
        [Parameter(
            Position = 0
        )]
        [string] $Extension
        , $Depth = 2
        , $Exclude = ""
    )

    # Echo the Current Job
    Write-Output "Selecting Extension of $Extension"

    # If Extension is Empty, all the Available Extension are listed
    if ($Extension -eq "") {
        Get-ChildItem -Recurse -Depth $Depth -File | Where-Object { $_.FullName -NotLike '*\.*' } | Group-Object Extension | Sort-Object Count
        return
    }

    # If Extension is Inputed, all the Files with the Extension are listed
    $all = Get-ChildItem -Recurse -Depth $Depth -File | Where-Object { $_.FullName -NotLike '*\.*' } | Select-Object FullName, Name, LastWriteTime, Extension | Group-Object Extension

    $select = $all | Where-Object { $_.Name -like $Extension } | Select-Object Group

    $select.Group | Select-Object Name, LastWriteTime, fullname, extension | Sort-Object LastWriteTime
}

由于首次涉及 PS 函数脚本,我想这里有必要对其功能进行解释。

  1. 此部分是规定该脚本有哪些输入参数,其中默认参数是$Extension,也就是说,该函数的默认输入参数就是$Extension参数。另外,用户还可以指定$Depth$Exclude两个参数,它们的意义似乎并不是很重要,在此不多着墨;
  2. 此部分是反馈给用户目前是在查找$Extension这个扩展名的文件;
  3. 下面有两个分支,第一个分支是当$Extension参数是空值时,该函数将列出所有可选的扩展名,如图1所示;
  4. 第二个分支是列出所有$Extension扩展名文件的绝对目录,如图2所示。

图1、扩展名列表示意图

图2、文件列表示意图

  1. 到此,该函数的全部功能介绍完毕。

函数功能介绍完毕后,我们回到 PS 的抽象类特性,以 #3 为例进行介绍,

  • Write-Output 获取所有子文件,并将它们存储为抽象的文件类;
  • Where-Object 是管道的第一站,它将忽略所有以点开始的文件或目录;
  • Group-Object 是按文件类的扩展名进行分组,也就是说把相同扩展名的文件排到在一起;
  • Sort-Object 是对扩展名计数进行排列,结果可见图1的第一列。

可见,在抽象类的基础上,PS 可以将处理的对象进行流动,并且在流动的每一站,以处理类的方式对成员进行加工。 有加工,就可以自然地联想到工厂模式,我们可以通过工厂模式来为该函数添加 TAB 补全功能。所需的代码如下

$getExtensions = {
    param($commandName$parameterName$wordToComplete$commandAst$fakeBoundParameters)
    Get-ChildItem -Recurse -Depth 1 -File | Where-Object { $_.FullName -NotLike '*\.*' } | Select-Object Extension | Sort-Object Extension | get-unique -AsString | Where-Object { $_.Extension -like "$wordToComplete*" } | ForEach-Object { $_.Extension }
}
Register-ArgumentCompleter -CommandName Get-FilesByExtension -ParameterName 'Extension' -ScriptBlock $getExtensions

不难看出,Register-ArgumentCompleter是一个注册补全功能的方法,它将目标函数的 $Extension 参数注册到一个补全脚本上,补全脚本是在 $getExtensions 上定义的。 补全脚本所做的事情则比较简单,即在用户按下 TAB 键要求补全时,它会查找所有可行的扩展名,并且过滤出以当前输入字符串开头的扩展名,并将这些扩展名以列表的形式返回。 这样,列表元素可以通过 PS 的内置补全机制一个一个地出现在用户的手边。 至此,功能实现完毕。

总 结

可以看到,PS 的抽象类理念允许用户以对象的方式对功能实体进行管道式的处理,并且可以通过面向对象的工厂设计模式进行功能开发,从而大大简化实现较为复杂功能时的代码量。 但是,同样由于其抽象类特性,PS 在执行较为简单命令的时候却显得较为复杂和臃肿。可见,PS 的设计理念在代码量方面,大大降低了复杂功能的代码量上限,却同时提高了简单功能的代码量下限。 但有一点,在面向对象的特性方面,PS 的代码可读性较高,几乎每个操作是合理的抽象语言。

如果你足够细心,可以发现所有的 PS 命令都是“动宾短语”这样的结构,这恰恰是功能抽象的体现。

参考资料

[1]

墨滴社区: https://www.mdnice.com/

[2]

我的 Github 网站: https://github.com/listenzcc

[3]

PowerShell: https://www.powershellgallery.com/

[4]

TAB: https://www.howtogeek.com/195207/use-tab-completion-to-type-commands-faster-on-any-operating-system/#:~:text=For%20example%2C%20you%20can%20type%20a%20~%20and,use%20tab%20completion%20for%20commands%20and%20their%20options.

张春成

2021/06/09  阅读:86  主题:默认主题

作者介绍

张春成