vop 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +50 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +66 -0
  5. data/README.md +2 -0
  6. data/Rakefile +6 -0
  7. data/bin/vop.rb +28 -0
  8. data/bin/vop.sh +4 -0
  9. data/exe/vop +28 -0
  10. data/lib/vop.rb +242 -0
  11. data/lib/vop/command.rb +168 -0
  12. data/lib/vop/command_loader.rb +47 -0
  13. data/lib/vop/entity.rb +61 -0
  14. data/lib/vop/loader.rb +35 -0
  15. data/lib/vop/plugin.rb +141 -0
  16. data/lib/vop/plugin_loader.rb +88 -0
  17. data/lib/vop/plugins/core/commands/clear_context.rb +3 -0
  18. data/lib/vop/plugins/core/commands/collect_contributions.rb +31 -0
  19. data/lib/vop/plugins/core/commands/edit.rb +12 -0
  20. data/lib/vop/plugins/core/commands/help.rb +38 -0
  21. data/lib/vop/plugins/core/commands/identity.rb +4 -0
  22. data/lib/vop/plugins/core/commands/list_contributors.rb +8 -0
  23. data/lib/vop/plugins/core/commands/list_entities.rb +3 -0
  24. data/lib/vop/plugins/core/commands/pry.rb +9 -0
  25. data/lib/vop/plugins/core/commands/reset.rb +5 -0
  26. data/lib/vop/plugins/core/commands/show_context.rb +3 -0
  27. data/lib/vop/plugins/core/commands/source.rb +5 -0
  28. data/lib/vop/plugins/core/commands/system_call.rb +5 -0
  29. data/lib/vop/plugins/core/core.plugin +4 -0
  30. data/lib/vop/plugins/core/helpers/command_loader/command_syntax.rb +45 -0
  31. data/lib/vop/plugins/core/helpers/command_loader/contributions.rb +28 -0
  32. data/lib/vop/plugins/core/helpers/command_loader/entities.rb +57 -0
  33. data/lib/vop/plugins/core/helpers/helper.rb +3 -0
  34. data/lib/vop/plugins/core/helpers/plugin_loader/plugin_syntax.rb +0 -0
  35. data/lib/vop/plugins/meta/commands/add_search_path.rb +6 -0
  36. data/lib/vop/plugins/meta/commands/delete_plugin.rb +13 -0
  37. data/lib/vop/plugins/meta/commands/list_commands.rb +17 -0
  38. data/lib/vop/plugins/meta/commands/list_plugins.rb +8 -0
  39. data/lib/vop/plugins/meta/commands/new_command.rb +14 -0
  40. data/lib/vop/plugins/meta/commands/new_plugin.rb +25 -0
  41. data/lib/vop/plugins/meta/commands/show_search_path.rb +3 -0
  42. data/lib/vop/plugins/meta/commands/who_provides.rb +5 -0
  43. data/lib/vop/plugins/meta/meta.plugin +1 -0
  44. data/lib/vop/plugins/ssh/commands/scp.rb +11 -0
  45. data/lib/vop/plugins/ssh/commands/ssh.rb +19 -0
  46. data/lib/vop/plugins/ssh/ssh.plugin +1 -0
  47. data/lib/vop/shell.rb +52 -0
  48. data/lib/vop/shell/backend.rb +28 -0
  49. data/lib/vop/shell/base_shell.rb +112 -0
  50. data/lib/vop/shell/formatter.rb +46 -0
  51. data/lib/vop/shell/vop_shell_backend.rb +257 -0
  52. data/lib/vop/version.rb +3 -0
  53. data/vop.gemspec +31 -0
  54. metadata +223 -0
