脚本语言

一个 AutoHotkey 脚本从根本上说是使用 AutoHotkey 独有的自定义语言编写的程序要遵循的指令集合. 这种语言与其他几种脚本语言有一些相似之处, 但也有其独特的优势和缺陷. 本文档描述了该语言, 并试图指出常见的缺陷.

有关 AutoHotkey 所使用的各种概念的更一般的解释, 请参阅概念和约定.

目录

常规约定

名称: 变量和函数名称不区分大小写(例如, CurrentDate 等同于 currentdate). 有关详细信息(如最大长度和可用字符), 请参阅名称.

无类型变量: 变量没有显式定义的类型; 相反, 任何类型的值都可以存储在任何变量中(不包括常量和内置变量). 数字可能会自动转换为字符串(文本), 反之亦然, 这取决于实际情况.

声明是可选的: 除了在函数页面上注明的地方外, 变量不需要声明. 然而, 在给定一个变量的值之前, 试图读取该变量被视为一个错误.

空格通常被忽略: 缩进(前导空格) 对于编写可读代码非常重要, 但是程序不需要, 通常会被忽略. 通常 在行尾的, 表达式内的(引号之间的除外), 以及命令参数之前和之后的空格和制表符会被忽略. 然而, 在一些情况下, 空格是重要的, 包括:

换行符是有意义的: 换行符通常作为语句分隔符, 终止前一个函数或其他语句. (语句 只是语言中表示要执行某些操作的最小的独立元素.) 这个例外是行延续(请参见下文).

行延续: 长行可以分成一些小行, 以提高可读性和可维护性. 这是通过预处理实现的, 所以不属于这种语言的一部分. 有三种方法:

注释

注释 是脚本中被程序忽略的那部分文本. 它们通常用于添加解释或禁用部分代码.

可以通过在行的开头使用分号来注释脚本. 例如:

; 这一整行都是注释.

也可以在行的末尾添加注释, 此时分号左侧必须至少有一个空格或 tab. 例如:

Run "Notepad"  ; 这是与函数调用在同一行上的注释.

此外, 可以使用 /**/ 符号注释掉整块代码, 但仅当它们出现在行首时才有效(对于 */, 可以在行的开头或结尾处), 不包括空白字符, 如下例所示:

/*
MsgBox "这行被注释(禁用) 掉了."
MsgBox "常见的错误:" */ " 这不会结束注释."
MsgBox "这行被注释掉了."
*/
MsgBox "这行没有被注释."
/* 这也是有效注释, 但是这一行不能有其他代码. */
MsgBox "这行没有被注释."

除了制表符和空格, /* 必须出现在行首, 而 */ 只能出现在行首或行尾. 省略 */ 也是有效的, 在这种情况下, 文件的其余部分将被注释掉.

由于脚本运行时会忽略注释, 所以它们不会影响脚本性能或占用内存.

表达式

表达式 是一个或多个, 变量, 运算符函数调用的组合. 例如, 10, 1+1MyVar 都是有效的表达式. 通常, 表达式将一个或多个值作为输入, 执行一个或多个操作, 并生成一个值作为结果. 找出表达式值的过程被称为 计算. 例如, 表达式 1+1 计算 出数字 2.

简单的表达式可以拼凑在一起形成更复杂的表达式. 例如, 如果 Discount/100 将折扣百分比转换为分数, 1 - Discount/100 计算剩余金额的分数, 而 Price * (1 - Discount/100) 应用它来产生净价格.

数字, 对象字符串. 原义 值是在脚本中实际写入的值; 当您在查看代码时可以看到该值(文字).

字符串 / 文本

有关字符串的更一般的解释, 请参阅字符串.

字符串字符组成的串, 只是一个文本值. 在表达式中, 原义的文本必须用引号引起来, 以区分变量名称或其他表达式. 这通常被称为 加引号的原义字符串, 或者为 加引号的字符串. 例如, "this is a quoted string"'so is this'.

要在原义字符串中包含 真实的 引号字符, 请使用转义序列 `"`', 或将字符括在相反类型的引号中. 例如: 'She said, "An apple a day."'.

