Update 2021-09-17 23:09:10:写了一半不太想继续写下去,废稿。

脚本(Script)的出现是为了减少重复的劳动,将复杂的操作转换为一个简单的命令,或者说——实现计算机任务的自动化。这是从外部看,「Script」这个命名也非常形象地解释了它的内部原理:像电影的剧本一行一行被演员按顺序读出,脚本中的文本由计算机逐行解析然后运行。电影剧本使用的是自然语言,而脚本使用的是脚本语言——能被计算机读懂的语言。

脚本语言有很多:Javascript,Python 、Bash 和本文的主角 PowerShell。这些语言都是高级语言,「高级」是相对于传统的汇编语言而言,高级语言同我们讲出来的自然语言非常相似,因此学习起来相对简单。

本文展示了一个 PowerShell 脚本,它的后缀名应该是.ps1。读者不必觉得畏惧,此脚本核心只有三行。你大可以先跳过这串代码,直接阅读正文,正文部分将逐行逐块地解析 PowerShell 脚本的构成、语法以及其注意事项。

<#
    .SYNOPSIS
    Hardlink Obsidian Markdown file to Hexo source folder file.
    
    .DESCRIPTION
    此脚本用于将 Obsidian 中的文本硬链接到 Hexo 库中。
    
    脚本将获取剪贴板,并将其自动格式化为文本路径,然后在 Hexo 库中按计数新建一个 Markdown 文件并硬链接到 Obsidian 文件。
    
    处理结果使用系统通知的方式告知用户

    .INPUTS
    无需参数。脚本将获取剪贴板首位值。

    .OUTPUTS
    无

    .EXAMPLE
    PS> .\AutoLink-Blog.ps1

    .EXAMPLE
    PS> & "$home\Script\Demo PowerShell\AutoLink-Blog.ps1"
    
    .LINK
    https://mirtle.cn/posts/153

#>

# Function 部分实现了通知

function New-Notification{
    param (
        [Parameter(mandatory=$True)][string]$Content,
        [Parameter(mandatory=$True)][string]$Title,
        [Parameter(mandatory=$True)][string]$Type
    )
    [reflection.assembly]::loadwithpartialname("System.Windows.Forms")
    [reflection.assembly]::loadwithpartialname("System.Drawing")
    $notify = new-object system.windows.forms.notifyicon
    $notify.icon = [System.Drawing.SystemIcons]::Information
    $notify.visible = $true
    $notify.showballoontip(5,$Title,$Content,[system.windows.forms.tooltipicon]::$Type) 
}

[String]$v = get-clipboard -raw

if( $v.Indexof('obsidian://') -eq 0){
    $count = (dir -Path $home/Hexo/source/_posts).Length
    $v = [System.Web.HttpUtility]::UrlDecode($v.Substring(32))
    $Source = "$home/Documents/Note/" + "$v" + ".md"
    New-Item "$home/Hexo/source/_posts/$count.md" -Type HardLink -Target "$Source"
    if($?){
        New-Notification -Title 'Success' -Content "链接 $count.md 到 $Source 完成" -Type 'Info'
        }
    else{
        New-Notification -Title 'Error' -Content "新建失败,请确认「 $Source 」地址无误" -Type 'Error'
    }
}
else{
    New-Notification -Title 'Error' -Content "「 $v 」格式不符合要求" -Type 'Error'
}
exit

注释和帮助系统

观察上面的脚本,首先映入眼帘的是被<# #>框起来,没有高亮的数行文字。这是脚本的注释。注释没有任何功能性的作用,计算机在读取解析脚本时会直接跳过注释部分,就像演员念台词时不会念出「深情地说」等提示性词汇。

在我看过的几本 PowerShell 书籍中,印象最深的一句话就和注释有关:

分享脚本,却不给脚本写注释的人都是坏人

严肃的技术教学书中出现这样一句俏皮的话让人忍俊不禁,不过作者说的是事实。注释是写给人看的,脚本的作者需要向用户——也许是未来的合作者——解释这个脚本的作用,使用方法,注意事项等。这能大大减少用户探索的成本。

PowerShell 的注释

像其他语言一样,PowerShell 也有两种注释,在示例脚本中都有体现:

  1. 多行注释:<# Comment #>
  2. 行内注释:# Comment

注释可以写在任何位置,不过有约定俗成的习惯:就近原则。我们应该把注释放在离被注释的命令仅可能近的位置。如果放在脚本的开头,那么一般认为这是对整个脚本的注释,类似于我在脚本中做的;如果放在块(Block)的开头,就是对这个脚本块的解释;如果放在一行命令的末尾或者前一行,那么就是对这行命令的解释。

注释的内容也有公认的习惯。在 PowerShell 中,一般需要讲明描述、语法、参数、示例、链接这五种内容。而在示例脚本中,我还采用了格式化的方式书写注释:每一类注释都以. + 类别开头,然后换行写注释主体,这不仅是为了好看,而且是为了让用户不必用编辑器查看脚本的源码,直接用 PowerShell 从外部读取脚本的注释。

从注释到帮助

在脚本编辑器中我们看到的是注释,而对于用户而言,使用脚本前总是用编辑器打开脚本是不现实的。用户需要按需求展示注释内容,获得帮助。这就是 PowerShell 的核心命令Get-Help所能做的。

下载示例脚本,或者将本文的脚本另存为.ps1文件,在脚本目录打开 PowerShell,键入Get-Help -Name .\script.ps1

内置的帮助系统

  • Get-Help 和 Help 和 ?
  • Get-Command

最基础的要素——命令

脚本是由一行一行的命令组成的,命令之间以看不见的换行符被分隔开来。

脚本中有三种命令:外部命令,.Net 命令 和 Cmdlet

Cmdlet 命令

动词 - 单数名词。动词以Get-Verb获得。

尽管 PowerShell 在命令和参数上不区分大小写,但为了读取方便,统一规范是帕斯卡命名方式——复合名词间首字母大写。

Cmdlet 命令后是参数,其基本结构为Do-Cmdlet -ParameterName ParameterValue

.Net 命令

调用 .Net 几乎可以实现 Windows 中的任何操作。其基本的格式是[.Net Name]::Property(Parameter)

外部命令

调用外部命令,例如.ps1脚本,exe文件。按照外部命令的要求来。唯一需要注意的是,当外部命令的路中有空格时,需要用双引号"扩起来。但当使用双引号时,PowerShell 会认为引号中的是一个字符串,因此我们需要使用& 字符,如

& "Drive:\Path Name\to Command\Test.exe

丰富命令的表达——操作符和运算符

只是命令参数未免太过单调,PowerShell 最难理解的就是出现在命令内部的一个个操作符和运算符号:

操作符 符号 功能
A.B . 调用 A 的属性和方法
. Command . 在会话中引入命令
!A ! 取反
`n ` 转义
'a' ' 让 a 被视为一个字符串
"a" " 让 a 被视为一个字符串,但解析其中的变量
$a $ 声明变量
#a # 注释
(a) () 分组
{a} {} 代码块
[a] [] 界定
& Command & 执行
| | 管道

从命令到语句

条件

if(){}else{}

循环

for(){}

声明

function Verb-Noun{}

param()

……