# 脚本语言

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

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

在 AutoHotkey 中使用两种不同的语法样式: 传统语法和表达式.

## 目录

- 常规约定
- 注释
- 表达式
  - 字符串 / 文本
  - 变量
  - 运算符
  - 函数调用
  - 对象的运算符
  - 表达式语句
- 传统语法
- 命令
  - OutputVar 和 InputVar 参数
  - 文本参数
  - 数字参数
  - % 表达式
  - 文档约定
  - 可选参数
- 表达式与传统语法
  - 不同的等号
  - 命令与函数
- 控制流语句
  - 控制流与命令
  - If 语句
  - Loop 语句
  - 非控制流
- 脚本的结构
  - 自动执行段
  - 子程序
  - 用户定义的函数
  - #Include
- 杂项
  - 动态变量
    - 伪数组
    - 关联伪数组
    - 创建伪数组的命令
  - 标签

## 常规约定

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

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

声明是可选的: 除了在函数页面上注明的地方外, 不需要声明变量; 它们的存在仅仅是通过使用它们(每个变量都是从空/空白开始的).

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

- 函数和方法调用要求在函数/方法名称和 `(` 之间没有空格.
- 连接时需要空格.
- 两个运算符之间可能需要空格, 以消除歧义.
- 单行注释如果不在行首, 则需要前导空格.

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

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

- 行延续, 以表达式运算符(++ 和 -- 除外) 开始的行与前一行合并. 行被合并, 而不管该行是否实际包含表达式.
- 延续片段, 其中多行与片段上方的行合并. 延续片段的开始和结束标记为 `(` 和 `)`(两个符号必须出现在行的开头, 不计空格).

## 注释

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

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

    ; 这整行是注释.

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

    Run Notepad  ; 这是和命令在同一行的注释.

此外, 可以使用 `/*` 和 `*/` 符号注释整块代码, 但仅当它们出现在行首时才有效(空白除外), 如下例所示:

    /*
    MsgBox, 这行被注释掉了(禁用).
    MsgBox, 常见的错误: */ 这不会结束注释.
    MsgBox, 这行被注释掉了.
    */

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

使用 #CommentFlag 可以把默认的注释符号(分号) 改为其他字符或字符串.

## 表达式

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

命令的目的是获取参数列表, 每行只执行一个动作, 而简单的表达式可以拼凑在一起形成越来越复杂的表达式. 例如, 如果 `Discount/100` 将折扣百分比转换为分数, `1 - Discount/100` 计算剩余金额的分数, 而 `Price * (1 - Discount/100)` 应用它来产生净价格. (译者注: 英语和汉语在打折的说法上是相反的, 例如 10% off, discount prices at 10% 表示去除 10%, 汉语打九折的意思.)

值 是数字, 对象或字符串. 字面值是在脚本中写入的值; 当您在查看代码时可以看到该值.

### 字符串 / 文本

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

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

要在原义字符串中包含 真实的 引号字符, 请指定两个连续的引号, 如下面例子中的演示: `"她说, ""一个苹果一天."""`.

加引号的字符串能包含转义序列例如 ``t`(tab 制表符), ``n`(换行) 和 ``r`(回车). 与不加引号的字符串不同的是, 不需要转义逗号或百分号, 因为加引号的字符串不能包含变量. 现在不支持使用 ``"` 转义序列来产生原义的引号字符; 而是使用两个连续的引号, 如上所示.

### 变量

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

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

- 隐式连接: `"The value is " MyVar`
- 显式连接: `"The value is " . MyVar`

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

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

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

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

表达式中的 百分号 用于创建动态变量引用和动态函数调用. 大多数情况下, 这些结构是不需要的, 所以一般来说, 变量名不应该包含在表达式的百分号中.

### 运算符

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