加引号的字符串能包含其他的转义序列, 如 `t(制表符), `n(换行) 和 `r(回车).

变量

有关变量的基本解释和常规细节, 请参阅变量.

变量 可以简单地通过写变量的名称来用于表达式. 例如, A_ScreenWidth/2. 但是, 变量不能在加引号的字符串中使用. 作为替代, 变量和其他值可以通过名为 连接 的过程与文本组合起来. 有两种方法能用于 连接 表达式中的值:

隐式连接也被称为 自动连接. 在这两种情况下, 变量和点之前的空格都是必需的.

Format 函数也可以用于此目的. 例如:

MsgBox Format("You are using AutoHotkey v{1} {2}-bit.", A_AhkVersion, A_PtrSize*8)

要为变量赋值, 请使用 := 赋值运算符, 如 MyVar := "Some text".

表达式中的 百分号 用于创建动态变量引用, 但这些都是很少需要的.

常量关键字

常量就是一个不可改变的值, 给定一个符号名称. AutoHotkey 目前有以下常量:

名称类型描述
False0整数布尔值 false, 有时表示 "off", "no", 等等.
True1整数布尔值 true, 有时表示 "on", "yes", 等等.

与只读内置变量不同, 这些变量不能通过动态引用返回.

运算符

运算符 采用符号或符号组的形式(如 +:=), 或者下列其中一个单词 and, or, not, is, incontains. 它们将一个, 两个或三个值作为输入, 并返回一个值作为结果. 用作运算输入的值或子表达式称为 运算元.

一些一元和二元运算符共享相同的符号, 在这种情况下, 运算符的含义取决于它是写在两个值之前, 之后还是之间. 例如, x-y 执行减法, 而 -x 反转 x 的符号(从负值产生正值, 反之亦然).

除非在运算符表中另有规定, 否则优先级相等的运算符(如, 乘号(*) 和除号(/) 按从左到右的顺序计算. 相反, 诸如加(+) 之类的优先级较低的运算符在诸如乘(*) 之类的优先级较高运算符之后被计算. 例如, 3 + 2 * 2 作为 3 + (2 * 2) 计算. 括号可以用来覆盖优先级, 如以下示例所示: (3 + 2) * 2

函数调用

有关函数和相关术语的一般解释, 请参阅函数.

函数 接受可变数量的输入, 去执行一些动作或计算, 然后 return(返回) 一个结果. 函数的输入被称为参数(parametersarguments). 一个函数被 called(调用), 只需写下目标函数, 后面的参数用括号括起来即可. 例如, 如果 Shift 键被按下, 则 GetKeyState("Shift") 返回(计算为) 1, 否则返回 0.

注意: 函数和左括号之间不能有任何空格.

对于刚接触编程的人来说, 括号的要求起初可能看起来很神秘或冗长, 但它们允许将函数调用与其他操作结合起来. 例如, 只有当两个键被物理按下时, 表达式 GetKeyState("Shift", "P") and GetKeyState("Ctrl", "P") 才会返回 1.

虽然函数调用表达式通常以原义的函数名开头, 但调用的目标可以是生成函数对象的任何表达式. 在表达式 GetKeyState("Shift") 中, GetKeyState 实际上是一个变量引用, 尽管它通常引用包含内置函数的只读变量.

函数调用语句

如果不需要函数的返回值, 并且函数名写在行首(或者在其他允许语句的上下文中, 如下面的 elsehotkey), 则可以省略括号. 在这种情况下, 行的其余部分作为函数的参数列表. 例如:

result := MsgBox("This one requires parentheses.",, "OKCancel")
MsgBox "This one doesn't. The result was " result "."

在同一上下文中调用方法时, 也可以省略圆括号, 但仅当目标对象是变量或直接命名的属性时才可以省略圆括号, 如 myVar.myMethodmyVar.myProp.myMethod.

与函数调用表达式一样, 函数调用语句的目标不一定是预定义的函数; 它可以是一个包含函数对象的变量.

函数调用语句可以跨越多行.

函数调用语句有以下限制:

可选参数

可选参数可以简单地留空, 但是分隔逗号仍然是必需的, 除非所有后续参数也被省略. 例如, Run 函数可接受一至四个参数. 下列各项均有效:

Run "notepad.exe", "C:\"
Run "notepad.exe",, "Min"
Run("notepad.exe", , , &notepadPID)

对象的运算符

这里表达式中使用的其他符号不完全符合上面定义的任何类别, 或影响表达式其他部分的含义, 如下所述. 这些都以某种方式与 对象 有关. 对于每个构造所做的事情提供一个完整的解释, 需要引入更多的概念, 而这不属于本节的范围.

Alpha.Beta 通常称为 成员访问. Alpha 是一个普通变量, 可以用函数调用或其他一些返回对象的子表达式替换. 当计算时, 对象发送一个请求 "给我属性 Beta 的值", "在属性 Beta 中存储这个值" 或 "调用名为 Beta 的方法". 换句话说, Beta 是一个对对象有意义的名字; 它不是一个局部或全局变量.

Alpha.Beta() 是一个 方法调用, 如上所述. 括号可在特定情况下省略; 请参阅函数调用语句.

Alpha.Beta[Param] 是成员访问的一种特殊形式, 其中包括了请求中的附加参数. Beta 只是一个简单的名称, Param 是一个普通的变量或子表达式, 或者是由逗号分隔的子表达式列表(与函数的参数列表中相同). 允许可变参数调用.

Alpha.%vBeta%, Alpha.%vBeta%[Param]Alpha.%vBeta%() 也是成员访问, 但 vBeta 是一个变量或子表达式. 这允许在脚本运行时确定属性或方法的名称. 以这种方式调用方法时需要括号.

Alpha[Index] 访问 Alpha默认属性, 将 Index 作为参数. 在这种情况下, AlphaIndex 都是变量, 几乎可以用任何子表达式替换. 此语法通常用于检索数组Map 中的元素.

[A, B, C] 创建一个初始内容为 A, B 和 C(本例中的所有变量) 的数组, 其中 A 是元素 1.

{Prop1: Value1, Prop2: Value2} 创建一个对象, 其中 Prop1Prop2 为原义的属性名. 稍后可以使用上面描述的 成员访问 语法检索值. 要将属性名作为表达式进行计算, 请用百分号将其括起来. 例如: {%NameVar%: ValueVar}.

MyFunc(Params*) 是一个可变函数调用. 星号必须紧接在函数参数列表末尾的右括号之前. Params 必须是返回数组或其他可枚举对象的变量或子表达式. 虽然在任何地方使用 Params* 都是无效的, 但它可以用在数组标识符([A, B, C, ArrayToAppend*]) 或属性参数列表(Alpha.Beta[Params*]Alpha[Params*]) 中.

表达式语句

并不是所有的表达式都可以单独在一行上使用. 例如, 只包含 21*2"Some text" 的行就没有任何意义. 表达式 语句 是一个单独使用的表达式, 通常利用它的附加作用. 大多数带有附加作用的表达式都可以这样使用, 所以一般不需要记住本节的细节.

以下类型的表达式可以用作语句:

赋值(如 x := y), 复合赋值(如 x += y) 和增量/减量运算符(如 ++xx--).

已知限制: 对于 x++x--, 目前变量名和运算符之间不能有空格.

函数调用(如 MyFunc(Params)). 但是, 一个独立的函数调用不能跟随一个左大括号 {(在行尾或下一行), 因为它会与函数声明混淆.

方法调用(如 MyObj.MyMethod()).

使用方括号的成员访问(如 MyObj[Index]), 它可能有类似于函数调用的附加作用.

三元表达式(如 x? CallIfTrue() : CallIfFalse()). 但是, 使用下面的规则更安全; 也就是说, 始终将表达式(或条件) 括在括号中.

已知限制: 由于函数调用语句的模糊性, 以变量名和空格开头的条件(还包含其他操作符) 应该用圆括号括起来. 例如, (x + 1) ? y : zx+1 ? y : z 是表达式语句, 但是 x + 1 ? y : z 是函数调用语句.

注意: 条件不能以 ! 或任何其他表达式运算符开头, 因为它将被解释为延续行.

( 开始的表达式. 但是, 通常必须在同一行有一个匹配的 ), 否则该行将被解释为延续片段的开始.

以双百分号开始的表达式(如 %varname% := 1). 这主要是由于实现的复杂性.

为简单起见, 也允许以上面描述的任一表达式(但不包括下面描述的) 开始的表达式. 例如, MyFunc()+1 目前是允许的, 尽管 +1 没有效果, 其结果会被丢弃. 由于错误检查的增强, 这些表达式在将来可能会失效.

函数调用语句类似于表达式语句, 但在技术上不是纯表达式. 例如, MsgBox "Hello, world!", myGui.Showx.y.z "my parameter".

控制流语句

有关控制流的一般说明, 请参阅控制流.

语句通过将他们括在大括号({}) 中(如 C, JavaScript 和类似语言) 组合成 , 但通常大括号必须出现在行的开头. 控制流语句可以应用于整个块或者只是单一语句.

控制流程语句的主体总是 一组 语句. 块被视为一组语句, 就像控制流语句及其主体一样. 以下相关语句与其主体一起彼此分组: IfElse; Loop/ForUntilElse; TryCatch 和/或 Else 和/或 Finally. 换句话说, 当这些语句组作为一个整体使用时, 并不总是需要用大括号括起来(但是, 为了清楚起见, 一些编码样式总是包含大括号).

控制流语句, 它具有一个主体, 因此必须总是跟着一个相关的语句或一组语句: If, Else, Loop, While, For, Try, CatchFinally.

下面的控制流语句如下::

控制流与其他语句

控制流语句与函数调用语句在以下几个方面不同:

Loop 语句

有几种类型的 loop 语句:

Break 退出(终止) 一个循环, 有效地跳到循环主体后面的下一行.

Continue 跳过当前循环迭代的其余部分, 并开始一个新的循环.

Until 表达式计算结果为 true 时, 循环终止. 表达式在每次迭代之后被重新计算.

标签可以用来 "命名" ContinueBreak 的循环. 这允许脚本轻松地继续或跳出任何数量的嵌套循环而不使用 Goto.

内置变量 A_Index 包含当前循环迭代的编号. 它在第一次执行循环主体时为 1. 第二次时为 2; 依次类推. 如果一个内部循环被外部循环包围, 则内部循环优先. A_Index 适用于所有类型的循环, 但在循环之外为 0.

对于某些循环类型, 其他内置变量返回有关当前循环项 (注册表键/值, 文件, 子字符串或文本行) 的信息. 这些变量的名称以 A_Loop 开头, 如 A_LoopFileName 和 A_LoopReadLine. 它们的值总是对应于最近开始的(但还没有停止) 循环的适应类型. 例如, A_LoopField 返回最里层解析循环中的当前子字符串, 即使它在文件或注册表循环中使用.

t := "column 1`tcolumn 2`nvalue 1`tvalue 2"
Loop Parse t, "`n"
{
    rowtext := A_LoopField
    rownum := A_Index  ; 保存这个用于下面的第二个循环中.
    Loop Parse rowtext, "`t"
    {
        MsgBox rownum ":" A_Index " = " A_LoopField
    }
}

