开始

通常在AHK中使用配置文件是通过内置的INI函数,也可以使用json或yaml,已有外部ahk脚本可以操作它们。
但这些文件在ahk场景实际使用上都不怎么方便。
于是我计划设计一种简洁的配置文件语法,可以在轻量级场景下替代这些常用的文件。
其中最重要的一点是简单,以至于可以轻松的在记事本中编辑。

设计语法

数据类型

对于任意一种配置文件,都具有一对一、一对多和可选的多对多表示。
此文件同理,但由于数据是顺序表示的,从上到下,上层数据无法感知下层数据,所以不存在多对多关系。
一对一使用键值对表示;一对多使用数组对象表示。

数据表示

参考其他文件的表示形式,去除层级的yml十分适合,所以设计以下表示:

  • 一对一
key : value
  • 一对多
key :
- v1
- v2
key :
+ k1 : v1
+ k2 : v2

因为去除了层级表示,无法表示复杂的嵌套类型。这样做是个好决定,在脱离智能编辑器时也可以轻松编辑。


目前为止,决定了文件的数据类型和表示形式,还需要更多语法来使它不那么‘贫瘠’。

注释

使用#作为注释符,使用上与yml一致,可以出现在行头或行尾。
同时,以---开头的行也是注释,这是语法糖,可以作为分界线使用。

导入文件

导入语法允许抽离出部分数据到单独文件中,由其他文件导入。
导入以@开头,可以全局或局部导入。

  • 全局导入在文档开头,为了使文档不那么混乱,规定导入语句必须出现在数据之前。
  • 局部导入在部分,可以将独立文档的数据作为值,故可以实现间接的嵌套表示。
    示例文档如下:
# 导入前可以使用注释
@./cfg1.txt        # 全局导入
key : @./cfg2.txt  # 局部导入

引用

引用允许在值部分引用上层数据的值,在基本和复杂类型中有不同表示。
使用$包围的部分被认为是引用,复杂类型只能引用子项,使用[]取值。
以下示例说明对基本类型和复杂类型中的引用:

key : 基本类型的值
arr :
- 数组的值
obj : 
+ k1 : 对象的值
ref : $key$_$arr[1]$_$obj[k1]$

转义

使用```(重音符)将特殊字符转义。

修饰符

可以在基本类型键前添加符号,使它具有特殊语法约束。
目前只有两种修饰符:*~
*用于在文件被导入时,使此值无法被覆盖。
~用于表示此值是原义的,特殊字符被忽略。
示例如下:

*vital   : value  # 被导入时无法再使用此键名
~literal : @#$`

优化

以上设计了文件的语法,现在添加更多语法糖来改进它。

美观

注意到上面提供的示例文件都是对齐的,这是添加特殊语法的缘故。
允许:左右添加空格用来对齐,这些空格将被忽略;同时,行尾注释前的空格也被忽略。

内置引用

在脚本中预设一组值,可以在文档中直接引用,如A_Desktop表示桌面路径。


更多优化有待探索,目前只有这两个。

实现

接下来,使用ahk实现对此文档的解析。
为了更好的错误提示,脚本使用逐行逐字符的方式解析,这样做可以使警告和错误精确到字符。

分析

对于文档中的每一行,可以分为以下情况:

  • 导入
  • 普通键值
  • 带修饰符的键值
  • 数组键
  • 数组值
  • 对象键
  • 对象值

每行又可分为:

  • 注释

故,声明三个方法:processImportprocessLineprocessValue分别处理全局导入、行及行中的值。
有这三个方法之后,解析文档就变的简单了,在逐行解析中:

  • 如果遇到@符,调用processImport,处理当前行及接下来可能的导入行
  • 如果遇到#符或空行,跳过
  • 其他情况都调用processLine

在处理行中:

  • 提取键声明
    • 如果以':'结尾,则是复杂类型,进入新的循环处理。
    • 否则是基本类型,值部分使用processValue处理。
  • 提取值内容
    • 处理引用
    • 处理导入
    • ...

经过以上分析,将文档剖析的很彻底了,再写代码便很简单。

代码

以下是完整代码,包含的脚本过长,便没有贴出。
最好是拉取仓库中的代码,此脚本位于/util/config下,同时包括readme详细介绍了完整语法。

https://gitee.com/dkwd/ahk-lib

#Requires AutoHotkey v2.0

#Include ..\..\Extend.ahk
#Include ..\..\Path.ahk

class CustomFS {

