ruby-puppetdb 1.6.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +8 -8
- data/bin/find-nodes +34 -21
- data/lib/hiera/backend/puppetdb_backend.rb +15 -12
- data/lib/puppet/application/query.rb +10 -4
- data/lib/puppet/face/query.rb +37 -31
- data/lib/puppet/parser/functions/query_facts.rb +9 -4
- data/lib/puppet/parser/functions/query_nodes.rb +13 -8
- data/lib/puppet/parser/functions/query_resources.rb +35 -6
- data/lib/puppetdb.rb +1 -1
- data/lib/puppetdb/astnode.rb +92 -44
- data/lib/puppetdb/connection.rb +14 -69
- data/lib/puppetdb/grammar.racc +75 -40
- data/lib/puppetdb/lexer.rb +23 -4
- data/lib/puppetdb/lexer.rex +30 -24
- data/lib/puppetdb/parser.rb +245 -223
- data/lib/puppetdb/parser_helper.rb +48 -0
- data/spec/unit/puppetdb/parser_spec.rb +129 -0
- metadata +7 -28
- data/lib/puppet/parser/functions/pdbfactquery.rb +0 -31
- data/lib/puppet/parser/functions/pdbnodequery.rb +0 -38
- data/lib/puppet/parser/functions/pdbnodequery_all.rb +0 -40
- data/lib/puppet/parser/functions/pdbquery.rb +0 -41
- data/lib/puppet/parser/functions/pdbresourcequery.rb +0 -39
- data/lib/puppet/parser/functions/pdbresourcequery_all.rb +0 -37
- data/lib/puppet/parser/functions/pdbstatusquery.rb +0 -31
- data/lib/puppetdb/util.rb +0 -18
- data/spec/unit/puppet/parser/functions/pdbfactquery_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbnodequery_all_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbnodequery_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbquery_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbresourcequery_all_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbresourcequery_spec.rb +0 -19
- data/spec/unit/puppet/parser/functions/pdbstatusquery_spec.rb +0 -19
- data/spec/unit/puppetdb/connection_spec.rb +0 -67
data/lib/puppetdb.rb
CHANGED
data/lib/puppetdb/astnode.rb
CHANGED
@@ -1,16 +1,14 @@
|
|
1
1
|
class PuppetDB::ASTNode
|
2
2
|
attr_accessor :type, :value, :children
|
3
3
|
|
4
|
-
def initialize(type, value, children=[])
|
4
|
+
def initialize(type, value, children = [])
|
5
5
|
@type = type
|
6
6
|
@value = value
|
7
7
|
@children = children
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
@children.each { |c| c.capitalize! }
|
13
|
-
return self
|
10
|
+
def capitalize_class(name)
|
11
|
+
name.to_s.split('::').collect(&:capitalize).join('::')
|
14
12
|
end
|
15
13
|
|
16
14
|
# Generate the the query code for a subquery
|
@@ -26,9 +24,9 @@ class PuppetDB::ASTNode
|
|
26
24
|
if from_mode == :none
|
27
25
|
return query
|
28
26
|
else
|
29
|
-
return ['in',
|
30
|
-
|
31
|
-
|
27
|
+
return ['in', 'certname',
|
28
|
+
['extract', 'certname',
|
29
|
+
["select_#{to_mode}", query]]]
|
32
30
|
end
|
33
31
|
end
|
34
32
|
|
@@ -40,58 +38,108 @@ class PuppetDB::ASTNode
|
|
40
38
|
case @type
|
41
39
|
when :booleanop
|
42
40
|
@children.each do |c|
|
43
|
-
if c.type == :booleanop
|
41
|
+
if c.type == :booleanop && c.value == @value
|
44
42
|
c.children.each { |cc| @children << cc }
|
45
43
|
@children.delete c
|
46
44
|
end
|
47
45
|
end
|
48
46
|
end
|
49
|
-
@children.each
|
50
|
-
|
47
|
+
@children.each(&:optimize)
|
48
|
+
self
|
51
49
|
end
|
52
50
|
|
53
51
|
# Evalutate the node and all children
|
54
52
|
#
|
55
53
|
# @param mode [Symbol] The query mode we are evaluating for
|
56
54
|
# @return [Array] the resulting PuppetDB query
|
57
|
-
def evaluate(mode = :nodes)
|
55
|
+
def evaluate(mode = [:nodes])
|
58
56
|
case @type
|
59
|
-
when :
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
when :match then op = '~'
|
69
|
-
end
|
70
|
-
|
71
|
-
case mode
|
72
|
-
when :nodes,:facts # Do a subquery to match nodes matching the facts
|
73
|
-
return subquery(mode, :facts, ['and', ['=', 'name', @children[0].evaluate(mode)], [op, 'value', @children[1].evaluate(mode)]])
|
74
|
-
when :resources
|
75
|
-
paramname = @children[0].evaluate(mode)
|
76
|
-
case paramname
|
77
|
-
when "tag"
|
78
|
-
return [op, paramname, @children[1].evaluate(mode)]
|
57
|
+
when :comparison
|
58
|
+
left = @children[0].evaluate(mode)
|
59
|
+
right = @children[1].evaluate(mode)
|
60
|
+
if mode.last == :subquery
|
61
|
+
left = left[0] if left.length == 1
|
62
|
+
comparison(left, right)
|
63
|
+
elsif mode.last == :resources
|
64
|
+
if left[0] == 'tag'
|
65
|
+
comparison(left[0], right)
|
79
66
|
else
|
80
|
-
|
67
|
+
comparison(['parameter', left[0]], right)
|
81
68
|
end
|
69
|
+
else
|
70
|
+
subquery(mode.last,
|
71
|
+
:fact_contents,
|
72
|
+
['and', left, comparison('value', right)])
|
82
73
|
end
|
74
|
+
when :boolean
|
75
|
+
value
|
83
76
|
when :string
|
84
|
-
|
77
|
+
value
|
85
78
|
when :number
|
86
|
-
|
87
|
-
when :
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
79
|
+
value
|
80
|
+
when :date
|
81
|
+
require 'chronic'
|
82
|
+
ret = Chronic.parse(value, :guess => false).first.iso8601
|
83
|
+
fail "Failed to parse datetime: #{value}" if ret.nil?
|
84
|
+
ret
|
85
|
+
when :booleanop
|
86
|
+
[value.to_s, *evaluate_children(mode)]
|
87
|
+
when :subquery
|
88
|
+
mode.push :subquery
|
89
|
+
ret = subquery(mode.last, value + 's', children[0].evaluate(mode))
|
90
|
+
mode.pop
|
91
|
+
ret
|
92
|
+
when :regexp_node_match
|
93
|
+
mode.push :regexp
|
94
|
+
ret = ['~', 'certname', Regexp.escape(value.evaluate(mode))]
|
95
|
+
mode.pop
|
96
|
+
ret
|
97
|
+
when :identifier_path
|
98
|
+
if mode.last == :subquery || mode.last == :resources
|
99
|
+
evaluate_children(mode)
|
100
|
+
elsif mode.last == :regexp
|
101
|
+
evaluate_children(mode).join '.'
|
102
|
+
else
|
103
|
+
# Check if any of the children are of regexp type
|
104
|
+
# in that case we need to escape the others and use the ~> operator
|
105
|
+
if children.any? { |c| c.type == :regexp_identifier }
|
106
|
+
mode.push :regexp
|
107
|
+
ret = ['~>', 'path', evaluate_children(mode)]
|
108
|
+
mode.pop
|
109
|
+
ret
|
110
|
+
else
|
111
|
+
['=', 'path', evaluate_children(mode)]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
when :regexp_identifier
|
115
|
+
value
|
116
|
+
when :identifier
|
117
|
+
mode.last == :regexp ? Regexp.escape(value) : value
|
118
|
+
when :resource
|
119
|
+
mode.push :resources
|
120
|
+
regexp = value[:title].type == :regexp_identifier
|
121
|
+
if !regexp && value[:type].capitalize == 'Class'
|
122
|
+
title = capitalize_class(value[:title].evaluate)
|
123
|
+
else
|
124
|
+
title = value[:title].evaluate
|
125
|
+
end
|
126
|
+
ret = subquery(mode.last, :resources,
|
127
|
+
['and',
|
128
|
+
['=', 'type', capitalize_class(value[:type])],
|
129
|
+
[regexp ? '~' : '=', 'title', title],
|
130
|
+
['=', 'exported', value[:exported]],
|
131
|
+
*evaluate_children(mode)])
|
132
|
+
mode.pop
|
133
|
+
ret
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Helper method to produce a comparison expression
|
138
|
+
def comparison(left, right)
|
139
|
+
if @value[0] == '!'
|
140
|
+
['not', [@value[1], left, right]]
|
141
|
+
else
|
142
|
+
[@value, left, right]
|
95
143
|
end
|
96
144
|
end
|
97
145
|
|
@@ -99,6 +147,6 @@ class PuppetDB::ASTNode
|
|
99
147
|
#
|
100
148
|
# @return [Array] The evaluate results of the children nodes
|
101
149
|
def evaluate_children(mode)
|
102
|
-
|
150
|
+
children.collect { |c| c.evaluate mode }
|
103
151
|
end
|
104
152
|
end
|
data/lib/puppetdb/connection.rb
CHANGED
@@ -9,79 +9,24 @@ class PuppetDB::Connection
|
|
9
9
|
|
10
10
|
include Puppet::Util::Logging
|
11
11
|
|
12
|
-
def initialize(host='puppetdb', port=443, use_ssl=true)
|
12
|
+
def initialize(host = 'puppetdb', port = 443, use_ssl = true)
|
13
13
|
@host = host
|
14
14
|
@port = port
|
15
15
|
@use_ssl = use_ssl
|
16
|
-
@parser = PuppetDB::Parser.new
|
17
16
|
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
# Get the listed facts for all nodes matching query
|
31
|
-
# return it as a hash of hashes
|
32
|
-
#
|
33
|
-
# @param facts [Array] the list of facts to fetch
|
34
|
-
# @param nodequery [Array] the query to find the nodes to fetch facts for
|
35
|
-
# @return [Hash] a hash of hashes with facts for each node mathing query
|
36
|
-
def facts(facts, nodequery, http = nil)
|
37
|
-
if facts.empty?
|
38
|
-
q = ['in', 'certname', ['extract', 'certname', ['select-facts', nodequery]]]
|
39
|
-
else
|
40
|
-
q = ['and', ['in', 'certname', ['extract', 'certname', ['select-facts', nodequery]]], ['or', *facts.collect { |f| ['=', 'name', f]}]]
|
41
|
-
end
|
42
|
-
facts = {}
|
43
|
-
query(:facts, q, http).each do |fact|
|
44
|
-
if facts.include? fact['certname']
|
45
|
-
facts[fact['certname']][fact['name']] = fact['value']
|
46
|
-
else
|
47
|
-
facts[fact['certname']] = { fact['name'] => fact['value'] }
|
18
|
+
def self.check_version
|
19
|
+
begin
|
20
|
+
require 'puppet/util/puppetdb'
|
21
|
+
unless Puppet::Util::Puppetdb.config.respond_to?('server_urls')
|
22
|
+
Puppet.warning <<-EOT
|
23
|
+
It looks like you are using a PuppetDB version < 3.0.
|
24
|
+
This version of puppetdbquery requires at least PuppetDB 3.0 to work.
|
25
|
+
Downgrade to puppetdbquery 1.x to use it with PuppetDB 2.x.
|
26
|
+
EOT
|
48
27
|
end
|
28
|
+
rescue LoadError
|
49
29
|
end
|
50
|
-
facts
|
51
|
-
end
|
52
|
-
|
53
|
-
# Get the listed resources for all nodes matching query
|
54
|
-
# return it as a hash of hashes
|
55
|
-
#
|
56
|
-
# @param resquery [Array] a resources query for what resources to fetch
|
57
|
-
# @param nodequery [Array] the query to find the nodes to fetch resources for, optionally empty
|
58
|
-
# @param grouphosts [Boolean] whether or not to group the results by the host they belong to
|
59
|
-
# @return [Hash|Array] a hash of hashes with resources for each node mathing query or array if grouphosts was false
|
60
|
-
def resources(nodequery, resquery, http=nil, grouphosts=true)
|
61
|
-
if resquery and ! resquery.empty?
|
62
|
-
if nodequery and ! nodequery.empty?
|
63
|
-
q = ['and', resquery, nodequery]
|
64
|
-
else
|
65
|
-
q = resquery
|
66
|
-
end
|
67
|
-
else
|
68
|
-
raise RuntimeError, "PuppetDB resources query error: at least one argument must be non empty; arguments were: nodequery: #{nodequery.inspect} and requery: #{resquery.inspect}"
|
69
|
-
end
|
70
|
-
resources = {}
|
71
|
-
results = query(:resources, q, http)
|
72
|
-
|
73
|
-
if grouphosts
|
74
|
-
results.each do |resource|
|
75
|
-
unless resources.has_key? resource['certname']
|
76
|
-
resources[resource['certname']] = []
|
77
|
-
end
|
78
|
-
resources[resource['certname']] << resource
|
79
|
-
end
|
80
|
-
else
|
81
|
-
resources = results
|
82
|
-
end
|
83
|
-
|
84
|
-
return resources
|
85
30
|
end
|
86
31
|
|
87
32
|
# Execute a PuppetDB query
|
@@ -89,7 +34,7 @@ class PuppetDB::Connection
|
|
89
34
|
# @param endpoint [Symbol] :resources, :facts or :nodes
|
90
35
|
# @param query [Array] query to execute
|
91
36
|
# @return [Array] the results of the query
|
92
|
-
def query(endpoint, query = nil, http = nil, version = :
|
37
|
+
def query(endpoint, query = nil, http = nil, version = :v4)
|
93
38
|
require 'json'
|
94
39
|
|
95
40
|
unless http
|
@@ -98,13 +43,13 @@ class PuppetDB::Connection
|
|
98
43
|
end
|
99
44
|
headers = { 'Accept' => 'application/json' }
|
100
45
|
|
101
|
-
uri = "/#{version}/#{endpoint}"
|
46
|
+
uri = "/pdb/query/#{version}/#{endpoint}"
|
102
47
|
uri += URI.escape "?query=#{query.to_json}" unless query.nil? || query.empty?
|
103
48
|
|
104
49
|
debug("PuppetDB query: #{query.to_json}")
|
105
50
|
|
106
51
|
resp = http.get(uri, headers)
|
107
|
-
|
52
|
+
fail "PuppetDB query error: [#{resp.code}] #{resp.msg}, query: #{query.to_json}" unless resp.is_a?(Net::HTTPSuccess)
|
108
53
|
JSON.parse(resp.body)
|
109
54
|
end
|
110
55
|
end
|
data/lib/puppetdb/grammar.racc
CHANGED
@@ -4,7 +4,9 @@
|
|
4
4
|
class PuppetDB::Parser
|
5
5
|
|
6
6
|
token LPAREN RPAREN LBRACK RBRACK LBRACE RBRACE
|
7
|
-
token EQUALS NOTEQUALS MATCH
|
7
|
+
token EQUALS NOTEQUALS MATCH NOTMATCH
|
8
|
+
token LESSTHAN LESSTHANEQ GREATERTHAN GREATERTHANEQ
|
9
|
+
token AT HASH ASTERISK DOT
|
8
10
|
token NOT AND OR
|
9
11
|
token NUMBER STRING BOOLEAN EXPORTED
|
10
12
|
|
@@ -15,48 +17,81 @@ class PuppetDB::Parser
|
|
15
17
|
left OR
|
16
18
|
preclow
|
17
19
|
|
20
|
+
options no_result_var
|
21
|
+
|
18
22
|
rule
|
19
|
-
query
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
23
|
+
query
|
24
|
+
: expression
|
25
|
+
|
|
26
|
+
|
27
|
+
expression
|
28
|
+
: identifier_path { ASTNode.new :regexp_node_match, val[0] }
|
29
|
+
| NOT expression { ASTNode.new :booleanop, :not, [val[1]] }
|
30
|
+
| expression AND expression { ASTNode.new :booleanop, :and, [val[0], val[2]] }
|
31
|
+
| expression OR expression { ASTNode.new :booleanop, :or, [val[0], val[2]] }
|
32
|
+
| LPAREN expression RPAREN { val[1] }
|
33
|
+
| resource_expression
|
34
|
+
| comparison_expression
|
35
|
+
| subquery
|
36
|
+
|
37
|
+
literal
|
38
|
+
: boolean { ASTNode.new :boolean, val[0] }
|
39
|
+
| string { ASTNode.new :string, val[0] }
|
40
|
+
| integer { ASTNode.new :number, val[0] }
|
41
|
+
| float { ASTNode.new :number, val[0] }
|
42
|
+
| AT string { ASTNode.new :date, val[1] }
|
43
|
+
|
44
|
+
comparison_op
|
45
|
+
: MATCH
|
46
|
+
| NOTMATCH
|
47
|
+
| EQUALS
|
48
|
+
| NOTEQUALS
|
49
|
+
| GREATERTHAN
|
50
|
+
| GREATERTHANEQ
|
51
|
+
| LESSTHAN
|
52
|
+
| LESSTHANEQ
|
53
|
+
|
54
|
+
comparison_expression
|
55
|
+
: identifier_path comparison_op literal { ASTNode.new :comparison, val[1], [val[0], val[2]] }
|
56
|
+
|
57
|
+
identifier
|
58
|
+
: string { ASTNode.new :identifier, val[0] }
|
59
|
+
| integer { ASTNode.new :identifier, val[0] }
|
60
|
+
| MATCH string { ASTNode.new :regexp_identifier, val[1] }
|
61
|
+
| ASTERISK { ASTNode.new :regexp_identifier, '.*' }
|
62
|
+
|
63
|
+
identifier_path
|
64
|
+
: identifier { ASTNode.new :identifier_path, nil, [val[0]] }
|
65
|
+
| identifier_path DOT identifier { val[0].children.push val[2]; val[0] }
|
66
|
+
|
67
|
+
subquery
|
68
|
+
: HASH string DOT comparison_expression { ASTNode.new :subquery, val[1], [val[3]] }
|
69
|
+
| HASH string block_expression { ASTNode.new :subquery, val[1], [val[2]] }
|
70
|
+
|
71
|
+
block_expression
|
72
|
+
: LBRACE expression RBRACE { val[1] }
|
73
|
+
|
74
|
+
resource_expression
|
75
|
+
: string LBRACK identifier RBRACK
|
76
|
+
{ ASTNode.new :resource, {:type => val[0], :title => val[2], :exported => false} }
|
77
|
+
| string LBRACK identifier RBRACK block_expression
|
78
|
+
{ ASTNode.new :resource, {:type => val[0], :title => val[2], :exported => false}, [val[4]] }
|
79
|
+
| EXPORTED string LBRACK identifier RBRACK
|
80
|
+
{ ASTNode.new :resource, {:type => val[1], :title => val[3], :exported => true} }
|
81
|
+
| EXPORTED string LBRACK identifier RBRACK block_expression
|
82
|
+
{ ASTNode.new :resource, {:type => val[1], :title => val[3], :exported => true}, [val[5]] }
|
83
|
+
|
84
|
+
boolean: BOOLEAN
|
85
|
+
integer: NUMBER
|
86
|
+
string: STRING
|
87
|
+
float: NUMBER DOT NUMBER { "#{val[0]}.#{val[2]}".to_f }
|
57
88
|
|
58
89
|
end
|
59
|
-
----
|
90
|
+
---- inner
|
91
|
+
include PuppetDB::ParserHelper
|
92
|
+
|
93
|
+
---- header
|
60
94
|
require 'puppetdb'
|
61
95
|
require 'puppetdb/lexer'
|
62
96
|
require 'puppetdb/astnode'
|
97
|
+
require 'puppetdb/parser_helper'
|
data/lib/puppetdb/lexer.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
#--
|
2
2
|
# DO NOT MODIFY!!!!
|
3
3
|
# This file is automatically generated by rex 1.0.5
|
4
|
-
# from lexical definition file "lib/puppetdb/lexer.
|
4
|
+
# from lexical definition file "lib/puppetdb/lexer.rex".
|
5
5
|
#++
|
6
6
|
|
7
7
|
require 'racc/parser'
|
8
8
|
# vim: syntax=ruby
|
9
9
|
|
10
10
|
require 'yaml'
|
11
|
+
require 'puppetdb'
|
11
12
|
|
12
13
|
class PuppetDB::Lexer < Racc::Parser
|
13
14
|
require 'strscan'
|
@@ -91,12 +92,30 @@ class PuppetDB::Lexer < Racc::Parser
|
|
91
92
|
when (text = @ss.scan(/~/))
|
92
93
|
action { [:MATCH, text] }
|
93
94
|
|
95
|
+
when (text = @ss.scan(/\!~/))
|
96
|
+
action { [:NOTMATCH, text] }
|
97
|
+
|
94
98
|
when (text = @ss.scan(/</))
|
95
99
|
action { [:LESSTHAN, text] }
|
96
100
|
|
101
|
+
when (text = @ss.scan(/<=/))
|
102
|
+
action { [:LESSTHANEQ, text] }
|
103
|
+
|
97
104
|
when (text = @ss.scan(/>/))
|
98
105
|
action { [:GREATERTHAN, text] }
|
99
106
|
|
107
|
+
when (text = @ss.scan(/>=/))
|
108
|
+
action { [:GREATERTHANEQ, text] }
|
109
|
+
|
110
|
+
when (text = @ss.scan(/\*/))
|
111
|
+
action { [:ASTERISK, text] }
|
112
|
+
|
113
|
+
when (text = @ss.scan(/\#/))
|
114
|
+
action { [:HASH, text] }
|
115
|
+
|
116
|
+
when (text = @ss.scan(/\./))
|
117
|
+
action { [:DOT, text] }
|
118
|
+
|
100
119
|
when (text = @ss.scan(/not(?![\w_:])/))
|
101
120
|
action { [:NOT, text] }
|
102
121
|
|
@@ -112,9 +131,6 @@ class PuppetDB::Lexer < Racc::Parser
|
|
112
131
|
when (text = @ss.scan(/false(?![\w_:])/))
|
113
132
|
action { [:BOOLEAN, false]}
|
114
133
|
|
115
|
-
when (text = @ss.scan(/-?\d+\.\d+/))
|
116
|
-
action { [:NUMBER, text.to_f] }
|
117
|
-
|
118
134
|
when (text = @ss.scan(/-?\d+/))
|
119
135
|
action { [:NUMBER, text.to_i] }
|
120
136
|
|
@@ -130,6 +146,9 @@ class PuppetDB::Lexer < Racc::Parser
|
|
130
146
|
when (text = @ss.scan(/@@/))
|
131
147
|
action { [:EXPORTED, text] }
|
132
148
|
|
149
|
+
when (text = @ss.scan(/@/))
|
150
|
+
action { [:AT, text] }
|
151
|
+
|
133
152
|
else
|
134
153
|
text = @ss.string[@ss.pos .. -1]
|
135
154
|
raise ScanError, "can not match: '" + text + "'"
|