- 一元 运算符可以写在单个运算元之前或之后, 这具体取决于运算符. 例如, `-x` 或 `not keyIsDown`.
- 二元 运算符写在两个运算元之间. 例如, `1+1` 或 `2 * 5`.
- AutoHotkey 只有一个 三元 运算符, 它采用如下的形式 `condition(条件) ? valueIfTrue(条件为真时的值) : valueIfFalse(条件为假时的值)`.

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

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

### 函数调用

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

函数 需要可变化数量的输入, 去执行一些动作或计算, 然后返回一个结果. 函数的输入被称为参数(parameters 或 arguments). 一个函数被简单地通过写它的名字, 后跟着用括号括起来的参数来调用. 例如, 如果 `Shift` 被按下, 则 `GetKeyState("Shift")` 返回(计算出) 1, 否则返回 0.

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

与命令相比, 括号的要求起初可能看起来含糊不清或冗长, 但是它们允许将函数调用与其他操作结合起来. 例如, 只有当两个键被物理按下时, 表达式 `GetKeyState("Shift", "P") and GetKeyState("Ctrl", "P")` 才会返回 1.

函数名称始终是全局的, 并且与变量名称是分开的. 例如, `Round` 可以同时是一个变量名和一个函数名, 然而 `Round := 1` 不会以任何方式影响 `Round(n)`.

### 对象的运算符

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

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

`Alpha.Beta()` 是一个 方法调用, 如上所述.

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

`Alpha[Index]` 具有与 `Alpha.Beta` 类似的功能, 但每个部分都以更加标准的方式进行解释. 也就是说, 在这种情况下, Alpha 和 Index 都是变量, 实际上可以用任何子表达式替换. 此语法通常用于检索数组或关联数组中的元素.

`new ClassName()` 用于实例化一个类, 或者创建一个从另一个对象派生的对象. 虽然这看起来像一个函数调用, 但 ClassName 实际上是一个普通的变量. 同样, `new Alpha.Beta()`将创建一个从 `Alpha.Beta` 返回的对象派生的对象; Beta 既不是函数也不是方法. 如果存在可选的括号, 则可能包含对象的 __New 方法的参数.

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

`{Key1: Value1, Key2: Value2}` 从键值对列表中创建一个关联数组. 以后值可以通过其关联的键来检索. 在 `:` 左侧写入一个简单的单词(由字母数字字符, 下划线和非 ASCII 字符组成) 等同于将该单词括在引号中. 例如, `{A: B}` 等同于 `{"A": B}`. 但是, `{(A): B}` 使用变量 `A` 的内容作为键.

`MyFunc(Params*)` 是一个可变函数调用. 星号必须紧跟在函数参数列表末尾的右括号之前. 参数 必须是返回数组对象的变量或子表达式. 尽管在任何地方使用 `Params*` 都是无效的, 但它可以用在数组文字(`[A, B, C, ArrayToAppend*]`) 或索引器(`Alpha[Params*]`) 中.

### 表达式语句

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

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

赋值, 如 `x := y`, 复合赋值, 如 `x += y` 和增量/减量运算符, 如 `++x` 和 `x--`. 但是, 在 AutoHotkey v1 中, `++`, `--`, `+=`, `-=`, `*=` 和 `/=` 在单独使用时会有稍微不同的行为, 因为它们实际上等同于 EnvAdd, EnvSub, EnvMult 或 EnvDiv. 有关详细信息, 请参阅运算符列表中的赋值下的 "已知限制".

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

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

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

表达式以 `new` 运算符开始, 如在 `new ClassName` 中, 因为有时一个类可以被实例化, 只是为了它的附加作用.

三元表达式如 `x ? CallIfTrue() : CallIfFalse()`. 但是, 在 AutoHotkey v1 中, 命令名称优先. 例如, `MsgBox ? 1 : 0` 显示一个消息框.

注意: 在 AutoHotkey v1 中, 命令名称优先于三元运算. 例如, `MsgBox ? 1 : 0` 显示一个消息框.

注意: 条件不能以 `!` 或任何其他表达式操作符开始, 因为它将被解释为连续行.

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

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

