tac 0.6.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 (43) hide show
  1. data/LICENSE +24 -0
  2. data/README.md +102 -0
  3. data/Rakefile +101 -0
  4. data/bin/tac +6 -0
  5. data/caldecott_helper/Gemfile +10 -0
  6. data/caldecott_helper/Gemfile.lock +48 -0
  7. data/caldecott_helper/server.rb +43 -0
  8. data/config/clients.yml +17 -0
  9. data/config/micro/offline.conf +2 -0
  10. data/config/micro/paths.yml +22 -0
  11. data/config/micro/refresh_ip.rb +20 -0
  12. data/lib/cli/commands/admin.rb +108 -0
  13. data/lib/cli/commands/apps.rb +1133 -0
  14. data/lib/cli/commands/base.rb +232 -0
  15. data/lib/cli/commands/manifest.rb +56 -0
  16. data/lib/cli/commands/micro.rb +115 -0
  17. data/lib/cli/commands/misc.rb +129 -0
  18. data/lib/cli/commands/services.rb +242 -0
  19. data/lib/cli/commands/user.rb +65 -0
  20. data/lib/cli/config.rb +173 -0
  21. data/lib/cli/console_helper.rb +160 -0
  22. data/lib/cli/core_ext.rb +122 -0
  23. data/lib/cli/errors.rb +19 -0
  24. data/lib/cli/frameworks.rb +276 -0
  25. data/lib/cli/manifest_helper.rb +341 -0
  26. data/lib/cli/runner.rb +547 -0
  27. data/lib/cli/services_helper.rb +92 -0
  28. data/lib/cli/tunnel_helper.rb +332 -0
  29. data/lib/cli/usage.rb +115 -0
  30. data/lib/cli/version.rb +7 -0
  31. data/lib/cli/zip_util.rb +77 -0
  32. data/lib/cli.rb +47 -0
  33. data/lib/vmc/client.rb +573 -0
  34. data/lib/vmc/const.rb +24 -0
  35. data/lib/vmc/micro/switcher/base.rb +97 -0
  36. data/lib/vmc/micro/switcher/darwin.rb +19 -0
  37. data/lib/vmc/micro/switcher/dummy.rb +15 -0
  38. data/lib/vmc/micro/switcher/linux.rb +16 -0
  39. data/lib/vmc/micro/switcher/windows.rb +31 -0
  40. data/lib/vmc/micro/vmrun.rb +158 -0
  41. data/lib/vmc/micro.rb +56 -0
  42. data/lib/vmc.rb +3 -0
  43. metadata +295 -0
