Acetyl
2021-05-14 11:17:15
对于 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
控制台插件的默认终端。
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 算法的效果一样。
相信大家对如何使用 cmd、如何使用 C++ 写对拍脚本都非常熟悉,但是这几种对拍方案各有各的不足,如 cmd 中对拍脚本拓展性差(语法比较难看,不跨系统,而且 Linux 中的 shell 语法更难看),而 C++ 写对拍脚本非常麻烦(一堆 system
、c_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
即可开始对拍。
(此图仅为效果展示)
前面已经有人介绍过如何用 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 张图片之后,就不再有任何新的图片出现了(大家也可以自己尝试一下)。
什么,这也有时间复杂度?
由于图片随机出现,设总共有
故获取所有图片的期望次数为
这是一个调和级数的形式,故期望获取图片的次数为
Powershell 还有很多很高级的功能,特别的,作为用 C# 开发的终端,Powershell 中还支持许多 C# 中的类(比如 System.Collections.*
,内置了许多数据结构)。如果您想要了解更多关于 Powershell 的使用,您可以前往 Microsoft Docs 查看更详细的文档。
(图:用 Powershell 播放音乐,此功能仅限 Windows 系统使用)