成员访问使用点(一次或一系列), 如 `ExcelApp.Quit` 或 `x.y.z`. 但是, 除非使用圆括号(在方法调用中), 否则这不能是更大表达式的前缀. 例如, `ExcelApp.Quit, xxx` 是被禁止的, 由于与命令语法明显相似.

## 传统语法

传统语法 或 命令 语法通常只允许每行执行一个操作, 但是使用更少的字符来执行简单的任务, 例如, 发送按键或运行一个程序. 该语法由命令和变量名称, 不加引号的文本 和一些符号, 如 `,`, `=` 和 `%` 组成.

不加引号的文本 只是文本, 不用括在双引号中, 直接使用. 由于文本没有明确的开始和结束标记, 所以在行尾或参数结束时结束. 前导和尾随空格和制表符被忽略. 在不加引号的文本中, 以下字符具有特殊含义:

- `%`: 用百分号括起一个变量名以包含该变量的内容. 例如, `The year is %A_Year%.`

  注意: 变量名并非 总是 用百分号括起来; 百分号只在变量名和不加引号的文字一起时才需要. 除了创建动态变量引用或动态函数调用之外, 不应在其他任何地方使用百分号.

  注意: 只能使用普通变量名. 不支持数组元素, 属性和其他表达式.

- `,`: 逗号用于分隔命令的参数, 但有一些例外. 在赋值或比较中使用时, 它没有特殊的含义, 因此在这种情况下被原义解释.

- ```: 转义符通常用于指示紧跟其后的字符应与通常的解释不同. 例如, ``%` 生成一个原义百分号 而 ``,` 生成一个原义逗号. 其他一些常见的转义序列产生特殊字符, 如 ``t`(制表符 tab), ``n`(换行) 和 ``r`(回车).

命令接收不加引号的文本, 变量名和数字表达式的混合使用.

    Send, The time is %A_Hour% o'clock.

传统赋值将不加引号的文本赋值到变量.

    Clipboard = This text is copied to the clipboard.

If 语句仅在满足指定的条件时才执行操作.

    If Var = Text value

还有其他一些控制流语句(如 loop), 它们使用与命令相似的传统语法.

## 命令

命令 是执行特定预定义操作的指令. "命令" 也可以指一个特定的预定义的操作, 如 MsgBox. 可用命令集是预定义的, 不能被脚本改变.

一个命令简单地通过在一行的开始处写入它的名称来 调用, 后面可以跟着参数. 例如:

    MsgBox, The time is %A_Hour% o'clock.

将命令名与参数分开的逗号是可选的, 但以下情况除外:

- 当需要防止该行被解释为传统赋值或赋值表达式时.

      MsgBox, := This would be an assignment without the comma.

- 当第一个参数是空的.

      MsgBox,, Second, Third

- 单独的命令在延续片段的顶部.

命令的每个参数都可以接受不同的语法, 具体取决于命令. 有四种类型的参数:

- OutputVar(输出变量)
- InputVar(输入变量)
- Text(文本)
- Number(数字)

在大多数情况下, 百分号前缀可以用来传递一个表达式.

### OutputVar 和 InputVar 参数

OutputVar 和 InputVar 参数需要一个变量名或动态变量引用. 例如:

    ; 用加号替换所有空格:
    StringReplace, NewStr, OldStr, %A_Space%, +, All

该命令从 OldStr(InputVar) 中读取值并将结果存储在 NewStr(OutputVar) 中.

注意: 只有一个普通变量可以用作 OutputVar. 数组元素, 属性和其他的表达式不受支持.

只有当百分号前缀使用时, InputVar 参数才能接受一个表达式. 但是, 百分号前缀在传统 If 命令的 Var 参数中不受支持, 所以应该使用 If (表达式) .

### 文本参数

文本参数接受不加引号的文本. 例如:

    MsgBox, The time is %A_Hour% o'clock.

