tickly 2.1.0 → 2.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|