Ruby & Lmstat:parslet 和结构化多行块:换行语句放在哪里?

Ruby & Lmstat : parslet and structured multi-line block : where to put the newline statement?

我有一个 Flexlm/Flexnet 许可服务,我想解析该服务的输出。所有输出都是多行的结构化块。我的第一步是解析 lmutil lmstat -c <port@server> -a 的输出以使用许可证和增量。

我尝试使用 Ruby 和 Parslet。所有行都被单独解析。我有一个规则来解析特定类型行的重复,但我无法解析结构化的行块。

我正在寻找定义在多行结构化块中放置 'newline' 语句的位置的法则(在这种情况下最好的词是 'rule')。

我使用 Debian Jessie (stable/x86_64) 和 Ruby 2.1.5p273 和 Parslet 1.6.1-1。

我联系了作者,他很抱歉,但他没有足够的时间来帮助我。看到的网页是:

我花了很多时间试图理解如何构建多行结构化块的规则。下面是我的源代码,其中包含所有测试字符串和输出。

我的方法是构建:

  1. 解析行片段的基本规则
  2. 解析没有'newline'语句的完整行的规则;
  3. 一个规则来解析重复的相同类型的信息,比如使用的标记行;
  4. 解析一组数据的规则:表头+重复行;
  5. 解析重复组的规则。

我不确定第 3 点,我完全迷失了“4”和“5”。

在此先感谢您的帮助。 [ 2017 年 7 月 14 日:部分代码已删除]

#!/usr/bin/env ruby
# This code try to parse the output of 'lmutil lmstat -c <port@server> -a'.
require 'parslet'
require 'parslet/convenience'
require 'pp'

