开始

先前,我们设计了一种简单的配置文件语法,可以方便的在记事本中编辑,然后进行读取;

但是,功能停留在了读取上。事实上,我们还需要修改和写入功能,所以,今天我们来实现它。

实现功能

首先明确目标:

  • 实现修改功能
  • 实现写入功能

要实现这两个目标,显然需要一个额外的变量来记录读取的文档结构,当修改数据时,同步修改此变量;而写入时,根据此变量的描述信息拼成字符串文本。

我将它设计为数组:

__data := []

在读取时将数据信息一条条push进去,这些信息包括:

  • 注释
  • 子项信息
  • 子项注释信息

所以该结构会长这样:

{
  K : key,
  v : val,
  t : NORMAL,
  c : # comment,
}

{
  K : key,
  v : [
    [val, comment]
  ],
  t : ARRAY,
  c : # comment,
}

其他数据类型也相似。


为了方便,添加几个方法如:_normal(), _array(), _object()等,在读取代码的适当位置添加这些方法。

这样就完成了信息记录,在修改时注意同步修改此变量的信息;而写入时只要一次遍历此数组,将信息拼接就好了。

还一个问题是导入功能使用了递归,所以会将导入文件的数据信息也记录下来,这显然是错误的;所以需要在调用记录方法时判断递归深度是否是0,这一点通过在递归方法上添加一个参数,像下面这样:

Init(_path, _serId := 0){}
; 调用时
Inint(_p, _serId + 1)

同时在记录前判断_serId是否等于0。

接下来,是细节方面的描述。

修改

在分析完后,完成这两个功能似乎不是什么难事,但在真正写时还是觉得挺难的。所以相对于之前的版本,作了许多修改。

在上面的分析中,只用到了一个变量__data,考虑到修改时的时间复杂度,还需要添加一个map记录变量名和该变量在__data中的下标,这样就能做到o(1)的修改复杂度。

__map := {}

只需要在记录时添加this.__map[k] := this.__data.Length就行。

所以,相关的记录方法如下:

_ignore(v) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.ignore })
_comment(v) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.comment })
_import(v, c) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.import, c: c })
_empty() => _serId = 0 && this.__data.Push({ t: this.NT.empty })
_literal(k, v) => _serId = 0 && this.__data.Push({ k: k, v: v, t: this.NT.literal })
_vital(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.vital, c: c }), this.__map[k] := this.__data.Length)
_normal(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.normal, c: c }), this.__map[k] := this.__data.Length)
_array(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.arr, c: c }), this.__map[k] := this.__data.Length)
_object(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.obj, c: c }), this.__map[k] := this.__data.Length)

其中NT是唯一标识:

NT := {
  deleted: -1,
  ignore: 0,
  comment: 1,
  import: 3,
  empty: 4,
  literal: 5,
  vital: 12,
  normal: 10,
  arr: 20,
  obj: 21
}

然后在之前版本代码中的特定地方调用,传递相关信息;要注意传递的信息都是原始的,要保证写入的样子和原文档一致。

这些需要细致的处理,小心翼翼的才不会出错。

对外的修改接口

以上是修改功能的基础,还需要实现对外的修改方法。

计划提供以下方法:

  • Set() 设置已有的数据的值
  • Del() 删除数据
  • Add() 添加数据
  • Append() 添加复杂类型的子项数据

先给出一个Set()的实现:

; 如果key不存在,做add操作。<br/>
; 如果是val对象,则删除此键,再重新添加(到末尾)。<br/>
; 如果传入index,则认为是修改复合类型的子项;否则,直接设置key的值为val,是修改操作;
Set(key, val, index?, comment?) {
  if !(i := this.__map[key]) {
    this.Add(key, val, comment?)
    return
  }
  if IsObject(val) {
    this.Del(key), this.Add(key, val, comment?)
    return
  }
  if IsSet(index) {
    if IsArray(this.data[key]) {
      this.data[key][index] := this.__data[i].v[index][1] := val
      this.__data[i].t := this.NT.arr
      IsSet(comment) && this.__data[i].v[index][2] := Format(' {} {}', this.commentChar, comment)
    } else {
      this.data[key][index] := (_v := (_v := this.__data[i].v)[_v.findIndex(_ => _[1] = index)])[2] := val
      this.__data[i].t := this.NT.obj
      IsSet(comment) && _v[3] := Format(' {} {}', this.commentChar, comment)
    }
  } else {
    this.data[key] := this.__data[i].v := val
    this.__data[i].t := this.NT.normal
    IsSet(comment) && this.__data[i].c := Format(' {} {}', this.commentChar, comment)
  }

}