因为逗号和百分号具有特殊的意义, 使用转义序列 ``,` 去指定原义逗号 ``%` 去指定原义百分号. 为了清楚起见, 最好总是转义任何旨在为原义的逗号, 但在以下情况下转义逗号是可选的:

- 在任何命令的最后一个参数.
- MsgBox 的 Text 参数, 它有智能逗号处理.

要包含前导或尾随空格或制表符, 请使用内置变量 %A_Space% 和 %A_Tab% 或强制表达式如 `% " x "`. [v1.1.06+]: 空格也可以通过在空格或制表符之前加上一个转义序列来保留, 除了行末的空格之外.

文本参数也接受强制表达式.

### 数字参数

数字参数接受原义数字或表达式, 并可以通过类似 "此参数可以是表达式." 的措辞进行标识.

由于历史原因, 只是单独的变量引用或与数字结合不被解释为表达式. 例如:

    Sleep %n%000  ; 等待 n 秒.
    Sleep %m%     ; 等待 m 毫秒.

要在这种情况下执行双重解引, 请将表达式括在括号内: `Sleep (%m%)`

请注意, 混合类型参数, 如 SetTimer 的第二个参数, 有时接受一个数字有时接受如 `On` 或 `Off` 的字符串, 实际上是文本参数, 除非使用百分号前缀, 否则它们不接受表达式.

数字参数允许但忽略百分号前缀.

### % 表达式

尽管纯粹的数字参数在默认情况下接受一个表达式, 但所有其他的命令参数都不会. 指定一个百分号后跟一个空格或制表符来强制一个参数接受一个表达式. 例如, 以下所有内容都是完全相同的, 因为 Sleep 的第一个参数是接受表达式的:

    Sleep MillisecondsToWait
    Sleep %MillisecondsToWait%
    Sleep % MillisecondsToWait

注意: 在数字参数中使用百分号空格前缀不一定会强制它成为表达式.

除以下各项外, 所有参数都支持百分号空格前缀:

- 任何传统的 If 命令的 Var 参数. 用户可以通过总是使用 if (表达式) 来避免混淆.
- OutputVar 参数, 它使用与表达式相同的语法接受变量引用.

一些用户可能会发现, 总是强制执行一个表达式更容易使用, 并尽可能保持一致的语法(表达式语法).

### 文档约定

在每个命令文档的顶部, 通常有一个显示语法的块, 如下所示:

    StringLower, OutputVar, InputVar , T

方括号表示可选参数; 方括号本身必须在实际代码中省略.

有时, 参数所接受的值直接写入语法块中. 例如, 上面显示的 StringLower 的第三个参数接受字母 T 作为文本. 参数的确切语法在 参数 部分中进行了描述, 并在命令之间有所不同.

### 可选参数

可选参数可以简单地留空. 如果省略所有后续参数, 则可选参数前面的逗号也可以省略. 例如, Run 命令能接受一到四个参数. 以下全部有效:

    Run, notepad.exe, C:\
    Run, notepad.exe,, Min
    Run notepad.exe, , , notepadPID

## 表达式与传统语法

许多命令参数默认情况下不接受表达式. 在参数的开始处使用百分号-空格前缀为表达式来计算该参数. 在以下示例中, 表达式显示在第一行(在百分号 之后 开始), 第二行显示纯传统语法.

    MsgBox % 1+1  ; Shows "2"
    MsgBox   1+1  ; Shows "1+1"

表达式中的原义文本总是用引号括起来. 这些被称为 加引号字符串.

    MsgBox % "This is text."
    MsgBox    This is text.

表达式中的变量不包围在百分号中, 除非创建双重引用.

    MsgBox %  A_AhkVersion
    MsgBox   %A_AhkVersion%

变量不能在加引号的字符串中使用.

    MsgBox % "Hello %A_UserName%."  ; 显示 "%A_UserName%"
    MsgBox    Hello %A_UserName%.   ; 显示你的用户名.