  data := Map(), cfgs := Map(), vital := Map()
    , escChar := '``', refChar := '$', commentChar := '#', importChar := '@', vitalChar := '*', literalChar := '~', q := "'"

  static preset := Map(
    'a_mydocuments', A_MyDocuments,
    'a_username', A_UserName,
    'a_startup', A_Startup,
    'a_now', A_Now,
    'a_desktop', A_Desktop,
    'a_scriptdir', A_ScriptDir,
    'a_scriptfullpath', A_ScriptFullPath,
    'a_ahkpath', A_AhkPath,
    'a_tab', A_Tab,
    'a_newline', '`n',
  )

  __New(_path, _warn) {
    if !FileExist(_path)
      throw Error('读取的文件不存在:' _path)
    this.path := !Path.IsAbsolute(_path) ? Path.Resolve(A_ScriptDir, _path) : _path, this.doWarn := _warn, this.Init(this.path)
  }

  static Of(_path, _warn := false) => CustomFS(_path, _warn)

  Init(_path) {
    f := FileOpen(_path, 'r', 'utf-8'), r := 0, ec := this.escChar, rc := this.refChar, cc := this.commentChar, lc := this.literalChar
      , ic := this.importChar, vc := this.vitalChar, import := false, cp := _path, this.cfgs.Set(cp.toLowerCase(), this.cfgs.Count + 1)
    while !f.AtEOF {
      r++, l := f.ReadLine()
      if !import and l and l[1] = ic
        l := _processImport(l, _path)
      if !l or l[1] = cc or l ~= '^---'
        continue
      if l[1] = A_Space {
        Warn('忽略了一行以空格开头的内容(' l ')', 0, l, cp)
        continue
      }
      if l[1] = '-'
        ThrowErr('错误的语法: 以-开头而不在数组定义范围内', 0, l, cp)
      else if l[1] = '+'
        ThrowErr('错误的语法: 以+开头而不在对象定义范围内', 0, l, cp)

      import := true, _processLine(l)
    }
    return f.Close()

    _processImport(_l, _cp) {
      while true {
        if this.cfgs.Has((_cp := _getNextPath()).toLowerCase())
          ThrowErr('导入重复文件:' _cp, 1, _l, cp)
        if !FileExist(_cp)
          ThrowErr('导入不存在的文件:' _cp, 1, _l, cp)
        this.Init(_cp), _l := f.ReadLine(), r++
        if !_l or _l[1] != ic or f.AtEOF
          break
      }
      return _l
      _getNextPath() => Path.IsAbsolute(_cp) ? _cp : Path.Join(Path.Dir(_cp), _processValue(_l, 2))
    }

    _processLine(_line) {
      if _line[1] = ic
        Warn('以导入符开头的键,考虑是否为导入语句', 1, _line, cp)
      else if _line[1] = lc {
        _line := _line.subString(2), ori := true
      } else if _line[1] = vc
        _line := _line.subString(2), impt := true
      i := 1, cs := _line.toCharArray(), _jumpToChar(cs, ':', &i, '无效的键,键须以:结尾'), k := _processValue(_line.substring(1, i++), 1, true)
      if k[1] = ':'
        Warn('以键值分隔符开头的键会造成混淆', 1, _line, cp)
      if IsSet(ori) and ori
        return _set(k, _line.substring(i), i, l, cp)
      if i <= cs.Length and cs[i] = A_Space
        _skipChar(cs, A_Space, &i)
      if i > cs.Length or cs[i] = cc {
        if f.AtEOF
          ThrowErr('不允许空的复杂类型', i, _line, cp)
        l := f.ReadLine(), r++, isArr := l[1] = '-'
        if l[1] != '-' and l[1] != '+'
          ThrowErr('第一个子项必须与键连续', i, l, cp)
        vs := isArr ? [] : {}, pc := isArr ? '-' : '+', _set(k, vs, 1, l, cp)
        while true {
          if !l or l[1] != pc
            break
          if isArr
            _l := LTrim(l.substring(2), A_Space), vs.Push(_processValue(_l, 1))
          else {
            cs := (_l := LTrim(l.substring(2), A_Space)).toCharArray(), _jumpToChar(cs, ':', &_i := 1, '无效的键')
            _k := RTrim(_l.substring(1, _i)), vs.%_k% := _processValue(LTrim(_l.substring(_i + 1)), 1)
          }
          l := f.ReadLine(), r++
        }
        if !f.AtEOF and l
          _processLine(l)
      } else _set(k, _processValue(_line, i), 1, _line, cp)
      IsSet(impt) && this.vital.Set(k, [cp, r])
    }

    _processValue(_lt, _idx, _raw := false) {
      s := '', cs := _lt.toCharArray(), inQ := false, q := this.q
      if !_raw and cs[_idx] = ic {
        _p := _processValue(_lt, _idx + 1, true)
        if !FileExist(_p)
          ThrowErr('文件不存在:' _p, _idx, _lt, cp)
        else return CustomFS(Path.IsAbsolute(_p) ? _p : Path.Join(Path.Dir(cp), _p), true).data
      }
      while _idx <= cs.Length {
        esc := false
        if cs[_idx] = A_Tab
          ThrowErr('不允许使用Tab', _idx, _lt, cp)
        else if cs[_idx] = ec
          esc := true, _idx++
        if _idx > cs.Length
          ThrowErr('转义符后应接任意字符', _idx, _lt, cp)
        if !inQ and cs[_idx] = A_Space {
          _i := _idx, _skipChar(cs, A_Space, &_idx)
          if _idx <= cs.Length and cs[_idx] != cc
            Warn(JoinStr('', '忽略了一条值的后续内容(', _lt.substring(_i), '),因为没有在', q, '内使用空格'), _idx, _lt, cp)
          break
        } else if !esc and cs[_idx] = q
          inQ := !inQ
        else if !esc and cs[_idx] = cc {
          if inQ
            Warn('错误的读取到注释符,考虑是否需要转义', _idx, _lt, cp), s .= cs[_idx]
          else Warn('错误的读取到注释符,考虑是否正确闭合引号', _idx, _lt, cp), s .= cs[_idx]
        } else if !_raw and cs[_idx] = rc and !esc {
          _i := ++_idx, _jumpToChar(cs, rc, &_idx, '未找到成对的引用符'), _k := _lt.substring(_i, _idx)
          if !_has(_k) {
            if RegExMatch(_k, '\[(.*?)\]$', &re) {
              _k := _k.substring(1, re.Pos)
              try _v := (_o := _get(_k))[re[1]]
              catch
                ThrowErr('无效的引用:' re[1], _idx, _lt, cp)
              if !_v and TypeIsObj(_o)
                ThrowErr('无效的对象子项引用:' re[1], _idx, _lt, cp)
            } else ThrowErr('引用不存在的键或预设值:' _k, _idx, _lt, cp)
          } else _v := _get(_k)
          if !IsPrimitive(_v)
            ThrowErr('无法引用复杂类型', _idx, _lt, cp)
          s .= _v
        } else s .= cs[_idx]
        if _idx = cs.Length and inQ
          ThrowErr('未正确闭合引号', _idx, _lt, cp)
        _idx++
      }
      return s
    }

    _set(_k, _v, _c, _l, _f) {
      if this.vital.Has(_k)
        DCon(this.vital.Get(_k), &_f, &_r), ThrowErr('无法覆盖标记为重要的键:' _k, 1, '*' _l, '(重要键所在文件)' _f, _r)
      if this.data.Has(_k)
        Warn('覆盖已有的键:' _k, _c, _l, _f)
      this.data.Set(_k, _v)
    }
    _has(_k) => this.data.Has(_k) || CustomFS.preset.Has(_k.toLowerCase())
    _get(_k) => this.data.Has(_k) ? this.data.Get(_k) : CustomFS.preset.Get(_k.toLowerCase())

    _jumpToChar(_chars, _char, &_idx, _msg) {
      while _idx <= _chars.Length and _chars[_idx] != _char
        _idx++
      if _msg and _idx > _chars.Length
        ThrowErr(_msg, _idx - 1, _chars.Join(''), cp)
    }

    _skipChar(_chars, _char, &_idx) {
      while _idx <= _chars.Length and _chars[_idx] = _char
        _idx++
    }

    ThrowErr(msg, _c, _l, _f, _r := r) {
      throw Error(JoinStr('', msg, '`n异常文件:', _f, '`n[行' _r, '列' _c ']', _l))
    }

    Warn(msg, _c, _l, _f, _r := r) => (
      this.doWarn && MsgBox(JoinStr(
        '', '`n' msg, '`n异常文件:', _f, '`n[行' _r, '列' _c ']', _l
      ), , 0x30)
    )

  }

  Get(key, default := '') => this.data.Get(key, default)
}
posted on 2024-05-18 17:34  落寞的雪  阅读(137)  评论(0编辑  收藏  举报