@@ -0,0 +1,242 @@
1
+ require "uuidtools"
2
+
3
+ module VMC::Cli::Command
4
+
5
+ class Services < Base
6
+ include VMC::Cli::ServicesHelper
7
+ include VMC::Cli::TunnelHelper
8
+
9
+ def services
10
+ ss = client.services_info
11
+ ps = client.services
12
+ ps.sort! {|a, b| a[:name] <=> b[:name] }
13
+
14
+ if @options[:json]
15
+ services = { :system => ss, :provisioned => ps }
16
+ return display JSON.pretty_generate(services)
17
+ end
18
+ display_system_services(ss)
19
+ display_provisioned_services(ps)
20
+ end
21
+
22
+ def create_service(service=nil, name=nil, appname=nil)
23
+ unless no_prompt || service
24
+ services = client.services_info
25
+ err 'No services available to provision' if services.empty?
26
+ service = ask(
27
+ "Which service would you like to provision?",
28
+ { :indexed => true,
29
+ :choices =>
30
+ services.values.collect { |type|
31
+ type.keys.collect(&:to_s)
32
+ }.flatten
33
+ }
34
+ )
35
+ end
36
+ name = @options[:name] unless name
37
+
38
+ unless name
39
+ name = random_service_name(service)
40
+ picked_name = true
41
+ end
42
+
43
+ if "mysql" == service
44
+ databaseChoice = %w[internal_database external_database]
45
+ dbflag = ask("which type would you like to choice?", {
46
+ :indexed => true,
47
+ :choices => databaseChoice
48
+ }
49
+ )
50
+ dbsize = ask "max_db_size"
51
+ regix = /^[1-9]\d*$/
52
+ err "max_db_size required" unless dbsize
53
+ err "max_db_size must be int and max_db_size more than 0" unless regix.match(dbsize)
54
+ end
55
+ if "external_database" == dbflag
56
+ dbjndi = ask "jndi name"
57
+ dburl = ask "dburl"
58
+ dbuname = ask "dbusername "
59
+ dbpasswd = ask("dbpasswd", :echo => "*")
60
+
61
+ err "jndi required" unless dbjndi
62
+ err "dburl required" unless dburl
63
+ err "dbuname required" unless dbuname
64
+ err "dbpasswd required" unless dbpasswd
65
+ end
66
+
67
+ # if "mysql" == service
68
+ # create_service_banner(service, name, picked_name)
69
+ # elsif
70
+ # create_service_banner(service, name, picked_name)
71
+ # end
72
+
73
+ create_service_banner(service, name, dbflag, dbjndi, dbuname, dbpasswd, dburl, dbsize, picked_name)
74
+
75
+ appname = @options[:bind] unless appname
76
+ bind_service_banner(name, appname) if appname
77
+ end
78
+
79
+ def delete_service(service=nil)
80
+ unless no_prompt || service
81
+ user_services = client.services
82
+ err 'No services available to delete' if user_services.empty?
83
+ service = ask(
84
+ "Which service would you like to delete?",
85
+ { :indexed => true,
86
+ :choices => user_services.collect { |s| s[:name] }
87
+ }
88
+ )
89
+ end
90
+ err "Service name required." unless service
91
+ display "Deleting service [#{service}]: ", false
92
+ client.delete_service(service)
93
+ display 'OK'.green
94
+ end
95
+
96
+ def quota_service(service=nil)
97
+ unless no_prompt || service
98
+ user_services = client.services
99
+ err 'No services available to delete' if user_services.empty?
100
+ service = ask(
101
+ "Which service would you like to update?",
102
+ { :indexed => true,
103
+ :choices => user_services.collect { |s| s[:name] }
104
+ }
105
+ )
106
+ end
107
+ err "Service name required." unless service
108
+ display "Update service's quota [#{service}]: ", false
109
+ # flag = service.index("mysql-")
110
+ #if flag
111
+ max_db_size = client.check_quota(service)
112
+ display "\ncurrent max_db_size is [#{max_db_size} MB], Please enter the number of more than #{max_db_size} : "
113
+ dbsize = ask "max_db_size"
114
+ regix = /^[1-9]\d*$/
115
+ err "max_db_size required" unless dbsize
116
+ err "max_db_size must be int and max_db_size more than 0" unless regix.match(dbsize)
117
+ err "Please enter the number of more than #{max_db_size}" unless Integer(dbsize) > Integer(max_db_size)
118
+ # else
119
+ # err "Temporary does not support"
120
+ # end
121
+ client.quota_service(service, dbsize, "update")
122
+ display 'OK'.green
123
+ end
124
+
125
+ def bind_service(service, appname)
126
+ bind_service_banner(service, appname)
127
+ end
128
+
129
+ def unbind_service(service, appname)
130
+ unbind_service_banner(service, appname)
131
+ end
132
+
133
+ def clone_services(src_app, dest_app)
134
+ begin
135
+ src = client.app_info(src_app)
136
+ dest = client.app_info(dest_app)
137
+ rescue
138
+ end
139
+
140
+ err "Application '#{src_app}' does not exist" unless src
141
+ err "Application '#{dest_app}' does not exist" unless dest
142
+
143
+ services = src[:services]
144
+ err 'No services to clone' unless services && !services.empty?
145
+ services.each { |service| bind_service_banner(service, dest_app, false) }
146
+ check_app_for_restart(dest_app)
147
+ end
148
+
149
+ def tunnel(service=nil, client_name=nil)
150
+ unless defined? Caldecott
151
+ display "To use `vmc tunnel', you must first install Caldecott:"
152
+ display ""
153
+ display "\tgem install caldecott"
154
+ display ""
155
+ display "Note that you'll need a C compiler. If you're on OS X, Xcode"
156
+ display "will provide one. If you're on Windows, try DevKit."
157
+ display ""
158
+ display "This manual step will be removed in the future."
159
+ display ""
160
+ err "Caldecott is not installed."
161
+ end
162
+
163
+ ps = client.services
164
+ err "No services available to tunnel to" if ps.empty?
165
+
166
+ unless service
167
+ choices = ps.collect { |s| s[:name] }.sort
168
+ service = ask(
169
+ "Which service to tunnel to?",
170
+ :choices => choices,
171
+ :indexed => true
172
+ )
173
+ end
174
+
175
+ info = ps.select { |s| s[:name] == service }.first
176
+
177
+ err "Unknown service '#{service}'" unless info
178
+
179
+ port = pick_tunnel_port(@options[:port] || 10000)
180
+
181
+ raise VMC::Client::AuthError unless client.logged_in?
182
+
183
+ if not tunnel_pushed?
184
+ display "Deploying tunnel application '#{tunnel_appname}'."
185
+ auth = UUIDTools::UUID.random_create.to_s
186
+ push_caldecott(auth)
187
+ bind_service_banner(service, tunnel_appname, false)
188
+ start_caldecott
189
+ else
190
+ auth = tunnel_auth
191
+ end
192
+
193
+ if not tunnel_healthy?(auth)
194
+ display "Redeploying tunnel application '#{tunnel_appname}'."
195
+
196
+ # We don't expect caldecott not to be running, so take the
197
+ # most aggressive restart method.. delete/re-push
198
+ client.delete_app(tunnel_appname)
199
+ invalidate_tunnel_app_info
200
+
201
+ push_caldecott(auth)
202
+ bind_service_banner(service, tunnel_appname, false)
203
+ start_caldecott
204
+ end
205
+
206
+ if not tunnel_bound?(service)
207
+ bind_service_banner(service, tunnel_appname)
208
+ end
209
+
210
+ conn_info = tunnel_connection_info info[:vendor], service, auth
211
+ display_tunnel_connection_info(conn_info)
212
+ display "Starting tunnel to #{service.bold} on port #{port.to_s.bold}."
213
+ start_tunnel(port, conn_info, auth)
214
+
215
+ clients = get_clients_for(info[:vendor])
216
+
217
+ if clients.empty?
218
+ client_name ||= "none"
219
+ else
220
+ client_name ||= ask(
221
+ "Which client would you like to start?",
222
+ :choices => ["none"] + clients.keys,
223
+ :indexed => true
224
+ )
225
+ end
226
+
227
+ if client_name == "none"
228
+ wait_for_tunnel_end
229
+ else
230
+ wait_for_tunnel_start(port)
231
+ unless start_local_prog(clients, client_name, conn_info, port)
232
+ err "'#{client_name}' execution failed; is it in your $PATH?"
233
+ end
234
+ end
235
+ end
236
+
237
+ def get_clients_for(type)
238
+ conf = VMC::Cli::Config.clients
239
+ conf[type] || {}
240
+ end
241
+ end
242
+ end
@@ -0,0 +1,65 @@
1
+ module VMC::Cli::Command
2
+
3
+ class User < Base
4
+
5
+ def info
6
+ info = client_info
7
+ username = info[:user] || 'N/A'
8
+ return display JSON.pretty_generate([username]) if @options[:json]
9
+ display "\n[#{username}]"
10
+ end
11
+
12
+ def login(email=nil)
13
+ email = @options[:email] unless email
14
+ password = @options[:password]
15
+ tries ||= 0
16
+
17
+ unless no_prompt
18
+ display "Attempting login to [#{target_url}]" if target_url
19
+ email ||= ask("Email")
20
+ password ||= ask("Password", :echo => "*")
21
+ end
22
+
23
+ err "Need a valid email" unless email
24
+ err "Need a password" unless password
25
+ login_and_save_token(email, password)
26
+ say "Successfully logged into [#{target_url}]".green
27
+ rescue VMC::Client::TargetError
28
+ display "Problem with login, invalid account or password when attempting to login to '#{target_url}'".red
29
+ retry if (tries += 1) < 3 && prompt_ok && !@options[:password]
30
+ exit 1
31
+ rescue => e
32
+ display "Problem with login to '#{target_url}', #{e}, try again or register for an account.".red
33
+ exit 1
34
+ end
35
+
36
+ def logout
37
+ VMC::Cli::Config.remove_token_file
38
+ say "Successfully logged out of [#{target_url}]".green
39
+ end
40
+
41
+ def change_password(password=nil)
42
+ info = client_info
43
+ email = info[:user]
44
+ err "Need to be logged in to change password." unless email
45
+ say "Changing password for '#{email}'\n"
46
+ unless no_prompt
47
+ password = ask "New Password", :echo => "*"
48
+ password2 = ask "Verify Password", :echo => "*"
49
+ err "Passwords did not match, try again" if password != password2
50
+ end
51
+ err "Password required" unless password
52
+ client.change_password(password)
53
+ say "\nSuccessfully changed password".green
54
+ end
55
+
56
+ private
57
+
58
+ def login_and_save_token(email, password)
59
+ token = client.login(email, password)
60
+ VMC::Cli::Config.store_token(token, @options[:token_file])
61
+ end
62
+
63
+ end
64
+
65
+ end
data/lib/cli/config.rb ADDED
@@ -0,0 +1,173 @@
1
+ require "yaml"
2
+ require 'fileutils'
3
+
4
+ require 'rubygems'
5
+ require 'json/pure'
6
+
7
+ module VMC::Cli
8
+ class Config
9
+
10
+ DEFAULT_TARGET = 'api.vcap.me'
11
+
12
+ TARGET_FILE = '~/.vmc_target'
13
+ TOKEN_FILE = '~/.vmc_token'
14
+ INSTANCES_FILE = '~/.vmc_instances'
15
+ ALIASES_FILE = '~/.vmc_aliases'
16
+ CLIENTS_FILE = '~/.vmc_clients'
17
+ MICRO_FILE = '~/.vmc_micro'
18
+
19
+ STOCK_CLIENTS = File.expand_path("../../../config/clients.yml", __FILE__)
20
+
21
+ class << self
22
+ attr_accessor :colorize
23
+ attr_accessor :output
24
+ attr_accessor :trace
25
+ attr_accessor :nozip
26
+
27
+ def target_url
28
+ return @target_url if @target_url
29
+ target_file = File.expand_path(TARGET_FILE)
30
+ if File.exists? target_file
31
+ @target_url = lock_and_read(target_file).strip
32
+ else
33
+ @target_url = DEFAULT_TARGET
34
+ end
35
+ @target_url = "http://#{@target_url}" unless /^https?/ =~ @target_url
36
+ @target_url = @target_url.gsub(/\/+$/, '')
37
+ @target_url
38
+ end
39
+
40
+ def base_of(url)
41
+ url.sub(/^[^\.]+\./, "")
42
+ end
43
+
44
+ def suggest_url
45
+ @suggest_url ||= base_of(target_url)
46
+ end
47
+
48
+ def store_target(target_host)
49
+ target_file = File.expand_path(TARGET_FILE)
50
+ lock_and_write(target_file, target_host)
51
+ end
52
+
53
+ def all_tokens(token_file_path=nil)
54
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
55
+ return nil unless File.exists? token_file
56
+ contents = lock_and_read(token_file).strip
57
+ JSON.parse(contents)
58
+ end
59
+
60
+ alias :targets :all_tokens
61
+
62
+ def auth_token(token_file_path=nil)
63
+ return @token if @token
64
+ tokens = all_tokens(token_file_path)
65
+ @token = tokens[target_url] if tokens
66
+ end
67
+
68
+ def remove_token_file
69
+ FileUtils.rm_f(File.expand_path(TOKEN_FILE))
70
+ end
71
+
72
+ def store_token(token, token_file_path=nil)
73
+ tokens = all_tokens(token_file_path) || {}
74
+ tokens[target_url] = token
75
+ token_file = File.expand_path(token_file_path || TOKEN_FILE)
76
+ lock_and_write(token_file, tokens.to_json)
77
+ end
78
+
79
+ def instances
80
+ instances_file = File.expand_path(INSTANCES_FILE)
81
+ return nil unless File.exists? instances_file
82
+ contents = lock_and_read(instances_file).strip
83
+ JSON.parse(contents)
84
+ end
85
+
86
+ def store_instances(instances)
87
+ instances_file = File.expand_path(INSTANCES_FILE)
88
+ lock_and_write(instances_file, instances.to_json)
89
+ end
90
+
91
+ def aliases
92
+ aliases_file = File.expand_path(ALIASES_FILE)
93
+ # bacward compatible
94
+ unless File.exists? aliases_file
95
+ old_aliases_file = File.expand_path('~/.vmc-aliases')
96
+ FileUtils.mv(old_aliases_file, aliases_file) if File.exists? old_aliases_file
97
+ end
98
+ aliases = YAML.load_file(aliases_file) rescue {}
99
+ end
100
+
101
+ def store_aliases(aliases)
102
+ aliases_file = File.expand_path(ALIASES_FILE)
103
+ File.open(aliases_file, 'wb') {|f| f.write(aliases.to_yaml)}
104
+ end
105
+
106
+ def micro
107
+ micro_file = File.expand_path(MICRO_FILE)
108
+ return {} unless File.exists? micro_file
109
+ contents = lock_and_read(micro_file).strip
110
+ JSON.parse(contents)
111
+ end
112
+
113
+ def store_micro(micro)
114
+ micro_file = File.expand_path(MICRO_FILE)
115
+ lock_and_write(micro_file, micro.to_json)
116
+ end
117
+
118
+ def deep_merge(a, b)
119
+ merge = proc do |_, old, new|
120
+ if new.is_a?(Hash) and old.is_a?(Hash)
121
+ old.merge(new, &merge)
122
+ else
123
+ new
124
+ end
125
+ end
126
+
127
+ a.merge(b, &merge)
128
+ end
129
+
130
+ def clients
131
+ return @clients if @clients
132
+
133
+ stock = YAML.load_file(STOCK_CLIENTS)
134
+ clients = File.expand_path CLIENTS_FILE
135
+ if File.exists? clients
136
+ user = YAML.load_file(clients)
137
+ @clients = deep_merge(stock, user)
138
+ else
139
+ @clients = stock
140
+ end
141
+ end
142
+
143
+ def lock_and_read(file)
144
+ File.open(file, File::RDONLY) {|f|
145
+ if defined? JRUBY_VERSION
146
+ f.flock(File::LOCK_SH)
147
+ else
148
+ f.flock(File::LOCK_EX)
149
+ end
150
+ contents = f.read
151
+ f.flock(File::LOCK_UN)
152
+ contents
153
+ }
154
+ end
155
+
156
+ def lock_and_write(file, contents)
157
+ File.open(file, File::RDWR | File::CREAT, 0600) {|f|
158
+ f.flock(File::LOCK_EX)
159
+ f.rewind
160
+ f.puts contents
161
+ f.flush
162
+ f.truncate(f.pos)
163
+ f.flock(File::LOCK_UN)
164
+ }
165
+ end
166
+ end
167
+
168
+ def initialize(work_dir = Dir.pwd)
169
+ @work_dir = work_dir
170
+ end
171
+
172
+ end
173
+ end
@@ -0,0 +1,160 @@
1
+ require 'net/telnet'
2
+ require 'readline'
3
+
4
+ module VMC::Cli
5
+ module ConsoleHelper
6
+
7
+ def console_connection_info(appname)
8
+ app = client.app_info(appname)
9
+ fw = VMC::Cli::Framework.lookup_by_framework(app[:staging][:model])
10
+ if !fw.console
11
+ err "'#{appname}' is a #{fw.name} application. " +
12
+ "Console access is not supported for #{fw.name} applications."
13
+ end
14
+ instances_info_envelope = client.app_instances(appname)
15
+ instances_info_envelope = {} if instances_info_envelope.is_a?(Array)
16
+
17
+ instances_info = instances_info_envelope[:instances] || []
18
+ err "No running instances for [#{appname}]" if instances_info.empty?
19
+
20
+ entry = instances_info[0]
21
+ if !entry[:console_port]
22
+ begin
23
+ client.app_files(appname, '/app/cf-rails-console')
24
+ err "Console port not provided for [#{appname}]. Try restarting the app."
25
+ rescue VMC::Client::TargetError, VMC::Client::NotFound
26
+ err "Console access not supported for [#{appname}]. " +
27
+ "Please redeploy your app to enable support."
28
+ end
29
+ end
30
+ conn_info = {'hostname' => entry[:console_ip], 'port' => entry[:console_port]}
31
+ end
32
+
33
+ def start_local_console(port, appname)
34
+ auth_info = console_credentials(appname)
35
+ display "Connecting to '#{appname}' console: ", false
36
+ prompt = console_login(auth_info, port)
37
+ display "OK".green
38
+ display "\n"
39
+ initialize_readline
40
+ run_console prompt
41
+ end
42
+
43
+ def console_login(auth_info, port)
44
+ if !auth_info["username"] || !auth_info["password"]
45
+ err "Unable to verify console credentials."
46
+ end
47
+ @telnet_client = telnet_client(port)
48
+ prompt = nil
49
+ err_msg = "Login attempt timed out."
50
+ 5.times do
51
+ begin
52
+ results = @telnet_client.login("Name"=>auth_info["username"],
53
+ "Password"=>auth_info["password"])
54
+ lines = results.sub("Login: Password: ", "").split("\n")
55
+ last_line = lines.pop
56
+ if last_line =~ /[$%#>] \z/n
57
+ prompt = last_line
58
+ elsif last_line =~ /Login failed/
59
+ err_msg = last_line
60
+ end
61
+ break
62
+ rescue TimeoutError
63
+ sleep 1
64
+ rescue EOFError
65
+ #This may happen if we login right after app starts
66
+ close_console
67
+ sleep 5
68
+ @telnet_client = telnet_client(port)
69
+ end
70
+ display ".", false
71
+ end
72
+ unless prompt
73
+ close_console
74
+ err err_msg
75
+ end
76
+ prompt
77
+ end
78
+
79
+ def send_console_command(cmd)
80
+ results = @telnet_client.cmd(cmd)
81
+ results.split("\n")
82
+ end
83
+
84
+ def console_credentials(appname)
85
+ content = client.app_files(appname, '/app/cf-rails-console/.consoleaccess', '0')
86
+ YAML.load(content)
87
+ end
88
+
89
+ def close_console
90
+ @telnet_client.close
91
+ end
92
+
93
+ def console_tab_completion_data(cmd)
94
+ begin
95
+ results = @telnet_client.cmd("String"=> cmd + "\t", "Match"=>/\S*\n$/, "Timeout"=>10)
96
+ results.chomp.split(",")
97
+ rescue TimeoutError
98
+ [] #Just return empty results if timeout occurred on tab completion
99
+ end
100
+ end
101
+
102
+ private
103
+ def telnet_client(port)
104
+ Net::Telnet.new({"Port"=>port, "Prompt"=>/[$%#>] \z|Login failed/n, "Timeout"=>30, "FailEOF"=>true})
105
+ end
106
+
107
+ def readline_with_history(prompt)
108
+ line = Readline::readline(prompt)
109
+ return nil if line == nil || line == 'quit' || line == 'exit'
110
+ Readline::HISTORY.push(line) if not line =~ /^\s*$/ and Readline::HISTORY.to_a[-1] != line
111
+ line
112
+ end
113
+
114
+ def run_console(prompt)
115
+ prev = trap("INT") { |x| exit_console; prev.call(x); exit }
116
+ prev = trap("TERM") { |x| exit_console; prev.call(x); exit }
117
+ loop do
118
+ cmd = readline_with_history(prompt)
119
+ if(cmd == nil)
120
+ exit_console
121
+ break
122
+ end
123
+ prompt = send_console_command_display_results(cmd, prompt)
124
+ end
125
+ end
126
+
127
+ def exit_console
128
+ #TimeoutError expected, as exit doesn't return anything
129
+ @telnet_client.cmd("String"=>"exit","Timeout"=>1) rescue TimeoutError
130
+ close_console
131
+ end
132
+
133
+ def send_console_command_display_results(cmd, prompt)
134
+ begin
135
+ lines = send_console_command cmd
136
+ #Assumes the last line is a prompt
137
+ prompt = lines.pop
138
+ lines.each {|line| display line if line != cmd}
139
+ rescue TimeoutError
140
+ display "Timed out sending command to server.".red
141
+ rescue EOFError
142
+ err "The console connection has been terminated. Perhaps the app was stopped or deleted?"
143
+ end
144
+ prompt
145
+ end
146
+
147
+ def initialize_readline
148
+ if Readline.respond_to?("basic_word_break_characters=")
149
+ Readline.basic_word_break_characters= " \t\n`><=;|&{("
150
+ end
151
+ Readline.completion_append_character = nil
152
+ #Assumes that sending a String ending with tab will return a non-empty
153
+ #String of comma-separated completion options, terminated by a new line
154
+ #For example, "app.\t" might result in "to_s,nil?,etc\n"
155
+ Readline.completion_proc = proc {|s|
156
+ console_tab_completion_data s
157
+ }
158
+ end
159
+ end
160
+ end