作为代替, 通过将它们按顺序写入, 使用空格或制表符, 或被空格包围的点分隔, 将值连接起来.

    MsgBox % "Hello " . A_UserName . "."  ; 显示你的用户名.

一种替代方法是使用 Format 函数, 它也可以以各种方式格式化参数值.

    MsgBox % Format("Hello {1}.", A_UserName)  ; {} 也可以代替 {1}.

一个值被赋值给一个变量, 使用 `:=` 代替 `=`:

    MyVar := "This is text."
    MyVar = This is text.

使用与传统的 If 命令相同的符号执行比较: `=`, `<>` 或 `!=`, `>`, `>=`, `<` 和 `<=`.

    if (Var1 = Var2)
    if Var1 = %Var2%

在表达式中, 这两个值可以是简单的值或复杂的子表达式. 比较也可以使用, 如 `and` 和 `or` (这等同于 `&&` 和 `||`) 等运算符与其他条件结合在一起.

    if (Var1 >= Low and Var1 <= High)
    if Var1 between %Low% and %High%

### 不同的等号

一个常见的错误是在需要 `:=` 的地方写 `=`. 例如:

    Total = A + B   ; 赋值原义文字 "A + B"

这可能很难避免(至少在删除传统赋值语法之前), 但是赋值时总是使用 `:=` 会有所帮助.

等号(当不用另一个符号, 如 `<=`) 具有以下含义:

- 传统赋值: `Var = Value`
- 传统的 If 相等: `if Var = Value`
- 不区分大小写的相等: `if (Expr1 = Expr2)` (在其他表达式中也有效, 不只是 `if`)
- 在逗号后的赋值: `x:=1, y=2, a=b=c` (所有的都是赋值, 由于一个特殊的规则)
- 声明和初始化: `local x = Expr` (总是接受一个表达式)
- 设置参数的默认值: `MyFunc(Param="Default value") {`...

前两种情况可以通过始终使用 `:=` 赋值运算符和 if (表达式) 来避免.

对于最后三种情况, `:=` 应该用来代替 `=`.

### 命令与函数

在 AutoHotkey v1 中, 目前无法在表达式中调用命令, 或者使用 命令语法 调用函数. 但是, 这几个命令有函数替换.

命令 | 替换
--- | ---
FileAppend | FileOpen 和 File.Write
FileGetAttrib | FileExist
FileRead | FileOpen 和 File.Read
GetKeyState | GetKeyState (函数返回 0 或 1, 不是 "U" 或 "D")
IfExist | FileExist
IfInString | InStr
IfWinActive | WinActive
IfWinExist | WinExist
StringGetPos | InStr
StringLen | StrLen
StringReplace | StrReplace
StringSplit | StrSplit
StringLower StringUpper | `Format("{:L}", input)`, `Format("{:U}", input)` 或 `Format("{:T}", input)`
StringLeft StringMid StringRight StringTrimLeft StringTrimRight | SubStr

## 控制流语句

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

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

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

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

下面的控制流语句如下:

- 一个块(用一对大括号表示) 将零个或多个语句组合为一个语句.
- 一个 If 语句导致其主体被执行或不依赖条件. 其后可以跟一个 Else 语句, 只有当条件不满足时才执行.
- Goto 跳转到指定的标签并继续执行.
- Gosub 调用一个子程序.
- Return 从一个子程序或函数返回.
- 一个 Loop(循环) 语句 (Loop, While 或 For) 重复执行其主体.
  - Break 退出(终止) 一个循环.
  - Continue 跳过当前循环迭代的其余部分, 并开始一个新的循环.
  - Until 直到表达式计算结果为 true 时, 循环终止. 表达式在每次迭代之后被计算.
- Switch 从互斥候选项列表中执行其中一个实例.
- 异常处理:
  - Try 保护它的主体不受运行错误和 throw 命令抛出的异常的影响.
  - Catch 在 try 语句中抛出异常后执行它的主体(仅当在抛出异常的情况下).
  - Finally 在当控制权从 try 或 catch 语句的主体转出时, 执行其主体.
  - Throw 抛出一个异常, 通过 try/catch 或显示错误对话框来处理.

