tickly 0.0.4 → 0.0.5
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 +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:
|