Powershell 的一些应用

Acetyl

2021-05-14 11:17:15

Personal

# 前言 对于 Windows 用户来说,Powershell 已经是再熟悉不过的东西了,大部分时候,我们使用 Powershell 的目的和使用 cmd 差不多,只是运行一些命令而已。 实际上,运行命令只是 Powershell 的第一层功能。正如它的名字,这个看似不起眼的终端内部,隐藏着巨大的能量(Power)。之前我用 Windows 的时候开始使用 Powershell,被 Powershell 不同于其他终端的语法所吸引,后来换成 macOS 了,依然无法放弃 Powershell,现在许多终端的任务都是在 Powershell 中进行的。 **注,由于美元符号会被转义成公式,所以文中所有美元符号均用人民币符号(¥)代替,请在见到 ¥ 符号的时候自动脑补转换为美元符号。** # 安装 对于 Windows 用户来说,系统中已经内置了一个 Powershell 了,但是版本非常古老,而且界面比较难看,所以不建议使用。 ![](https://ae01.alicdn.com/kf/Ub2a1c8e482ea4753b0a8d77f4f2a3f145.jpg) 最新版本的 Powershell(以下简称 pwsh)可以在 [GitHub](https://github.com/PowerShell/PowerShell) 下载:翻到这张表格的位置,找到您的操作系统,点击 Stable 或者 LTS 中的链接下载。当前(2021-5-14)的最新版本为 7.1.3。 ![](https://ae01.alicdn.com/kf/U4722f8f267dc446e93fcf93e75f98d59c.jpg) 安装完成之后,在终端中输入 `pwsh`(注意不是 `powershell`),即可运行。下面是 macOS 系统中的运行截图: ![](https://ae01.alicdn.com/kf/Ued1d9739e44d40659789c0b9e3da981bU.jpg) **注意:由于 pwsh 中的命令格式与 Shell 命令格式不完全兼容,所以不建议将 pwsh 设置为 Linux 或 macOS 的默认终端,建议设置为 VSCode 默认终端或者 Atom 中 `platformio-ide-terminal` 控制台插件的默认终端。** # 1. Code Runner VSCode 中的 `Code Runner` 插件是一个非常好用的终端运行程序的插件,其最大的不足就是每次运行程序之前都要编译一遍。如果代码非常短、编译非常快的话那还好,就怕代码长的时候,每次编译需要 10s,如果要连续测试多个数据,效率就非常低(要么直接在终端中输入 `./prog` 运行,但是这样又比较麻烦)。那么,我们能否通过编写 pwsh 代码,实现“只有在代码修改后才重新编译”的目标呢? 答案是可以的。下面就来编写这段代码。 首先是编译的命令(编译选项可以自行添加): ``` powershell g++ ¥fileName -o ¥fileNameWithoutExt ``` 我们要实现的就是在这行编译命令外面套一个 if 语句,使得这行命令只有在源代码更改的时候才会执行。下面,就需要判断源代码是否有更改。 我们需要用一个变量存储上一次编译运行时代码的哈希,在 pwsh 中,变量以一个美元符号开头,没有被赋值的变量存储的内容默认为空(不会因为一个变量没有初始化就调用而报错)。这里,我们把存储上一次哈希值的变量命名为 `¥filehash`, 存储当前哈希值的变量命名为 `¥Temp`。 第一阶段,我们需要获取当前文件的哈希值。Pwsh 中有一个内置命令 `Get-FileHash` 用于获取文件的哈希值,语法如下: ``` powershell Get-FileHash [文件名] [[-Algorithm] {哈希算法:SHA1 | SHA256 | SHA384 | SHA512 | MD5}] ``` 这里我们使用 MD5 算法(事实上其他的算法也没问题)。随便找一个 cpp 文件,代入这条命令: ![](https://ae01.alicdn.com/kf/Ua0a0736fc951463d8954f00733972bb7l.jpg) 此时,pwsh 返回了一个结构体,其中 `Hash` 为哈希值。只需要将这条语句用括号括起来,后面加上 `.Hash`,就得到了哈希值的字符串。 ![](https://ae01.alicdn.com/kf/Ue1a0dd2d5b7944acb65803583ae910b7H.jpg) 将这个字符串赋值到 `¥Temp` 变量中,第一阶段就完成了。Pwsh 中变量的赋值直接使用 `¥var = value` 的形式即可。除了赋值以外,pwsh 的其他语法也比较 C-like(毕竟是用 C# 开发的)。 下面是第一部分的代码: ``` powershell 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` 语句的写法也很简单: ``` powershell cd ¥dir ¥Temp = (Get-FileHash -Algorithm MD5 ¥fileName).Hash if (¥Temp -ne ¥filehash) { ¥filehash = ¥Temp g++ ¥fileName -o ¥fileNameWithoutExt } ``` 这样,我们就完成了只有在代码修改后才重新编译的功能。 注意,如果程序编译失败了,假设在没有改动的情况下再进行一次编译运行,就会导致编译环节被跳过,直接运行。所以,我们还需要使用一个变量判断编译是否成功(取名为 `¥LastOk`): ``` powershell 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`。 至此,我们就完成了编译环节。下面的运行环节也不复杂,如果程序成功编译,则运行: ``` powershell if (¥LastOk) { ./¥fileNameWithoutExt } ``` 注意,这里的大括号不能删除。我们还可以加一些修饰,提醒我们“编译成功了,开始运行了”: ``` powershell if (¥LastOk) { echo ("=" * 50) ./¥fileNameWithoutExt echo ("`n" + "=" * 50) } ``` 这里 `("=" * 50)` 返回的是一个包含 50 个 `=` 的字符串,转义字符的前缀不是 `\`,而是 \`(如换行符就是 \`n)。 总的代码如下: ``` powershell 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` 中(双引号要改成 `\"`),试着运行一个程序: ![](https://ae01.alicdn.com/kf/U1a4cdc80356f4c6a82d13ef25309514fz.jpg) 大功告成!(这里不方便演示,实际上第二次运行的时候不会重新编译) 注:我使用的是 SHA256 哈希算法,MD5 算法的效果一样。 # 2. 对拍 相信大家对如何使用 cmd、如何使用 C++ 写对拍脚本都非常熟悉,但是这几种对拍方案各有各的不足,如 cmd 中对拍脚本拓展性差(语法比较难看,不跨系统,而且 Linux 中的 shell 语法更难看),而 C++ 写对拍脚本非常麻烦(一堆 `system`、`c_str`)。 而 Powershell 写对拍就非常的舒适,语法好看,代码量不大,而且简洁。更重要的是,您可以轻松将您的对拍脚本封装起来,之后直接调用,非常方便。 一次对拍一般包含以下几个部分: * 随机生成一个数据 * 用暴力程序运行这个数据,得到正确的输出 * 用待测试的程序运行这个数据,得到可能错误的输出 * 比较两个输出(可以直接 diff 或 fc,也可以 SPJ) 假设当前的暴力程序是 `./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`。总的指令如下: ``` powershell Get-Random | ./gen > dat.in ``` 下一步,从 `dat.in` 读入数据进 `./good`,输出到 `dat.ans`。Pwsh 中不支持 `./good < dat.in` 这样的写法,所以可以改写成 `Get-Content dat.in | ./good`。总的指令如下: ``` powershell Get-Content dat.in | ./good > dat.ans ``` 接下来是 `./bad`,与前面类似,语句如下: ``` powershell Get-Content dat.in | ./bad > dat.out ``` 最后,判断两者输出是否相同,可以直接用 `diff`(判断返回值是否为 0 可以用前面说的方法)。在外面套一层 `while (1)`,即可不断重复对拍。代码如下: ``` powershell 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!" ``` 如果要加上一个计数器,也没有问题: ``` powershell ¥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 的定义也很简单: ``` powershell function FuncName(¥param) { # Function Contents } ``` 将上面几条对拍指令放到 function 中(注:pwsh 中如果想要运行一条用字符串存储的命令,只需要在字符串前面加上 `&` 即可): ``` powershell 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` 即可开始对拍。 ![](https://ae01.alicdn.com/kf/Ue4e47fab52c14405a09f76bd885c10ecF.jpg) (此图仅为效果展示) # 3. 爬图 前面已经有人介绍过如何用 Python 进行爬虫了,但是 Python 爬虫需要查阅一大堆资料,而 Powershell 爬虫就非常方便,只需要一个命令、几句话就行了。 找到需要爬虫的网站,这里使用 `https://api.btstu.cn/sjbz/?lx=dongman`,这个网站每次会随机显示一张动漫图片,其图片数量之多,基本上可以做到刷新个十几次都看不到一张重复的图片。下面,我们的目标就是将这上面的所有图全部爬下来。 ![](https://api.btstu.cn/sjbz/?lx=dongman) (可以多刷新几次页面,每次看到的图都是不同的) 在此介绍一个 pwsh 命令:`Invoke-WebRequest`,这个命令可以直接获取网页内容并下载到文件。文档的内容较长,这里就放一个简化版的命令说明了,详情可以在 pwsh 中输入 `help Invoke-WebRequest` 命令查看详细说明。 ``` powershell Invoke-WebRequest <uri> -OutFile <file> ``` 该命令可以将 `<uri>` 中的内容下载到 `<file>`。为了防止下载重复的图片,我们需要加入一个判断条件:维护一个列表,如果该文件的 MD5 已经在列表中出现过,那就跳过,否则当前编号加 1。前面已经提到计算文件哈希的方法了,所以这里仅写出指令: ``` powershell ¥hsh = (Get-FileHash "image.jpg" -Algorithm "MD5").Hash ``` 新建一个空列表,设这个列表的名字叫 `¥arr`: ``` powershell ¥arr = @() ``` 与 C# 类似,pwsh 的列表类里面也有一个 `Contains` 函数,表示该列表中是否包含某个元素,有一个 `Count` 函数表示列表中元素的个数。 有了这些之后,就可以开始写了(注:`Write-Host` 用途与 `echo` 一样): ``` powershell ¥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](https://docs.microsoft.com/zh-cn/powershell/) 查看更详细的文档。 ![](https://ae01.alicdn.com/kf/U6a938e7a37ef4d4c95f95cf1ed60e26bI.jpg) (图:用 Powershell 播放音乐,此功能仅限 Windows 系统使用)