### 控制流与命令

控制流语句具有类似于命令的语法, 并且经常被引用为这样, 但也有不同于命令的地方:

- If 语句有几种类型, 每种都有不同的语法.
- For 几种类型的 If 语句使用关键字或运算符而不是逗号来分隔它们的一些参数.
- 块的左大括号可以写在 If (expression), Else, Loop Count, While, For, Try, Catch 或 Finally 语句的同一行的末尾(基本上任何不使用传统语法的控制流语句). 这被称为 One True Brace(OTB) 风格. 它不受其他 Loop 子命令或传统 If 语句的支持, 因为大括号将被解释为原义文本 `{` 字符.
- Else, Try 和 Finally 允许任何有效的语句在其右侧, 因为它们需要一个主体但没有参数.
- If (表达式) 和 While 允许在名称后立即使用左括号. 例如, `if(表达式)`.
- For, While, Until 和 Throw 总是接受表达式. 他们将 `%var%`, `%var%000` 和类似的视为表达式, 而其他命令的数字参数则不会. 向后兼容性的要求不适用于这些控制流语句, 因为它们相对较新.

### If 语句

If (表达式) 计算表达式并仅在结果为真时执行以下语句.

混淆的常见原因: 有几种其他类型的 If 语句, 其中一些看起来非常类似 If (表达式). 在新的脚本中应该避免这些. 如果有疑问, 最好总是以左括号作为表达式的开始. "传统" If 语句如下所示:

- If Var op Value, op 是以下运算符之一: `=`, `<>`, `!=`, `>`, `>=`, `<`, `<=`.
- If Var [not] between Lower and Upper
- If Var [not] in/contains MatchList
- If Var is [not] Type

任何不符合上述用法之一的 If 语句被解释为 If (表达式).

这些是与传统的 If 语句相关的一些常见的混淆点:

- 变量名称 只有 在运算符的右侧时才必须用百分号括起来.
- `between`, `in`, `contains` 和 `is` 只在上下文中是有效的; 他们不能用在表达式中.
- 多个条件不能写在同一行上(如使用 `and` 运算符).
- 没有任何参数是表达式.

If 语句同样有以下的 "传统" 名称:

- IfEqual, IfNotEqual, IfLess, IfLessOrEqual, IfGreater 和 IfGreaterOrEqual
- If[Not]Exist
- If[Not]InString
- If[Not]WinActive
- If[Not]WinExist
- IfMsgBox

除了 IfMsgBox, 这些都是过时的, 一般应该避免用在新的脚本中.

命名的 If 语句允许与命令写在同一行, 但是拼写错误的命令名称被视为文字文本. 这样的错误可能难以检测到.

### Loop 语句

有几种类型的 Loop(循环) 语句:

- Loop Count 重复执行一个语句: 可以指定表示重复次数的数字(Count) 或直到遇到 break.
- Loop Reg 获取指定的注册表子键的内容, 每次一个项目.
- Loop Files 获取指定的文件或文件夹, 每次一个.
- Loop Parse 从一个字符串中获取子字符串(片段), 每次一个.
- Loop Read 获取文本文件中的行, 每次一行.
- While 重复执行一个语句, 一直到指定的表达式计算结果为假. 每次迭代(重复) 前计算表达式的结果.
- For 对每一个值或枚举器返回的每对键值对执行一个语句, 如对象中的每对键值对.

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

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

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

标签可以用来 "命名" Continue 和 Break 的循环. 这允许脚本轻松地继续或跳出任何数量的嵌套循环而不使用 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%
        }
    }

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

### 非控制流

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

## 脚本的结构

### 自动执行段

脚本加载完成后, 从顶行开始执行, 直到遇到 Return, Exit, 脚本的第一个热键/热字串标签, 或脚本的物理结束(无论哪一种). 这个脚本的顶部被称为 自动执行段(部分), 但它实际上只是程序启动后调用的一个子程序.

