tickly 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.rdoc +64 -1
- data/lib/tickly.rb +7 -2
- data/lib/tickly/evaluator.rb +16 -1
- data/lib/tickly/parser.rb +16 -9
- data/test/test_evaluator.rb +17 -0
- data/tickly.gemspec +5 -2
- metadata +19 -3
data/Gemfile
CHANGED
data/README.rdoc
CHANGED
@@ -1,6 +1,69 @@
|
|
1
1
|
= tickly
|
2
2
|
|
3
|
-
A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts)
|
3
|
+
A highly simplistic TCL parser and evaluator (primarily designed for parsing Nuke scripts). It structures
|
4
|
+
the passed Nuke scripts into a TCL AST and return it. You can use some cheap tricks to discard the nodes you are not interested in.
|
5
|
+
|
6
|
+
== Parsing
|
7
|
+
|
8
|
+
Create a Parser object and pass TCL expressions/scripts to it. You can pass IO obejcts or strings. For example:
|
9
|
+
|
10
|
+
p = Tickly::Parser.new
|
11
|
+
|
12
|
+
# One expression
|
13
|
+
p.parse '2' #=> ["2"]
|
14
|
+
|
15
|
+
# TCL command
|
16
|
+
p.parse "tail $list" #=> ["tail", "$list"]
|
17
|
+
|
18
|
+
# Multiple expressions
|
19
|
+
p.parse "2\n2" #=> [["2", "2"]]
|
20
|
+
|
21
|
+
# Expressions in curly braces
|
22
|
+
p.parse '{2 2}' #=> [[:c, "2", "2"]]
|
23
|
+
|
24
|
+
# Expressions in square brackets
|
25
|
+
p.parse '{exec cmd [fileName]}' #=> [[:c, "exec", "cmd", [:b, "fileName"]]]
|
26
|
+
|
27
|
+
The AST is represented by simple arrays. An array is a TCL expression. An array with the :c symbol at the beginning
|
28
|
+
element is an expression in curly braces. An array with the :b symbol at the beginning represents an expression with
|
29
|
+
string interpolations in it. If you are curious, :c stands for "curlies" and :b for "brackets". All the other array
|
30
|
+
elements are guaranteed to be strings.
|
31
|
+
|
32
|
+
Multiple expressions separated by ; or a newline will be accumulated as multiple arrays.
|
33
|
+
|
34
|
+
Lots and lots of TCL features are not supported - remember that most Nuke scripts are machine-generated and they do not
|
35
|
+
use most of the esoteric language features.
|
36
|
+
|
37
|
+
== Evaulating
|
38
|
+
|
39
|
+
Nuke uses the following syntax for it's nodes:
|
40
|
+
|
41
|
+
SomeNode {
|
42
|
+
someknob 15
|
43
|
+
anotherknob 3
|
44
|
+
andanother {curve x1 12 45 67}
|
45
|
+
}
|
46
|
+
|
47
|
+
and so on. You can use a simple Evaluator object to run through the nodes returned by the parser. To set up
|
48
|
+
the evaulation you need to create classes matching the node classes by name. For example, a Blur class:
|
49
|
+
|
50
|
+
class Blur
|
51
|
+
def initialize(string_keyed_knobs_hash)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
e = Tickly::Evaluator.new
|
56
|
+
e.add_node_handler_class Blur
|
57
|
+
|
58
|
+
some_script_expressions = Tickly::Parser.new.parse(File.open("/mnt/raid/nuke/scripts/HugeShot_123.nk"))
|
59
|
+
|
60
|
+
some_script_expressions.each do | expression_in_ast |
|
61
|
+
# Everytime a Blur node is found in the script it will be instantiated,
|
62
|
+
# and the knobs of the node will be passed to the constructor that you define
|
63
|
+
e.evaluate(expression_in_ast) do | blur_node |
|
64
|
+
# Now a Blur node instance is all yours
|
65
|
+
end
|
66
|
+
end
|
4
67
|
|
5
68
|
== Contributing to tickly
|
6
69
|
|
data/lib/tickly.rb
CHANGED
@@ -4,9 +4,10 @@ require File.dirname(__FILE__) + "/tickly/evaluator"
|
|
4
4
|
require 'forwardable'
|
5
5
|
|
6
6
|
module Tickly
|
7
|
-
VERSION = '0.0.
|
7
|
+
VERSION = '0.0.5'
|
8
8
|
|
9
|
-
# Provides the methods for quickly emitting the
|
9
|
+
# Provides the methods for quickly emitting the expression arrays,
|
10
|
+
# is used in tests
|
10
11
|
module Emitter
|
11
12
|
def le(*elems)
|
12
13
|
[:c] + elems
|
@@ -21,11 +22,15 @@ module Tickly
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
25
|
+
# Converts a passed Array (received from a Parser)
|
26
|
+
# into a TCL expression. This is only ever used in tests
|
24
27
|
def self.to_tcl(e)
|
25
28
|
if e.is_a?(Array) && e[0] == :c
|
26
29
|
'{%s}' % e.map{|e| to_tcl(e)}.join(' ')
|
27
30
|
elsif e.is_a?(Array) && e[0] == :b
|
28
31
|
'[%s]' % e.map{|e| to_tcl(e)}.join(' ')
|
32
|
+
elsif e.is_a?(Array)
|
33
|
+
e.map{|e| to_tcl(e)}.join(" ")
|
29
34
|
elsif e.is_a?(String) && (e.include?('"') || e.include?("'"))
|
30
35
|
e.inspect
|
31
36
|
else
|
data/lib/tickly/evaluator.rb
CHANGED
@@ -9,10 +9,19 @@ module Tickly
|
|
9
9
|
@node_handlers = []
|
10
10
|
end
|
11
11
|
|
12
|
+
# Add a Class object that can instantiate node handlers. The last part of the class name
|
13
|
+
# has to match the name of the Nuke node that you want to capture.
|
14
|
+
# For example, to capture Tracker3 nodes a name like this will do:
|
15
|
+
# Whatever::YourModule::Better::Tracker3
|
12
16
|
def add_node_handler_class(handler_class)
|
13
17
|
@node_handlers << handler_class
|
14
18
|
end
|
15
19
|
|
20
|
+
# Evaluates a single Nuke TCL command, and if it is a node constructor
|
21
|
+
# and a class with a corresponding name has been added using add_node_handler_class
|
22
|
+
# the class will be instantiated and yielded to the block. The instance will also be returned
|
23
|
+
# at the end of the method. This method evaluates one expression at a time
|
24
|
+
# (it's more of a pattern matcher really)
|
16
25
|
def evaluate(expr)
|
17
26
|
if multiple_atoms?(expr) && has_subcommand?(expr) && has_handler?(expr)
|
18
27
|
handler_class = @node_handlers.find{|e| unconst_name(e) == expr[0]}
|
@@ -25,10 +34,16 @@ module Tickly
|
|
25
34
|
end
|
26
35
|
|
27
36
|
# Instantiate the handler with the options
|
28
|
-
handler_class.new(hash_of_args)
|
37
|
+
handler_instance = handler_class.new(hash_of_args)
|
38
|
+
|
39
|
+
# Both return and yield it
|
40
|
+
yield handler_instance if block_given?
|
41
|
+
handler_instance
|
29
42
|
end
|
30
43
|
end
|
31
44
|
|
45
|
+
private
|
46
|
+
|
32
47
|
def multiple_atoms?(expr)
|
33
48
|
expr.length > 1
|
34
49
|
end
|
data/lib/tickly/parser.rb
CHANGED
@@ -1,23 +1,32 @@
|
|
1
1
|
require 'stringio'
|
2
|
+
require 'bychar'
|
2
3
|
|
3
4
|
module Tickly
|
4
5
|
|
6
|
+
# Simplistic, incomplete and most likely incorrect TCL parser
|
5
7
|
class Parser
|
6
8
|
|
7
9
|
# Parses a piece of TCL and returns it converted into internal expression
|
8
|
-
# structures
|
10
|
+
# structures. A basic TCL expression is just an array of Strings. An expression
|
11
|
+
# in curly braces will have the symbol :c tacked onto the beginning of the array.
|
12
|
+
# An expression in square braces will have :b at the beginning.
|
9
13
|
def parse(io_or_str)
|
10
|
-
|
11
|
-
|
14
|
+
bare_io = io_or_str.respond_to?(:read) ? io_or_str : StringIO.new(io_or_str)
|
15
|
+
# Wrap the IO in a Bychar buffer to read faster
|
16
|
+
reader = Bychar::Reader.new(bare_io)
|
17
|
+
sub_parse(reader)
|
12
18
|
end
|
13
19
|
|
14
|
-
# Override this to remove any unneeded subexpressions
|
20
|
+
# Override this to remove any unneeded subexpressions Modify the passed expr
|
21
|
+
# array in-place.
|
15
22
|
def expand_subexpr!(expr, at_depth)
|
16
23
|
end
|
17
24
|
|
18
25
|
private
|
19
26
|
|
20
27
|
LAST_CHAR = -1..-1 # If we were 1.9 only we could use -1
|
28
|
+
TERMINATORS = ["\n", ";"]
|
29
|
+
ESC = 92.chr # Backslash (\)
|
21
30
|
|
22
31
|
# Parse from a passed IO object either until an unescaped stop_char is reached
|
23
32
|
# or until the IO is exhausted. The last argument is the class used to
|
@@ -29,7 +38,7 @@ module Tickly
|
|
29
38
|
buf = ''
|
30
39
|
last_char_was_linebreak = false
|
31
40
|
until io.eof?
|
32
|
-
char = io.
|
41
|
+
char = io.read_one_byte
|
33
42
|
|
34
43
|
if buf[LAST_CHAR] != ESC
|
35
44
|
if char == stop_char # Bail out of a subexpr
|
@@ -42,7 +51,7 @@ module Tickly
|
|
42
51
|
stack << buf
|
43
52
|
buf = ''
|
44
53
|
end
|
45
|
-
if char
|
54
|
+
if TERMINATORS.include?(char) # Introduce a stack separator! This is a new line
|
46
55
|
if stack.any? && !last_char_was_linebreak
|
47
56
|
last_char_was_linebreak = true
|
48
57
|
stack = handle_expr_terminator(stack, stack_depth)
|
@@ -87,8 +96,6 @@ module Tickly
|
|
87
96
|
return stack
|
88
97
|
end
|
89
98
|
|
90
|
-
ESC = 92.chr # Backslash (\)
|
91
|
-
|
92
99
|
def chomp!(stack)
|
93
100
|
stack.delete_at(-1) if stack.any? && stack[-1].nil?
|
94
101
|
end
|
@@ -118,7 +125,7 @@ module Tickly
|
|
118
125
|
def parse_str(io, stop_char)
|
119
126
|
buf = ''
|
120
127
|
until io.eof?
|
121
|
-
c = io.
|
128
|
+
c = io.read_one_byte
|
122
129
|
if c == stop_char && buf[LAST_CHAR] != ESC
|
123
130
|
return buf
|
124
131
|
elsif buf[LAST_CHAR] == ESC # Eat out the escape char
|
data/test/test_evaluator.rb
CHANGED
@@ -39,5 +39,22 @@ class TestEvaluator < Test::Unit::TestCase
|
|
39
39
|
ref_o = {"foo" => "bar", "baz" => "bad"}
|
40
40
|
assert_equal ref_o, node.options
|
41
41
|
end
|
42
|
+
|
43
|
+
class TargetError < RuntimeError
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_yields_the_handler_instance
|
47
|
+
stack = e("SomeNode", le(e("foo", "bar"), e("baz", "bad")))
|
48
|
+
e = Tickly::Evaluator.new
|
49
|
+
e.add_node_handler_class(SomeNode)
|
50
|
+
ref_o = {"foo" => "bar", "baz" => "bad"}
|
51
|
+
|
52
|
+
assert_raise(TargetError) do
|
53
|
+
e.evaluate(stack) do | some_node |
|
54
|
+
assert_kind_of SomeNode, some_node
|
55
|
+
raise TargetError
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
42
59
|
|
43
60
|
end
|
data/tickly.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "tickly"
|
8
|
-
s.version = "0.0.
|
8
|
+
s.version = "0.0.5"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Julik Tarkhanov"]
|
12
|
-
s.date = "2013-03-
|
12
|
+
s.date = "2013-03-15"
|
13
13
|
s.description = "Parses the subset of the TCL grammar needed for Nuke scripts"
|
14
14
|
s.email = "me@julik.nl"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -52,17 +52,20 @@ Gem::Specification.new do |s|
|
|
52
52
|
s.specification_version = 3
|
53
53
|
|
54
54
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
55
|
+
s.add_runtime_dependency(%q<bychar>, ["~> 1.0"])
|
55
56
|
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
56
57
|
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
57
58
|
s.add_development_dependency(%q<bundler>, [">= 0"])
|
58
59
|
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
59
60
|
else
|
61
|
+
s.add_dependency(%q<bychar>, ["~> 1.0"])
|
60
62
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
61
63
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
62
64
|
s.add_dependency(%q<bundler>, [">= 0"])
|
63
65
|
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
64
66
|
end
|
65
67
|
else
|
68
|
+
s.add_dependency(%q<bychar>, ["~> 1.0"])
|
66
69
|
s.add_dependency(%q<shoulda>, [">= 0"])
|
67
70
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
71
|
s.add_dependency(%q<bundler>, [">= 0"])
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tickly
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,8 +9,24 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bychar
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.0'
|
14
30
|
- !ruby/object:Gem::Dependency
|
15
31
|
name: shoulda
|
16
32
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,7 +138,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
122
138
|
version: '0'
|
123
139
|
segments:
|
124
140
|
- 0
|
125
|
-
hash: -
|
141
|
+
hash: -4354134908046767929
|
126
142
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
143
|
none: false
|
128
144
|
requirements:
|