nuri 0.5.1 → 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d02365a26a71b3202e77accdc43826f5da8c63ca
4
- data.tar.gz: 00b72bf869f9802a32fe1bc4a2f4b350ffb889d2
3
+ metadata.gz: 5713187406a70381abeffa8e382892172e1cab3a
4
+ data.tar.gz: 589c3291070bf16e6ee05e4846192ae3d0ddca81
5
5
  SHA512:
6
- metadata.gz: 63f4c79c68d7e624ec3ac944fdb7445edd8a78ee5370122c5133e4c24c2fe7d998ed01900ccd7a3c4bd0315ab33d4005473b8f13a8d0db0dae1c26ff028ed7fe
7
- data.tar.gz: ee932dd09a18574c7ec58c9f55ca21a2085bad5be3be735d90de26be4610e599106a40eb273168db60834647b7e308b035c619fd5ba0fef6056b94ecdd3d627e
6
+ metadata.gz: 5917ac035100aeb2e1dc75ff326034d9bbc7b5ef8a8679307581d389cca20dc8b9138bafddcdddccfbe9e6457184418eaa1ffb42ea6ddfd6c5fed6d8d30b8728
7
+ data.tar.gz: d14d4cfd96af587cdf02a4f622e1ad7773db83052eef68de3e304f1196313814aca4e0673d3069d721f662fba89db8158bef6a91b1b2f09eeb3e0ee13248c01a
data/README.md CHANGED
@@ -1,8 +1,9 @@
1
- Nuri
1
+ [Nuri](http://herry13.github.io/nuri)
2
2
  ====
3
3
  - author: Herry [herry13@gmail.com]
4
4
  - license: BSD
5
5
 
6
+ [![Build Status](https://travis-ci.org/herry13/nuri.png?branch=master)](https://travis-ci.org/herry13/nuri)
6
7
  [![Gem Version](https://badge.fury.io/rb/nuri.png)](https://badge.fury.io/rb/nuri)
7
8
 
8
9
  Nuri is an automated workflow configuration tool. It allows us to define the desired state of
@@ -29,19 +30,19 @@ Requirements
29
30
  ------------
30
31
  - Ruby (>= 1.9.1)
31
32
  - Ruby Gems:
32
- - sfplanner (>= 0.1.1)
33
- - colorize (>= 0.5.8)
34
- - coderay (>= 1.0.9)
33
+ - sfplanner
34
+ - sfpagent
35
+ - colorize
36
+ - coderay
35
37
 
36
- Tested on: **Ubuntu 12.04**, **Debian Squeeze**
38
+ Tested on Linux: **Ubuntu 12.04**, **Debian Squeeze**, and **Fedora 13**
37
39
 
38
40
 
39
41
  To install
40
42
  ----------
41
43
 
42
- $ apt-get install git make gcc curl ruby1.9.1 ruby1.9.1-dev libz-dev libaugeas-ruby1.9.1 libxml2-dev libxslt-dev
43
- $ gem install sfplanner colorize coderay
44
- $ git clone https://github.com/herry13/nuri
44
+ $ apt-get install curl ruby1.9.1 ruby1.9.1-dev libz-dev libaugeas-ruby1.9.1 libxml2-dev libxslt-dev make gcc
45
+ $ gem install nuri
45
46
 
46
47
 
47
48
  Usage
@@ -49,16 +50,46 @@ Usage
49
50
  Assume that the model of your system is in file "model.sfp".
50
51
  - to get the current state
51
52
 
52
- $ ./bin/nuri -h -m model.sfp -s
53
+ $ nuri state -m model.sfp
53
54
 
54
55
  - to generate the plan
55
56
 
56
- $ ./bin/nuri -h -m model.sfp -p
57
+ $ nuri plan -m model.sfp
57
58
 
58
59
  press 'Y' and enter to execute the plan (if the plan exists).
59
60
 
60
61
  - to execute any generated plan
61
62
 
62
- $ ./bin/nuri -h -m model.sfp -a
63
+ $ nuri plan -m model.sfp -a
63
64
 
64
65
 
66
+ Console mode
67
+ ------------
68
+ We could enter Nuri console mode and do operations:
69
+ - enter console mode
70
+
71
+ $ nuri console
72
+
73
+ - edit the model which is stored in ~/.nuri/main.sfp
74
+
75
+ nuri@user> edit
76
+
77
+ - compile and print the model
78
+
79
+ nuri@user> model
80
+
81
+ - generate the current state of the system based on the model
82
+
83
+ nuri@user> state
84
+
85
+ - generate the plan
86
+
87
+ nuri@user> plan
88
+
89
+ - orchestrate the execution of any plan generated by Nuri
90
+
91
+ nuri@user> plan -a
92
+
93
+ - choreograph the execution of any plan generated by Nuri
94
+
95
+ nuri@user> bsig -a
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.1
1
+ 0.5.2
@@ -1,11 +1,35 @@
1
- #!/bin/bash
2
-
3
- if [[ "$1" == "" ]]
4
- then
5
- echo "Usage: delete_modules <address> [port]"
6
- elif [[ "$2" == "" ]]
7
- then
8
- curl -i -X PUT $1:1314/modules
9
- else
10
- curl -i -X PUT $1:$2/modules
11
- fi
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'uri'
4
+ require 'net/http'
5
+
6
+ module Nuri
7
+ module Util
8
+ def self.delete_modules(address, port, protocol="http")
9
+ url = "#{protocol}://#{address}:#{port}/modules"
10
+ uri = URI.parse(url)
11
+ http = Net::HTTP.new(uri.host, uri.port)
12
+ request = Net::HTTP::Delete.new(uri.request_uri)
13
+ response = http.request(request)
14
+ (response.code == '200')
15
+ end
16
+ end
17
+ end
18
+
19
+ if $0 == __FILE__
20
+ DefaultPort = 1314
21
+
22
+ if ARGV.length < 1
23
+ puts "Usage: delete_modules <address> [port]"
24
+ exit(1)
25
+ end
26
+
27
+ address = ARGV.shift
28
+ port = (ARGV[0].nil? ? DefaultPort : ARGV[0].to_i)
29
+
30
+ if Nuri::Util.delete_modules(address, port)
31
+ puts '{"status":"ok"}'
32
+ else
33
+ puts '{"status":"failed"}'
34
+ end
35
+ end
@@ -1,65 +1,54 @@
1
- #!/bin/bash
2
-
3
- DefaultPort=1314
4
-
5
- # verify the arguments
6
- if [[ "$1" == "" ]] || [[ "$2" == "" ]]; then
7
- echo "Usage: install_module <address> [port] <module-name> ..."
8
- exit 1
9
- fi
10
-
11
- # set the agent's address
12
- address=$1
13
- shift
14
-
15
- # set the agent's port number
16
- re='^[0-9]+$'
17
- if [[ $1 =~ $re ]]; then
18
- port=$1
19
- shift
20
- else
21
- port=$DefaultPort
22
- fi
23
-
24
- # set a template command for sending the modules
25
- cmd="curl -s -i -X PUT $address:$port/modules"
26
-
27
- # setup directory for temporary files
28
- dir="/tmp/"$(date +%s%N)"$RANDOM"
29
- mkdir -p $dir
30
-
31
- # for every module in the arguments:
32
- # - archive the module's files to a temporary file
33
- # - update the sending command by adding the module
34
- # if the module is not exist then the program will
35
- # set missing_module flag and then break from the loop
36
- missing_module=0
37
- for module in "$@" ; do
38
- if [[ -d "$module" ]]; then
39
- tar cvzhf $dir/$module.tgz $module 1>/dev/null
40
- cmd="$cmd -F $module=@$dir/$module.tgz"
41
- else
42
- echo "Module $module is not exist!"
43
- missing_module=`expr $missing_module + 1`
44
- fi
45
- done
46
-
47
- if [[ $missing_module == 0 ]]; then
48
- # execute the sending command there is no missing module
49
- result=`$cmd`
50
- re='.*HTTP/1\.1 200 OK.*'
51
- if ! [[ $result =~ $re ]]; then
52
- missing_module=`expr $missing_module + 1`
53
- fi
54
- fi
55
-
56
- # delete directory that holds temporary files
57
- rm -rf $dir
58
-
59
- if [[ $missing_module == 0 ]]; then
60
- echo "status: ok"
61
- else
62
- echo "status: failed"
63
- fi
64
-
65
- exit $missing_module
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require File.dirname(__FILE__) + '/../lib/nuri/targz'
7
+
8
+ module Nuri
9
+ module Util
10
+ TarGzip = Object.new.extend(Util::Tar)
11
+
12
+ def self.install_modules(address, port, modules, protocol="http")
13
+ data = {}
14
+ modules.each do |module_name|
15
+ raise Exception, "Module #{module_name} is not exist!" if not ::File.directory?(module_name)
16
+ data[module_name] = TarGzip.targzip(module_name, module_name).read
17
+ end
18
+
19
+ url = "#{protocol}://#{address}:#{port}/modules"
20
+ uri = URI.parse(url)
21
+ http = Net::HTTP.new(uri.host, uri.port)
22
+ request = Net::HTTP::Put.new(uri.request_uri)
23
+ request.set_form_data(data)
24
+ response = http.request(request)
25
+
26
+ (response.code == '200')
27
+ end
28
+ end
29
+ end
30
+
31
+ if $0 == __FILE__
32
+ if ARGV.length < 2
33
+ puts "Usage: install_module <address> [port] <module-name> ..."
34
+ exit(1)
35
+ end
36
+
37
+ DefaultPort = 1314
38
+
39
+ address = ARGV.shift
40
+ port = (ARGV.length >= 2 ? ARGV.shift : DefaultPort)
41
+ modules = ARGV
42
+ missing = modules.select { |mod| not ::File.directory?(mod) }
43
+ modules = modules - missing
44
+
45
+ success = Nuri::Util.install_modules(address, port, modules),
46
+ output = {
47
+ :status => success,
48
+ :installed_modules => modules,
49
+ :missing_modules => missing
50
+ }
51
+ puts JSON.generate(output)
52
+
53
+ exit (success ? 0 : 1)
54
+ end
data/bin/nuri CHANGED
@@ -6,8 +6,6 @@ require "#{dir}/lib/nuri"
6
6
  Version = File.read(File.dirname(__FILE__) + "/../VERSION").strip
7
7
  About = "Nuri #{Version} (c) 2013"
8
8
 
9
- HTTP = Object.new.extend(Nuri::Net::Helper)
10
-
11
9
  Nuri.init
12
10
 
13
11
  def console(*args)
@@ -15,8 +13,9 @@ def console(*args)
15
13
  $stdout.puts(args)
16
14
  end
17
15
 
18
- class Sfp::Console
19
- PrettyStateGenerator = Sfp::Visitor::PrettyStateGenerator.new
16
+ class Nuri::Console
17
+ include Nuri::Helper
18
+
20
19
  ParentEliminator = Sfp::Visitor::ParentEliminator.new
21
20
 
22
21
  def process_args(args, parser)
@@ -77,6 +76,7 @@ EOS
77
76
  opt :plain, 'print output in plain JSON'
78
77
  opt :no_interactive, 'disable interactive input'
79
78
  opt :no_push_module, 'disable automatic push module'
79
+ opt :image, 'image graph of the generated plan', :default => Dir.home + '/.nuri/plan.png'
80
80
  end
81
81
  help, args = check_help(args)
82
82
  opts = process_args args, parser
@@ -91,6 +91,13 @@ EOS
91
91
  plan = master.get_plan(opts)
92
92
  if plan.is_a?(Hash) and !plan['workflow'].nil?
93
93
  if plan['workflow'].length > 0
94
+ plan_file = Dir.home + '/.nuri/plan.json'
95
+ File.open(plan_file, 'w') { |f| f.write(JSON.generate(plan)) }
96
+
97
+ if opts[:image]
98
+ system "#{File.dirname(__FILE__)}/sfw2graph #{plan_file} #{opts[:image]}"
99
+ end
100
+
94
101
  if opts[:plain]
95
102
  puts JSON.generate(plan)
96
103
  else
@@ -120,6 +127,7 @@ EOS
120
127
  puts "Execution failed!".red
121
128
  end
122
129
  end
130
+
123
131
  elsif plan['workflow'].length == 0
124
132
  puts (opts[:plain] ? 'Goal state has been achieved.' : 'Goal state has been achieved.').green
125
133
  end
@@ -212,7 +220,7 @@ where <subcommand> is:
212
220
  model get current model
213
221
  bsig get current Behavioural Signature model
214
222
  exec <action> execute given action description
215
- <action> = <path> [param1=value1 param2=value2 ...]
223
+ <action> := <path> [param1=value1 param2=value2 ...]
216
224
  module get modules list
217
225
  log get last 100 lines of logs
218
226
  list list of available agents from model file
@@ -224,6 +232,7 @@ EOS
224
232
  opt :port, "port", :default => 1314
225
233
  opt :ssh_user, "SSH username", :short => '-u', :default => 'root'
226
234
  opt :ssh_port, "SSH port", :short => '-p', :default => '22'
235
+ opt :raw, "print a RAW output"
227
236
  end
228
237
  help, args = check_help(args)
229
238
  subcommand = (args.length > 0 ? args.shift : '')
@@ -237,7 +246,7 @@ EOS
237
246
 
238
247
  if help
239
248
  parser.educate(STDOUT)
240
- elsif `which sfpagent`.strip.length > 0
249
+ elsif `which sfpagent`.strip.length > 0 or ssh_opt.length > 0
241
250
  case subcommand
242
251
  when 'install'
243
252
  system("#{ssh_opt} sudo gem install sfpagent --no-ri --no-rdoc")
@@ -259,32 +268,52 @@ EOS
259
268
  system("#{ssh_opt} sudo gem update sfpagent --no-ri --no-rdoc")
260
269
 
261
270
  when 'state'
262
- code, state = HTTP.get_data(opts[:address], 1314, "/state")
271
+ code, state = get_data(opts[:address], 1314, "/state")
263
272
  if code == '200' and state =~ /{.*}/
264
273
  state = JSON[state]['state']
265
- state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' }
266
- puts CodeRay.encode(JSON.pretty_generate(state), :json, :terminal)
274
+ if not opts[:raw]
275
+ state.keys.each { |key| state.delete(key) if state[key]['_context'] != 'object' }
276
+ state.accept(Sfp::Helper::Sfp2Ruby)
277
+ end
278
+ puts CodeRay.encode(YAML.dump(state), :yaml, :terminal)
267
279
  else
268
280
  puts state
269
281
  end
270
282
 
271
283
  when 'model'
272
- code, model = HTTP.get_data(opts[:address], 1314, "/model")
284
+ code, model = get_data(opts[:address], 1314, "/model")
273
285
  if code == '200' and model =~ /{.*}/
274
286
  model = JSON[model]
275
- model.keys.each { |key| model.delete(key) if model[key]['_context'] != 'object' }
276
- puts CodeRay.encode(JSON.pretty_generate(model), :json, :terminal)
287
+ if not opts[:raw]
288
+ model.select! { |k,v| k[0,1] != '_' and (not v.is_a?(Hash) or v['_context'] == 'object') }
289
+ model.accept(Sfp::Helper::Sfp2Ruby)
290
+ end
291
+ puts CodeRay.encode(YAML.dump(model), :yaml, :terminal)
292
+ elsif code == '404'
293
+ puts "The model of desired state is not exist!"
277
294
  else
278
- puts model
295
+ $stderr.puts "Error: #{code}"
279
296
  end
280
297
 
281
298
  when 'bsig'
282
- code, json = HTTP.get_data(opts[:address], 1314, "/bsig")
283
- puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json)
299
+ code, json = get_data(opts[:address], 1314, "/bsig")
300
+ if code == '200' and json.length > 0
301
+ puts CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal)
302
+ elsif code == '404'
303
+ puts 'Behavioural Signature model is not exist!'
304
+ else
305
+ $stderr.puts "Error: #{code}"
306
+ end
284
307
 
285
308
  when 'module'
286
- code, json = HTTP.get_data(opts[:address], 1314, "/bsig")
287
- puts (code == '200' and json.length >= 2 ? CodeRay.encode(JSON.pretty_generate(JSON[json]), :json, :terminal) : json)
309
+ code, json = get_data(opts[:address], 1314, "/modules")
310
+ if code == '200' and json.length >= 2
311
+ puts CodeRay.encode(YAML.dump(JSON[json]), :yaml, :terminal)
312
+ elsif code == '404'
313
+ puts 'No module is available!'
314
+ else
315
+ $stderr.puts "Error: #{code}"
316
+ end
288
317
 
289
318
  when 'exec', 'execute'
290
319
  if args.length > 0
@@ -298,7 +327,7 @@ EOS
298
327
  end
299
328
  end
300
329
  data = { 'action' => JSON.generate({ 'name' => name, 'parameters' => parameters }) }
301
- code, _ = HTTP.post_data(opts[:address], 1314, "/execute", data)
330
+ code, _ = post_data(opts[:address], 1314, "/execute", data)
302
331
  puts (code == '200' ? "Executing #{name} [OK]".green : "Executing #{name} [Failed]".red)
303
332
  else
304
333
  $stderr.puts 'Invalid parameters (usage: agent exec <action-path> [action-arguments]).'
@@ -497,23 +526,4 @@ EOS
497
526
  end
498
527
  end
499
528
 
500
- module Sfp::Helper
501
- Sfp2Ruby = Object.new
502
- def Sfp2Ruby.visit(name, value, parent)
503
- if name[0] == '_'
504
- parent.delete(name)
505
- elsif value.is_a?(Hash)
506
- case value['_context']
507
- when 'null'
508
- parent[name] = nil
509
- when 'any_value', 'constraint', 'procedure'
510
- parent.delete(name)
511
- when 'set'
512
- parent[name] = value['_values']
513
- end
514
- end
515
- true
516
- end
517
- end
518
-
519
- Sfp::Console.new.run if $0 == __FILE__
529
+ Nuri::Console.new.run if $0 == __FILE__
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'json'
5
+
6
+ module Sfp
7
+ end
8
+
9
+ module Sfp::Graph
10
+ ActionColor = 'white'
11
+ ActionLabelWithParameters = false
12
+
13
+ def self.dot2image(dot, image_file)
14
+ dot_file = "/tmp/#{Time.now.getutc.to_i}.dot"
15
+ File.open(dot_file, 'w') { |f|
16
+ f.write(dot)
17
+ f.flush
18
+ }
19
+ !!system("dot -Tpng -o #{image_file} #{dot_file}")
20
+ ensure
21
+ File.delete(dot_file) if File.exist?(dot_file)
22
+ end
23
+
24
+ def self.clean(value)
25
+ return value[2, value.length-2] if value[0,2] == '$.'
26
+ return value
27
+ end
28
+
29
+ def self.get_label(action, withparameters=true)
30
+ label = clean(action["name"])
31
+ if withparameters and ActionLabelWithParameters
32
+ label += "("
33
+ if action["parameters"].length > 0
34
+ action["parameters"].each { |key,value|
35
+ label += "#{clean(key)}=#{clean(value.to_s)},"
36
+ }
37
+ label.chop!
38
+ end
39
+ label += ')'
40
+ end
41
+ return label
42
+ end
43
+
44
+ def self.partial2dot(json)
45
+ dot = "digraph {\n"
46
+
47
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
48
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
49
+ last_actions = Hash.new
50
+ json["workflow"].each { |action|
51
+ dot += "#{action["id"]}[label=\"#{get_label(action)}\", shape=rect, style=filled, fillcolor=#{ActionColor}];\n"
52
+ last_actions[action["id"].to_s] = action
53
+ }
54
+
55
+ json["workflow"].each { |action|
56
+ has_predecessor = false
57
+ action["predecessors"].each { |prevId|
58
+ dot += "#{prevId} -> #{action["id"]};\n"
59
+ has_predecessor = true
60
+ last_actions.delete(prevId.to_s)
61
+ }
62
+ if not has_predecessor
63
+ dot += "init_state -> #{action["id"]};\n"
64
+ end
65
+ }
66
+
67
+ last_actions.each { |id,action|
68
+ dot += "#{id} -> final_state;\n"
69
+ }
70
+
71
+ dot += "}"
72
+
73
+ return dot
74
+ end
75
+
76
+ def self.stage2dot(json)
77
+ dot = "digraph {\n"
78
+
79
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
80
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
81
+ index = 0
82
+ prevState = "init_state"
83
+ json["workflow"].each { |stage|
84
+ id = 0
85
+ stage.each { |action|
86
+ dot += "a" + index.to_s + "a" + id.to_s + '[label="' + get_label(action) + '", shape=rect]' + ";\n"
87
+ dot += prevState + " -> a" + index.to_s + "a" + id.to_s + ";\n"
88
+ id += 1
89
+ }
90
+ if index < json["workflow"].length-1
91
+ dot += "state" + index.to_s + ' [label="", shape=circle, fixedsize=true, width=0.35]' + ";\n"
92
+ prevState = "state" + index.to_s
93
+ id = 0
94
+ stage.each { |action|
95
+ dot += "a" + index.to_s + "a" + id.to_s + " -> " + prevState + ";\n"
96
+ id += 1
97
+ }
98
+ else
99
+ id = 0
100
+ stage.each { |action|
101
+ dot += "a" + index.to_s + "a" + id.to_s + " -> final_state;\n"
102
+ id += 1
103
+ }
104
+ end
105
+ index += 1
106
+ }
107
+ dot += "}"
108
+ return dot
109
+ end
110
+
111
+ def self.sequential2dot(json)
112
+ dot = "digraph {\n"
113
+
114
+ dot += "init_state [label=\"\", shape=doublecircle, fixedsize=true, width=0.35];\n"
115
+ dot += "final_state [label=\"\", shape=doublecircle, style=filled, fillcolor=black, fixedsize=true, width=0.35];\n"
116
+ id = 0
117
+ json["workflow"].each { |action|
118
+ dot += id.to_s + '[label="' + get_label(action) + '", shape=rect]' + ";\n"
119
+ id += 1
120
+ }
121
+
122
+ id = 0
123
+ prevActionId = nil
124
+ json["workflow"].each { |action|
125
+ if id == 0
126
+ dot += "init_state -> " + id.to_s + ";\n"
127
+ elsif id == json["workflow"].length-1
128
+ dot += id.to_s + " -> final_state;\n"
129
+ end
130
+ if prevActionId != nil
131
+ dot += prevActionId.to_s + " -> " + id.to_s + ";\n"
132
+ end
133
+ prevActionId = id
134
+ id += 1
135
+ }
136
+ dot += "}"
137
+ return dot
138
+ end
139
+ end
140
+
141
+ def main
142
+ if ARGV.length < 1
143
+ puts "Convert SFP plan to an image graph with Graphviz (dot).\n\nUsage: sfw2dot.rb <input-file> [output-file]\n\n"
144
+ exit
145
+ end
146
+
147
+ fp = open(ARGV[0], "rb")
148
+ json = JSON.parse(fp.read)
149
+ fp.close
150
+
151
+ dot = ""
152
+ case json["type"]
153
+ when 'partial-order', 'parallel'
154
+ dot = Sfp::Graph::partial2dot(json)
155
+ when 'sequential'
156
+ dot = Sfp::Graph::sequential2dot(json)
157
+ when 'stage'
158
+ dot = Sfp::Graph::stage2dot(json)
159
+ else
160
+ throw Exception, "Unrecognised type of workflow: #{json['type']}"
161
+ end
162
+
163
+ outfile = "/tmp/#{Time.now.to_f}.dot"
164
+ fp = open(outfile, "w");
165
+ fp.write(dot)
166
+ fp.close
167
+
168
+ cmd = 'dot -Tpng -o';
169
+ if ARGV.length > 1
170
+ cmd += "#{ARGV[1]} #{outfile}"
171
+ else
172
+ cmd += ARGV[0].sub(/\.[a-zA-Z]*/,'') + ".png < #{outfile}"
173
+ end
174
+ system(cmd)
175
+ File.delete(outfile)
176
+ end
177
+
178
+ main if __FILE__ == $0