Powershell 的一些应用

Acetyl

2021-05-14 11:17:15

Tech. & Eng.

前言

对于 Windows 用户来说,Powershell 已经是再熟悉不过的东西了,大部分时候,我们使用 Powershell 的目的和使用 cmd 差不多,只是运行一些命令而已。

实际上,运行命令只是 Powershell 的第一层功能。正如它的名字,这个看似不起眼的终端内部,隐藏着巨大的能量(Power)。之前我用 Windows 的时候开始使用 Powershell,被 Powershell 不同于其他终端的语法所吸引,后来换成 macOS 了,依然无法放弃 Powershell,现在许多终端的任务都是在 Powershell 中进行的。

注,由于美元符号会被转义成公式,所以文中所有美元符号均用人民币符号(¥)代替,请在见到 ¥ 符号的时候自动脑补转换为美元符号。

安装

对于 Windows 用户来说,系统中已经内置了一个 Powershell 了,但是版本非常古老,而且界面比较难看,所以不建议使用。

最新版本的 Powershell(以下简称 pwsh)可以在 GitHub 下载:翻到这张表格的位置,找到您的操作系统,点击 Stable 或者 LTS 中的链接下载。当前(2021-5-14)的最新版本为 7.1.3。

安装完成之后,在终端中输入 pwsh(注意不是 powershell),即可运行。下面是 macOS 系统中的运行截图:

注意:由于 pwsh 中的命令格式与 Shell 命令格式不完全兼容,所以不建议将 pwsh 设置为 Linux 或 macOS 的默认终端,建议设置为 VSCode 默认终端或者 Atom 中 platformio-ide-terminal 控制台插件的默认终端。

1. Code Runner

VSCode 中的 Code Runner 插件是一个非常好用的终端运行程序的插件,其最大的不足就是每次运行程序之前都要编译一遍。如果代码非常短、编译非常快的话那还好,就怕代码长的时候,每次编译需要 10s,如果要连续测试多个数据,效率就非常低(要么直接在终端中输入 ./prog 运行,但是这样又比较麻烦)。那么,我们能否通过编写 pwsh 代码,实现“只有在代码修改后才重新编译”的目标呢?

答案是可以的。下面就来编写这段代码。

首先是编译的命令(编译选项可以自行添加):

g++ ¥fileName -o ¥fileNameWithoutExt

我们要实现的就是在这行编译命令外面套一个 if 语句,使得这行命令只有在源代码更改的时候才会执行。下面,就需要判断源代码是否有更改。

我们需要用一个变量存储上一次编译运行时代码的哈希,在 pwsh 中,变量以一个美元符号开头,没有被赋值的变量存储的内容默认为空(不会因为一个变量没有初始化就调用而报错)。这里,我们把存储上一次哈希值的变量命名为 ¥filehash, 存储当前哈希值的变量命名为 ¥Temp

第一阶段,我们需要获取当前文件的哈希值。Pwsh 中有一个内置命令 Get-FileHash 用于获取文件的哈希值,语法如下:

Get-FileHash [文件名] [[-Algorithm] {哈希算法:SHA1 | SHA256 | SHA384 | SHA512 | MD5}]

这里我们使用 MD5 算法(事实上其他的算法也没问题)。随便找一个 cpp 文件,代入这条命令:

此时,pwsh 返回了一个结构体,其中 Hash 为哈希值。只需要将这条语句用括号括起来,后面加上 .Hash,就得到了哈希值的字符串。