注意: 虽然脚本的 第一个 热键/热字串标签有与 return 相同的效果, 但是其他热键和标签不会.

自动执行段通常用于配置适用于每个新启动的线程的设置. 有关详细信息, 请参阅脚本的顶部.

### 子程序

子程序 是一个可重复使用的代码块, 可以 调用 它来执行一些任务.

脚本使用子程序来定义当按下特定热键或发生其他事件时应该发生什么. 脚本也可以通过使用 Gosub 直接调用子程序.

任何标签都可以作为子程序的起点. 一个子程序没有明确的标记结束点, 而是通过 Return 或当线程退出时, 控制权返回到子程序的调用者时作为结束. 例如:

    gosub Label1  ; 代码运行时, 第一个对话框内容为 "Label1".

    Label1:
    MsgBox %A_ThisLabel%  ; 第二个对话框内容为空.
    return

请注意: 当正常运行时, 标签无效, 在上个例子中对话框将显示两次: 一次在子程序运行时, 一次在它返回之后. 一个重要的结果是不能在另一个子程序中定义一个子程序 , 因为内部子程序的 "主体" 会自动执行然后 return, 从而有效地终止外部子程序.

子程序通常应该单独定义到任何其他代码块, 但也可以在函数内定义, 允许子程序访问该函数的静态变量(和局部变量, 但仅在函数运行时).

注意: 函数内定义的子程序在使用局部变量和动态变量引用, 包括 Gui 控件变量方面有一定的限制. 有关详细信息, 请参阅在函数中使用子程序.

### 用户自定义函数

一般来说, 函数是一种子程序. 但是, 在 AutoHotkey 文档中, "子程序" 通常指的是由标签定义的子程序类型(如上所述).

用户定义的函数与子程序的不同之处在于它们可以 接受参数 并 返回一个值, 并且它们可以具有局部变量. 它们可以通过脚本中的函数调用或程序本身调用, 例如, 如果函数被传递给 Hotkey 或 SetTimer 命令.

函数是使用类似于函数调用的语法来定义的, 随后是用大括号括起来的代码块:

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

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

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

ByRef 表示该参数接受变量引用, 使该参数成为调用者传递的变量的别名. 如果调用者没有传递一个变量, 那么这个参数就像一个普通的局部变量. ByRef 参数也可以是可选的.

可选参数通过在参数名称后面指定 `:=` 或 `=` 和一个默认值, 该值必须是加引号的原义字符串, 数字, `true` 或 `false`. 由于历史原因, 运算符 `:=` 和 `=` 可以互换, 但最好使用 `:=` 以与表达式中的赋值保持一致.

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

一个函数不能在另一个函数中定义. 另外, 函数定义的位置并不重要; 在脚本中定义的任何函数都可以从其他地方调用.

请参阅函数以了解更多详细信息.

### #Include

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

注意: 以下段落详细说明了一些常见的混淆点.

使用 #Include 时, 重要的是要考虑文件的内容如果放置在该位置将会产生什么效果, 因为 #Include 将具有相同的效果. 例如:

- #Include 一般不应该在子程序或函数的中间使用.

- 在脚本的自动执行段使用 #Include 需要特别的考虑, 因为自动执行段基本上只是一个子程序. 如果子程序的执行会在到达一个 `return` 时终止, 而不管 `return` 是在哪个文件中. 同样, 如果文件包含一个热键/热字串, 它可能被认为是脚本的 第一个 热键/热字串, 它将扮演 `return` 的角色.

- 脚本只有一个自动执行段, 而不是每个文件一个.

#Include 可以在自动执行段安全地使用, 以包含仅包含函数定义的文件, 因为函数定义(但不是函数调用) 在执行期间被跳过. 如果一个文件包含其他代码, 可以通过 Goto 跳过文件的内容来避免破坏自动执行段.

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

