fig18 0.1.45-i386-mingw32

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.
@@ -0,0 +1,248 @@
1
+ require 'optparse'
2
+ require 'fig/package'
3
+ require 'fig/package/archive'
4
+ require 'fig/package/include'
5
+ require 'fig/package/path'
6
+ require 'fig/package/resource'
7
+ require 'fig/package/set'
8
+
9
+ module Fig
10
+ def parse_descriptor(descriptor)
11
+ # todo should use treetop for these:
12
+ package_name = descriptor =~ /^([^:\/]+)/ ? $1 : nil
13
+ config_name = descriptor =~ /:([^:\/]+)/ ? $1 : nil
14
+ version_name = descriptor =~ /\/([^:\/]+)/ ? $1 : nil
15
+ return package_name, config_name, version_name
16
+ end
17
+
18
+ USAGE = <<EOF
19
+
20
+ Usage: fig [--debug] [--update] [--config <config>] [--get <var> | --list | <package> | -- <command>]
21
+
22
+ Relevant env vars: FIG_REMOTE_URL (required), FIG_HOME (path to local repository cache, defaults
23
+ to $HOME/.fighome).
24
+
25
+ EOF
26
+
27
+ LOG_LEVELS = %w[ off fatal error warn info debug all ]
28
+ LOG_ALIASES = { 'warning' => 'warn' }
29
+
30
+ # Returns hash of option values, the remainders of argv, and an exit code if
31
+ # full program processing occured in this method, otherwise nil.
32
+ def parse_options(argv)
33
+ options = {}
34
+
35
+ parser = OptionParser.new do |opts|
36
+ opts.banner = USAGE
37
+ opts.on('-?', '-h','--help','display this help text') do
38
+ puts opts.help
39
+ puts "\n -- end of fig options; everything following is a command to run in the fig environment.\n\n"
40
+ return nil, nil, 0
41
+ end
42
+
43
+ opts.on('-v', '--version', 'Print fig version') do
44
+ line = nil
45
+
46
+ begin
47
+ File.open(
48
+ "#{File.expand_path(File.dirname(__FILE__) + '/../../VERSION')}"
49
+ ) do |file|
50
+ line = file.gets
51
+ end
52
+ rescue
53
+ $stderr.puts 'Could not retrieve version number. Something has mucked with your gem install.'
54
+ return nil, nil, 1
55
+ end
56
+
57
+ if line !~ /\d+\.\d+\.\d+/
58
+ $stderr.puts %Q<"#{line}" does not look like a version number. Something has mucked with your gem install.>
59
+ return nil, nil, 1
60
+ end
61
+
62
+ puts File.basename($0) + ' v' + line
63
+
64
+ return nil, nil, 0
65
+ end
66
+
67
+ options[:modifiers] = []
68
+ opts.on(
69
+ '-p',
70
+ '--append VAR=VAL',
71
+ 'append (actually, prepend) VAL to environment var VAR, delimited by separator'
72
+ ) do |var_val|
73
+ var, val = var_val.split('=')
74
+ options[:modifiers] << Package::Path.new(var, val)
75
+ end
76
+
77
+ options[:archives] = []
78
+ opts.on(
79
+ '--archive FULLPATH',
80
+ 'include FULLPATH archive in package (when using --publish)'
81
+ ) do |path|
82
+ options[:archives] << Package::Archive.new(path)
83
+ end
84
+
85
+ options[:cleans] = []
86
+ opts.on('--clean PKG', 'remove package from $FIG_HOME') do |descriptor|
87
+ options[:cleans] << descriptor
88
+ end
89
+
90
+ options[:config] = 'default'
91
+ opts.on(
92
+ '-c',
93
+ '--config CFG',
94
+ %q<apply configuration CFG, default is 'default'>
95
+ ) do |config|
96
+ options[:config] = config
97
+ end
98
+
99
+ options[:debug] = false
100
+ opts.on('-d', '--debug', 'print debug info') do
101
+ options[:debug] = true
102
+ end
103
+
104
+ options[:input] = nil
105
+ opts.on(
106
+ '--file FILE',
107
+ %q<read fig file FILE. Use '-' for stdin. See also --no-file>
108
+ ) do |path|
109
+ options[:input] = path
110
+ end
111
+
112
+ options[:force] = nil
113
+ opts.on(
114
+ '--force',
115
+ 'force-overwrite existing version of a package to the remote repo'
116
+ ) do |force|
117
+ options[:force] = force
118
+ end
119
+
120
+ options[:echo] = nil
121
+ opts.on(
122
+ '-g',
123
+ '--get VAR',
124
+ 'print value of environment variable VAR'
125
+ ) do |echo|
126
+ options[:echo] = echo
127
+ end
128
+
129
+ opts.on(
130
+ '-i',
131
+ '--include PKG',
132
+ 'include PKG (with any variable prepends) in environment'
133
+ ) do |descriptor|
134
+ package_name, config_name, version_name = parse_descriptor(descriptor)
135
+ options[:modifiers] << Package::Include.new(package_name, config_name, version_name, {})
136
+ end
137
+
138
+ options[:list] = false
139
+ opts.on('--list', 'list packages in $FIG_HOME') do
140
+ options[:list] = true
141
+ end
142
+
143
+ options[:list_configs] = []
144
+ opts.on(
145
+ '--list-configs PKG', 'list configurations in package'
146
+ ) do |descriptor|
147
+ options[:list_configs] << descriptor
148
+ end
149
+
150
+ options[:list_remote] = false
151
+ opts.on('--list-remote', 'list packages in remote repo') do
152
+ options[:list_remote] = true
153
+ end
154
+
155
+ options[:login] = false
156
+ opts.on(
157
+ '-l', '--login', 'login to remote repo as a non-anonymous user'
158
+ ) do
159
+ options[:login] = true
160
+ end
161
+
162
+ opts.on(
163
+ '--no-file', 'ignore package.fig file in current directory'
164
+ ) do |path|
165
+ options[:input] = :none
166
+ end
167
+
168
+ options[:publish] = nil
169
+ opts.on(
170
+ '--publish PKG', 'install PKG in $FIG_HOME and in remote repo'
171
+ ) do |publish|
172
+ options[:publish] = publish
173
+ end
174
+
175
+ options[:publish_local] = nil
176
+ opts.on(
177
+ '--publish-local PKG', 'install package only in $FIG_HOME'
178
+ ) do |publish_local|
179
+ options[:publish_local] = publish_local
180
+ end
181
+
182
+ options[:resources] =[]
183
+ opts.on(
184
+ '--resource FULLPATH',
185
+ 'include FULLPATH resource in package (when using --publish)'
186
+ ) do |path|
187
+ options[:resources] << Package::Resource.new(path)
188
+ end
189
+
190
+ opts.on(
191
+ '-s', '--set VAR=VAL', 'set environment variable VAR to VAL'
192
+ ) do |var_val|
193
+ var, val = var_val.split('=')
194
+ options[:modifiers] << Package::Set.new(var, val)
195
+ end
196
+
197
+ options[:update] = false
198
+ opts.on(
199
+ '-u',
200
+ '--update',
201
+ 'check remote repo for updates and download to $FIG_HOME as necessary'
202
+ ) do
203
+ options[:update] = true; options[:retrieve] = true
204
+ end
205
+
206
+ options[:update_if_missing] = false
207
+ opts.on(
208
+ '-m',
209
+ '--update-if-missing',
210
+ 'check remote repo for updates only if package missing from $FIG_HOME'
211
+ ) do
212
+ options[:update_if_missing] = true; options[:retrieve] = true
213
+ end
214
+
215
+ opts.on(
216
+ '--figrc PATH', 'add PATH to configuration used for Fig'
217
+ ) do |path|
218
+ options[:figrc] = path
219
+ end
220
+
221
+ opts.on('--no-figrc', 'ignore ~/.figrc') { options[:no_figrc] = true }
222
+
223
+ opts.on(
224
+ '--log-config PATH', 'use PATH file as configuration for Log4r'
225
+ ) do |path|
226
+ options[:log_config] = path
227
+ end
228
+
229
+ level_list = LOG_LEVELS.join(', ')
230
+ opts.on(
231
+ '--log-level LEVEL',
232
+ LOG_LEVELS,
233
+ LOG_ALIASES,
234
+ 'set logging level to LEVEL',
235
+ " (#{level_list})"
236
+ ) do |log_level|
237
+ options[:log_level] = log_level
238
+ end
239
+
240
+ options[:home] = ENV['FIG_HOME'] || File.expand_path('~/.fighome')
241
+ end
242
+
243
+ # Need to catch the exception thrown from parser and retranslate into a fig exception
244
+ parser.parse!(argv)
245
+
246
+ return options, argv, nil
247
+ end
248
+ end
data/lib/fig/os.rb ADDED
@@ -0,0 +1,416 @@
1
+ require 'fileutils'
2
+ # Must specify absolute path of ::Archive when using
3
+ # this module to avoid conflicts with Fig::Package::Archive
4
+ require 'libarchive_ruby' unless RUBY_PLATFORM == 'java'
5
+ require 'uri'
6
+ require 'net/http'
7
+ require 'net/ssh'
8
+ require 'net/sftp'
9
+ require 'net/netrc'
10
+ require 'tempfile'
11
+ require 'highline/import'
12
+
13
+ require 'fig/logging'
14
+ require 'fig/networkerror'
15
+ require 'fig/notfounderror'
16
+
17
+ module Fig
18
+ class OS
19
+ def initialize(login)
20
+ @login = login
21
+ @username = ENV['FIG_USERNAME']
22
+ @password = ENV['FIG_PASSWORD']
23
+ end
24
+
25
+ def get_username()
26
+ @username ||= ask('Username: ') { |q| q.echo = true }
27
+ end
28
+
29
+ def get_password()
30
+ @password ||= ask('Password: ') { |q| q.echo = false }
31
+ end
32
+
33
+ def ftp_login(ftp, host)
34
+ if @login
35
+ rc = Net::Netrc.locate(host)
36
+ if rc
37
+ @username = rc.login
38
+ @password = rc.password
39
+ end
40
+ ftp.login(get_username, get_password)
41
+ else
42
+ ftp.login()
43
+ end
44
+ ftp.passive = true
45
+ end
46
+
47
+ def list(dir)
48
+ Dir.entries(dir) - ['.','..']
49
+ end
50
+
51
+ def exist?(path)
52
+ File.exist?(path)
53
+ end
54
+
55
+ def mtime(path)
56
+ File.mtime(path)
57
+ end
58
+
59
+ def read(path)
60
+ File.read(path)
61
+ end
62
+
63
+ def write(path, content)
64
+ File.open(path, 'wb') { |f| f.binmode; f << content }
65
+ end
66
+
67
+ SUCCESS = 0
68
+ NOT_MODIFIED = 3
69
+ NOT_FOUND = 4
70
+
71
+ def strip_paths_for_list(ls_output, packages, path)
72
+ if not ls_output.nil?
73
+ ls_output = ls_output.gsub(path + '/', '').gsub(path, '').split("\n")
74
+ ls_output.each do |line|
75
+ parts = line.gsub(/\\/, '/').sub(/^\.\//, '').sub(/:$/, '').chomp().split('/')
76
+ packages << parts.join('/') if parts.size == 2
77
+ end
78
+ end
79
+ end
80
+
81
+ def download_list(url)
82
+ begin
83
+ uri = URI.parse(url)
84
+ rescue
85
+ Logging.fatal %Q<Unable to parse url: "#{url}">
86
+ raise NetworkError.new(%Q<Unable to parse url: "#{url}">)
87
+ end
88
+ case uri.scheme
89
+ when 'ftp'
90
+ ftp = Net::FTP.new(uri.host)
91
+ ftp_login(ftp, uri.host)
92
+ ftp.chdir(uri.path)
93
+ dirs = ftp.nlst
94
+ ftp.close
95
+
96
+ download_ftp_list(uri, dirs)
97
+ when 'ssh'
98
+ packages = []
99
+ Net::SSH.start(uri.host, uri.user) do |ssh|
100
+ ls = ssh.exec!("[ -d #{uri.path} ] && find #{uri.path}")
101
+ strip_paths_for_list(ls, packages, uri.path)
102
+ end
103
+ packages
104
+ when 'file'
105
+ packages = []
106
+ ls = %x<[ -d #{uri.path} ] && find #{uri.path}>
107
+ strip_paths_for_list(ls, packages, uri.path)
108
+ return packages
109
+ else
110
+ Logging.fatal "Protocol not supported: #{url}"
111
+ raise NetworkError.new("Protocol not supported: #{url}")
112
+ end
113
+ end
114
+
115
+ def download_ftp_list(uri, dirs)
116
+ # Run a bunch of these in parallel since they're slow as hell
117
+ num_threads = (ENV['FIG_FTP_THREADS'] || '16').to_i
118
+ threads = []
119
+ all_packages = []
120
+ (0..num_threads-1).each { |num| all_packages[num] = [] }
121
+ (0..num_threads-1).each do |num|
122
+ threads << Thread.new do
123
+ packages = all_packages[num]
124
+ ftp = Net::FTP.new(uri.host)
125
+ ftp_login(ftp, uri.host)
126
+ ftp.chdir(uri.path)
127
+ pos = num
128
+ while pos < dirs.length
129
+ pkg = dirs[pos]
130
+ begin
131
+ ftp.nlst(dirs[pos]).each do |ver|
132
+ packages << pkg + '/' + ver
133
+ end
134
+ rescue Net::FTPPermError
135
+ # ignore
136
+ end
137
+ pos += num_threads
138
+ end
139
+ ftp.close
140
+ end
141
+ end
142
+ threads.each { |thread| thread.join }
143
+ all_packages.flatten.sort
144
+ end
145
+
146
+ def download(url, path)
147
+ FileUtils.mkdir_p(File.dirname(path))
148
+ uri = URI.parse(url)
149
+ case uri.scheme
150
+ when 'ftp'
151
+ ftp = Net::FTP.new(uri.host)
152
+ ftp_login(ftp, uri.host)
153
+ begin
154
+ if File.exist?(path) && ftp.mtime(uri.path) <= File.mtime(path)
155
+ Logging.debug "#{path} is up to date."
156
+ return false
157
+ else
158
+ log_download(url, path)
159
+ ftp.getbinaryfile(uri.path, path, 256*1024)
160
+ return true
161
+ end
162
+ rescue Net::FTPPermError => e
163
+ Logging.warn e
164
+ raise NotFoundError.new
165
+ end
166
+ when 'http'
167
+ http = Net::HTTP.new(uri.host)
168
+ log_download(url, path)
169
+ File.open(path, 'wb') do |file|
170
+ file.binmode
171
+ http.get(uri.path) do |block|
172
+ file.write(block)
173
+ end
174
+ end
175
+ when 'ssh'
176
+ # TODO need better way to do conditional download
177
+ timestamp = File.exist?(path) ? File.mtime(path).to_i : 0
178
+ # Requires that remote installation of fig be at the same location as the local machine.
179
+ cmd = `which fig-download`.strip + " #{timestamp} #{uri.path}"
180
+ log_download(url, path)
181
+ ssh_download(uri.user, uri.host, path, cmd)
182
+ when 'file'
183
+ begin
184
+ FileUtils.cp(uri.path, path)
185
+ rescue Errno::ENOENT => e
186
+ raise NotFoundError.new
187
+ end
188
+ else
189
+ Logging.fatal "Unknown protocol: #{url}"
190
+ raise NetworkError.new("Unknown protocol: #{url}")
191
+ end
192
+ end
193
+
194
+ def download_resource(url, dir)
195
+ FileUtils.mkdir_p(dir)
196
+ download(url, File.join(dir, URI.parse(url).path.split('/').last))
197
+ end
198
+
199
+ def download_archive(url, dir)
200
+ FileUtils.mkdir_p(dir)
201
+ basename = URI.parse(url).path.split('/').last
202
+ path = File.join(dir, basename)
203
+ download(url, path)
204
+ case basename
205
+ when /\.tar\.gz$/
206
+ unpack_archive(dir, path)
207
+ when /\.tgz$/
208
+ unpack_archive(dir, path)
209
+ when /\.tar\.bz2$/
210
+ unpack_archive(dir, path)
211
+ when /\.zip$/
212
+ unpack_archive(dir, path)
213
+ else
214
+ Logging.fatal "Unknown archive type: #{basename}"
215
+ raise NetworkError.new("Unknown archive type: #{basename}")
216
+ end
217
+ end
218
+
219
+ def upload(local_file, remote_file, user)
220
+ Logging.debug "Uploading #{local_file} to #{remote_file}."
221
+ uri = URI.parse(remote_file)
222
+ case uri.scheme
223
+ when 'ssh'
224
+ ssh_upload(uri.user, uri.host, local_file, remote_file)
225
+ when 'ftp'
226
+ # fail unless system "curl -T #{local_file} --create-dirs --ftp-create-dirs #{remote_file}"
227
+ require 'net/ftp'
228
+ ftp_uri = URI.parse(ENV['FIG_REMOTE_URL'])
229
+ ftp_root_path = ftp_uri.path
230
+ ftp_root_dirs = ftp_uri.path.split('/')
231
+ remote_publish_path = uri.path[0, uri.path.rindex('/')]
232
+ remote_publish_dirs = remote_publish_path.split('/')
233
+ # Use array subtraction to deduce which project/version folder to upload to,
234
+ # i.e. [1,2,3] - [2,3,4] = [1]
235
+ remote_project_dirs = remote_publish_dirs - ftp_root_dirs
236
+ Net::FTP.open(uri.host) do |ftp|
237
+ ftp_login(ftp, uri.host)
238
+ # Assume that the FIG_REMOTE_URL path exists.
239
+ ftp.chdir(ftp_root_path)
240
+ remote_project_dirs.each do |dir|
241
+ # Can't automatically create parent directories, so do it manually.
242
+ if ftp.nlst().index(dir).nil?
243
+ ftp.mkdir(dir)
244
+ ftp.chdir(dir)
245
+ else
246
+ ftp.chdir(dir)
247
+ end
248
+ end
249
+ ftp.putbinaryfile(local_file)
250
+ end
251
+ when 'file'
252
+ FileUtils.mkdir_p(File.dirname(uri.path))
253
+ FileUtils.cp(local_file, uri.path)
254
+ else
255
+ Logging.fatal "Unknown protocol: #{uri}"
256
+ raise NetworkError.new("Unknown protocol: #{uri}")
257
+ end
258
+ end
259
+
260
+ def clear_directory(dir)
261
+ FileUtils.rm_rf(dir)
262
+ FileUtils.mkdir_p(dir)
263
+ end
264
+
265
+ def copy(source, target, msg = nil)
266
+ if File.directory?(source)
267
+ FileUtils.mkdir_p(target)
268
+ Dir.foreach(source) do |child|
269
+ if child != '.' and child != '..'
270
+ copy(File.join(source, child), File.join(target, child), msg)
271
+ end
272
+ end
273
+ else
274
+ if !File.exist?(target) || File.mtime(source) != File.mtime(target)
275
+ log_info "#{msg} #{target}" if msg
276
+ FileUtils.mkdir_p(File.dirname(target))
277
+ FileUtils.cp(source, target)
278
+ File.utime(File.atime(source), File.mtime(source), target)
279
+ end
280
+ end
281
+ end
282
+
283
+ def move_file(dir, from, to)
284
+ Dir.chdir(dir) { FileUtils.mv(from, to, :force => true) }
285
+ end
286
+
287
+ def log_info(msg)
288
+ Logging.info msg
289
+ end
290
+
291
+ # Expects files_to_archive as an Array of filenames.
292
+ def create_archive(archive_name, files_to_archive)
293
+ if OS.java?
294
+ `tar czvf #{archive_name} #{files_to_archive.join(' ')}`
295
+ else
296
+ # TODO: Need to verify files_to_archive exists.
297
+ ::Archive.write_open_filename(archive_name, ::Archive::COMPRESSION_GZIP, ::Archive::FORMAT_TAR) do |ar|
298
+ files_to_archive.each do |fn|
299
+ ar.new_entry do |entry|
300
+ entry.copy_stat(fn)
301
+ entry.pathname = fn
302
+ ar.write_header(entry)
303
+ if !entry.directory?
304
+ ar.write_data(open(fn) {|f| f.binmode; f.read })
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ # This method can handle the following archive types:
313
+ # .tar.bz2
314
+ # .tar.gz
315
+ # .tgz
316
+ # .zip
317
+ def unpack_archive(dir, file)
318
+ Dir.chdir(dir) do
319
+ if OS.java?
320
+ `tar xzvf #{file}`
321
+ else
322
+ ::Archive.read_open_filename(file) do |ar|
323
+ while entry = ar.next_header
324
+ ar.extract(entry)
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+ def self.windows?
332
+ Config::CONFIG['host_os'] =~ /mswin|mingw/
333
+ end
334
+
335
+ def self.java?
336
+ RUBY_PLATFORM == 'java'
337
+ end
338
+
339
+ def self.unix?
340
+ !windows?
341
+ end
342
+
343
+ def shell_exec(cmd)
344
+ if OS.windows?
345
+ Windows.shell_exec_windows(cmd)
346
+ else
347
+ shell_exec_unix(cmd)
348
+ end
349
+ end
350
+
351
+ private
352
+
353
+ def shell_exec_unix(cmd)
354
+ Kernel.exec(ENV['SHELL'], '-c', cmd.join(' '))
355
+ end
356
+
357
+ def shell_exec_windows(cmd)
358
+ #command = ['C:/WINDOWS/system32/cmd.exe', '/C', 'call'] + cmd
359
+ command = ['cmd.exe', '/C'] + cmd
360
+ command = command.join(' ')
361
+ Kernel.exec(command)
362
+ end
363
+
364
+ # path = The local path the file should be downloaded to.
365
+ # cmd = The command to be run on the remote host.
366
+ def ssh_download(user, host, path, cmd)
367
+ return_code = nil
368
+ tempfile = Tempfile.new('tmp')
369
+ Net::SSH.start(host, user) do |ssh|
370
+ ssh.open_channel do |channel|
371
+ channel.exec(cmd)
372
+ channel.on_data() { |ch, data| tempfile << data }
373
+ channel.on_extended_data() { |ch, type, data| Logging.error "SSH Download ERROR: #{data}" }
374
+ channel.on_request('exit-status') { |ch, request|
375
+ return_code = request.read_long
376
+ }
377
+ end
378
+ end
379
+
380
+ tempfile.close()
381
+
382
+ case return_code
383
+ when NOT_MODIFIED
384
+ tempfile.delete
385
+ return false
386
+ when NOT_FOUND
387
+ tempfile.delete
388
+ raise NotFoundError.new
389
+ when SUCCESS
390
+ FileUtils.mv(tempfile.path, path)
391
+ return true
392
+ else
393
+ tempfile.delete
394
+ Logging.fatal "Unable to download file #{path}: #{return_code}"
395
+ raise NetworkError.new("Unable to download file #{path}: #{return_code}")
396
+ end
397
+ end
398
+
399
+ def ssh_upload(user, host, local_file, remote_file)
400
+ uri = URI.parse(remote_file)
401
+ dir = uri.path[0, uri.path.rindex('/')]
402
+ Net::SSH.start(host, user) do |ssh|
403
+ ssh.exec!("mkdir -p #{dir}")
404
+ end
405
+ Net::SFTP.start(host, user) do |sftp|
406
+ sftp.upload!(local_file, uri.path)
407
+ end
408
+ end
409
+
410
+ private
411
+
412
+ def log_download(url, path)
413
+ Logging.debug "Downloading #{url} to #{path}."
414
+ end
415
+ end
416
+ end
@@ -0,0 +1,24 @@
1
+ require 'fig/logging'
2
+ require 'fig/packageerror'
3
+ require 'fig/package/statement'
4
+
5
+ module Fig; end
6
+ class Fig::Package; end
7
+
8
+ class Fig::Package::Archive
9
+ include Fig::Package::Statement
10
+
11
+ attr_reader :url
12
+
13
+ def initialize(url)
14
+ @url = url
15
+ end
16
+
17
+ def urls
18
+ return [@url]
19
+ end
20
+
21
+ def unparse(indent)
22
+ %Q<#{indent}archive "#{url}">
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ require 'fig/logging'
2
+ require 'fig/packageerror'
3
+ require 'fig/package/statement'
4
+
5
+ module Fig; end
6
+ class Fig::Package; end
7
+
8
+ class Fig::Package::Command
9
+ include Fig::Package::Statement
10
+
11
+ attr_reader :command
12
+
13
+ def initialize(command)
14
+ @command = command
15
+ end
16
+
17
+ def unparse(indent)
18
+ %Q<#{indent}command "#{@command}">
19
+ end
20
+ end