tickly 2.1.0 → 2.1.2
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.
- data/Gemfile +0 -1
- data/README.rdoc +30 -16
- data/lib/tickly/evaluator.rb +5 -5
- data/lib/tickly/node_processor.rb +1 -2
- data/lib/tickly/parser.rb +78 -49
- data/lib/tickly.rb +1 -18
- data/test/helper.rb +17 -1
- data/test/test-data/windows_linebreaks.nk +1794 -0
- data/test/test_benchmark.rb +21 -0
- data/test/test_emitter.rb +20 -0
- data/test/test_evaluator.rb +1 -2
- data/test/test_node_processor.rb +2 -1
- data/test/test_parser.rb +34 -9
- data/tickly.gemspec +6 -6
- metadata +7 -20
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
= tickly
|
2
2
|
|
3
|
-
A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts).
|
4
|
-
the passed Nuke scripts into a TCL AST
|
3
|
+
A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts).
|
4
|
+
It transforms the passed Nuke scripts into a TCL AST.
|
5
|
+
It also supports some cheap tricks to discard the nodes you are not interested in, since Nuke
|
6
|
+
scripts easily grow into tens of megabytes.
|
7
|
+
|
8
|
+
The AST format is extremely simple (nested arrays).
|
5
9
|
|
6
10
|
== Plain parsing
|
7
11
|
|
@@ -25,14 +29,18 @@ will always return an Array of expressions, even if you only fed it one expressi
|
|
25
29
|
# Expressions in square brackets
|
26
30
|
p.parse '{exec cmd [fileName]}' #=> [[:c, "exec", "cmd", [:b, "fileName"]]]
|
27
31
|
|
28
|
-
The AST is represented by simple arrays.
|
29
|
-
|
30
|
-
|
31
|
-
elements are guaranteed to be strings.
|
32
|
+
The AST is represented by simple arrays. Each TCL expression becomes an array. An array starting
|
33
|
+
with the :c symbol ("c" for "curlies") is a literal expression in curly braces ({}). An array with the
|
34
|
+
:b symbol at the beginning is an expression with string interpolations (square brackets).
|
35
|
+
All the other array elements are guaranteed to be strings.
|
36
|
+
|
37
|
+
String literals are expanded to string array elements.
|
38
|
+
|
39
|
+
p.parse( '"a string with \"quote"') #=> [['a string with "quote']]
|
32
40
|
|
33
|
-
Multiple expressions separated by
|
41
|
+
Multiple expressions separated by semicolons or newlines will be accumulated as multiple arrays.
|
34
42
|
|
35
|
-
Lots and lots of TCL features are not supported - remember that most Nuke scripts are machine-generated and they do not
|
43
|
+
Lots and lots of TCL features are probably not supported - remember that most Nuke scripts are machine-generated and they do not
|
36
44
|
use most of the esoteric language features.
|
37
45
|
|
38
46
|
== Evaulating nodes in Nuke scripts
|
@@ -42,6 +50,7 @@ are actially arguments for a node constructor written out in TCL. Consider this
|
|
42
50
|
hypothetic SomeNode in your script:
|
43
51
|
|
44
52
|
SomeNode {
|
53
|
+
name SomeNode4
|
45
54
|
someknob 15
|
46
55
|
anotherknob 3
|
47
56
|
animation {curve x1 12 45 67}
|
@@ -49,14 +58,17 @@ hypothetic SomeNode in your script:
|
|
49
58
|
y_pos -10
|
50
59
|
}
|
51
60
|
|
52
|
-
and so on. You can use a NodeProcessor to capture these node constructors right as they are being parsed.
|
61
|
+
and so on. You can use a +NodeProcessor+ to capture these node constructors right as they are being parsed.
|
62
|
+
The advantage of this workflow is that the processor will discard all the nodes you don't need, saving time
|
63
|
+
and memory.
|
53
64
|
|
54
|
-
|
65
|
+
To match nodes you create Ruby classes matching the node classes by name. It doesn't matter if your
|
66
|
+
custom node handler is inside a module since the processor will only use the last part of the name.
|
55
67
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
class SomeNode
|
68
|
+
For example, to capture every +SomeNode+ in your script:
|
69
|
+
|
70
|
+
# Remember, only the last part of the class name matters
|
71
|
+
class MyAwesomeDirtyScript::SomeNode
|
60
72
|
attr_reader :knobs
|
61
73
|
def initialize(string_keyed_knobs_hash)
|
62
74
|
@knobs = string_keyed_knobs_hash
|
@@ -67,8 +79,9 @@ of ours:
|
|
67
79
|
e = Tickly::NodeProcessor.new
|
68
80
|
|
69
81
|
# Add the class
|
70
|
-
e.add_node_handler_class
|
82
|
+
e.add_node_handler_class SomeNode
|
71
83
|
|
84
|
+
# Open the ginormous Nuke script
|
72
85
|
file = File.open("/mnt/raid/nuke/scripts/HugeShot_123.nk")
|
73
86
|
|
74
87
|
e.parse(file) do | every_some_node |
|
@@ -78,7 +91,8 @@ of ours:
|
|
78
91
|
...
|
79
92
|
end
|
80
93
|
|
81
|
-
|
94
|
+
Of course you can capture multiple node classes. This is how Tracksperanto parses various
|
95
|
+
nodes containing tracking data:
|
82
96
|
|
83
97
|
parser = Tickly::NodeProcessor.new
|
84
98
|
parser.add_node_handler_class(Tracker3)
|
data/lib/tickly/evaluator.rb
CHANGED
@@ -32,7 +32,7 @@ module Tickly
|
|
32
32
|
# end
|
33
33
|
class Evaluator
|
34
34
|
def initialize
|
35
|
-
@node_handlers =
|
35
|
+
@node_handlers = {}
|
36
36
|
end
|
37
37
|
|
38
38
|
# Add a Class object that can instantiate node handlers. The last part of the class name
|
@@ -40,7 +40,7 @@ module Tickly
|
|
40
40
|
# For example, to capture Tracker3 nodes a name like this will do:
|
41
41
|
# Whatever::YourModule::Better::Tracker3
|
42
42
|
def add_node_handler_class(handler_class)
|
43
|
-
@node_handlers
|
43
|
+
@node_handlers[class_name_without_modules(handler_class)] = handler_class
|
44
44
|
end
|
45
45
|
|
46
46
|
# Evaluates a single Nuke TCL command, and if it is a node constructor
|
@@ -50,7 +50,7 @@ module Tickly
|
|
50
50
|
# (it's more of a pattern matcher really)
|
51
51
|
def evaluate(expr)
|
52
52
|
if will_capture?(expr)
|
53
|
-
handler_class = @node_handlers
|
53
|
+
handler_class = @node_handlers[expr[0]]
|
54
54
|
handler_arguments = expr[1]
|
55
55
|
hash_of_args = {}
|
56
56
|
# Use 1..-1 to skip the curly brace symbol
|
@@ -81,10 +81,10 @@ module Tickly
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def has_handler?(expr)
|
84
|
-
@node_handlers.
|
84
|
+
@node_handlers.has_key? expr[0]
|
85
85
|
end
|
86
86
|
|
87
|
-
def
|
87
|
+
def class_name_without_modules(some_module)
|
88
88
|
some_module.to_s.split('::').pop
|
89
89
|
end
|
90
90
|
|
@@ -24,7 +24,6 @@ module Tickly
|
|
24
24
|
# e.add_node_handler_class Blur
|
25
25
|
# e.parse(File.open("/path/to/script.nk")) do | blur_node |
|
26
26
|
# # do whatever you want to the node instance
|
27
|
-
# end
|
28
27
|
# end
|
29
28
|
class NodeProcessor
|
30
29
|
def initialize
|
@@ -78,4 +77,4 @@ module Tickly
|
|
78
77
|
return nil
|
79
78
|
end
|
80
79
|
end
|
81
|
-
end
|
80
|
+
end
|
data/lib/tickly/parser.rb
CHANGED
@@ -5,6 +5,10 @@ module Tickly
|
|
5
5
|
# Simplistic, incomplete and most likely incorrect TCL parser
|
6
6
|
class Parser
|
7
7
|
|
8
|
+
# Gets raised on invalid input
|
9
|
+
class Error < RuntimeError
|
10
|
+
end
|
11
|
+
|
8
12
|
# Parses a piece of TCL and returns it converted into internal expression
|
9
13
|
# structures. A basic TCL expression is just an array of Strings. An expression
|
10
14
|
# in curly braces will have the symbol :c tacked onto the beginning of the array.
|
@@ -19,10 +23,10 @@ module Tickly
|
|
19
23
|
def parse(io_or_str)
|
20
24
|
bare_io = io_or_str.respond_to?(:read) ? io_or_str : StringIO.new(io_or_str)
|
21
25
|
# Wrap the IO in a Bychar buffer to read faster
|
22
|
-
reader = Bychar.wrap(bare_io)
|
26
|
+
reader = R.new(Bychar.wrap(bare_io))
|
23
27
|
# Use multiple_expressions = true so that the top-level parsed script is always an array
|
24
28
|
# of expressions
|
25
|
-
|
29
|
+
parse_expr(reader, stop_char = nil, stack_depth = 0, multiple_expressions = true)
|
26
30
|
end
|
27
31
|
|
28
32
|
# Override this to remove any unneeded subexpressions.
|
@@ -37,13 +41,30 @@ module Tickly
|
|
37
41
|
|
38
42
|
private
|
39
43
|
|
40
|
-
LAST_CHAR = -1..-1 # If we were 1.9 only we could use -1
|
41
44
|
TERMINATORS = ["\n", ";"]
|
42
45
|
ESC = 92.chr # Backslash (\)
|
46
|
+
QUOTES = %w( " ' )
|
47
|
+
|
48
|
+
# TODO: this has to go into Bychar. We should not use exprs for flow control.
|
49
|
+
class R #:nodoc: :all
|
50
|
+
def initialize(bychar)
|
51
|
+
@bychar = bychar
|
52
|
+
end
|
53
|
+
|
54
|
+
def read_one_char
|
55
|
+
begin
|
56
|
+
c = @bychar.read_one_char!
|
57
|
+
rescue Bychar::EOF
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
43
62
|
|
44
63
|
# Package the expressions, stack and buffer.
|
45
|
-
# We use a special flag to tell us whether we need multuple expressions
|
46
|
-
#
|
64
|
+
# We use a special flag to tell us whether we need multuple expressions.
|
65
|
+
# If we do, the expressions will be returned. If not, just the stack.
|
66
|
+
# Also, anything that remains on the stack will be put on the expressions
|
67
|
+
# list if multiple_expressions is true.
|
47
68
|
def wrap_up(expressions, stack, buf, stack_depth, multiple_expressions)
|
48
69
|
stack << buf if (buf.length > 0)
|
49
70
|
return stack unless multiple_expressions
|
@@ -53,21 +74,35 @@ module Tickly
|
|
53
74
|
return expressions
|
54
75
|
end
|
55
76
|
|
77
|
+
# If the passed buf contains any bytes, put them on the stack and
|
78
|
+
# empty the buffer
|
79
|
+
def consume_remaining_buffer(stack, buf)
|
80
|
+
return if buf.length == 0
|
81
|
+
stack << buf.dup
|
82
|
+
buf.replace('')
|
83
|
+
end
|
84
|
+
|
56
85
|
# Parse from a passed IO object either until an unescaped stop_char is reached
|
57
86
|
# or until the IO is exhausted. The last argument is the class used to
|
58
87
|
# compose the subexpression being parsed. The subparser is reentrant and not
|
59
88
|
# destructive for the object containing it.
|
60
|
-
def
|
89
|
+
def parse_expr(io, stop_char = nil, stack_depth = 0, multiple_expressions = false)
|
61
90
|
# A standard stack is an expression that does not evaluate to a string
|
62
91
|
expressions = []
|
63
92
|
stack = []
|
64
93
|
buf = ''
|
65
|
-
last_char_was_linebreak = false
|
66
94
|
|
67
|
-
|
68
|
-
char = io.read_one_char
|
95
|
+
loop do
|
96
|
+
char = io.read_one_char
|
69
97
|
|
70
|
-
|
98
|
+
# Ignore carriage returns
|
99
|
+
next if char == "\r"
|
100
|
+
|
101
|
+
if stop_char && char.nil?
|
102
|
+
raise Error, "IO ran out when parsing a subexpression (expected to end on #{stop_char.inspect})"
|
103
|
+
elsif char == stop_char # Bail out of a subexpr or bail out on nil
|
104
|
+
# TODO: default stop_char is nil, and this is also what gets returned from a depleted
|
105
|
+
# IO on IO#read(). We should do that in Bychar.
|
71
106
|
# Handle any remaining subexpressions
|
72
107
|
return wrap_up(expressions, stack, buf, stack_depth, multiple_expressions)
|
73
108
|
elsif char == " " || char == "\n" # Space
|
@@ -75,68 +110,62 @@ module Tickly
|
|
75
110
|
stack << buf
|
76
111
|
buf = ''
|
77
112
|
end
|
78
|
-
if TERMINATORS.include?(char) && stack.any?
|
79
|
-
|
80
|
-
#
|
113
|
+
if TERMINATORS.include?(char) && stack.any? # Introduce a stack separator! This is a new line
|
114
|
+
|
115
|
+
# First get rid of the remaining buffer data
|
116
|
+
consume_remaining_buffer(stack, buf)
|
117
|
+
|
118
|
+
# Since we now finished an expression and it is on the stack,
|
119
|
+
# we can run this expression through the filter
|
81
120
|
filtered_expr = compact_subexpr(stack, stack_depth + 1)
|
82
|
-
stack = []
|
83
121
|
|
84
122
|
# Only preserve the parsed expression if it's not nil
|
85
123
|
expressions << filtered_expr unless filtered_expr.nil?
|
86
124
|
|
87
|
-
|
125
|
+
# Reset the stack for the next expression
|
126
|
+
stack = []
|
127
|
+
|
128
|
+
# Note that we will return multiple expressions instead of one
|
88
129
|
multiple_expressions = true
|
89
|
-
#puts "Next expression! #{expressions.inspect} #{stack.inspect} #{buf.inspect}"
|
90
|
-
else
|
91
|
-
last_char_was_linebreak = false
|
92
130
|
end
|
93
131
|
elsif char == '[' # Opens a new string expression
|
94
|
-
stack
|
95
|
-
stack << [:b] +
|
132
|
+
consume_remaining_buffer(stack, buf)
|
133
|
+
stack << [:b] + parse_expr(io, ']', stack_depth + 1)
|
96
134
|
elsif char == '{' # Opens a new literal expression
|
97
|
-
stack
|
98
|
-
stack << [:c] +
|
99
|
-
elsif char
|
100
|
-
stack
|
101
|
-
stack << parse_str(io,
|
102
|
-
elsif char == "'"
|
103
|
-
stack << buf if (buf.length > 0)
|
104
|
-
stack << parse_str(io, "'")
|
135
|
+
consume_remaining_buffer(stack, buf)
|
136
|
+
stack << [:c] + parse_expr(io, '}', stack_depth + 1)
|
137
|
+
elsif QUOTES.include?(char) # String
|
138
|
+
consume_remaining_buffer(stack, buf)
|
139
|
+
stack << parse_str(io, char)
|
105
140
|
else
|
106
141
|
buf << char
|
107
142
|
end
|
108
143
|
end
|
109
144
|
|
110
|
-
|
111
|
-
end
|
112
|
-
|
113
|
-
def chomp!(stack)
|
114
|
-
stack.delete_at(-1) if stack.any? && stack[-1].nil?
|
115
|
-
end
|
116
|
-
|
117
|
-
def no_eof(&blk)
|
118
|
-
begin
|
119
|
-
loop(&blk)
|
120
|
-
rescue Bychar::EOF
|
121
|
-
end
|
145
|
+
raise Error, "Should never happen"
|
122
146
|
end
|
123
147
|
|
124
|
-
|
148
|
+
# Parse a string literal, in single or double quotes.
|
149
|
+
def parse_str(io, stop_quote)
|
125
150
|
buf = ''
|
126
|
-
|
127
|
-
c = io.read_one_char
|
128
|
-
if c
|
129
|
-
|
130
|
-
elsif buf
|
131
|
-
|
151
|
+
loop do
|
152
|
+
c = io.read_one_char
|
153
|
+
if c.nil?
|
154
|
+
raise Error, "The IO ran out before the end of a literal string"
|
155
|
+
elsif buf.length > 0 && last_char(buf) == ESC # If this char was escaped
|
156
|
+
# Trim the escape character at the end of the buffer
|
157
|
+
buf = buf[0..-2]
|
132
158
|
buf << c
|
159
|
+
elsif c == stop_quote
|
160
|
+
return buf
|
133
161
|
else
|
134
162
|
buf << c
|
135
163
|
end
|
136
164
|
end
|
137
|
-
|
138
|
-
return buf
|
139
165
|
end
|
140
166
|
|
167
|
+
def last_char(str)
|
168
|
+
RUBY_VERSION < '1.9' ? str[-1].chr : str[-1]
|
169
|
+
end
|
141
170
|
end
|
142
171
|
end
|
data/lib/tickly.rb
CHANGED
@@ -4,22 +4,5 @@ require File.dirname(__FILE__) + "/tickly/curve"
|
|
4
4
|
require File.dirname(__FILE__) + "/tickly/node_processor"
|
5
5
|
|
6
6
|
module Tickly
|
7
|
-
VERSION = '2.1.
|
8
|
-
|
9
|
-
# Provides the methods for quickly emitting the expression arrays,
|
10
|
-
# is used in tests
|
11
|
-
module Emitter #:nodoc :all
|
12
|
-
def le(*elems)
|
13
|
-
[:c] + elems
|
14
|
-
end
|
15
|
-
|
16
|
-
def e(*elems)
|
17
|
-
elems
|
18
|
-
end
|
19
|
-
|
20
|
-
def se(*elems)
|
21
|
-
[:b] + elems
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
7
|
+
VERSION = '2.1.2'
|
25
8
|
end
|
data/test/helper.rb
CHANGED
@@ -8,11 +8,27 @@ rescue Bundler::BundlerError => e
|
|
8
8
|
exit e.status_code
|
9
9
|
end
|
10
10
|
require 'test/unit'
|
11
|
-
require 'shoulda'
|
12
11
|
|
13
12
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
13
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
14
|
require 'tickly'
|
16
15
|
|
17
16
|
class Test::Unit::TestCase
|
17
|
+
# Provides the methods for quickly emitting the expression arrays,
|
18
|
+
# is used in tests
|
19
|
+
module Emitter #:nodoc :all
|
20
|
+
def le(*elems)
|
21
|
+
e(*elems).unshift :c
|
22
|
+
end
|
23
|
+
|
24
|
+
def e(*elems)
|
25
|
+
elems
|
26
|
+
end
|
27
|
+
|
28
|
+
def se(*elems)
|
29
|
+
e(*elems).unshift :b
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
include Emitter
|
18
34
|
end
|