rhcp_shell 0.0.1

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