可以看到,思路就是在修改data的同时修改__data的数据;只是因为数据结构的特殊性看起来十分混乱。

其他方法也是同样的道理,具体代码见最后一节。

写入

Sync()方法提供写入功能,此方法需要按照__data文档语法拼接数据,最后组成完整文档。

其实现如下:

Sync(_path?) {
  t := ''
  for v in this.__data {
    switch v.t {
      case this.NT.ignore:
      case this.NT.comment: t .= Format('{}`n', v.v)
      case this.NT.import: t .= Format('{}{}`n', v.v, v.c)
      case this.NT.empty: t .= '`n'
      case this.NT.literal: t .= Format('{}{} :{}`n', this.literalChar, v.k, v.v)
      case this.NT.vital: t .= Format('{}{} : {}{}`n', this.vitalChar, v.k, v.v, v.c)
      case this.NT.normal: t .= Format('{} : {}{}`n', v.k, v.v, v.c)
      case this.NT.arr:
        t .= Format('{}: {}`n', v.k, v.c)
        for vv in v.v
          _t .= Format('- {}{}`n', vv[1], vv[2])
        t .= _t, _t := ''
      case this.NT.obj:
        t .= Format('{}: {}`n', v.k, v.c)
        for vv in v.v
          _t .= Format('+ {} : {}{}`n', vv[1], vv[2], vv[3])
        t .= _t, _t := ''
      default:
    }
  }

  f := FileOpen(IsSet(_path) ? _path : this.path, 'w', 'utf-8')
  f.Write(t)
  f.Close()

  _Format(s) {

  }
}

其中留有一个待办方法_Format(),作用是给文档美化,比如对齐:和注释,只是现在还没有什么时间去研究。

至此,就实现了修改和写入功能。

代码

此脚本位于仓库目录\util\config下。

#Requires AutoHotkey v2.0

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

; cfs := CustomFSEx.Of('./test/_.txt')
; cfs.Add('normal', 'normal_value', 'comment', , false)
; cfs.Add('arr', [1, 2, 3], 'comment', ['', 'comment', ''])
; cfs.Add('obj', { a: 1, b: 2 }, 'comment', { a: 'comment' })

; cfs.Sync('./out.txt')

class CustomFSEx {