@@ -0,0 +1,25 @@
1
+ description "initializes a new plugin (folder)"
2
+
3
+ param! 'name'
4
+ param! 'path', :description => 'the path in which to create the new plugin.'
5
+
6
+ require 'fileutils'
7
+
8
+ run do |params|
9
+ raise "no such path: #{params['path']}" unless File.exists? params['path']
10
+
11
+ # a plugin is a directory
12
+ plugin_path = File.join(params['path'], params['name'])
13
+ Dir.mkdir(plugin_path)
14
+
15
+ # with subfolders for commands and helpers
16
+ %w|commands helpers|.each do |thing|
17
+ Dir.mkdir(File.join(plugin_path, thing))
18
+ end
19
+
20
+ # and a metadata file called '<name>.plugin'
21
+ plugin_file = params['name'] + '.plugin'
22
+ FileUtils.touch(File.join(plugin_path, plugin_file))
23
+
24
+ @op.reset
25
+ end
@@ -0,0 +1,3 @@
1
+ run do
2
+ @op._search_path
3
+ end
@@ -0,0 +1,5 @@
1
+ param! 'name', :lookup => lambda { |params| @op.list_commands.map { |x| x[:name] } }
2
+
3
+ run do |name|
4
+ @op.command(name).plugin.name
5
+ end
@@ -0,0 +1 @@
1
+ dependency :core
@@ -0,0 +1,11 @@
1
+ require 'net/scp'
2
+
3
+ param! 'machine'
4
+ param 'user'
5
+ param! 'local_path'
6
+ param! 'remote_path'
7
+
8
+ run do |machine, local_path, remote_path, params|
9
+ user = params.has_key?('user') ? params['user'] : ENV['USER']
10
+ Net::SCP.upload!(params['machine'], user, local_path, remote_path)
11
+ end
@@ -0,0 +1,19 @@
1
+ require 'net/ssh'
2
+
3
+ param! 'machine'
4
+ param! 'command', :default_param => true
5
+ param 'user' #, :default => ENV['USER']
6
+ param 'key_file', :multi => true
7
+
8
+ run do |params|
9
+ machine_name = params['machine']
10
+ user = params.has_key?('user') ? params['user'] : ENV['USER']
11
+
12
+ options = {}
13
+ if params.has_key? 'key_file'
14
+ options[:keys] = params['key_file']
15
+ end
16
+ Net::SSH.start(machine_name, user, options) do |ssh|
17
+ ssh.exec! params['command']
18
+ end
19
+ end
@@ -0,0 +1 @@
1
+ depends :core
data/lib/vop/shell.rb ADDED
@@ -0,0 +1,52 @@
1
+ require 'vop'
2
+ require 'vop/shell/vop_shell_backend'
3
+ require 'vop/shell/base_shell'
4
+
5
+ require 'docopt'
6
+ require 'pp'
7
+
8
+ module Vop
9
+
10
+ class Shell
11
+
12
+ USAGE = <<DOCOPT
13
+ virtualop
14
+
15
+ Usage:
16
+ vop [options]
17
+
18
+ Options:
19
+ -h --help show this help screen
20
+ -v --verbose enable debug output
21
+ -e --execute=<command> to run a command directly
22
+
23
+ DOCOPT
24
+
25
+ attr_reader :options
26
+ attr_reader :op
27
+
28
+ def initialize(vop = nil, options = {})
29
+ @op = vop || Vop.new
30
+ @options = options
31
+
32
+ backend = VopShellBackend.new(@op, :color_prompt => true)
33
+ @base_shell = BaseShell.new(backend)
34
+ end
35
+
36
+ def execute(string)
37
+ @base_shell.backend.process_input(string)
38
+ end
39
+
40
+ def run_cli
41
+ @base_shell.run
42
+ end
43
+
44
+ def self.setup()
45
+ options = Docopt::docopt(USAGE, {:help => true})
46
+
47
+ vop = Vop.new(options)
48
+ return Shell.new(vop, options)
49
+ end
50
+
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ # responsible for what should happen when the user interacts with the shell
2
+ # provides tab completion options and processes the user's input
3
+ class Backend
4
+
5
+ # is called whenever the user submits a command (hitting enter)
6
+ def process_input(command_line)
7
+ raise "not implemented in abstract Backend!"
8
+ end
9
+
10
+ # is called whenever the user requests tab completion
11
+ # should return an array of completion proposals
12
+ def complete(word)
13
+ raise "not implemented in abstract Backend!"
14
+ end
15
+
16
+ def prompt
17
+ ">"
18
+ end
19
+
20
+ def process_ctrl_c
21
+
22
+ end
23
+
24
+ def show_banner
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,112 @@
1
+ require 'readline'
2
+
3
+ # This class is an abstract implementation of a command shell
4
+ # It handles command completion and history functions.
5
+ # For the actual business logic, you need to pass it an implementation of ShellBackend
6
+ class BaseShell
7
+ attr_reader :backend
8
+
9
+ def initialize(backend)
10
+ @logger = $logger
11
+ @backend = backend
12
+
13
+ at_exit { console.close }
14
+
15
+ trap("INT") {
16
+ Thread.kill(@thread)
17
+ @backend.process_ctrl_c
18
+ }
19
+ end
20
+
21
+ class SimpleConsole
22
+ def initialize(input = $stdin)
23
+ @input = input
24
+ end
25
+
26
+ def readline
27
+ begin
28
+ line = @input.readline
29
+ line.chomp! if line
30
+ line
31
+ rescue EOFError
32
+ nil
33
+ end
34
+ end
35
+
36
+ def close
37
+ end
38
+ end
39
+
40
+ class ReadlineConsole
41
+ HISTORY_FILE = ".vop_history"
42
+ MAX_HISTORY = 200
43
+
44
+ def history_path
45
+ File.join(ENV['HOME'] || ENV['USERPROFILE'], HISTORY_FILE)
46
+ end
47
+
48
+ def initialize(shell)
49
+ @shell = shell
50
+
51
+ if File.exist?(history_path)
52
+ hist = File.readlines(history_path).map{|line| line.chomp}
53
+ Readline::HISTORY.push(*hist)
54
+ end
55
+
56
+ #Readline.basic_word_break_characters = " \t\n\\`@><=;|&{([+*%"
57
+
58
+ # see http://stackoverflow.com/questions/13876024/how-to-write-a-ruby-command-line-app-that-supports-tab-completion#13876556
59
+ Readline.completer_word_break_characters = ""
60
+
61
+ Readline.completion_append_character = nil
62
+ Readline.completion_proc = @shell.backend.method(:complete).to_proc
63
+ end
64
+
65
+ def close
66
+ open(history_path, "wb") do |f|
67
+ history = Readline::HISTORY.to_a
68
+ if history.size > MAX_HISTORY
69
+ history = history[history.size - MAX_HISTORY, MAX_HISTORY]
70
+ end
71
+ history.each{|line| f.puts(line)}
72
+ end
73
+ end
74
+
75
+ def readline
76
+ line = Readline.readline(@shell.backend.prompt, true)
77
+ Readline::HISTORY.pop if /^\s*$/ =~ line
78
+ line
79
+ end
80
+ end
81
+
82
+ def console
83
+ @console ||= $stdin.tty? ? ReadlineConsole.new(self) : SimpleConsole.new
84
+ end
85
+
86
+ def run
87
+ backend.show_banner
88
+ loop do
89
+ @thread = Thread.new {
90
+ line = console.readline
91
+
92
+ if line
93
+ backend.process_input line
94
+ end
95
+ }
96
+ begin
97
+ @thread.join
98
+ rescue
99
+ error = $!
100
+ if error == "exit"
101
+ Kernel.exit
102
+ else
103
+ $stderr.puts "error: >>#{error}<<"
104
+ $stderr.puts error.backtrace.join("\n")
105
+ end
106
+ end
107
+
108
+ end
109
+ $stderr.puts "Exiting shell..."
110
+ end
111
+
112
+ end
@@ -0,0 +1,46 @@
1
+ require 'pp'
2
+ require 'terminal-table'
3
+
4
+ def format_output(command, data)
5
+ if data.is_a? Array
6
+ first_row = data.first
7
+ if first_row.is_a? Hash
8
+ # show all columns unless defined otherwise in the command
9
+ columns_to_display = first_row.keys
10
+ if command.show_options.include? :columns
11
+ columns_to_display = command.show_options[:columns]
12
+ end
13
+ column_headers = columns_to_display
14
+
15
+ rearranged = [] # array of hashes -> array of arrays
16
+ data.each do |row|
17
+ values = []
18
+ columns_to_display.each do |key|
19
+ values << row[key]
20
+ end
21
+ rearranged << values
22
+ end
23
+
24
+ begin
25
+ rearranged.sort_by! { |row| row.first }
26
+ rescue
27
+ puts "[WARN] ran into trouble sorting the result (by the first column); results may be not quite sorted."
28
+ begin
29
+ rearranged.sort_by! { |row| row.first || "zaphod" }
30
+ rescue
31
+ puts "[SHRUG] could not sort even when accounting for potential nil values, giving up."
32
+ end
33
+ end
34
+
35
+ puts Terminal::Table.new rows: rearranged, headings: column_headers
36
+ else
37
+ puts data.join("\n")
38
+ end
39
+ elsif data.is_a? Hash
40
+ data.each do |k,v|
41
+ puts "#{k} : #{v}"
42
+ end
43
+ else
44
+ puts data
45
+ end
46
+ end
@@ -0,0 +1,257 @@
1
+ require 'vop/shell/backend'
2
+ require 'vop/shell/formatter'
3
+
4
+ class VopShellBackend < Backend
5
+
6
+ def initialize(op, options = {})
7
+ @op = op
8
+ @options = options
9
+ @local_context = {}
10
+
11
+ reset_to_command_mode
12
+ end
13
+
14
+ def context
15
+ @local_context
16
+ end
17
+
18
+ def reset_to_command_mode
19
+ # this shell has two modes that determine the available tab completion proposals
20
+ # command_mode
21
+ # we're waiting for the user to pick a command that should be executed
22
+ # parameter mode
23
+ # the command to execute has already been selected, but the user needs to specify additional parameters
24
+ # we'll start in the mode where no command has been selected yet
25
+ @command_selected = nil
26
+
27
+ # if the user selected a command already, we'll have to collect parameters for this command until
28
+ # we've got all mandatory parameters so that we can execute the command
29
+ @collected_values = Hash.new { |h,k| h[k] = [] }
30
+
31
+ @missing_params = []
32
+
33
+ @current_param = nil
34
+ end
35
+
36
+ def process_ctrl_c
37
+ puts "\n"
38
+ if @command_selected
39
+ reset_to_command_mode
40
+ else
41
+ Kernel.exit
42
+ end
43
+ end
44
+
45
+ def set_prompt(p)
46
+ @prompt = p
47
+ end
48
+
49
+ def prompt
50
+ @command_selected && @current_param ?
51
+ "#{@command_selected.short_name}.#{@current_param[:name]} ? " :
52
+ @prompt || '>> '
53
+ end
54
+
55
+ def show_banner
56
+ s = @options[:banner]
57
+ puts s if s
58
+ end
59
+
60
+ def complete(word)
61
+ $logger.debug "completing #{word}"
62
+
63
+ command_list = @op.commands.keys
64
+
65
+ list = []
66
+
67
+ parts = nil
68
+
69
+ if @command_selected
70
+ #$logger.debug("asking for lookup values for command '#{@command_selected.name}' and param '#{@current_param[:name]}'")
71
+ list = @command_selected.lookup(@current_param[:name], @collected_values)
72
+ else
73
+ begin
74
+ (parts, command, param_values) = parse_command_string(word)
75
+
76
+ if command
77
+ $logger.debug "command selected (#{command.name}), fetching param lookups"
78
+
79
+ # all lookup values for a default_param (if exists)
80
+ if command.default_param
81
+ list += command.lookup(command.default_param[:name], @collected_values)
82
+ $logger.debug "added lookups for default param, list now #{list.length} elements"
83
+ end
84
+
85
+ # names of all params that have not been specified yet or are :multi
86
+ command.params.each do |param|
87
+ if not param_values.keys.include? param[:name] || param[:multi]
88
+ list << param[:name]
89
+ end
90
+ end
91
+
92
+ else
93
+ $logger.debug "no command selected yet, returning command list"
94
+ list = command_list
95
+ end
96
+ rescue => e
97
+ $logger.debug "can't parse >>#{word}<< : #{e.message}"
98
+ $logger.debug e.backtrace
99
+ end
100
+ end
101
+
102
+ the_filter = parts ? parts.last : word
103
+ if the_filter
104
+ $logger.debug "filtering completion list against : #{the_filter}"
105
+ list.delete_if do |x|
106
+ x[0...the_filter.size] != the_filter
107
+ end
108
+ end
109
+
110
+ prefix = ''
111
+ if parts
112
+ prefix = parts.length > 1 ? parts[0..-2].join(" ").strip : ''
113
+ end
114
+ if $logger.debug?
115
+ more_text = list.length > 1 ? " (and #{list.length-1} more)" : ''
116
+ $logger.debug "completion from >#{word}< to #{prefix} + #{list.first}#{more_text}"
117
+ end
118
+ list.map do |x|
119
+ [prefix, x].join(' ').strip
120
+ end
121
+ end
122
+
123
+ def parse_command_string(command_line, presets = {})
124
+ parts = command_line.split.map { |x| x.chomp.strip }
125
+ (command_name, *params) = parts
126
+ command = @op.commands[command_name]
127
+
128
+ param_values = Hash.new { |h,k| h[k] = [] }
129
+ presets.each do |k,v|
130
+ param_values[k] = [v]
131
+ end
132
+ if params
133
+ params.each do |param|
134
+ if param =~ /(.+?)=(.+)/ then
135
+ # --> named param
136
+ key = $1
137
+ value = $2
138
+ else
139
+ # --> unnamed param
140
+ value = param
141
+ if command
142
+ default_param = command.default_param
143
+ if default_param != nil then
144
+ key = default_param[:name]
145
+ $logger.debug "collecting value '#{value}' for default param '#{default_param[:name]}'"
146
+ else
147
+ $logger.debug "ignoring param '#{value}' because there's no default param"
148
+ end
149
+ else
150
+ # can't process an unnamed param unless we've got a command
151
+ end
152
+ end
153
+
154
+ if key
155
+ param_values[key] << value
156
+ end
157
+ end
158
+ end
159
+ [parts, command, param_values]
160
+ end
161
+
162
+ def process_input(command_line)
163
+ $logger.debug "+++ process_input #{command_line} +++"
164
+ if @command_selected
165
+ # we're in parameter processing mode - so check which parameter
166
+ # we've got now and switch modes if necessary
167
+
168
+ # we might have been waiting for multiple param values - check if the user finished
169
+ # adding values by entering an empty string as value
170
+ if @current_param && (@current_param[:multi] and command_line == "") then
171
+ @missing_params.shift
172
+ execute_command_if_possible
173
+ else
174
+ # TODO check +command_line+ against lookups/general validity?
175
+ @collected_values[@current_param[:name]] << command_line
176
+ if @current_param[:multi] then
177
+ $logger.debug "param '#{@current_param[:name]}' expects multiple values...deferring mode switching"
178
+ else
179
+ @missing_params.shift
180
+ execute_command_if_possible
181
+ end
182
+ end
183
+ else
184
+ (unused, @command_selected, values) = parse_command_string(command_line, @local_context)
185
+
186
+ if @command_selected
187
+ values.each do |key, value_list|
188
+ begin
189
+ value_list.each do |value|
190
+ @current_param = @command_selected.param(key)
191
+ if @current_param
192
+ @collected_values[@current_param[:name]] << value
193
+ else
194
+ # TODO handle extra params?
195
+ end
196
+ end
197
+ rescue Exception => ex
198
+ # TODO broken (error: undefined method `accepts_extra_params' for Vop::Command machines.select_machine:Vop::Command)
199
+ if @command_selected && false && @command_selected.accepts_extra_params
200
+ puts "collecting value for extra param : #{key} => #{value}"
201
+ @collected_values["extra_params"] = {} unless @collected_values.has_key?("extra_params")
202
+ @collected_values["extra_params"][key] = Array.new if @collected_values["extra_params"][key] == nil
203
+ @collected_values["extra_params"][key] << value
204
+ else
205
+ #puts "ignoring parameter value '#{value_list}' for param '#{key}' : " + ex.to_s
206
+ raise ex
207
+ end
208
+ end
209
+ end
210
+
211
+
212
+ execute_command_if_possible
213
+ end
214
+ end
215
+ end
216
+
217
+ def execute_command_if_possible
218
+ mandatory = @command_selected.mandatory_params
219
+ @missing_params = mandatory.select { |p| ! @collected_values.include? p[:name] }
220
+
221
+ if @missing_params.size > 0
222
+ @current_param = @missing_params.first
223
+ else
224
+ execute_command
225
+ end
226
+ end
227
+
228
+ def execute_command
229
+ command = @command_selected
230
+ $logger.debug "vop_shell_backend executing command '#{command.short_name}'"
231
+
232
+ begin
233
+ extras = {
234
+ 'shell' => self
235
+ }
236
+ (response, context) = @op.execute_command(command.short_name, @collected_values, extras)
237
+
238
+ if command.short_name == 'exit'
239
+ $logger.info "exiting on user request"
240
+ Kernel.exit(0)
241
+ end
242
+
243
+ if context
244
+ if context['prompt']
245
+ set_prompt context['prompt']
246
+ end
247
+
248
+ @local_context.merge! context
249
+ end
250
+
251
+ format_output(command, response)
252
+ ensure
253
+ reset_to_command_mode
254
+ end
255
+ end
256
+
257
+ end