解析INI文件

为了总结一下本章介绍的内容,我们来看一下如何调用正则表达式来解决问题。假设我们编写一个程序从因特网上获取我们敌人的信息(这里我们实际上不会编写该程序,仅仅编写读取配置文件的那部分代码,对不起)。配置文件如下所示。

  1. searchengine=https://duckduckgo.com/?q=$1
  2. spitefulness=9.7
  3. ; comments are preceded by a semicolon...
  4. ; each section concerns an individual enemy
  5. [larry]
  6. fullname=Larry Doe
  7. type=kindergarten bully
  8. website=http://www.geocities.com/CapeCanaveral/11451
  9. [davaeorn]
  10. fullname=Davaeorn
  11. type=evil wizard
  12. outputdir=/home/marijn/enemies/davaeorn

该配置文件格式的语法规则如下所示(它是广泛使用的格式,我们通常称之为INI文件):

  • 忽略空行和以分号起始的行。

  • 使用[]包围的行表示一个新的节(section)。

  • 如果行中是一个标识符(包含字母和数字),后面跟着一个=字符,则表示向当前节添加选项。

  • 其他的格式都是无效的。

我们的任务是将这样的字符串转换为一个对象,该对象的属性包含没有节的设置的字符串,和节的子对象的字符串,节的子对象也包含节的设置。

由于我们需要逐行处理这种格式的文件,因此预处理时最好将文件分割成一行行文本。我们使用第 6 章中的string.split("\n")来分割文件内容。但是一些操作系统并非使用换行符来分隔行,而是使用回车符加换行符("\r\n")。考虑到这点,我们也可以使用正则表达式作为split方法的参数,我们使用类似于/\r?\n/的正则表达式,这样可以同时支持"\n""\r\n"两种分隔符。

  1. function parseINI(string) {
  2. // Start with an object to hold the top-level fields
  3. let currentSection = {name: null, fields: []};
  4. let categories = [currentSection];
  5. string.split(/\r?\n/).forEach(line => {
  6. let match;
  7. if (match = line.match(/^(\w+)=(.*)$/)) {
  8. section[match[1]] = match[2];
  9. section = result[match[1]] = {};
  10. } else if (!/^\s*(;.*)?$/.test(line)) {
  11. throw new Error("Line '" + line + "' is not valid.");
  12. }
  13. });
  14. return result;
  15. }
  16. console.log(parseINI(`
  17. name=Vasilis
  18. [address]
  19. city=Tessaloniki`));
  20. // → {name: "Vasilis", address: {city: "Tessaloniki"}}

代码遍历文件的行并构建一个对象。 顶部的属性直接存储在该对象中,而在节中找到的属性存储在单独的节对象中。 section绑定指向当前节的对象。

有两种重要的行 - 节标题或属性行。 当一行是常规属性时,它将存储在当前节中。 当它是一个节标题时,创建一个新的节对象,并设置section来指向它。

这里需要注意,我们反复使用^$确保表达式匹配整行,而非一行中的一部分。如果不使用这两个符号,大多数情况下程序也可以正常工作,但在处理特定输入时,程序就会出现不合理的行为,我们一般很难发现这个缺陷的问题所在。

if (match = string.match(...))类似于使用赋值作为while的条件的技巧。你通常不确定你对match的调用是否成功,所以你只能在测试它的if语句中访问结果对象。 为了不打破else if形式的令人愉快的链条,我们将匹配结果赋给一个绑定,并立即使用该赋值作为if语句的测试。