### Begin of the class Lmstat
class Lmstat < Parslet::Parser

 ###
 # Small parts to parse
 rule(:digit)       { match(/\d/).repeat(1) }
 rule(:space)       { str(' ').repeat }
 rule(:eof)         { any.absent? }
 rule(:blank_line)  { space.maybe >> newline >> space.maybe }
 rule(:newline)     { str("\r").maybe >> str("\n") }
 rule(:txt)         { match(/[\w\d\s,_.'",-:]/).repeat }

 def parenthese( atom, qte='()' )
   if (qte == '()' )
    str('(') >> atom >> str(')')
   else
    str(qte) >> atom >> str(qte)
   end
 end
 ###


 ###
 # The header is not parsed for the moment, while I can't
 rule (:header) do
   # Not define until the other parts are OK.
 end


 rule(:feature_line) do
   feature_usage.as(:feature_line) >> # newline >>
   feature_line_id.as(:feature_line_id).repeat.as(:f_line)
 end

 rule(:feature_line_id) do
   feature_version >> newline >> feature_type >>  newline >>
   feature_user_group >> newline
 end

 rule(:feature_line_id_group) do
   (newline >> feature_line_id).repeat(1).as(:f_line_group) >> newline
 end


 rule(:feature_usage) do
   str("Users of ") >> feature.as(:feature_usage) >> str(':') >> space >>
   parenthese( feature_used ) >> space.maybe
 end

 rule(:feature) { match(/[\w_-]/).repeat }

 # Total of 1 license issued;  Total of 0 licenses in use
 rule(:feature_used) do
   feature_token.as(:feature_token_issued) >>
   feature_token.as(:feature_token_used) >> space.maybe >> newline.maybe
 end

 # (Total of 1 license issued;  Total of 0 licenses in use)
 rule(:feature_token) do
   space.maybe >> str('Total of ') >> digit.repeat.as(:feature_token_value) >>
   space >> license >> issued_used >>
   str(';').maybe >> space.maybe
 end

 rule(:license) { str('license') >> str('s').maybe >> space }

 rule(:issued_used) do
   str('issued') | str('in use')
 end

 # v2015.1231
 rule(:version) { match(/[\w\d.-]/).repeat }

 # "incr-1"
 rule(:vendor) { match(/[\w-]/).repeat }

 #  "incr-1" v2015.1231, vendor: ansoftd
 rule(:feature_version) do
   # newline >>
   space.maybe >> parenthese( feature.as(:feature), '"' ) >>
   space >> version.as(:version) >> str(', vendor: ') >>
   vendor.as(:vendor) >> space.maybe >>
   str(', expiry: ').maybe >> match(/[\w\d-]/).repeat.as(:expiration).maybe
 end

 # floating license
 # nodelocked license, locked to "ID=12345"
 rule(:feature_type) do
   space.maybe >>
     ( (space.maybe >> str("floating license").as(:floating) >> space.maybe) |
       (space.maybe >> str('nodelocked license, locked to "ID=') >>
       digit.as(:license_id) >> str('"') >> space.maybe)).as(:feature_type) >>
     space.maybe
 end

 # \t 28 RESERVATIONs for GROUP Better_Group (server/27000)
 rule(:reserve) do
   space.maybe >> str("\t").maybe >> digit.as(:reserve_value) >>
   str(" RESERVATION") >> str("s").maybe >> str(" for ") >>
   word.as(:reserve_type) >> space >> word.as(:reserve_who) >>
   space >>
   parenthese( host.as(:server) >>  str("/") >> digit.as(:port) )
 end

 rule(:reserve_group) do
   (newline >> reserve).repeat(1).as(:reservation)
 end

 rule(:feature_user) do
   space.maybe >>
   word.as(:login) >> space >> host.as(:host_user) >> space >> host.as(:id) >>
   space >> parenthese( version.as(:version) ) >> space >> port >> date_queue
 end

 rule(:feature_user_group) do
   (newline >> feature_user).repeat(1).as(:feature_user_group)
 end

 # queued for 1 license
 rule(:queue) do
  str('queued for ') >> digit.as(:queued) >> str(' license') >> str('s').maybe
 end

 rule(:date_queue) do
   ( ( str(',') >> space >> date >> cmt.as(:comment)) | (space >> queue) )
 end

 rule(:cmt) do
   space.maybe >> match(/[^\r\n]/).repeat#.as(:cmt)
 end

 rule(:word) { match(/[\w\d-]/).repeat }
 rule(:host) { match(/[\w\d_.-]/).repeat }

 rule(:port) do
   parenthese( host.as(:server) >> str('/') >> digit.as(:server_port) >>
               space >> digit.as(:vendor_port) )
 end

 rule(:date) do
   str('start ') >> word.as(:date_dayname) >> space >>
   digit.as(:date_month) >> str('/') >> digit.as(:date_day) >> space >>
   digit.as(:date_hour) >> str(':') >> digit.as(:date_minute)
 end

end
### End of the class Lmstat

###
# Some multiline tests case.

t_feature_line_id = %q{ "incr-2" v9999.9999, vendor: vendor-daemon
    floating license

    henry abc057 abc057 (v2015.0623) (shoe/28512 3886) queued for 1 license
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37 queued for 1 license
}


t_feature_line_id_group = %q{ "incr-2" v9999.9999, vendor: vendor-daemon
    floating license

    henry abc057 abc057 (v2015.0623) (shoe/28512 3886) queued for 1 license
    jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37 queued for 1 license

   "inc2" v9999.9999, vendor: inc2vendor
   floating license

    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37 queued for 1 license }


t_feature_line = %q{Users of ansys:  (Total of 9 licenses issued;  Total of 6 licenses in use)

  "incr-2" v9999.9999, vendor: vendor-daemon
  floating license

    jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:3}

t_feature_line_group = %q{
  "incr-2" v9999.9999, vendor: vendor-daemon
  floating license

    jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37 queued for 1 license

  "incr-2" v9999.9999, vendor: vendor-daemon
  floating license

    jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37}

t_feature_user= %q{jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41}

t_feature_group = %q{ "incr-2" v9999.9999, vendor: vendor-daemon
  floating license

    jason abc057 abc057 (v2015.0623) (shoe/28512 3886), start Fri 11/20 14:41
    simon abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37, 10 licenses
    jessica abc057 abc057 (v2014.1110) (shoe/28512 4166), start Fri 11/20 15:37

  "MATLAB" v35, vendor: MLM, expiry: 01-jan-0000
  nodelocked license, locked to "ID=12345"

    albert node7563 node7563 (v34) (shoe/27000 201), start Mon 5/23 6:16 (linger: 1235700)
    victoria abc087 /dev/pts/1 (v29) (shoe/27000 3401), start Mon 5/23 6:30}
###

###
# Method to test the parsing.
def parse_method(method,str)
  lmstat = Lmstat.new
  unless lmstat.respond_to?(method)
    raise ArgumentError,
          "\n\n\t*****   ERROR: Unknown method -> '#{method}'  ******\n\n",
          caller[1..-1]
  end
  begin
    m = "lmstat.#{method}.parse('"+ str + '\')'
    puts "=> Test of #{m}"
    eval (m)
  rescue Parslet::ParseFailed => failure
    puts failure.cause.ascii_tree
  end
end
###

###
# Not called if 'irb' is used to load the program.
if __FILE__ == $PROGRAM_NAME
    puts "\n ###### Multilines #####"
      parse_method('feature_user_group',t_feature_user_group)
      parse_method("feature_line_id",t_feature_line_id)
      pp parse_method("feature_line_id_group",t_feature_line_id_group)
end

输出[ 07/13/2017 : 删除以放置功能版本]

[更新 - 2017 年 4 月 29 日 - 问题已解决] 感谢 Nigel Thorne 的回答,它解决了我的问题。我已根据您的建议更正了 'space' 的规则。

[ 2014 年 7 月 13 日:删除一些文本以放置功能齐全的版本。 ]

[更新 - 2017 年 7 月 13 日 - 申请测试解析]

我已经完成了一个应用程序,用于测试使用 Ruby 和 Parslet 解析 lmstat 的输出。由于解析依赖于每个编辑器,有些情况可能无法涵盖,但使用了 30 多个许可服务来验证解析。

我可以给3个文件:

  1. parse_lmstat.rb : 使用 './parse_lmstat --help' 获得帮助。测试解析的应用程序。
  2. readstdin_lmstat.rb :从 STDIN 读取以 YAML 格式生成的经过解析的 lmstat 输出。
  3. display_lmstat.rb :显示如何访问数据,它用于改进解析的结构。该脚本比 irb 会话更好。它从 STDIN 读取具有 YAML 格式的已解析 lmstat 输出。

一个例子:

    ~/bin/lmutil lmstat -a -c 1234@licserver | ./parse_lmstat.rb --screen | ./display_lmstat.rb

一个已知错误:当 [CTRL-C] 完成时,信号似乎没有被很好地捕获,Ruby 在某些情况下会发送一些错误消息。

现在,我梦想有一个小型 WEB 应用程序(SINATRA?)到 select 许可证服务器并显示数据,但我不会说 HTML 或 CSS ...任何帮助将不胜感激;-)

您将在下面找到 只有 类 来解析和转换 lmstat 的输出,因为限制为 30000 个字符。

#!/usr/bin/env ruby
#
# class_lmstat.rb
#
# This code try to parse the output of 'lmutil lmstat -c <port@server> -a'.
#
# Scapin - 11/07/2017
#
# For the Whosebug forums
#
require 'parslet'
require 'parslet/convenience'
require 'open3'

### Begin of the class Lmstat
class Lmstat < Parslet::Parser

 ###
 # Small parts to parse
 rule(:digit)       { match(/\d/).repeat(1) }
 rule(:space)       { str(' ').repeat(1) }
 rule(:eof)         { any.absent? }
 rule(:blank_line)  { space.maybe >> newline >> space.maybe }
 rule(:newline)     { str("\r").maybe >> str("\n") }
 rule(:txt)         { match(/[\w_.\)\('\t ",-:\]/).repeat }
 rule(:word)        { match(/[\w-]/).repeat }
 rule(:host)        { match(/[\w_\.-]/).repeat }
 rule(:cnx_id)      { match(/[\/\w_.:]/).repeat }
 rule(:cmt)         { space.maybe >> match(/[^\r\n]/).repeat }
 rule(:error_code)  { match(/[,\d-]/).repeat }

 def parenthese( atom, qte='()' )
   if (qte == '()' )
    str('(') >> atom >> str(')')
   else
    str(qte) >> atom >> str(qte)
   end
 end
 ###

 root(:lmstat)

 rule(:lmstat) do
   (header.as(:header) >> body.repeat.as(:service) >> newline).as(:lmstat)
 end

 ###
 # The header is not parsed for the moment, while I can't
 # handle the multiline block correctly.
 #
 # lmutil - Copyright (c) 1989-2013 Flexera Software LLC. All Rights Reserved.
 # Flexible License Manager status on Fri 11/20/2015 16:39
 #
 # License server status: 1141@lic-server
 #     License file(s) on lic-server: /opt/license/soft/vendor1.lic:/opt/license/soft/vendor2.lic:
 #
 #     lic-server: license server UP (MASTER) v11.13
 #
 # Vendor daemon status (on lic-server):
 #
 #   vendor-daemon: UP v11.13
 # Feature usage info:
 #
 ###
 rule (:header) do
   copyright >> status_date >> newline >>
   server >> license_file >> newline >>
   server_status >> newline >>
   vendor_daemon_status >> newline
 end

 rule (:body) do
   (vendor_daemon.as(:vendor_daemon) >> feature_info.maybe >> newline >>
    feature_line.repeat.maybe.as(:features))
 end

 # lmutil - Copyright (c) 1989-2013 Flexera Software LLC. All Rights Reserved.
 rule (:copyright) do
   space.maybe >> (str("lmutil - Copyright ") >> match(/./).repeat).as(:copyright) >> newline
 end

 # Flexible License Manager status on Fri 11/20/2015 16:39
 rule(:status_date) do
   space.maybe >> str("Flexible License Manager status on ") >>
   word.as(:status_dayname) >> space >>
   digit.as(:status_month) >> str("/") >> digit.as(:status_day) >>
   str("/") >> digit.as(:status_year) >>
   str(" ") >> digit.as(:status_hour) >>
   str(":") >> digit.as(:status_min) >> newline
 end

 rule(:server) do
   str("License server status: ") >>
   digit.as(:server_port1) >> str("@") >> host.as(:server1) >>
   ( str(",") >> digit.as(:server_port2) >> str("@") >> host.as(:server2) >>
     str(",") >> digit.as(:server_port3) >> str("@") >> host.as(:server3) ).maybe >>
   newline
 end

 #  License file(s) on lic-server: /opt/license/soft/licfile-1.lic:/opt/soft/licfile-2.lic:
 rule(:license_file) do
   space.maybe >> str("License file(s) on ") >>
   match(/[\w\d._-]/).repeat.as(:license_files_server) >>
   str(": ") >> txt.as(:license_files_names) >> newline
 end

 rule(:server_status) do
   (space.maybe >> host.as(:server_host) >>  str(": ") >>
    ( server_up | server_down) >> newline).repeat(0).as(:server_list)
 end

 rule(:server_up) do
   str("license server ")>>str("UP").as(:server_up)>>
   server_pos.maybe >> str(" ") >> cmt.as(:server_version)
 end

 rule(:server_pos) do
   space >> parenthese( match(/[A-Za-z]/).repeat.as(:server_role))
 end


 # licserver: Cannot connect to license server system. (-15,570:115 "Operation now in progress")
 rule(:server_down) do
   space.maybe >> str("Cannot connect to license server system").as(:server_down) >>
   str(". ") >> cmt.as(:server_error)
 end

 rule(:vendor_daemon_status) do
   str("Vendor daemon status (on ") >> host.as(:server_daemon) >>
   str("):") >> space.maybe >> newline
 end

 rule(:vendor_daemon) do
   ( vendor_daemon_up |  vendor_daemon_down )
 end

 rule(:vendor_daemon_up) do
   space.maybe >> word.as(:daemon) >> str(": ") >> word.as(:daemon_status) >>
   space >> host.as(:daemon_version) >> newline
 end

 rule(:vendor_daemon_down_ini) do
   space.maybe >> word.as(:daemon) >> str(": The desired vendor daemon is down. ") >>
   parenthese( error_code.as(:daemon_status) ) >> space.maybe >> newline
 end

 # \n\n dconcept: No socket connection to license server manager. (-7,96)
 rule(:vendor_daemon_down) do
   space.maybe >> word.as(:daemon) >> str(": The desired vendor daemon is down. ") >>
   parenthese( error_code.as(:daemon_status) ) >> space.maybe >> newline
   space.maybe >> word.as(:vendor_daemon_down_msg_feature).maybe >>
   str(': No socket connection to license server manager.').maybe >> space.maybe >>
   cmt.as(:vendor_daemon_down_msg).maybe >> newline.maybe
 end

 rule(:feature_info) do
   space.maybe >> str("Feature usage info:") >> space.maybe >> newline
 end


 ###
 # Users of soft_a:  (Total of 1 license issued;  Total of 0 licenses in use)
 #
 #  "incr-1" v2015.1231, vendor: soft_ad
 #  floating license
 #
 #  28 RESERVATIONs for GROUP Better_Group (server/27000)
 #  1 RESERVATION for USER toni (server/27000)
 #   scott abc056 abc056 (v2015.0623) (shoe/28512 3644), start Fri 11/20 15:45, 2 licenses
 #   scott abc056 abc056 (v2015.0623) (shoe/28512 4669), start Fri 11/20 15:45, 10 licenses
 rule(:feature_line) do
   feature_usage.as(:feature_line) >> newline >>
   feature_line_id.repeat(0).as(:feature_line_id)
 end

 #  "incr-1" v2015.1231, vendor: soft_ad
 #  floating license
 #
 #   scott abc056 abc056 (v2015.0623) (shoe/28512 3644), start Fri 11/20 15:45, 2 licenses
 #   scott abc056 abc056 (v2015.0623) (shoe/28512 4669), start Fri 11/20 15:45, 10 licenses
 rule(:feature_line_id) do
   feature_version >> newline >> feature_type >>  newline >>
   # ( reserve.as(:reservation) | feature_user.as(:user)).repeat(1).as(:users) >> newline
   ( reserve.as(:reservation) | feature_user.as(:user)).repeat(1).as(:who) >> newline
 end

 # Users of soft_a:  (Total of 1 license issued;  Total of 0 licenses in use)
 # Users of SOFT_B:  (Uncounted, node-locked)
 # Users of soft_c:  (Error: 6 licenses, unsupported by licensed server)
 rule(:feature_usage) do
   str("Users of ") >> feature.as(:feature_name) >> str(':') >> space >>
   parenthese( feature_used ) >> space.maybe >> newline
 end


 # Total of 1 license issued;  Total of 0 licenses in use
 # Uncounted, node-locked
 rule(:feature_used) do
   ( ( feature_token.as(:feature_token_issued) >> feature_token.as(:feature_token_used)) |
     ( word.as(:feature_token_issued) >> str(', ') >> word.as(:feature_token_used) ) |
     ( str('Error: ') >> digit.repeat.as(:feature_token_error) >> space >> str( 'license') >>
       str('s').maybe >> str(', ') >>
       match(/[\w_', :-]/).repeat.as(:feature_token_error_cause) ) ) >>
   space.maybe >> newline.maybe
 end

 # (Total of 1 license issued;  Total of 0 licenses in use)
 rule(:feature_token) do
   space.maybe >> str('Total of ') >> digit.repeat.as(:feature_token_value) >>
   space >> license >> issued_used >>
   str(';').maybe >> space.maybe
 end

 rule(:license) { str('license') >> str('s').maybe >> space }

 rule(:issued_used) do
   str('issued') | str('in use')
 end

 # v2015.1231
 rule(:version) { match(/[\w\d.-]/).repeat }

 # "incr-1"
 rule(:vendor) { match(/[\w-]/).repeat }

 rule(:feature) { match(/[\w\d\/_+-]/).repeat }

 #  "incr-1" v2015.1231, vendor: soft_ad
 rule(:feature_version) do
   # newline >>
   space >> parenthese( feature.as(:feature), '"' ) >>
   space >> version.as(:version) >> str(', vendor: ') >>
   vendor.as(:vendor) >> space.maybe >>
   str(', expiry: ').maybe >> match(/[\w\d-]/).repeat.as(:expiration).maybe
 end

 rule(:feature_type) do
   space >> ( float_type | node_type ).as(:feature_type)
 end

 # floating license
 rule(:float_type) do
   str("floating license").as(:floating) >> cmt.maybe >> newline
 end

 # nodelocked license, locked to "ID=654321"
 # nodelocked license locked to NOTHING (hostid=ANY)
 # uncounted nodelocked license locked to NOTHING (hostid=ANY)
 # uncounted nodelocked license, locked to Vendor-defined "PTC_HOSTID=01-0A-01-0A-01"
 rule(:node_type) do
   str('uncounted ').maybe >> str("nodelocked license") >> str(',').maybe >> str(' locked to ').maybe >>
   ( ( str('"ID=') >> digit.as(:nodelocked_id) >> str('"') ) |
     ( host.as(:nodelocked_to) >> space >> parenthese(str('hostid=') >> host.as(:nodelocked_hostid)) ) |
     ( host.as(:nodelocked_to) >> space >>
                                  parenthese(match(/[\w:_=' -]/).repeat.as(:nodelocked_hostid), '"') ) ) >>
   space.maybe >> newline
 end

 # \t 28 RESERVATIONs for GROUP Better_Group (server/27000)
 rule(:reserve) do
   space.maybe >> str("\t").maybe >> digit.as(:reserve_value) >>
   str(" RESERVATION") >> str("s").maybe >> str(" for ") >>
   word.as(:reserve_type) >> space >> word.as(:reserve_who) >>
   space >>
   parenthese( host.as(:server) >>  str("/") >> digit.as(:port) ) >>
   newline
 end

 rule(:feature_user) do
   (u_std | u_aselta | u_ans | u_c1 | u_c2 )
 end

 # scott abc056 abc056 (v2015.0623) (shoe/28512 3644), start Fri 11/20 15:45, 2 licenses
 # albert node7563 node7563 (v34) (shoe/27000 201), start Mon 5/23 6:16 (linger: 1235700)
 # hector node088 dev/tty (v2015.0312) (licserver/1446 3730), start Thu 11/19 9:08
 # will pim.my.domain.org pim.my.domain.org 6656 (v2016.1129) (licserver/1446 2216), start Fri 5/12 14:51
 rule(:u_std) do
   space >> word.as(:login) >> space >> host.as(:host_user) >> space >>
   cnx_id.as(:host_id) >>
   space >> parenthese( version.as(:version) ) >> space >> port >> date_queue >>
   newline
 end

 # scott cat :0 Token Lic (v7.000) (shoe/5300 15434), start Thu 7/6 17:07
 rule(:u_c1) do
   space >> word.as(:login) >> space >> host.as(:host_user) >> space >>
   cnx_id.as(:host_id) >> space >> match(/[^(]/).repeat.as(:common_name) >>
   parenthese( version.as(:version) ) >> space >> port >> date_queue >>
   newline
 end

 # jessie cat bird:1144.0 APS Multi-core (Max. 16 cores) (v11.100) (licserver/5303 13188), start Thu 7/6 15:22, 4 licenses
 rule(:u_c2) do
   space >> word.as(:login) >> space >> host.as(:host_user) >> space >>
   cnx_id.as(:host_id) >> space >> match(/[^(]/).repeat.as(:common_name) >>
   str('(') >> match(/[^)]/).repeat.as(:common_info) >> str(')')  >> space >>
   parenthese( version.as(:version) ) >> space >> port >> date_queue >>
   newline
 end

 # tiger pam.my.domain.org pam.my.domain.org 6656 (v2016.1129) (licserver/1446 2216), start Fri 5/12 14:51
 rule(:u_ans) do
   space >> word.as(:login) >> space >> host.as(:host_user) >> space >>
   cnx_id.as(:host_id) >>  ( space >> host.as(:further) ).maybe >>
   space >> parenthese( version.as(:version) ) >> space >> port >> date_queue >>
   newline
 end


 # clark node07 SOMETHING Inscale / grid (worker) (v1.0) (licserv01/27016 5506), start Fri 4/28 13:42, 4 licenses
 # bunny orca SOMETHING Inscale / graphical (v1.0) (licserv01/27016 650), start Thu 4/13 10:27
 rule(:u_aselta) do
   space >> word.as(:login) >> space >> host.as(:host_user) >> space >>
   word.as(:daemon)  >> space >> word.as(:soft) >> str(" / ") >>
   word.as(:function) >> (space >> parenthese( word.as(:tools) )).maybe  >>
   space >> parenthese( version.as(:version) ) >> space >> port >> date_queue >>
   newline
 end

 # queued for 1 license
 rule(:queue) do
   str('queued for ') >> digit.as(:queued) >> str(' license') >> str('s').maybe
 end

 rule(:lic) do
   str(',') >> space >> digit.as(:licenses) >> str(' license') >> str('s').maybe
 end

 rule(:date_queue) do
   ( ( str(',') >> space >> date >> ( lic | cmt.as(:comment))) | (space >> queue) )
 end

 rule(:port) do
   parenthese( host.as(:server) >> str('/') >> digit.as(:server_port) >>
               space >> digit.as(:vendor_port) )
 end

 rule(:date) do
   str('start ') >> word.as(:date_dayname) >> space >>
   digit.as(:date_month) >> str('/') >> digit.as(:date_day) >> space >>
   digit.as(:date_hour) >> str(':') >> digit.as(:date_minute)
 end

end
### End of the class Lmstat


### Begin of the class Trans 
class Trans < Parslet::Transform
  rule(:feature_token_value => simple(:v)) { Integer(v) }

  rule(:user => subtree(:t)) do
    if ( t.has_key?(:date_month) )
      cal = { "Sun"=>"Dimanche", "Mon"=>"Lundi", "Tue"=>"Mardi",
              "Wed"=>"Mercredi", "Thu"=>"Jeudi", "Fri"=>"Vendredi"}
      clock = Time.now

      # Addition of keys
      t.merge!( { :date_year => 0, :since => "", :delay_min => 0, :delay_string => ""})

      # Convert to integer
      t[:date_minute] = t[:date_minute].to_s.sub(/^0/,"")  if (t.has_key?(:date_minute))
      t.each do |k,v|
        [ :server_port, :vendor_port, :date_month, :date_day,
          :date_hour, :date_minute, :queued, :licenses ].each do |symbol|
          t[k] = Integer(v)  if k == symbol
        end
      end

      t[:date_dayname] = cal[t[:date_dayname].to_s]
      t[:date_year] = clock.year
      t[:date_year] = t[:date_year] - 1 if (clock.month < t[:date_month])
      t[:since] = sprintf( "%2.2d/%2.2d/%2d-%2.2d:%2.2d", t[:date_day], t[:date_month],
                           t[:date_year], t[:date_hour], t[:date_minute])
      t[:delay_min], t[:delay_string] = Tools.delay( clock, t[:date_year],
                 t[:date_month], t[:date_day], t[:date_hour], t[:date_minute], 0 )
      t[:delay_min] = Integer(t[:delay_min] / 60)
      t[:delay_string].chop!.chop!.chop!

      # Add a key for a borrowed token.
      t.merge!( {:borrow => true} ) if ( /linger/ =~ t[:comment] )
    end

    # Restore the hash.
    { :user => t }
  end

end
###


####
module Tools

  def Tools.check_file( file )
    return false unless file
    if File.exist?(file)
      File.file?(file)
    else
      false
    end
  end


  def Tools.delay( clock = Time.now, year, month, day, hour, minute, second )
    delay = clock - Time.local(year.to_i,month.to_i,day.to_i,hour.to_i,minute.to_i, second.to_i)
    d = delay.divmod(3600.0*24.0)
    h = d[1].divmod(3600.0)
    m = h[1].divmod(3600.0)[1].divmod(60.0)
    s = m[1].divmod(60.0)[1].divmod(60.0)
    [ delay, sprintf("%3.3dj%2.2dh%2.2dmin%2.2ds", d[0], h[0], m[0], s[1].round) ]
  end


  def Tools.grab_list( list_file, separator = ' ' )
    return nil unless Tools.check_file(list_file) && File.stat(list_file).readable?
    lines = Array.new
    list = Array.new
    open(list_file).each_line { |l| lines << l.chomp if l }
    # 'split' ignore the multiple '/\s/'.
    lines.each { |l| list.concat(l.split(separator)) }
    # Suppress the spaces if the separator isn't a "\s".
    list.each_index { |i| list[i]= list[i].delete(" ") } unless
    list.delete_if { |l| l.length < 1 }
    list
  end

  def Tools.create_output( name_of_file, extension = '', mode = "w" )
    begin
      file_name =  name_of_file + extension
      line = __LINE__; File.new( file_name, mode )
    rescue Errno::EACCES => error_create
      STDERR.puts $PROGRAM_NAME + "(#{line})" +
        " ERREUR ! create_output(\"#{file_name}\")"
      STDERR.puts $PROGRAM_NAME + "(#{line})" +
        " ERREUR !   Message = '#{error_create.message}'"
      raise error_create
    end
  end
end
###

您似乎在使用 "newline" 时遇到了问题。

一个好的指导方针是…… * 在规则末尾使用它们(作为终止符) * 如果它们在语义上不是令牌的一部分,则让父规则使用它们。

假设我有一个文档:

A
B

C

我会将其解析为:

#tokens
rule :a do str("A") end
rule :b do str("B") end
rule :c do str("C") end
rule :nl do str("\n") end

# lines
rule :a_line do a>>nl end
rule :b_line do b>>nl end
rule :c_line do c>>nl end

# doc
rule :doc do a_line>>b_line>>nl>>c_line

请注意,"b_line" 不会同时使用“\n”,因为它应该对其上下文一无所知。

我还注意到您将 "space" 定义为 "str(' ').repeat"。这是 "str(' ').repeat(0)" 的缩写,可以匹配零次。这使得 "space" 是可选的...因此 "space.maybe" 没有意义。