循环变量也可以在循环主体外部使用, 例如在循环中调用的函数中.

非控制流

像指令, 标签, 双冒号热键和热字串标签, 和没有赋值的声明都会在脚本加载文件的时候被处理, 它们不受控制流的制约. 换句话说, 在脚本执行任何控制流程语句之前, 它们将无条件生效. 同样, #HotIf 指令不能影响控制流; 它只是设置代码中指定的任何热键和热字串的条件. 每次按下时都会计算热键的条件, 而不是在代码中遇到 #HotIf 指令时.

脚本的结构

全局代码

脚本加载完成后, 自动执行线程 从脚本的顶行开始执行, 一直到被指示停止为止, 比如通过 Return, ExitAppExit. 脚本的物理结束也作为 Exit.

全局代码, 或全局范围内的代码, 是任何不在函数或类定义内的可执行代码. 那里的任何变量引用都被称为全局的, 因为它们可以被任何函数(通过适当的声明) 访问. 这样的代码通常用于配置适用于每个新启动的线程的设置, 或者初始化热键和其他函数使用的全局变量.

在启动时执行的代码(在脚本启动时立即执行) 通常被放在文件的顶部. 然而, 这样的代码也可以放在整个文件中, 在函数和类定义之间(但不是在里面). 这是因为每当在执行过程中遇到的每个函数或类定义的主体都会被跳过. 在某些情况下, 脚本的整个目的可能用全局代码来执行.