将这个字符串赋值到 ¥Temp 变量中,第一阶段就完成了。Pwsh 中变量的赋值直接使用 ¥var = value 的形式即可。除了赋值以外,pwsh 的其他语法也比较 C-like(毕竟是用 C# 开发的)。

下面是第一部分的代码:

cd ¥dir
¥Temp = (Get-FileHash -Algorithm MD5 ¥fileName).Hash

接下来是第二阶段,即判断文件是否有变化。前面定义了一个 ¥filehash 变量,表示上一次文件的哈希,则只需要判断两个哈希值是否相等即可。

Pwsh 中值的比较与 C++ 稍有不同,比如,C++ 中的 a < b,在 Pwsh 中就是 ¥a -lt ¥b,下面是一张对照表:

C++ Pwsh
< -lt
<= -le
== -eq
!= -ne
> -gt
>= -ge
&& -and
|| -or
! -not!

¥Temp -ne ¥filehash 判断当前的哈希值是否与上一次哈希值不同,套进 if 语句中即可。前面说过,Pwsh 的一些语法与 C++、C# 比较类似,所以 if 语句的写法也很简单:

cd ¥dir
¥Temp = (Get-FileHash -Algorithm MD5 ¥fileName).Hash
if (¥Temp -ne ¥filehash) {
    ¥filehash = ¥Temp
    g++ ¥fileName -o ¥fileNameWithoutExt
}

这样,我们就完成了只有在代码修改后才重新编译的功能。

注意,如果程序编译失败了,假设在没有改动的情况下再进行一次编译运行,就会导致编译环节被跳过,直接运行。所以,我们还需要使用一个变量判断编译是否成功(取名为 ¥LastOk):

cd ¥dir
¥Temp = (Get-FileHash -Algorithm MD5 ¥fileName).Hash
if (!¥LastOk -or (¥Temp -ne ¥filehash)) {
    ¥LastOk = 0
    ¥filehash = ¥Temp
    g++ ¥fileName -o ¥fileNameWithoutExt
    ¥LastOk = ¥?
}

Pwsh 中 ¥? 相当于 cmd 中的 %errorlevel%,但是存储的值不一样,%errorlevel% 存储的是程序的返回值,而 ¥? 直接存储上一条指令是否成功运行,如果成功则为 True,否则为 False

至此,我们就完成了编译环节。下面的运行环节也不复杂,如果程序成功编译,则运行:

if (¥LastOk) {
    ./¥fileNameWithoutExt
}

注意,这里的大括号不能删除。我们还可以加一些修饰,提醒我们“编译成功了,开始运行了”:

if (¥LastOk) {
    echo ("=" * 50)
    ./¥fileNameWithoutExt
    echo ("`n" + "=" * 50)
}

这里 ("=" * 50) 返回的是一个包含 50 个 = 的字符串,转义字符的前缀不是 \,而是 `(如换行符就是 `n)。

总的代码如下:

cd ¥dir
¥Temp = (Get-FileHash -Algorithm MD5 ¥fileName).Hash
if (!¥LastOk -or (¥Temp -ne ¥filehash)) {
    ¥LastOk = 0
    ¥filehash = ¥Temp
    g++ ¥fileName -o ¥fileNameWithoutExt
    ¥LastOk = ¥?
}
if (¥LastOk) {
    echo ("=" * 50)
    ./¥fileNameWithoutExt
    echo ("`n" + "=" * 50)
}

