rhcp_shell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2009-01-23
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,12 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ README
6
+ lib/base_shell.rb
7
+ lib/rhcp_shell.rb
8
+ lib/rhcp_shell_backend.rb
9
+ lib/shell_backend.rb
10
+ start_shell.sh
11
+ test/rhcp_shell_backend_test.rb
12
+ test/setup_test_registry.rb
data/README ADDED
@@ -0,0 +1,3 @@
1
+ == rhcp_shell
2
+
3
+ You should document your project here.
data/README.txt ADDED
@@ -0,0 +1,48 @@
1
+ = sow_test
2
+
3
+ * FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2009 FIX
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,71 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require 'rake/packagetask'
5
+ require 'rake/gempackagetask'
6
+
7
+ PKG_NAME = 'rhcp_shell'
8
+ PKG_VERSION = '0.0.1'
9
+
10
+ desc "Default Task"
11
+ task :default => [ :test ]
12
+
13
+ ###############################################
14
+ ### TESTS
15
+ Rake::TestTask.new() { |t|
16
+ t.libs << "test"
17
+ t.test_files = FileList['test/*_test.rb']
18
+ t.verbose = true
19
+ }
20
+
21
+ ###############################################
22
+ ### RDOC
23
+ Rake::RDocTask.new { |rdoc|
24
+ rdoc.rdoc_dir = 'doc'
25
+ rdoc.title = "RHCP Shell"
26
+ rdoc.options << '--line-numbers' << '--inline-source' <<
27
+ '--accessor' << 'cattr_accessor=object'
28
+ rdoc.template = "#{ENV['template']}.rb" if ENV['template']
29
+ #rdoc.rdoc_files.include('README', 'CHANGELOG')
30
+ rdoc.rdoc_files.include('lib/**/*.rb')
31
+ }
32
+
33
+ ###############################################
34
+ ### METRICS
35
+ task :lines do
36
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
37
+
38
+ for file_name in FileList["lib/**/*.rb"]
39
+ f = File.open(file_name)
40
+
41
+ while line = f.gets
42
+ lines += 1
43
+ next if line =~ /^\s*$/
44
+ next if line =~ /^\s*#/
45
+ codelines += 1
46
+ end
47
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
48
+
49
+ total_lines += lines
50
+ total_codelines += codelines
51
+
52
+ lines, codelines = 0, 0
53
+ end
54
+
55
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
56
+ end
57
+
58
+ #rcov -I lib/ -I test/ -x rcov.rb -x var/lib -x lib/shell_backend.rb test/*test.rb
59
+
60
+ task :update_manifest do
61
+ system "rake check_manifest 2>/dev/null | grep -vE 'qooxdoo|nbproject|coverage' | grep -E '^\+' | grep -vE '^$' | grep -v '(in ' | grep -vE '^\+\+\+' | cut -b 2-200 | patch"
62
+ end
63
+
64
+ require 'rubygems'
65
+ require 'hoe'
66
+
67
+ Hoe.new('rhcp_shell', PKG_VERSION) do |p|
68
+ p.rubyforge_name = 'rhcp' # if different than lowercase project name
69
+ p.developer('Philipp T.', 'philipp@hitchhackers.net')
70
+ end
71
+
data/lib/base_shell.rb ADDED
@@ -0,0 +1,100 @@
1
+ # This class is an abstract implementation of a command shell
2
+ # It handles command completion and history functions.
3
+ # For the actual business logic, you need to pass it an implementation of ShellBackend
4
+ class BaseShell
5
+ attr_reader :backend
6
+
7
+ def initialize(backend)
8
+ @logger = $logger
9
+ @backend = backend
10
+
11
+ at_exit { console.close }
12
+
13
+ trap("INT") {
14
+ Thread.kill(@thread)
15
+ @backend.process_ctrl_c
16
+ }
17
+ end
18
+
19
+ class SimpleConsole
20
+ def initialize(input = $stdin)
21
+ @input = input
22
+ end
23
+
24
+ def readline
25
+ begin
26
+ line = @input.readline
27
+ line.chomp! if line
28
+ line
29
+ rescue EOFError
30
+ nil
31
+ end
32
+ end
33
+
34
+ def close
35
+ end
36
+ end
37
+
38
+ class ReadlineConsole
39
+ HISTORY_FILE = ".jscmd_history"
40
+ MAX_HISTORY = 200
41
+
42
+ def history_path
43
+ File.join(ENV['HOME'] || ENV['USERPROFILE'], HISTORY_FILE)
44
+ end
45
+
46
+ def initialize(shell)
47
+ @shell = shell
48
+ if File.exist?(history_path)
49
+ hist = File.readlines(history_path).map{|line| line.chomp}
50
+ Readline::HISTORY.push(*hist)
51
+ end
52
+ if Readline.methods.include?("basic_word_break_characters=")
53
+ #Readline.basic_word_break_characters = " \t\n\\`@><=;|&{([+-*/%"
54
+ Readline.basic_word_break_characters = " \t\n\\`@><=;|&{([+*%"
55
+ end
56
+ Readline.completion_append_character = nil
57
+ Readline.completion_proc = @shell.backend.method(:complete).to_proc
58
+ end
59
+
60
+ def close
61
+ open(history_path, "wb") do |f|
62
+ history = Readline::HISTORY.to_a
63
+ if history.size > MAX_HISTORY
64
+ history = history[history.size - MAX_HISTORY, MAX_HISTORY]
65
+ end
66
+ history.each{|line| f.puts(line)}
67
+ end
68
+ end
69
+
70
+ def readline
71
+ line = Readline.readline(@shell.backend.prompt, true)
72
+ Readline::HISTORY.pop if /^\s*$/ =~ line
73
+ line
74
+ end
75
+ end
76
+
77
+ def console
78
+ @console ||= $stdin.tty? ? ReadlineConsole.new(self) : SimpleConsole.new
79
+ end
80
+
81
+ def run
82
+ backend.show_banner
83
+ loop do
84
+ @thread = Thread.new {
85
+ break unless line = console.readline
86
+
87
+ if line then
88
+ @logger.debug "got : #{line}"
89
+ else
90
+ @logger.debug "got an empty line"
91
+ end
92
+
93
+ backend.process_input line
94
+ }
95
+ @thread.join
96
+ end
97
+ $stderr.puts "Exiting shell..."
98
+ end
99
+
100
+ end
data/lib/rhcp_shell.rb ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "readline"
4
+ require "logger"
5
+ require "getoptlong"
6
+
7
+ require 'rubygems'
8
+
9
+ require 'rhcp'
10
+
11
+ require 'base_shell'
12
+ require 'rhcp_shell_backend'
13
+
14
+ # TODO version number
15
+ SHELL_VERSION = "0.1"
16
+
17
+ class RhcpShell
18
+
19
+ HELP_STRING = <<EOF
20
+
21
+ RHCP Command Shell v #{SHELL_VERSION} (using RHCP library v #{RHCP::Version.to_s})
22
+
23
+ Usage:
24
+ rhcp_shell.rb [--hostname=<hostname>] [--username=<username> --password=<password>]
25
+ [--help]
26
+
27
+ Options:
28
+ --hostname=<hostname>
29
+ the URL to the RHCP server you want to connect against, e.g.
30
+ http://server.local.network/rhcp
31
+ If the specified hostname does not start with "http", it is automatically expanded to
32
+ http://<hostname>:42000/rhcp
33
+ You can optionally specify a port number to connect against like this:
34
+ http://myserver:42000
35
+ If you do not specify a hostname, the shell will try to connect against
36
+ http://localhost:42000
37
+
38
+ --username/--password
39
+ the authentication data you want to use for connecting to the RHCP server
40
+
41
+ --help
42
+ displays this help screen.
43
+
44
+ EOF
45
+
46
+ def run
47
+ opts = GetoptLong.new(
48
+ [ '--help', '-h', GetoptLong::NO_ARGUMENT ],
49
+ [ '--username', '-u', GetoptLong::REQUIRED_ARGUMENT ],
50
+ [ '--password', '-p', GetoptLong::REQUIRED_ARGUMENT ],
51
+ [ '--hostname', GetoptLong::REQUIRED_ARGUMENT ]
52
+ )
53
+
54
+ options = Hash.new
55
+ opts.each do |opt, arg|
56
+ case opt
57
+ when '--help'
58
+ puts HELP_STRING
59
+ Kernel.exit(1)
60
+ else
61
+ opt =~ /--(.+)/
62
+ $logger.debug "setting #{arg} for #{$1}" unless $1 == "password"
63
+ options[$1] = arg
64
+ end
65
+ end
66
+
67
+ host = options["hostname"]
68
+ if host == nil then
69
+ host = "http://localhost:42000"
70
+ else
71
+ if host !~ /http:/ then
72
+ host = "http://#{host}:42000/rhcp"
73
+ end
74
+ end
75
+ $logger.debug "now connecting to #{host}"
76
+
77
+ # TODO add interactive query for password!
78
+
79
+ begin
80
+ url = URI.parse(host)
81
+ @http_broker = RHCP::Client::HttpBroker.new(url)
82
+
83
+ backend = RHCPShellBackend.new(@http_broker)
84
+ backend.banner = <<EOF
85
+ Good morning, this is the generic RHCP Shell.
86
+ Press <tab> for command completion or type "help" for a list of commands.
87
+ If you want to exit this shell, please press Ctrl+C or type "exit".
88
+
89
+ EOF
90
+ $logger.debug "backend has been instantiated : #{backend}"
91
+
92
+ shell = BaseShell.new(backend)
93
+ shell.run
94
+ rescue => ex
95
+ puts "There occurred an HTTP error while connecting to the RHCP: #{ex}"
96
+ puts "Please connect against another server or fix the connection problem."
97
+ puts ex.backtrace.join("\n")
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ # TODO introduce something like the RAILS_ENVs
104
+ $logger = Logger.new("rhcp_shell.log")
105
+ RHCP::ModuleHelper.instance().logger = $logger
106
+
107
+ shell = RhcpShell.new
108
+ shell.run
@@ -0,0 +1,513 @@
1
+ require 'shell_backend.rb'
2
+
3
+ require 'rubygems'
4
+ require 'rhcp'
5
+
6
+ # This shell presents RHCP commands to the user and handles all the parameter
7
+ # lookup, validation and command completion stuff
8
+ #
9
+ # It uses a RHCP registry/broker as a data backend and possibly for communication with
10
+ # a server.
11
+ #
12
+ # This shell implementation handles two modes - one for entering/selecting
13
+ # a command, another for entering/selecting parameter values.
14
+ # If a command and all mandatory parameters are entered/selected, the command is
15
+ # executed.
16
+ class RHCPShellBackend < ShellBackend
17
+
18
+ attr_reader :prompt
19
+ attr_accessor :banner
20
+
21
+ def initialize(command_broker)
22
+ super()
23
+
24
+ local_broker = setup_local_broker
25
+ @command_broker = RHCP::DispatchingBroker.new()
26
+ @command_broker.add_broker(command_broker)
27
+ @command_broker.add_broker(local_broker)
28
+
29
+ reset_to_command_mode
30
+ end
31
+
32
+ def setup_local_broker
33
+ broker = RHCP::Broker.new()
34
+ begin
35
+ broker.register_command RHCP::Command.new("exit", "closes the shell",
36
+ lambda { |req,res|
37
+ puts "Have a nice day"
38
+ Kernel.exit(0)
39
+ }
40
+ )
41
+
42
+ def param_example(param, suffix = "")
43
+ result = ""
44
+ result += "<" if param.is_default_param
45
+
46
+ if param.is_default_param then
47
+ result += "#{param.name}#{suffix}"
48
+ else
49
+ result += "#{param.name}"
50
+ result += "=<value#{suffix}>"
51
+ end
52
+
53
+ result += ">" if param.is_default_param
54
+ result
55
+ end
56
+
57
+ command = RHCP::Command.new("help", "displays help about this shell",
58
+ lambda {
59
+ |req,res|
60
+
61
+ if (req.has_param_value("command"))
62
+ command_name = req.get_param_value("command")
63
+ puts "Syntax:"
64
+ command_line = " #{command_name}"
65
+ command = @command_broker.get_command(command_name)
66
+
67
+ command.params.values.sort { |a,b| a.name <=> b.name }.each do |param|
68
+ command_line += " "
69
+ command_line += "[" unless param.mandatory
70
+
71
+ command_line += param_example(param)
72
+
73
+ if param.allows_multiple_values then
74
+ command_line += ", "
75
+ command_line += param_example(param, "2")
76
+ command_line += ", ..."
77
+ end
78
+
79
+ command_line += "]" unless param.mandatory
80
+ end
81
+ puts command_line
82
+ puts "Description:"
83
+ puts " #{command.description}"
84
+ if command.params.size > 0 then
85
+ puts "Parameters:"
86
+ command.params.values.sort { |a,b| a.name <=> b.name }.each do |param|
87
+ puts sprintf(" %-20s %s\n", param.name, param.description)
88
+ end
89
+ end
90
+ puts ""
91
+ else
92
+ puts "The following commands are available:"
93
+ @command_broker.get_command_list.values.sort { |a,b| a.name <=> b.name }.each do |command|
94
+ # TODO calculate the maximum command name length dynamically
95
+ puts sprintf(" %-40s %s\n", command.name, command.description)
96
+ end
97
+ puts ""
98
+ puts "Type help <command name> for detailed information about a command."
99
+ end
100
+ }
101
+ ).add_param(RHCP::CommandParam.new("command", "the name of the command to display help for",
102
+ {
103
+ :mandatory => false,
104
+ :is_default_param => true,
105
+ :lookup_method => lambda {
106
+ @command_broker.get_command_list.values.map { |c| c.name }
107
+ }
108
+ }
109
+ )
110
+ )
111
+ command.result_hints[:display_type] = "hidden"
112
+ broker.register_command command
113
+
114
+ command = RHCP::Command.new("detail", "shows details about a single record of the last response (makes sense if you executed a command that returned a table)",
115
+ lambda { |req,res|
116
+ if @last_response == nil
117
+ puts "did not find any old response data...is it possible that you did not execute a command yet that returned a table?"
118
+ return
119
+ end
120
+ row_count = @last_response.data.length
121
+ begin
122
+ row_index = req.get_param_value("row_index").to_i
123
+ raise "invalid index" if (row_index < 1 || row_index > row_count)
124
+ rescue
125
+ puts "invalid row index - please specify a number between 1 and #{row_count}"
126
+ return
127
+ end
128
+ puts "displaying details about row \# #{row_index}"
129
+ @last_response.data[row_index - 1].each do |k,v|
130
+ puts " #{k}\t#{v}"
131
+ end
132
+
133
+ }
134
+ ).add_param(RHCP::CommandParam.new("row_index", "the index of the row you want to see details about",
135
+ {
136
+ :is_default_param => true,
137
+ :mandatory => true
138
+ }
139
+ )
140
+ )
141
+ command.result_hints[:display_type] = "hidden"
142
+ broker.register_command command
143
+
144
+ rescue RHCP::RhcpException => ex
145
+ # TODO do we really want to catch this here?
146
+ raise ex unless /duplicate command name/ =~ ex.to_s
147
+ end
148
+ broker
149
+ end
150
+
151
+ def reset_to_command_mode
152
+ set_prompt nil
153
+
154
+ # this shell has two modes that determine the available tab completion proposals
155
+ # command_mode
156
+ # we're waiting for the user to pick a command that should be executed
157
+ # parameter mode
158
+ # the command to execute has already been selected, but the user needs to specify additional parameters
159
+ # we'll start in the mode where no command has been selected yet
160
+ @command_selected = nil
161
+
162
+ # if the user selected a command already, we'll have to collect parameters for this command until
163
+ # we've got all mandatory parameters so that we can execute the command
164
+ @collected_params = Hash.new
165
+
166
+ # the mandatory params that are still missing (valid in parameter mode only)
167
+ @missing_params = Array.new
168
+
169
+ # the parameter that we're asking for right now
170
+ @current_param = nil
171
+ end
172
+
173
+ def execute_command_if_possible
174
+ # check if we got all mandatory params now
175
+ mandatory_params = @command_selected.params.select { |name,param| param.mandatory }.map { |k,v| v }
176
+ @missing_params = mandatory_params.select { |p| ! @collected_params.include? p.name }
177
+
178
+ if (@missing_params.size > 0) then
179
+ $logger.debug "got #{@missing_params.size} missing params : #{@missing_params.map{|param| param.name}}"
180
+ @current_param = @missing_params[0]
181
+ set_prompt "#{@command_selected.name}.#{@current_param.name}"
182
+ else
183
+ execute_command
184
+ end
185
+ end
186
+
187
+
188
+ def pre_process_param_value(new_value)
189
+ $logger.debug "resolving wildcards for param '#{@current_param.name}'"
190
+
191
+ # we can only resolve wildcards if we have lookup values
192
+ if (@current_param.has_lookup_values)
193
+ # TODO this is only necessary if we've got multiple values, right?
194
+ # TODO maybe we want to check if 'new_value' holds suspicious characters that necessitate wildcard resolution?
195
+ # TODO is the range handling possible only with lookup values?
196
+ # convert "*" into regexp notation ".*"
197
+ regex_str = new_value.gsub(/\*/, '.*')
198
+
199
+ # handle ranges (x..y)
200
+ result = /(.+?)(\d+)(\.{2})(\d+)(.*)/.match(regex_str)
201
+ ranged_regex = nil
202
+ if result then
203
+ $logger.debug "captures : #{result.captures.map { |v| " #{v} "}}"
204
+ result.captures[1].upto(result.captures[3]) do |loop|
205
+ regex_for_this_number = "#{result.captures[0]}#{loop}#{result.captures[4]}"
206
+ $logger.debug "regex for #{loop} : #{regex_for_this_number}"
207
+ if ranged_regex == nil then
208
+ ranged_regex = Regexp.new(regex_for_this_number)
209
+ else
210
+ ranged_regex = Regexp.union(ranged_regex, Regexp.new(regex_for_this_number))
211
+ end
212
+ end
213
+ else
214
+ ranged_regex = Regexp.new(regex_str)
215
+ end
216
+
217
+ $logger.debug "wildcard regexp : #{ranged_regex}"
218
+
219
+ re = ranged_regex
220
+
221
+ # get lookup values, filter and return them
222
+ lookup_values = @current_param.get_lookup_values()
223
+ lookup_values.select { |lookup_value| re.match(lookup_value) }
224
+ else
225
+ [ new_value ]
226
+ end
227
+ end
228
+
229
+ # checks param values for validity and adds them to our value collection
230
+ # expands wildcard parameters if appropriate
231
+ # returns the values that have been added (might be more than 'new_value' when wildcards are used)
232
+ def add_parameter_value(new_value)
233
+ # pre-process the value if necessary
234
+ processed_param_values = pre_process_param_value(new_value)
235
+ # TODO this check is already part of check_param_is_valid (which is called later in this method and when the request is created) - we do not want to check this three times...?
236
+ if processed_param_values.size == 0
237
+ raise RHCP::RhcpException.new("invalid value '#{new_value}' for parameter '#{@current_param.name}'")
238
+ end
239
+ #processed_param_values = [ new_value ]
240
+ processed_param_values.each do |value|
241
+ # TODO we need to pass a value array here, and we should include the already selected param values
242
+ @current_param.check_param_is_valid([ value ])
243
+ $logger.debug "accepted value #{value} for param #{@current_param.name}"
244
+
245
+ @collected_params[@current_param.name] = Array.new if @collected_params[@current_param.name] == nil
246
+ @collected_params[@current_param.name] << value
247
+ end
248
+ processed_param_values
249
+ end
250
+
251
+ def print_cell(col_name, the_value)
252
+ result = "| "
253
+ result += the_value.to_s
254
+ 1.upto(@max_width[col_name] - the_value.to_s.length) { |i|
255
+ result += " "
256
+ }
257
+ result += " "
258
+ result
259
+ end
260
+
261
+ def print_line
262
+ result = ""
263
+ @total_width.times { |i| result += "-" }
264
+ result += "\n"
265
+ result
266
+ end
267
+
268
+ def execute_command
269
+ begin
270
+ command = @command_broker.get_command(@command_selected.name)
271
+ request = RHCP::Request.new(command, @collected_params)
272
+ response = command.execute_request(request)
273
+ if (response.status == RHCP::Response::Status::OK)
274
+ $logger.debug "raw result : #{response.data}"
275
+ $logger.debug "display_type : #{command.result_hints[:display_type]}"
276
+ if command.result_hints[:display_type] == "table"
277
+ @last_response = response # we might want to access this response in further commands
278
+ # TODO make sure that the response really holds the correct data types and that we've got at least one column
279
+ # TODO check that all columns in overview_columns are valid
280
+ # TODO check that all columns in column_titles are valid and match overview_columns
281
+ output = ""
282
+
283
+ # let's find out which columns we want to display
284
+ $logger.debug "overview columns : #{command.result_hints[:overview_columns]}"
285
+ columns_to_display = command.result_hints.has_key?(:overview_columns) ?
286
+ command.result_hints[:overview_columns].clone() :
287
+ # by default, we'll display all columns, sorted alphabetically
288
+ columns_to_display = response.data[0].keys.sort
289
+
290
+ # and which titles they should have (default : column names)
291
+ column_title_list = command.result_hints.has_key?(:column_titles) ?
292
+ command.result_hints[:column_titles].clone() :
293
+ column_title_list = columns_to_display
294
+
295
+ # TODO the sorting column should be configurable
296
+ first_column = columns_to_display[0]
297
+ $logger.debug "sorting by #{first_column}"
298
+ response.data = response.data.sort { |a,b| a[first_column] <=> b[first_column] }
299
+
300
+ # add the index column
301
+ columns_to_display.unshift "__idx"
302
+ column_title_list.unshift "\#"
303
+ count = 1
304
+ response.data.each do |row|
305
+ row["__idx"] = count
306
+ count = count+1
307
+ end
308
+
309
+
310
+ column_titles = {}
311
+ 0.upto(column_title_list.length - 1) do |i|
312
+ column_titles[columns_to_display[i]] = column_title_list[i]
313
+ end
314
+ $logger.debug "column title : #{column_titles}"
315
+
316
+ # find the maximum column width for each column
317
+ @max_width = {}
318
+ response.data.each do |row|
319
+ row.each do |k,v|
320
+ if ! @max_width.has_key?(k) || v.to_s.length > @max_width[k]
321
+ @max_width[k] = v.to_s.length
322
+ end
323
+ end
324
+ end
325
+
326
+ # check the column_title
327
+ columns_to_display.each do |col_name|
328
+ if column_titles[col_name].length > @max_width[col_name]
329
+ @max_width[col_name] = column_titles[col_name].length
330
+ end
331
+ end
332
+ #@max_width["row_count"] = response.data.length.to_s.length
333
+ $logger.debug "max width : #{@max_width}"
334
+
335
+ # and build headers
336
+ @total_width = 2 + columns_to_display.length-1 # separators at front and end of table and between the values
337
+ columns_to_display.each do |col|
338
+ @total_width += @max_width[col] + 2 # each column has a space in front and behind the value
339
+ end
340
+ output += print_line
341
+
342
+ columns_to_display.each do |col|
343
+ output += print_cell(col, column_titles[col])
344
+ end
345
+ output += "|\n"
346
+
347
+ output += print_line
348
+
349
+ # print the table values
350
+ response.data.each do |row|
351
+ columns_to_display.each do |col|
352
+ output += print_cell(col, row[col])
353
+ end
354
+ output += "|\n"
355
+ end
356
+ output += print_line
357
+
358
+ puts output
359
+ elsif command.result_hints[:display_type] == "list"
360
+ output = ""
361
+ response.data.each do |row|
362
+ output += "#{row}\n"
363
+ end
364
+ puts output
365
+ elsif command.result_hints[:display_type] == "hidden"
366
+ $logger.debug "suppressing output due to display_type 'hidden'"
367
+ else
368
+ puts "executed '#{@command_selected.name}' successfully : #{response.data}"
369
+ end
370
+ else
371
+ puts "could not execute '#{@command_selected.name}' : #{response.error_text}"
372
+ $logger.error "#{response.error_text} : #{response.error_detail}"
373
+ end
374
+ reset_to_command_mode
375
+ rescue
376
+ puts "got an error : #{$!}"
377
+ raise
378
+ end
379
+ end
380
+
381
+ ## the following methods are overridden from ShellBackend
382
+
383
+ def process_input(command_line)
384
+ $logger.debug "processing input '#{command_line}'"
385
+
386
+ if (@command_selected) then
387
+ # we're in parameter processing mode - so check which parameter
388
+ # we've got now and switch modes if necessary
389
+
390
+ # we might have been waiting for multiple param values - check if the user finished
391
+ # adding values by selecting an empty string as value
392
+ if (@current_param.allows_multiple_values and command_line == "") then
393
+ $logger.debug "finished multiple parameter input mode for param #{@current_param.name}"
394
+ @missing_params.shift
395
+ execute_command_if_possible
396
+ else
397
+ accepted_params = add_parameter_value(command_line)
398
+ if accepted_params
399
+ # stop asking for more values if
400
+ # a) the parameter does not allow more than one value
401
+ # b) the user entered a wildcard parameter that has been expanded to multiple values
402
+ if (! @current_param.allows_multiple_values or accepted_params.length > 1) then
403
+ $logger.debug "finished parameter input mode for param #{@current_param.name}"
404
+ @missing_params.shift
405
+ execute_command_if_possible
406
+ else
407
+ $logger.debug "param '#{@current_param.name}' expects multiple values...deferring mode switching"
408
+ end
409
+ end
410
+ end
411
+ else
412
+ # we're waiting for the user to enter a command
413
+ # we might have a command with params
414
+ command, *params = command_line.split
415
+ $logger.debug "got command '#{command}' (params: #{params})"
416
+
417
+ # remember what the user specified so far
418
+ begin
419
+ @command_selected = @command_broker.get_command(command)
420
+ #if (@command_selected != nil) then
421
+ $logger.debug "command_selected: #{@command_selected}"
422
+
423
+ # apply the preset param values to this new command
424
+ # TODO reactivate parameter presets
425
+ # @command_broker.get_global_parameter_presets().each do |name, values|
426
+ # # check if the selected command needs this param
427
+ # @current_param = @command_broker.get_param(command, name)
428
+ # if @current_param and not @current_param.ignore_global_presets then
429
+ # $logger.debug "presetting global parameter '#{name}' to '#{values}'"
430
+ # values.each do |value|
431
+ # add_parameter_value(value)
432
+ # end
433
+ # end
434
+ # end
435
+
436
+ # process the params specified on the command line
437
+ if (params != nil) then
438
+ params.each do |param|
439
+ if param =~ /(.+?)=(.+)/ then
440
+ # --> named param
441
+ key = $1
442
+ value = $2
443
+ else
444
+ # TODO if there's only one param, we can always use this as default param (maybe do this in the command?)
445
+ # --> unnamed param
446
+ value = param
447
+ default_param = @command_selected.default_param
448
+ if default_param != nil then
449
+ key = default_param.name
450
+ $logger.debug "collecting value '#{value}' for default param '#{default_param.name}'"
451
+ else
452
+ $logger.info "ignoring param '#{value}' because there's no default param"
453
+ end
454
+ end
455
+
456
+ if key then
457
+ begin
458
+ @current_param = @command_selected.get_param(key)
459
+ add_parameter_value(value)
460
+ rescue RHCP::RhcpException => ex
461
+ puts ex.to_s
462
+ end
463
+ end
464
+ end
465
+ end
466
+
467
+ $logger.debug "selected command #{@command_selected.name}"
468
+ execute_command_if_possible
469
+ rescue RHCP::RhcpException => ex
470
+ puts "#{ex}"
471
+ end
472
+ end
473
+ rescue => err
474
+ $logger.error err
475
+ puts "exception raised: #{err.to_s}"
476
+ end
477
+
478
+ def complete(word = "")
479
+ $logger.debug "collection completion values for '#{word}'"
480
+
481
+ if (@command_selected) then
482
+ # TODO include partial_value here?
483
+ props = @command_selected.params[@current_param.name].get_lookup_values()
484
+ #props = @command_broker.get_lookup_values(@command_selected.name, @current_param.name)
485
+ else
486
+ props = @command_broker.get_command_list.values.map{|command| command.name}.sort
487
+ end
488
+
489
+ proposal_list = props.map { |p| "'#{p}'" }.join(" ")
490
+ $logger.debug "completion proposals: #{proposal_list}"
491
+
492
+ prefix = word
493
+ props.select{|name|name[0...(prefix.size)] == prefix}
494
+ end
495
+
496
+ def show_banner
497
+ puts @banner
498
+ end
499
+
500
+ def set_prompt(string)
501
+ @prompt = "#{string ? string + " " : ""}$ "
502
+ end
503
+
504
+ def process_ctrl_c
505
+ puts ""
506
+ if @command_selected then
507
+ reset_to_command_mode
508
+ else
509
+ Kernel.exit
510
+ end
511
+ end
512
+
513
+ end
@@ -0,0 +1,29 @@
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 ShellBackend
4
+
5
+ # is called whenever the user submits a command (hitting enter)
6
+ def process_input(command_line)
7
+ raise "not implemented in ShellBackend!"
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 ShellBackend!"
14
+ end
15
+
16
+ # is called by the shell to get the prompt that should be displayed
17
+ def prompt
18
+ ">"
19
+ end
20
+
21
+ def process_ctrl_c
22
+
23
+ end
24
+
25
+ def show_banner
26
+
27
+ end
28
+
29
+ end
data/start_shell.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+
3
+ ruby -Ilib lib/rhcp_shell.rb
@@ -0,0 +1,236 @@
1
+ #
2
+ # To change this template, choose Tools | Templates
3
+ # and open the template in the editor.
4
+
5
+
6
+ $:.unshift File.join(File.dirname(__FILE__),'..','lib')
7
+
8
+ require 'test/unit'
9
+ require 'rhcp_shell_backend'
10
+
11
+ require 'rubygems'
12
+ require 'rhcp'
13
+ require 'logger'
14
+
15
+ require 'setup_test_registry'
16
+
17
+ class RhcpShellBackendTest < Test::Unit::TestCase
18
+
19
+ # Backend that "remembers" everything it ever printed to the user
20
+ class BackendMock < RHCPShellBackend
21
+
22
+ def initialize(broker, on_receive)
23
+ super(broker)
24
+ @on_receive = on_receive
25
+ end
26
+
27
+ def process_input(line)
28
+ $stdout.puts "[user] #{line}"
29
+ super(line)
30
+ end
31
+
32
+ def puts(what)
33
+ $stdout.puts "[shell] #{what}"
34
+ @on_receive.call(what)
35
+ end
36
+
37
+ def set_prompt(new_prompt)
38
+ $stdout.puts "[shell: switching prompt to '#{new_prompt}']"
39
+ super(new_prompt)
40
+ end
41
+
42
+ def complete(word = "")
43
+ result = super(word)
44
+ proposal_list = result.map { |p| "'#{p}'" }.join(" ")
45
+ $stdout.puts "[completion proposals : #{proposal_list}]"
46
+ result
47
+ end
48
+
49
+ end
50
+
51
+ def setup
52
+ # set up a local broker that we'll use for testing
53
+ # TODO do something about this - it shouldn't be necessary to instantiate this beforehand
54
+ $logger = Logger.new($stdout)
55
+ @log = Array.new()
56
+ @backend = BackendMock.new($broker, self.method(:add_to_log))
57
+ end
58
+
59
+ def add_to_log(new_line)
60
+ #puts "adding to log (old size : #{@log.size}): *****#{new_line}*****"
61
+ @log << new_line
62
+ # TODO CAREFUL: if you un-comment the following line, @log will get screwed up
63
+ #puts "log now : >>#{@log.join("\n")}<<"
64
+ end
65
+
66
+ def assert_received(expected)
67
+ assert_equal(expected, @log.slice(- expected.length, expected.length))
68
+ # clean the log so that assert_no_error works
69
+ #@log.clear
70
+ end
71
+
72
+ def assert_no_error
73
+ assert_equal 0, @log.select { |line|
74
+ /error/i =~ line || /exception/i =~ line || /failed/i =~ line
75
+ }.size
76
+ end
77
+
78
+ def assert_log_contains(stuff)
79
+ is_ok = @log.grep(/#{stuff}/).size > 0
80
+ puts "checking log for #{stuff}"
81
+ assert is_ok, "log should contain '#{stuff}', but it doesn't : >>#{@log.join("\n")}<<"
82
+ end
83
+
84
+ def assert_prompt(expected)
85
+ if (expected == '')
86
+ assert_equal "$ ", @backend.prompt
87
+ else
88
+ assert_equal "#{expected} $ ", @backend.prompt
89
+ end
90
+ end
91
+
92
+ def test_banner
93
+ @backend.banner = "This is a test backend"
94
+ @backend.show_banner
95
+ assert_received [ "This is a test backend" ]
96
+ end
97
+
98
+ def test_simple_execute
99
+ @backend.process_input "test"
100
+ assert_received [ "executed 'test' successfully : 42" ]
101
+ end
102
+
103
+ def test_invalid_command
104
+ @backend.process_input "does not exist"
105
+ assert_received [ "no such command : does" ]
106
+ end
107
+
108
+ def test_missing_mandatory_param
109
+ @backend.process_input "reverse"
110
+ assert_prompt 'reverse.input'
111
+ @backend.process_input "zaphod"
112
+ assert_received [ "executed 'reverse' successfully : dohpaz" ]
113
+ end
114
+
115
+ def test_completion
116
+ @backend.process_input "reverse"
117
+ assert_prompt 'reverse.input'
118
+ assert_equal [ "zaphod", "beeblebrox" ], @backend.complete
119
+ end
120
+
121
+ def test_command_completion
122
+ commands = $broker.get_command_list.values.map { |command| command.name }
123
+ # we should have all remote commands plus "help" and "exit"
124
+ commands << "help"
125
+ commands << "exit"
126
+ commands << "detail"
127
+ assert_equal commands.sort, @backend.complete.sort
128
+ assert_no_error
129
+ end
130
+
131
+ def test_params_on_command_line
132
+ @backend.process_input "reverse input=zaphod"
133
+ assert_received [ "executed 'reverse' successfully : dohpaz" ]
134
+ end
135
+
136
+ def test_invalid_param_value
137
+ @backend.process_input "reverse input=bla"
138
+ assert_received [ "invalid value 'bla' for parameter 'input'" ]
139
+ end
140
+
141
+ def test_multi_params
142
+ @backend.process_input "cook"
143
+ assert_prompt 'cook.ingredient'
144
+ @backend.process_input "mascarpone"
145
+ @backend.process_input "chocolate"
146
+ @backend.process_input ""
147
+ assert_no_error
148
+ assert_received [ "executed 'cook' successfully : mascarpone chocolate" ]
149
+ end
150
+
151
+ # if the user is in command mode, he should be able to exit to command mode
152
+ # by pressing ctrl+c
153
+ def test_abort_param_mode
154
+ @backend.process_input "cook"
155
+ assert_prompt 'cook.ingredient'
156
+ @backend.process_ctrl_c
157
+ assert_prompt ''
158
+ end
159
+
160
+ def test_failing_command
161
+ @backend.process_input "perpetuum_mobile"
162
+ assert_received [ "could not execute 'perpetuum_mobile' : don't know how to do this" ]
163
+ end
164
+
165
+ def test_wildcard_support
166
+ @backend.process_input "cook ingredient=m*"
167
+ assert_received [ "executed 'cook' successfully : mascarpone marzipan" ]
168
+ end
169
+
170
+ def test_preprocess_without_lookup_values
171
+ @backend.process_input "test thoroughly=yes"
172
+ assert_no_error
173
+ end
174
+
175
+ def test_wildcard_ranges
176
+ @backend.process_input "echo input=string01..05"
177
+ assert_received [ "executed 'echo' successfully : string01 string02 string03 string04 string05" ]
178
+
179
+ @backend.process_input "echo input=string17..20"
180
+ assert_received [ "executed 'echo' successfully : string17 string18 string19 string20" ]
181
+ end
182
+
183
+ def test_help
184
+ @backend.process_input "help"
185
+ assert_log_contains "The following commands are available"
186
+ $broker.get_command_list.values.each do |command|
187
+ assert_log_contains command.name
188
+ end
189
+ end
190
+
191
+ def test_help_command
192
+ @log.clear
193
+ @backend.process_input "help cook"
194
+ puts "LOG >>#{@log}<<"
195
+ assert_log_contains "Syntax:"
196
+ assert_log_contains "cook ingredient=<value>, ingredient=<value2>, ..."
197
+ end
198
+
199
+ def test_help_with_default_param
200
+ @backend.process_input "help help"
201
+ puts "LOG >>#{@log}<<"
202
+ assert_log_contains "Syntax:"
203
+ assert_log_contains "help [<command>]"
204
+ end
205
+
206
+ def test_default_param
207
+ @backend.process_input "reverse zaphod"
208
+ assert_received [ "executed 'reverse' successfully : dohpaz" ]
209
+ end
210
+
211
+ # unnamed params should be ignored if no default params are specified
212
+ def test_unnamed_param_without_default_param
213
+ @backend.process_input "echo bla"
214
+ assert_received [ "executed 'echo' successfully : hello world" ]
215
+ end
216
+
217
+ def test_complete_without_lookup_values
218
+ @backend.process_input "length"
219
+ assert_prompt "length.input"
220
+ assert_equal [], @backend.complete
221
+ end
222
+
223
+ def test_table
224
+ # TODO write a separate test for this stuff
225
+ p $broker.get_command("build_a_table")
226
+ @backend.process_input "build_a_table"
227
+ # assert_received [
228
+ # "Zaphod\tBeeblebrox",
229
+ # "Arthur\tDent",
230
+ # "Prostetnik\tYoltz(?)"
231
+ # ]
232
+ end
233
+
234
+ # TODO test behaviour after an internal error occurred in the shell (e.g. some problem while formatting the result)
235
+
236
+ end
@@ -0,0 +1,109 @@
1
+ require 'rubygems'
2
+ require 'rhcp'
3
+
4
+
5
+ broker = RHCP::Broker.new()
6
+ broker.clear()
7
+ broker.register_command RHCP::Command.new("test", "just a test command",
8
+ lambda { |req,res|
9
+ 42
10
+ }
11
+ ).add_param(RHCP::CommandParam.new("thoroughly", "an optional param",
12
+ {
13
+ :mandatory => false
14
+ }
15
+ )
16
+ )
17
+ broker.register_command RHCP::Command.new("echo", "prints a string",
18
+ lambda { |req,res|
19
+ strings = req.has_param_value("input") ? req.get_param_value("input") : [ "hello world" ]
20
+ result = Array.new
21
+ strings.each do |s|
22
+ puts s
23
+ result << s
24
+ end
25
+ result.join(" ")
26
+ }
27
+ ).add_param(RHCP::CommandParam.new("input", "an optional param",
28
+ {
29
+ :mandatory => false,
30
+ :allows_multiple_values => true,
31
+ :lookup_method => lambda {
32
+ values = Array.new()
33
+ 1.upto(20) do |i|
34
+ values << "string#{sprintf("%02d", i)}"
35
+ end
36
+ values
37
+ }
38
+ }
39
+ )
40
+ )
41
+ broker.register_command RHCP::Command.new("reverse", "reversing input strings",
42
+ lambda { |req,res|
43
+ req.get_param_value("input").reverse
44
+ }
45
+ ).add_param(RHCP::CommandParam.new("input", "the string to reverse",
46
+ {
47
+ :lookup_method => lambda { [ "zaphod", "beeblebrox" ] },
48
+ :mandatory => true,
49
+ :is_default_param => true
50
+ }
51
+ )
52
+ )
53
+ broker.register_command RHCP::Command.new("cook", "cook something nice out of some ingredients",
54
+ lambda { |req,res|
55
+ ingredients = req.get_param_value("ingredient").join(" ")
56
+ puts "cooking something with #{ingredients}"
57
+ ingredients
58
+ }
59
+ ).add_param(RHCP::CommandParam.new("ingredient", "something to cook with",
60
+ {
61
+ :lookup_method => lambda { [ "mascarpone", "chocolate", "eggs", "butter", "marzipan" ] },
62
+ :allows_multiple_values => true,
63
+ :mandatory => true
64
+ }
65
+ )
66
+ )
67
+ broker.register_command RHCP::Command.new("perpetuum_mobile", "this command will fail",
68
+ lambda { |req,res|
69
+ raise "don't know how to do this"
70
+ }
71
+ )
72
+ broker.register_command RHCP::Command.new("length", "returns the length of a string",
73
+ lambda { |req,res|
74
+ req.get_param_value("input").length
75
+ }
76
+ ).add_param(RHCP::CommandParam.new("input", "the string to reverse",
77
+ {
78
+ :mandatory => true,
79
+ :is_default_param => true
80
+ }
81
+ )
82
+ )
83
+
84
+ command = RHCP::Command.new("list_stuff", "this command lists stuff",
85
+ lambda { |req,res|
86
+ [ "peace", "aquaeduct", "education" ]
87
+ }
88
+ )
89
+ command.mark_as_read_only()
90
+ command.result_hints[:display_type] = "list"
91
+ broker.register_command command
92
+
93
+ command2 = RHCP::Command.new("build_a_table", "this command returns tabular data",
94
+ lambda { |req,res|
95
+ [
96
+ { :first_name => "Zaphod", :last_name => "Beeblebrox", :heads => 2, :character => "dangerous" },
97
+ { :first_name => "Arthur", :last_name => "Dent", :heads => 1, :character => "harmless (mostly)" },
98
+ { :first_name => "Prostetnik", :last_name => "Yoltz (?)", :heads => 1, :character => "ugly" }
99
+ ]
100
+ }
101
+ )
102
+ command2.mark_as_read_only()
103
+ command2.result_hints[:display_type] = "table"
104
+ command2.result_hints[:overview_columns] = [ "first_name", "last_name" ]
105
+ broker.register_command command2
106
+
107
+ p broker.get_command("build_a_table")
108
+
109
+ $broker = broker
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rhcp_shell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Philipp T.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-23 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.3
24
+ version:
25
+ description: FIX (describe your package)
26
+ email:
27
+ - philipp@hitchhackers.net
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - README
42
+ - lib/base_shell.rb
43
+ - lib/rhcp_shell.rb
44
+ - lib/rhcp_shell_backend.rb
45
+ - lib/shell_backend.rb
46
+ - start_shell.sh
47
+ - test/rhcp_shell_backend_test.rb
48
+ - test/setup_test_registry.rb
49
+ has_rdoc: true
50
+ homepage: FIX (url)
51
+ post_install_message:
52
+ rdoc_options:
53
+ - --main
54
+ - README.txt
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project: rhcp
72
+ rubygems_version: 1.3.1
73
+ signing_key:
74
+ specification_version: 2
75
+ summary: FIX (describe your package)
76
+ test_files: []
77
+