相关: 脚本启动(自动执行线程)

子程序

子程序(也称为 subprocedure) 是一个可重复使用的代码块, 可按需执行. 子程序是通过定义 函数 创建的(见下文). 这些术语通常可以在 AutoHotkey v2 中互换, 函数是子程序的唯一类型.

函数

相关: 函数(有关函数定义)

除了调用许多有用的预置函数外, 脚本还可以定义自己的函数. 这些函数一般有两种使用方式:

  1. 函数可以被脚本本身调用. 这种函数可能被用来避免重复, 使代码更容易管理, 或者可能用于其他目的.
  2. 函数可以被程序调用, 以响应一些事件, 例如用户按下热键. 例如, 每个热键都与一个函数相关联, 每当热键被按下时就会执行.

有多种方法可以定义一个函数:

函数中的变量默认为函数的局部变量, 除了以下情况:

函数可以选择接受参数. 参数是通过在括号内列出它们来定义的. 例如:

MyFunction(FirstParameter, Second, &Third, Fourth:="")
{
    ;...
    return "a value"
}

和函数调用一样, 函数名和左括号之间不能有空格.

右括号和左大括号之间的换行符是可选的. 两者之间可以有任意数量的空格或注释.

ByRef 标记(&) 表示调用者必须传递一个变量引用. 在函数内部, 任何对参数的引用都将实际访问调用者的变量. 这类似于省略 & 并在函数内部显式地去解引用参数(例如 %Third%), 但在这种情况下, 百分号被省略. 如果参数是可选的, 而调用者省略了它, 那么该参数将作为一个普通的局部变量.