把代码压到一行(相邻两个语句之间要用分号隔开),贴进 settings.json 中(双引号要改成 \"),试着运行一个程序:

大功告成!(这里不方便演示,实际上第二次运行的时候不会重新编译)

注:我使用的是 SHA256 哈希算法,MD5 算法的效果一样。

2. 对拍

相信大家对如何使用 cmd、如何使用 C++ 写对拍脚本都非常熟悉,但是这几种对拍方案各有各的不足,如 cmd 中对拍脚本拓展性差(语法比较难看,不跨系统,而且 Linux 中的 shell 语法更难看),而 C++ 写对拍脚本非常麻烦(一堆 systemc_str)。

而 Powershell 写对拍就非常的舒适,语法好看,代码量不大,而且简洁。更重要的是,您可以轻松将您的对拍脚本封装起来,之后直接调用,非常方便。

一次对拍一般包含以下几个部分:

假设当前的暴力程序是 ./good,待测试的程序是 ./bad,生成器是 ./gen。第一步,需要输入一个种子进入 ./gen(也可以在程序中 srand(time(0)),但是这样会导致每一秒只有一个有效数据,或者使用 chrono::steady_clock,但是如果遇到一个不支持 C++11 的 g++ 编译器就难受了,所以输入种子进生成器是一个比较好的选择),并且将生成的数据输出到 dat.in

在 cmd 中可以使用 %random% 生成随机数,但是随机的范围只有 0 到 32767。在 pwsh 中,可以使用 Get-Random 获取随机数,范围从 0 到 INT_MAX。总的指令如下:

Get-Random | ./gen > dat.in

下一步,从 dat.in 读入数据进 ./good,输出到 dat.ans。Pwsh 中不支持 ./good < dat.in 这样的写法,所以可以改写成 Get-Content dat.in | ./good。总的指令如下:

Get-Content dat.in | ./good > dat.ans

接下来是 ./bad,与前面类似,语句如下:

Get-Content dat.in | ./bad > dat.out

最后,判断两者输出是否相同,可以直接用 diff(判断返回值是否为 0 可以用前面说的方法)。在外面套一层 while (1),即可不断重复对拍。代码如下:

while (1) {
    Get-Random | ./gen > dat.in
    Get-Content dat.in | ./good > dat.ans
    Get-Content dat.in | ./bad > dat.out
    diff dat.out dat.ans
    if (!¥?) { break; }
}
echo "Wrong Answer!"

如果要加上一个计数器,也没有问题:

¥dat = 0
while (1) {
    ++¥dat
    echo "Running on test ¥dat"
    Get-Random | ./gen > dat.in
    Get-Content dat.in | ./good > dat.ans
    Get-Content dat.in | ./bad > dat.out
    diff dat.out dat.ans
    if (!¥?) { break; }
}
echo "Wrong Answer!"

这样,大功告成。

我们还可以将它封装成一个命令,以后输入这个命令后直接对拍。首先,输入 echo ¥profile 获取 profile 文件的位置,然后在文件管理器中找到这个文件,开始编辑(也可以 vim ¥profile 直接开始编辑)。Pwsh 中 function 的定义也很简单:

function FuncName(¥param) {
    # Function Contents
}

将上面几条对拍指令放到 function 中(注:pwsh 中如果想要运行一条用字符串存储的命令,只需要在字符串前面加上 & 即可):

function Start-Stress(¥good, ¥bad, ¥gen) {
    ¥dat = 0
    while (1) {
        ++¥dat
        echo "Running on test ¥dat"
        Get-Random | & "./¥gen" > dat.in
        Get-Content dat.in | & "./¥good" > dat.ans
        Get-Content dat.in | & "./¥bad" > dat.out
        diff dat.out dat.ans
        if (!¥?) { break; }
    }
    echo "Wrong Answer!"
}

放进 ¥profile 里。重启 pwsh,输入 Start-Stress good bad gen 即可开始对拍。

(此图仅为效果展示)

3. 爬图

前面已经有人介绍过如何用 Python 进行爬虫了,但是 Python 爬虫需要查阅一大堆资料,而 Powershell 爬虫就非常方便,只需要一个命令、几句话就行了。

找到需要爬虫的网站,这里使用 https://api.btstu.cn/sjbz/?lx=dongman,这个网站每次会随机显示一张动漫图片,其图片数量之多,基本上可以做到刷新个十几次都看不到一张重复的图片。下面,我们的目标就是将这上面的所有图全部爬下来。

(可以多刷新几次页面,每次看到的图都是不同的)

在此介绍一个 pwsh 命令:Invoke-WebRequest,这个命令可以直接获取网页内容并下载到文件。文档的内容较长,这里就放一个简化版的命令说明了,详情可以在 pwsh 中输入 help Invoke-WebRequest 命令查看详细说明。

Invoke-WebRequest <uri> -OutFile <file>

该命令可以将 <uri> 中的内容下载到 <file>。为了防止下载重复的图片,我们需要加入一个判断条件:维护一个列表,如果该文件的 MD5 已经在列表中出现过,那就跳过,否则当前编号加 1。前面已经提到计算文件哈希的方法了,所以这里仅写出指令:

¥hsh = (Get-FileHash "image.jpg" -Algorithm "MD5").Hash

新建一个空列表,设这个列表的名字叫 ¥arr

¥arr = @()

与 C# 类似,pwsh 的列表类里面也有一个 Contains 函数,表示该列表中是否包含某个元素,有一个 Count 函数表示列表中元素的个数。

有了这些之后,就可以开始写了(注:Write-Host 用途与 echo 一样):

¥arr = @()

while (¥true) {
    Invoke-WebRequest "https://api.btstu.cn/sjbz/?lx=dongman" -OutFile ("" + (¥arr.Count + 1) + ".jpg")
    ¥hsh = (Get-FileHash ("" + (¥arr.Count + 1) + ".jpg") -Algorithm "MD5").Hash
    if (!¥arr.Contains(¥hsh)) {
        ¥arr += ¥hsh
        Write-Host ("Got file " + ¥arr.Count)
    }
}

这里的 ¥true 与 C++ 中的 true 一个意思,表示真。同理,还有以下几个保留值(或变量):

名称 表示
¥true
¥false
¥null 一个黑洞容器,会吃掉所有赋给它的值

下面开始运行,大约半天不到的时间,下载了 1111 张图片之后,就不再有任何新的图片出现了(大家也可以自己尝试一下)。

时间复杂度分析

什么,这也有时间复杂度?

由于图片随机出现,设总共有 n 张图,现在已经获取了 i 张,则下一张与前面不同的概率为 \frac{n-i}{n},期望次数为 \frac n{n-i}

故获取所有图片的期望次数为

\sum_{i=1}^n \frac ni

这是一个调和级数的形式,故期望获取图片的次数为 \mathcal O(n\log n)

后记

Powershell 还有很多很高级的功能,特别的,作为用 C# 开发的终端,Powershell 中还支持许多 C# 中的类(比如 System.Collections.*,内置了许多数据结构)。如果您想要了解更多关于 Powershell 的使用,您可以前往 Microsoft Docs 查看更详细的文档。

(图:用 Powershell 播放音乐,此功能仅限 Windows 系统使用)