vop 0.3.0

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.
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