可选参数通过在参数名称后面指定 := 和一个默认值, 该值必须是加引号的原义字符串, 数字, true, falseunset.

函数可以返回一个值. 如果不是, 则默认返回一个空字符串.

函数定义不需要在调用该函数之前.

有关详情, 请参阅函数.

#Include

#Include 指令使脚本的行为就像指定文件的内容出现在这个确切位置一样. 这通常用于将代码组织到单独的文件中, 或者使用其他用户编写的脚本库.

#Include 文件可以包含在脚本启动时要执行的全局代码, 但是和主脚本文件中的代码一样, 只有当自动执行线程在 #Include 指令之前没有被终止(比如用一个无条件的 Return) 才会执行这些代码. 如果任何代码由于之前的 Return 而无法执行, 默认会显示一个警告.

与 C/C++ 不同, 如果以前的指令已包含该文件, #Include 不做任何事情. 要多次包含同一文件的内容, 请使用 #IncludeAgain.

为了方便共享脚本, #Include 可以搜索一些标准位置的库脚本. 有关详情, 请参阅脚本库文件夹.

杂项

动态变量

动态变量引用 接受一个文本值, 并将其解释为变量的名称.

注意: 变量不能通过动态引用来 创建, 但是现有的变量可以被赋值. 这包括脚本中包含非动态引用的所有变量, 即使它们没有被赋值.

动态变量引用的最常见形式称为 双重引用双重解引. 在执行双重引用之前, 目标变量的名称存储在第二个变量中. 然后可以通过使用双重引用将第二个变量间接地将值赋给目标变量. 例如:

target := 42
second := "target"
MsgBox  second   ; 普通(单重) 变量引用 => target
MsgBox %second%  ; 双重解引 => 42

目前, 在第二种情况下, second 必须总是包含一个变量名; 不支持任意表达式.

动态变量引用也可以采用一个或多个原义文本和一个或多个变量的内容, 并将它们组合在一起组成一个单一变量名. 在没有空格的情况下, 这只需简单地按顺序写入名称和百分号括起来的变量. 例如, MyArray%A_Index%MyGrid%X%_%Y%. 这用于访问 伪数组, 如下所示.

这些技术也可以应用于对象的属性和方法. 例如:

clr := {}
for n, component in ["red", "green", "blue"]
    clr.%component% := Random(0, 255)
MsgBox clr.red "," clr.green "," clr.blue

伪数组

伪数组 实际上只是一堆分开的变量, 但是有一个命名模式, 可以像数组元素一样使用它. 例如:

MyArray1 := "A"
MyArray2 := "B"
MyArray3 := "C"
Loop 3
    MsgBox MyArray%A_Index%  ; 显示 A, 然后 B, 最后 C.

用于形成最终变量名的 "索引" 不一定是数字; 它可以是一个字母或关键字.

由于这些原因, 一般建议使用 ArrayMap 来代替伪数组:

标签

标签标识只是一行代码, 可以用作 Goto 的目标, 或指定一个循环来跳出或继续. 标签由一个名称后跟一个冒号组成:

this_is_a_label:

除了空格和注释外, 其他代码不能和标签写在同一行. 有关详情, 请参阅标签.

unixetc