如果包含函数的脚本文件被保存在一个标准的位置并进行适当的命名, 则它们可以 自动包含, 而不必使用 #Include. 其效果与在主脚本文件末尾使用 #Include 相似. 有关详细信息, 请参阅函数库.

## 杂项

### 动态变量

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

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

    target := 42       ; target(目标) := 42
    second := "target" ; second(第二个变量) := "target(目标)"
    MsgBox   %second%  ; 文本中的普通(单一) 变量引用 => target(目标)
    MsgBox %  second   ; 表达式中的普通(单一) 变量引用 => target(目标)
    MsgBox % %second%  ; 表达式中的双重引用 => 42

首先, 看起来百分号有不同的含义, 取决于它们是在文本中还是在表达式中使用. 然而, 在这 两种 情况下, 将 `%second%` 换为变量 `second` 的内容可能更有意义:

- `MsgBox %second%` → `MsgBox target`: 显示 "target".
- `MsgBox % %second%` → `MsgBox % target`: 显示 `target` 的内容, 如 "42".

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

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

有关如何解析函数内的动态变量引用的说明, 请参阅函数: 关于局部和全局变量的更多信息.

#### 伪数组

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

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

由于单个元素只是普通变量, 所以可以赋值或获取一个值, 但不能 删除 或 插入 元素. 因为伪数组本身并不存在, 所以不能将它传递给函数或从函数返回, 或者作为一个整体进行复制. 由于这些原因, 通常建议在可能的情况下使用普通数组.

#### 关联伪数组

用于形成最终变量名称的 "index(索引)" 不一定是数字; 它可以是一个字母或关键字, 使伪数组类似于关联数组或对象. 下面的例子创建一个元素为 "Left", "Top", "Right" 和 "Bottom" 的伪数组:

    SysGet, WA, MonitorWorkArea
    MsgBox, Left: %WALeft% -- Top: %WATop% -- Right: %WARight% -- Bottom: %WABottom%.

#### 创建伪数组的命令

有几个命令可以创建关联的伪数组:

- GuiControlGet Pos.
- RegExMatch, 除非指定了 `O)` 选项, 这会导致它输出包含所有匹配信息的单个对象.
- SysGet Monitor/MonitorWorkArea, 如上所示.
- StringSplit. 新的脚本应该使用 StrSplit() 作为代替, 因为它创建的是普通数组.
- WinGet List.

警告: 这些命令不遵循与 动态变量引用 相同的规则. 如果在一个函数中使用, 则所得到的伪数组或者全部是全局的或者全部是局部的, 这仅取决于数组的第一个元素(或基于名称). 如果没有单独声明, 伪数组中的一些变量可能是不可访问的. 有关详细信息, 请参阅函数: 关于局部变量和全局变量的更多信息.

AutoHotkey 还创建了一个全局伪数组, 以包含传递给脚本的所有命令行参数.

### 标签

标签标识只是一行代码, 可以用作 Goto 目标或形成子程序. 有三种标签: 普通标签, 热键标签和热字串标签.

普通标签由一个名称后跟一个冒号组成.

    this_is_a_label:

热键标签由一个热键后跟双冒号组成.

    ^a::

热字串标签由一个冒号, 零个或多个选项, 另一个冒号, 缩写字符和双冒号组成.

    :*:btw::

通常, 除了空格和注释之外, 其他代码不能与标签一起写在同一行上. 然而:

- 一个热键标签可以直接跟随一个命令或其他语句来创建 单行 热键. 换句话说, 如果一个命令, 赋值或表达式与热键标签位于同一行上, 它的行为就好像跟着 `return` 一样.
- 热键和写在双冒号右边的键名实际上是重映射, 这是热键对的快速写法. 例如, `a::b` 创建热键和标签 `*a` 和 `*a Up`, 并不创建名为 `a` 的标签.
- 热字串和写在最后的双冒号的右边的文本是一个 自动替换 热字串. 自动替换热字串不作为标签.

有关详细信息, 请参阅标签.

dftg