automateit 0.71102 → 0.71103
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +1 -2
- data/CHANGES.txt +14 -0
- data/Rakefile +27 -37
- data/bin/aissh +93 -0
- data/bin/aitag +7 -0
- data/lib/automateit.rb +2 -0
- data/lib/automateit/account_manager/etc.rb +3 -3
- data/lib/automateit/account_manager/nscd.rb +14 -11
- data/lib/automateit/address_manager/base.rb +2 -2
- data/lib/automateit/interpreter.rb +2 -19
- data/lib/automateit/package_manager.rb +2 -0
- data/lib/automateit/plugin/driver.rb +1 -1
- data/lib/automateit/plugin/manager.rb +1 -6
- data/lib/automateit/root.rb +1 -1
- data/lib/automateit/tag_manager/tag_parser.rb +14 -17
- data/lib/ext/shell_escape.rb +7 -0
- data/lib/nitpick.rb +29 -0
- data/spec/integration/account_manager_nscd_spec.rb +37 -0
- data/spec/integration/account_manager_spec.rb +39 -4
- data/spec/integration/shell_manager_spec.rb +135 -3
- data/spec/unit/address_manager_spec.rb +49 -0
- data/spec/unit/interpreter_spec.rb +32 -1
- data/spec/unit/nested_error_spec.rb +10 -0
- data/spec/unit/object_spec.rb +18 -0
- data/spec/unit/plugins_spec.rb +113 -0
- data/spec/unit/shell_escape_spec.rb +11 -0
- data/spec/unit/tag_manager_spec.rb +60 -15
- metadata +16 -3
- metadata.gz.sig +1 -2
- data/lib/automateit.rb.orig +0 -65
data.tar.gz.sig
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
|
2
|
-
�Z���3Ǩ��1P��{��q5��ʚ��s�NN"v����>`�w�p.�F����ر~�z0����#j
|
1
|
+
V]�2��0��Y����� L�������%^6&]�Ǎ���k�կY��
|
data/CHANGES.txt
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
0.71103:
|
2
|
+
Date: Sat, 03 Nov 2007 01:26:13 -0700
|
3
|
+
Desc:
|
4
|
+
- (+) Added "aissh" command, provides an easy way to run commands on a group of hosts. For example, see if there's a Rails process running on all hosts matching the "rails_servers" tag: "aissh -p . rails_servers 'ps -ef | grep rails'"
|
5
|
+
- Improved PackageManager, it can now accept package names as Symbols.
|
6
|
+
- Improved AccountManager spec with additional examples for checking password changes.
|
7
|
+
- Improved AccountManager::NSCD with refactoring and additional tests.
|
8
|
+
- Improved AddressManager spec with additional tests for address conversion routines and code generation helpers.
|
9
|
+
- Improved Plugin spec with more checks against invalid managers and drivers, and unavailable dependencies.
|
10
|
+
- Improved ShellManager spec with more checks against hard links and symbolic links; for #cp when contents, modes or ownership changes; for #rm with :force option: for #umask with and without block, and with caught exceptions.
|
11
|
+
- Added spec for String#shell_escape.
|
12
|
+
- Added spec for Object's extensions for #unique_methods and #args_and_opts.
|
13
|
+
- Added spec for NestedError.
|
14
|
+
|
1
15
|
0.71102:
|
2
16
|
Date: Fri, 02 Nov 2007 02:59:41 -0700
|
3
17
|
Desc:
|
data/Rakefile
CHANGED
@@ -82,50 +82,35 @@ class Numeric
|
|
82
82
|
def commify() (s=self.to_s;x=s.length;s).rjust(x+(3-(x%3))).gsub(/(\d)(?=\d{3}+(\.\d*)?$)/,'\1,').strip end
|
83
83
|
end
|
84
84
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
task :loclines do
|
90
|
-
require 'find'
|
91
|
-
lines = 0
|
92
|
-
bytes = 0
|
93
|
-
Find.find(*%w(bin lib spec Rakefile ../web/Rakefile ../web/src )) do |path|
|
94
|
-
Find.prune if path.match(/.*(\b(.hg|.svn|CVS)\b|(.sw.?|.pyc)$)/)
|
95
|
-
next if File.directory?(path)
|
96
|
-
if path.match(/(\bbin\b|.*\.(env|pl|py|rb|rake|java|sql|ftl|jsp|xml|properties|css|rcss|html|rhtml|erb|po|haml|sass)$)/)
|
97
|
-
data = File.read(path)
|
98
|
-
bytes += data.size
|
99
|
-
### lines += data.scan(/^.*\w.*$/).size # Skip spaces
|
100
|
-
lines += data.scan(/^\s*(?!#)\w.*$/).size # Skip blanks and comments
|
101
|
-
end
|
85
|
+
namespace :loc do
|
86
|
+
desc "Display lines of code using loccount"
|
87
|
+
task :count do
|
88
|
+
sh "loccount bin/* lib/ spec/ examples/ *.rake"
|
102
89
|
end
|
103
|
-
puts "Lines: "+lines.commify
|
104
|
-
puts "Bytes: "+bytes.commify
|
105
|
-
end
|
106
90
|
|
107
|
-
|
108
|
-
|
109
|
-
|
91
|
+
desc "Display the lines of code changed in the repository"
|
92
|
+
task :diff do
|
93
|
+
if File.directory?(".hg")
|
94
|
+
puts "%s lines added and removed through SCM operations" % `hg log --patch`.scan(/^[+-][^+-].+/).size.commify
|
95
|
+
else
|
96
|
+
raise NotImplementedError.new("Sorry, this only works for a Mercurial checkout")
|
97
|
+
end
|
98
|
+
end
|
110
99
|
|
111
|
-
desc "Display
|
112
|
-
task :
|
113
|
-
|
114
|
-
puts "%s lines
|
115
|
-
else
|
116
|
-
raise NotImplementedError.new("Sorry, this only works for a Mercurial checkout")
|
100
|
+
desc "Display lines of churn"
|
101
|
+
task :churn do
|
102
|
+
require 'active_support'
|
103
|
+
puts "%s lines of Hg churn" % (`hg churn`.scan(/^[^\s]+\s+(\d+)\s/).flatten.map(&:to_i).sum).commify
|
117
104
|
end
|
118
|
-
end
|
119
105
|
|
120
|
-
desc "Display lines of
|
121
|
-
task :
|
122
|
-
|
123
|
-
|
106
|
+
desc "Display lines of code based on sloccount"
|
107
|
+
task :sloc do
|
108
|
+
sh "sloccount lib spec misc examples bin"
|
109
|
+
end
|
124
110
|
end
|
125
111
|
|
126
|
-
|
127
|
-
|
128
|
-
end
|
112
|
+
desc "Display the lines of source code and how many lines were changed in the repository"
|
113
|
+
task :loc => ["loc:count", "loc:diff", "loc:churn", "loc:sloc"]
|
129
114
|
|
130
115
|
#---[ RubyGems ]--------------------------------------------------------
|
131
116
|
|
@@ -172,22 +157,26 @@ namespace :gem do
|
|
172
157
|
end
|
173
158
|
end
|
174
159
|
|
160
|
+
desc "Create a gem"
|
175
161
|
task :gem do
|
176
162
|
hoe(:gem)
|
177
163
|
end
|
178
164
|
|
165
|
+
desc "Publish to RubyForge"
|
179
166
|
task :publish do
|
180
167
|
automateit
|
181
168
|
hoe("release VERSION=#{AutomateIt::VERSION}")
|
182
169
|
Rake::Task[:after].invoke
|
183
170
|
end
|
184
171
|
|
172
|
+
desc "Tag a stable release"
|
185
173
|
task :tag do
|
186
174
|
automateit
|
187
175
|
sh "hg tag #{AutomateIt::VERSION}"
|
188
176
|
sh "hg tag -f stable"
|
189
177
|
end
|
190
178
|
|
179
|
+
desc "Push a stable release"
|
191
180
|
task :push do
|
192
181
|
sh "hg push -r stable ../app_stable"
|
193
182
|
end
|
@@ -233,6 +222,7 @@ namespace :install do
|
|
233
222
|
end
|
234
223
|
end
|
235
224
|
|
225
|
+
desc "Uninstall automateit gem"
|
236
226
|
task :uninstall do
|
237
227
|
automateit.package_manager.uninstall "automateit", :with => :gem
|
238
228
|
end
|
data/bin/aissh
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# XXX What can go wrong with this loading approach?
|
4
|
+
libdir = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
5
|
+
if File.directory?(libdir) and File.exists?(File.join(libdir, "automateit.rb"))
|
6
|
+
$LOAD_PATH.unshift(libdir)
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
require 'optparse'
|
11
|
+
require 'automateit'
|
12
|
+
include AutomateIt::Constants
|
13
|
+
|
14
|
+
OptionParser.new do |parser|
|
15
|
+
PROG = File.basename($0)
|
16
|
+
opts = {}
|
17
|
+
parser.banner = <<EOB
|
18
|
+
#{PROG} - tool for running command via SSH on hosts matching AutomateIt tags
|
19
|
+
|
20
|
+
Usage: #{PROG} [options] [arguments...]
|
21
|
+
|
22
|
+
IMPORTANT:
|
23
|
+
#{PROG} can only match against tags specified in tags.yml. It cannot
|
24
|
+
match against automatically generated tags like the OS, architecture and such
|
25
|
+
which are created at runtime by PlatformManager or AddressManager. So if you
|
26
|
+
want to run a command against all Ubuntu servers, you must define them
|
27
|
+
explicitly in tags.yml.
|
28
|
+
|
29
|
+
Examples:
|
30
|
+
# Read tags from project in current directory and run "ls" on all hosts
|
31
|
+
# tagged with "apache_servers" or "rails_servers":
|
32
|
+
#{PROG} --project . 'apache_servers | rails_servers' ls
|
33
|
+
|
34
|
+
# Preview SSH commands needed to run "ls" on "apache_servers":
|
35
|
+
#{PROG} -p . -n apache_servers ls
|
36
|
+
|
37
|
+
# Pass arguments to "ls", rather than #{PROG}:
|
38
|
+
#{PROG} -p . -n apache_servers -- ls -la
|
39
|
+
|
40
|
+
# Same as previous but using a quoted string:
|
41
|
+
#{PROG} -p . -n apache_servers 'ls -la'
|
42
|
+
|
43
|
+
# Execute a command which requires the target server to redirect output:
|
44
|
+
#{PROG} -p . -n apache_servers 'ps -ef | grep apache'
|
45
|
+
|
46
|
+
Options:
|
47
|
+
EOB
|
48
|
+
parser.on("-n", "--preview", "Preview without executing commands") do |v|
|
49
|
+
opts[:preview] = v
|
50
|
+
end
|
51
|
+
|
52
|
+
parser.on("-p", "--project PATH", "Set project path") do |v|
|
53
|
+
opts[:project] = v
|
54
|
+
end
|
55
|
+
|
56
|
+
parser.on("-q", "--quiet", "Don't display commands executed") do |v|
|
57
|
+
opts[:verbosity] = Logger::ERROR
|
58
|
+
end
|
59
|
+
|
60
|
+
parser.on("-v", "--version", "Display version") do |v|
|
61
|
+
puts AutomateIt::VERSION
|
62
|
+
exit 0
|
63
|
+
end
|
64
|
+
|
65
|
+
parser.on("-h", "--help", "Display this help message") do |v|
|
66
|
+
puts parser
|
67
|
+
exit
|
68
|
+
end
|
69
|
+
|
70
|
+
args = parser.parse!.dup
|
71
|
+
query = args.shift
|
72
|
+
commands = args
|
73
|
+
|
74
|
+
if commands.blank?
|
75
|
+
puts parser
|
76
|
+
puts "\nERROR: No commands specified"
|
77
|
+
exit 1
|
78
|
+
end
|
79
|
+
|
80
|
+
interpreter = AutomateIt.new(
|
81
|
+
:project => opts[:project],
|
82
|
+
:verbosity => opts[:verbosity]
|
83
|
+
)
|
84
|
+
|
85
|
+
interpreter.preview true if opts[:preview]
|
86
|
+
|
87
|
+
for host in interpreter.hosts_tagged_with(query).sort
|
88
|
+
cmd = "ssh %s %s" % [host, commands.join(" ").shell_escape]
|
89
|
+
interpreter.sh(cmd)
|
90
|
+
end
|
91
|
+
|
92
|
+
exit 0
|
93
|
+
end
|
data/bin/aitag
CHANGED
@@ -18,6 +18,13 @@ OptionParser.new do |parser|
|
|
18
18
|
|
19
19
|
Usage: #{PROG} [options] [arguments...]
|
20
20
|
|
21
|
+
IMPORTANT:
|
22
|
+
#{PROG} can only match against tags specified in tags.yml. It cannot
|
23
|
+
match against automatically generated tags like the OS, architecture and such
|
24
|
+
which are created at runtime by PlatformManager or AddressManager. So if you
|
25
|
+
want to run a command against all Ubuntu servers, you must define them
|
26
|
+
explicitly in tags.yml.
|
27
|
+
|
21
28
|
Examples:
|
22
29
|
# Load 'myproject' and see if it's tagged with 'apache' or 'svn':
|
23
30
|
#{PROG} -p myproject 'apache || svn'
|
data/lib/automateit.rb
CHANGED
@@ -35,6 +35,7 @@ Hash.module_eval{include ActiveSupport::CoreExtensions::Hash::Keys}
|
|
35
35
|
# Extensions
|
36
36
|
require 'ext/object.rb'
|
37
37
|
require 'ext/metaclass.rb'
|
38
|
+
require 'ext/shell_escape.rb'
|
38
39
|
|
39
40
|
# Helpers
|
40
41
|
require 'hashcache'
|
@@ -42,6 +43,7 @@ require 'queued_logger'
|
|
42
43
|
require 'tempster'
|
43
44
|
require 'helpful_erb'
|
44
45
|
require 'nested_error'
|
46
|
+
require 'nitpick'
|
45
47
|
|
46
48
|
# Core
|
47
49
|
require 'automateit/root'
|
@@ -15,8 +15,8 @@ class ::AutomateIt::AccountManager::Etc< ::AutomateIt::AccountManager::BaseDrive
|
|
15
15
|
# the 'etc' module?
|
16
16
|
def self.has_etc?
|
17
17
|
begin
|
18
|
-
require '
|
19
|
-
return defined?(Etc)
|
18
|
+
require 'etc'
|
19
|
+
return defined?(::Etc)
|
20
20
|
rescue LoadError
|
21
21
|
return false
|
22
22
|
end
|
@@ -24,7 +24,7 @@ class ::AutomateIt::AccountManager::Etc< ::AutomateIt::AccountManager::BaseDrive
|
|
24
24
|
|
25
25
|
# Alias for AccountManager::Etc.has_etc?
|
26
26
|
def has_etc?
|
27
|
-
self.has_etc?
|
27
|
+
self.class.has_etc?
|
28
28
|
end
|
29
29
|
|
30
30
|
#.......................................................................
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# AccountManager driver for invalidating records stored in the NSCD, Name
|
4
4
|
# Service Cache Daemon, found on Unix-like systems.
|
5
5
|
class ::AutomateIt::AccountManager::NSCD < ::AutomateIt::AccountManager::BaseDriver
|
6
|
-
depends_on :programs => %w(nscd ps),
|
6
|
+
depends_on :programs => %w(nscd ps),
|
7
7
|
:callbacks => [lambda{`ps -ef`.match(%r{/usr/sbin/nscd$})}]
|
8
8
|
|
9
9
|
def suitability(method, *args) # :nodoc:
|
@@ -11,18 +11,21 @@ class ::AutomateIt::AccountManager::NSCD < ::AutomateIt::AccountManager::BaseDri
|
|
11
11
|
return available? ? 5 : 0
|
12
12
|
end
|
13
13
|
|
14
|
+
# Returns the NSCD database for the specified shorthand +query+.
|
15
|
+
def database_for(query)
|
16
|
+
case query.to_sym
|
17
|
+
when :user, :users, :passwd, :password
|
18
|
+
:passwd
|
19
|
+
when :group, :groups
|
20
|
+
:group
|
21
|
+
else
|
22
|
+
raise ArgumentError.new("Unknown cache database: #{query}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
14
26
|
def invalidate(database)
|
15
27
|
return false unless available?
|
16
28
|
|
17
|
-
|
18
|
-
case database.to_sym
|
19
|
-
when :user, :users, :passwd
|
20
|
-
:passwd
|
21
|
-
when :group, :groups
|
22
|
-
:group
|
23
|
-
else
|
24
|
-
raise ArgumentError.new("Unknown cache database: #{database}")
|
25
|
-
end
|
26
|
-
interpreter.sh("nscd -i #{name}")
|
29
|
+
interpreter.sh("nscd -i #{database_for(database)}")
|
27
30
|
end
|
28
31
|
end
|
@@ -155,7 +155,7 @@ class AutomateIt::AddressManager::BaseDriver< AutomateIt::Plugin::Driver
|
|
155
155
|
ipcmd = "ifconfig"
|
156
156
|
ipcmd << " " << _interface_and_label(opts)
|
157
157
|
if helper_opts[:prepend]
|
158
|
-
ipcmd << " " << helper_opts[:prepend].join(" ")
|
158
|
+
ipcmd << " " << [helper_opts[:prepend]].flatten.join(" ")
|
159
159
|
end
|
160
160
|
ipcmd << " %s" % opts[:address]
|
161
161
|
ipcmd << " netmask %s" % opts[:mask] if opts[:mask]
|
@@ -164,7 +164,7 @@ class AutomateIt::AddressManager::BaseDriver< AutomateIt::Plugin::Driver
|
|
164
164
|
ipcmd << " down" if action == :del
|
165
165
|
end
|
166
166
|
if helper_opts[:append]
|
167
|
-
ipcmd << " " << helper_opts[:append].join(" ")
|
167
|
+
ipcmd << " " << [helper_opts[:append]].flatten.join(" ")
|
168
168
|
end
|
169
169
|
return ipcmd
|
170
170
|
end
|
@@ -82,6 +82,8 @@ module AutomateIt
|
|
82
82
|
# how to more easily dispatch commands from your program to the Interpreter
|
83
83
|
# instance.
|
84
84
|
class Interpreter < Common
|
85
|
+
include Nitpick
|
86
|
+
|
85
87
|
# Plugin instance that instantiated the Interpreter.
|
86
88
|
attr_accessor :parent
|
87
89
|
private :parent
|
@@ -627,24 +629,5 @@ module AutomateIt
|
|
627
629
|
text = ::HelpfulERB.new(template).result(binding)
|
628
630
|
object.instance_eval(text)
|
629
631
|
end
|
630
|
-
|
631
|
-
# Use to manage nitpick message for debugging AutomateIt internals.
|
632
|
-
#
|
633
|
-
# Arguments:
|
634
|
-
# * nil -- Returns boolean of whether nitpick messages will be displayed.
|
635
|
-
# * Boolean -- Sets nitpick state.
|
636
|
-
# * String or Symbol -- Displays nitpick message if state is on.
|
637
|
-
#
|
638
|
-
# Example:
|
639
|
-
# nitpick true
|
640
|
-
# nitpick "I'm nitpicking"
|
641
|
-
def nitpick(value=nil)
|
642
|
-
case value
|
643
|
-
when NilClass: @nitpick
|
644
|
-
when TrueClass, FalseClass: @nitpick = value
|
645
|
-
when String, Symbol: puts "%% #{value}" if @nitpick
|
646
|
-
else raise TypeError.new("Unknown nitpick type: #{value.class}")
|
647
|
-
end
|
648
|
-
end
|
649
632
|
end
|
650
633
|
end
|
@@ -222,6 +222,8 @@ class AutomateIt::PackageManager::BaseDriver < AutomateIt::Plugin::Driver
|
|
222
222
|
result = packages.map(&:to_s).grep(LIST_NORMALIZER_RE)
|
223
223
|
when Hash
|
224
224
|
result = packages.stringify_keys
|
225
|
+
when Symbol, String
|
226
|
+
result = packages.to_s
|
225
227
|
else
|
226
228
|
raise TypeError.new("Unknown input type: #{packages.class}")
|
227
229
|
end
|
@@ -199,12 +199,7 @@ module AutomateIt
|
|
199
199
|
# instance found, else raises a NotImplementedError if no suitable driver
|
200
200
|
# is found.
|
201
201
|
def driver_for(method, *args, &block)
|
202
|
-
|
203
|
-
driver, level = driver_suitability_levels_for(method, *args, &block).sort_by{|k,v| v}.last
|
204
|
-
rescue IndexError
|
205
|
-
driver = nil
|
206
|
-
level = -1
|
207
|
-
end
|
202
|
+
driver, level = driver_suitability_levels_for(method, *args, &block).sort_by{|k,v| v}.last
|
208
203
|
if driver and level > 0
|
209
204
|
return @drivers[driver]
|
210
205
|
else
|
data/lib/automateit/root.rb
CHANGED
@@ -2,13 +2,13 @@
|
|
2
2
|
#
|
3
3
|
# Helper class for parsing tags. Not useful for users -- for internal use only.
|
4
4
|
class AutomateIt::TagManager::TagParser
|
5
|
+
include Nitpick
|
6
|
+
|
5
7
|
attr_accessor :struct
|
6
|
-
attr_accessor :is_trace # FIXME replace is_trace with nitpick
|
7
8
|
|
8
9
|
# Create a parser for the +struct+, a hash of tag keys to values with arrays of items.
|
9
|
-
def initialize(struct
|
10
|
+
def initialize(struct)
|
10
11
|
self.struct = struct
|
11
|
-
self.is_trace = is_trace
|
12
12
|
normalize!
|
13
13
|
end
|
14
14
|
|
@@ -33,11 +33,6 @@ class AutomateIt::TagManager::TagParser
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
# Display debugging information if +is_trace+ is enabled.
|
37
|
-
def trace(msg)
|
38
|
-
puts msg if is_trace
|
39
|
-
end
|
40
|
-
|
41
36
|
HOSTS_FOR_VALUE = /(.+?)/
|
42
37
|
HOSTS_FOR_INCLUDE_TAG_RE = /^INCLUDE_TAG #{HOSTS_FOR_VALUE}$/
|
43
38
|
HOSTS_FOR_EXCLUDE_TAG_RE = /^EXCLUDE_TAG #{HOSTS_FOR_VALUE}$/
|
@@ -45,27 +40,29 @@ class AutomateIt::TagManager::TagParser
|
|
45
40
|
|
46
41
|
# Return array of hosts for the +tag+.
|
47
42
|
def hosts_for(tag)
|
48
|
-
raise IndexError.new("Unknown tag - #{tag}") unless struct
|
49
|
-
|
43
|
+
raise IndexError.new("Unknown tag - #{tag}") unless struct.has_key?(tag)
|
44
|
+
return [] if struct[tag].nil? # Tag has no leaves
|
45
|
+
|
46
|
+
nitpick "\nAA %s" % tag
|
50
47
|
hosts = Set.new
|
51
48
|
for item in struct[tag]
|
52
49
|
case item
|
53
50
|
when HOSTS_FOR_INCLUDE_TAG_RE
|
54
|
-
|
51
|
+
nitpick "+g %s" % $1
|
55
52
|
hosts.merge(hosts_for($1))
|
56
53
|
when HOSTS_FOR_EXCLUDE_TAG_RE
|
57
|
-
|
54
|
+
nitpick "-g %s" % $1
|
58
55
|
hosts.subtract(hosts_for($1))
|
59
56
|
when HOSTS_FOR_EXCLUDE_HOST_RE
|
60
|
-
|
57
|
+
nitpick "-h %s" % $1
|
61
58
|
hosts.delete($1)
|
62
59
|
else
|
63
|
-
|
60
|
+
nitpick "+h %s" % item
|
64
61
|
hosts << item
|
65
62
|
end
|
66
63
|
end
|
67
64
|
result = hosts.to_a
|
68
|
-
|
65
|
+
nitpick "ZZ %s for %s" % [result.inspect, tag]
|
69
66
|
return result
|
70
67
|
end
|
71
68
|
|
@@ -90,7 +87,7 @@ class AutomateIt::TagManager::TagParser
|
|
90
87
|
end
|
91
88
|
|
92
89
|
# Expand the +struct+.
|
93
|
-
def self.expand(struct
|
94
|
-
self.new(struct
|
90
|
+
def self.expand(struct)
|
91
|
+
self.new(struct).expand
|
95
92
|
end
|
96
93
|
end
|