  data := Map(), cfgs := Map(), vital := Map(), encoding := 'utf-8', __data := [], __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',
  )

  NT := {
    deleted: -1,
    ignore: 0,
    comment: 1,
    import: 3,
    empty: 4,
    literal: 5,
    vital: 12,
    normal: 10,
    arr: 20,
    obj: 21
  }
  __New(_path, _warn, _init := true) {
    if !FileExist(_path)
      throw Error('读取的文件不存在:' _path)
    this.path := !Path.IsAbsolute(_path) ? Path.Resolve(A_ScriptDir, _path) : _path, this.doWarn := _warn
    if _init
      this.Init(this.path)
  }

  static Of(_path, _warn := false) => CustomFSEx(_path, _warn)
  static Empty(_path) => CustomFSEx(_path, false, false)

  Init(_path, _serId := 0) {
    f := FileRead(_path, this.encoding).split('`r`n'), r := 1, ec := this.escChar, rc := this.refChar, cc := this.commentChar, lc := this.literalChar
      , e := f.Length, ic := this.importChar, vc := this.vitalChar, import := false, cp := _path, this.cfgs.Set(cp.toLowerCase(), this.cfgs.Count + 1)
    while r <= e {
      l := f[r++]
      if !import and l and l[1] = ic
        l := _processImport(l, _path)
      if !l {
        _empty()
        continue
      }
      if l[1] = cc or l ~= '^---' {
        _comment(l)
        continue
      }
      if l[1] = A_Space {
        _ignore(l), 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)
    }

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

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

    _processValue(_l, _idx, _raw := false, &_fi := 0) {
      s := '', cs := _l.toCharArray(), inQ := false, q := this.q
      if !_raw and cs[_idx] = ic {
        _p := _processValue(_l, _idx + 1, true)
        if !FileExist(_p)
          ThrowErr('文件不存在:' _p, _idx, _l, cp)
        else return CustomFSEx(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, _l, cp)
        else if cs[_idx] = ec
          esc := true, _idx++
        if _idx > cs.Length
          ThrowErr('转义符后应接任意字符', _idx, _l, cp)
        if !inQ and cs[_idx] = A_Space {
          _i := _idx, _go(cs, A_Space, &_idx)
          if _idx <= cs.Length and cs[_idx] != cc
            Warn(JoinStr('', '忽略了一条值的后续内容(', _l.substring(_i), '),因为没有在', q, '内使用空格'), _idx, _l, cp)
          break
        } else if !esc and cs[_idx] = q
          inQ := !inQ
        else if !esc and cs[_idx] = cc {
          inQ ? (Warn('错误的读取到注释符,考虑是否需要转义', _idx, _l, cp), s .= cs[_idx])
            : (Warn('错误的读取到注释符,考虑是否正确闭合引号', _idx, _l, cp), s .= cs[_idx])
        } else if !_raw and cs[_idx] = rc and !esc {
          _i := ++_idx, _to(cs, rc, &_idx, '未找到成对的引用符'), _k := _l.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, _l, cp)
              if !_v and TypeIsObj(_o)
                ThrowErr('无效的对象子项引用:' re[1], _idx, _l, cp)
            } else ThrowErr('引用不存在的键或预设值:' _k, _idx, _l, cp)
          } else _v := _get(_k)
          if !IsPrimitive(_v) {
            Warn('引用复杂类型', _idx, _l, cp)
            _fi := _idx
            return _v
          }
          s .= _v
        } else s .= cs[_idx]
        if _idx = cs.Length and inQ
          ThrowErr('未正确闭合引号', _idx, _l, cp)
        _idx++
      }
      _fi := _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) || CustomFSEx.preset.Has(_k.toLowerCase())
    _get(_k) => this.data.Has(_k) ? this.data.Get(_k) : CustomFSEx.preset.Get(_k.toLowerCase())

    _ignore(v) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.ignore })
    _comment(v) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.comment })
    _import(v, c) => _serId = 0 && this.__data.Push({ v: v, t: this.NT.import, c: c })
    _empty() => _serId = 0 && this.__data.Push({ t: this.NT.empty })
    _literal(k, v) => _serId = 0 && this.__data.Push({ k: k, v: v, t: this.NT.literal })
    _vital(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.vital, c: c }), this.__map[k] := this.__data.Length)
    _normal(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.normal, c: c }), this.__map[k] := this.__data.Length)
    _array(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.arr, c: c }), this.__map[k] := this.__data.Length)
    _object(k, v, c) => _serId = 0 && (this.__data.Push({ k: k, v: v, t: this.NT.obj, c: c }), this.__map[k] := this.__data.Length)

    _to(_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)
    }

    _go(_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)

  Append(key, val, subKey?, comment?) {
    if !(i := this.__map[key]) {
      return false
    }
    if IsArray(this.data[key]) {
      this.data[key].push(val)
      this.__data[i].v.Push({
        v: val,
        t: this.NT.arr,
        c: comment && Format(' {} {}', this.commentChar, comment)
      })
    } else if TypeIsObj(this.data[key]) {
      this.data[key][subKey] := val
      this.__data[i].v.Push({
        k: subKey,
        v: val,
        t: this.NT.obj,
        c: comment && Format(' {} {}', this.commentChar, comment)
      })
    } else return false
    return true
  }

  ; 如果key不存在,做add操作。<br/>
  ; 如果是val对象,则删除此键,再重新添加(到末尾)。<br/>
  ; 如果传入index,则认为是修改复合类型的子项;否则,直接设置key的值为val,是修改操作;
  Set(key, val, index?, comment?) {
    if !(i := this.__map[key]) {
      this.Add(key, val, comment?)
      return
    }
    if IsObject(val) {
      this.Del(key), this.Add(key, val, comment?)
      return
    }
    if IsSet(index) {
      if IsArray(this.data[key]) {
        this.data[key][index] := this.__data[i].v[index][1] := val
        this.__data[i].t := this.NT.arr
        IsSet(comment) && this.__data[i].v[index][2] := Format(' {} {}', this.commentChar, comment)
      } else {
        this.data[key][index] := (_v := (_v := this.__data[i].v)[_v.findIndex(_ => _[1] = index)])[2] := val
        this.__data[i].t := this.NT.obj
        IsSet(comment) && _v[3] := Format(' {} {}', this.commentChar, comment)
      }
    } else {
      this.data[key] := this.__data[i].v := val
      this.__data[i].t := this.NT.normal
      IsSet(comment) && this.__data[i].c := Format(' {} {}', this.commentChar, comment)
    }

  }

  Del(key, index?) {
    if !(i := this.__map[key])
      return false
    if IsSet(index) {
      if this.data[key].length = 1
        _DelItem()
      else
        IsArray(this.data[key]) ? _DelArrItem() : _DelObjItem()
    }
    else _DelItem()
    return true

    _DelItem() {
      this.data.Delete(key), this.__data[i].t := this.NT.deleted, this.__map[key] := 0
    }

    _DelArrItem() {
      this.data[key].RemoveAt(index), this.__data[i].v.RemoveAt(index)
    }

    _DelObjItem() {
      this.data[key].DeleteProp(index)
      (_v := this.__data[i].v).RemoveAt(_v.findIndex(_ => _[1] = index))
    }
  }

  Add(key, val, comment := '', subComment?, preEmpty := true) {
    this.data[key] := val
    if preEmpty
      this.__data.Push({ t: this.NT.empty })
    switch {
      case IsArray(val):
        this.__data.Push({
          k: key,
          v: val.map((_, _i) => [_, !IsSet(subComment) ? '' : !IsEmpty(subComment[_i]) ? Format(' {} {}', this.commentChar, subComment[_i]) : '']),
          t: this.NT.arr,
          c: comment && Format(' {} {}', this.commentChar, comment)
        })
      case TypeIsObj(val):
        _v := []
        for k, v in val.OwnProps()
          _v.push([k, v, !IsSet(subComment) ? '' : IsEmpty(subComment[k]) ? '' : Format(' {} {}', this.commentChar, subComment[k])])
        this.__data.Push({
          k: key,
          v: _v,
          t: this.NT.obj,
          c: comment && Format(' {} {}', this.commentChar, comment)
        })
      case IsString(val):
        this.__data.Push({ k: key,
          v: val,
          t: this.NT.normal,
          c: comment ? Format(' {} {}', this.commentChar, comment) : '' })
      default:
    }
    this.__map[key] := this.__data.Length
  }

  Sync(_path?) {
    t := ''
    for v in this.__data {
      switch v.t {
        case this.NT.ignore:
        case this.NT.comment: t .= Format('{}`n', v.v)
        case this.NT.import: t .= Format('{}{}`n', v.v, v.c)
        case this.NT.empty: t .= '`n'
        case this.NT.literal: t .= Format('{}{} :{}`n', this.literalChar, v.k, v.v)
        case this.NT.vital: t .= Format('{}{} : {}{}`n', this.vitalChar, v.k, v.v, v.c)
        case this.NT.normal: t .= Format('{} : {}{}`n', v.k, v.v, v.c)
        case this.NT.arr:
          t .= Format('{}: {}`n', v.k, v.c)
          for vv in v.v
            _t .= Format('- {}{}`n', vv[1], vv[2])
          t .= _t, _t := ''
        case this.NT.obj:
          t .= Format('{}: {}`n', v.k, v.c)
          for vv in v.v
            _t .= Format('+ {} : {}{}`n', vv[1], vv[2], vv[3])
          t .= _t, _t := ''
        default:
      }
    }

    f := FileOpen(IsSet(_path) ? _path : this.path, 'w', 'utf-8')
    f.Write(t)
    f.Close()

    _Format(s) {

    }
  }

}
posted on 2024-07-23 20:39  落寞的雪  阅读(36)  评论(0编辑  收藏  举报