pagoda 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +2 -0
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +39 -0
- data/README +3 -0
- data/Rakefile +11 -0
- data/bin/pagoda +13 -0
- data/lib/pagoda/client.rb +221 -0
- data/lib/pagoda/command.rb +93 -0
- data/lib/pagoda/commands/app.rb +243 -0
- data/lib/pagoda/commands/auth.rb +149 -0
- data/lib/pagoda/commands/base.rb +184 -0
- data/lib/pagoda/commands/help.rb +100 -0
- data/lib/pagoda/commands/tunnel.rb +49 -0
- data/lib/pagoda/helpers.rb +127 -0
- data/lib/pagoda/tunnel_proxy.rb +130 -0
- data/lib/pagoda/version.rb +3 -0
- data/lib/pagoda.rb +3 -0
- data/pagoda.gemspec +29 -0
- data/spec/base.rb +21 -0
- data/spec/client_spec.rb +255 -0
- data/spec/command_spec.rb +26 -0
- data/spec/commands/auth_spec.rb +57 -0
- metadata +167 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
module Pagoda::Command
|
2
|
+
class Auth < Base
|
3
|
+
attr_accessor :credentials
|
4
|
+
|
5
|
+
def initialize(args)
|
6
|
+
@args = args
|
7
|
+
check_for_credentials
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
def client
|
12
|
+
@client ||= init_client
|
13
|
+
end
|
14
|
+
|
15
|
+
def init_client
|
16
|
+
client = Pagoda::Client.new(user, password)
|
17
|
+
client.on_warning { |message| self.display("\n#{message}\n\n") }
|
18
|
+
client
|
19
|
+
end
|
20
|
+
|
21
|
+
# just a stub; will raise if not authenticated
|
22
|
+
def check
|
23
|
+
client.app_list
|
24
|
+
end
|
25
|
+
|
26
|
+
def check_for_credentials
|
27
|
+
if option_value("-u", "--username") && option_value("-p", "--password")
|
28
|
+
reauthorize
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def reauthorize
|
33
|
+
@credentials = ask_for_credentials
|
34
|
+
write_credentials
|
35
|
+
end
|
36
|
+
|
37
|
+
def user # :nodoc:
|
38
|
+
get_credentials
|
39
|
+
@credentials[0]
|
40
|
+
end
|
41
|
+
|
42
|
+
def password # :nodoc:
|
43
|
+
get_credentials
|
44
|
+
@credentials[1]
|
45
|
+
end
|
46
|
+
|
47
|
+
def credentials_file
|
48
|
+
"#{home_directory}/.pagoda/credentials"
|
49
|
+
end
|
50
|
+
|
51
|
+
def get_credentials # :nodoc:
|
52
|
+
return if @credentials
|
53
|
+
unless @credentials = read_credentials
|
54
|
+
@credentials = ask_for_credentials
|
55
|
+
save_credentials
|
56
|
+
end
|
57
|
+
@credentials
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_credentials
|
61
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
62
|
+
end
|
63
|
+
|
64
|
+
def echo_off
|
65
|
+
system "stty -echo"
|
66
|
+
end
|
67
|
+
|
68
|
+
def echo_on
|
69
|
+
system "stty echo"
|
70
|
+
end
|
71
|
+
|
72
|
+
def ask_for_credentials
|
73
|
+
unless username = option_value("-u", "--username")
|
74
|
+
username = ask "Username: "
|
75
|
+
end
|
76
|
+
unless password = option_value("-p", "--password")
|
77
|
+
display "Password: ", false
|
78
|
+
password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
|
79
|
+
end
|
80
|
+
[username, password] # return
|
81
|
+
end
|
82
|
+
|
83
|
+
def ask_for_password
|
84
|
+
echo_off
|
85
|
+
password = ask
|
86
|
+
puts
|
87
|
+
echo_on
|
88
|
+
return password
|
89
|
+
end
|
90
|
+
|
91
|
+
def ask_for_password_on_windows
|
92
|
+
require "Win32API"
|
93
|
+
char = nil
|
94
|
+
password = ''
|
95
|
+
|
96
|
+
while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
|
97
|
+
break if char == 10 || char == 13 # received carriage return or newline
|
98
|
+
if char == 127 || char == 8 # backspace and delete
|
99
|
+
password.slice!(-1, 1)
|
100
|
+
else
|
101
|
+
# windows might throw a -1 at us so make sure to handle RangeError
|
102
|
+
(password << char.chr) rescue RangeError
|
103
|
+
end
|
104
|
+
end
|
105
|
+
return password
|
106
|
+
end
|
107
|
+
|
108
|
+
def save_credentials
|
109
|
+
begin
|
110
|
+
write_credentials
|
111
|
+
Pagoda::Command.run_internal('auth:check', args)
|
112
|
+
rescue RestClient::Unauthorized => e
|
113
|
+
delete_credentials
|
114
|
+
raise e unless retry_login?
|
115
|
+
|
116
|
+
error "Authentication failed"
|
117
|
+
@credentials = ask_for_credentials
|
118
|
+
@client = init_client
|
119
|
+
retry
|
120
|
+
rescue Exception => e
|
121
|
+
delete_credentials
|
122
|
+
raise e
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def retry_login?
|
127
|
+
@login_attempts ||= 0
|
128
|
+
@login_attempts += 1
|
129
|
+
@login_attempts < 3
|
130
|
+
end
|
131
|
+
|
132
|
+
def write_credentials
|
133
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
134
|
+
File.open(credentials_file, 'w') do |file|
|
135
|
+
file.puts self.credentials
|
136
|
+
end
|
137
|
+
set_credentials_permissions
|
138
|
+
end
|
139
|
+
|
140
|
+
def set_credentials_permissions
|
141
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
142
|
+
FileUtils.chmod 0600, credentials_file
|
143
|
+
end
|
144
|
+
|
145
|
+
def delete_credentials
|
146
|
+
FileUtils.rm_f(credentials_file)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'iniparse'
|
2
|
+
|
3
|
+
module Pagoda
|
4
|
+
module Command
|
5
|
+
class Base
|
6
|
+
include Pagoda::Helpers
|
7
|
+
|
8
|
+
attr_accessor :args
|
9
|
+
|
10
|
+
def initialize(args)
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def client
|
15
|
+
@client ||= Pagoda::Command.run_internal('auth:client', args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def shell(cmd)
|
19
|
+
FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
|
20
|
+
end
|
21
|
+
|
22
|
+
def app(soft_fail=false)
|
23
|
+
if override = option_value("-a", "--app")
|
24
|
+
return override
|
25
|
+
else
|
26
|
+
if name = find_app
|
27
|
+
return name
|
28
|
+
else
|
29
|
+
if locate_app_root
|
30
|
+
if extract_git_clone_url
|
31
|
+
return false if soft_fail
|
32
|
+
errors = []
|
33
|
+
errors << "This repo is either not launched, or not paired with a launched app"
|
34
|
+
errors << ""
|
35
|
+
errors << "To launch this app run 'pagoda launch <app-name>'"
|
36
|
+
errors << ""
|
37
|
+
errors << "To pair this project with a deployed app, run 'pagoda pair <app-name>'"
|
38
|
+
errors << ""
|
39
|
+
errors << "To see a list of currently deployed apps, run 'pagoda list'"
|
40
|
+
error errors
|
41
|
+
else
|
42
|
+
return false if soft_fail
|
43
|
+
errors = []
|
44
|
+
errors << "It appears you are using git (fantastic)."
|
45
|
+
errors << "However we only support git repos hosted with github."
|
46
|
+
errors << "Please ensure your repo is hosted with github."
|
47
|
+
errors << ""
|
48
|
+
errors << "If you are trying to reference a specific app, try argument: -a <app-name>"
|
49
|
+
error errors
|
50
|
+
end
|
51
|
+
else
|
52
|
+
return false if soft_fail
|
53
|
+
errors = []
|
54
|
+
errors << "Unable to find git config in this directory or in any parent directory"
|
55
|
+
errors << ""
|
56
|
+
errors << "If you are trying to reference a specific app, try argument: -a <app-name>"
|
57
|
+
error errors
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
name
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_branch
|
65
|
+
option_value("-b", "--branch") || find_branch
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_commit
|
69
|
+
option_value("-c", "--commit") || find_commit
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_app
|
73
|
+
read_apps.each do |line|
|
74
|
+
app = line.split(" ")
|
75
|
+
return app[0] if app[2] == locate_app_root
|
76
|
+
end
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
def find_branch
|
81
|
+
begin
|
82
|
+
line = File.new("#{locate_app_root}/.git/HEAD").gets
|
83
|
+
line.strip.split(' ').last.split("/").last
|
84
|
+
rescue
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_commit
|
90
|
+
begin
|
91
|
+
File.new("#{locate_app_root}/.git/refs/heads/#{parse_branch}").gets.strip
|
92
|
+
rescue
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def read_apps
|
98
|
+
return [] if !File.exists?(apps_file)
|
99
|
+
File.read(apps_file).split(/\n/).inject([]) {|apps, line| apps << line if line.include?("git@github.com"); apps}
|
100
|
+
end
|
101
|
+
|
102
|
+
def write_app(name, git_url=nil, app_root=nil)
|
103
|
+
git_url = extract_git_clone_url unless git_url
|
104
|
+
app_root = locate_app_root unless app_root
|
105
|
+
FileUtils.mkdir_p(File.dirname(apps_file)) if !File.exists?(apps_file)
|
106
|
+
current_apps = read_apps
|
107
|
+
File.open(apps_file, 'w') do |file|
|
108
|
+
current_apps.each do |app|
|
109
|
+
file.puts app
|
110
|
+
end
|
111
|
+
file.puts "#{name} #{git_url} #{app_root}"
|
112
|
+
end
|
113
|
+
set_apps_file_permissions
|
114
|
+
end
|
115
|
+
alias :add_app :write_app
|
116
|
+
|
117
|
+
def remove_app(name)
|
118
|
+
current_apps = read_apps
|
119
|
+
current_apps.delete_if do |app|
|
120
|
+
app.split(" ")[0] == name
|
121
|
+
end
|
122
|
+
File.open(apps_file, 'w') do |file|
|
123
|
+
current_apps.each do |app|
|
124
|
+
file.puts app
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_apps_file_permissions
|
130
|
+
FileUtils.chmod 0700, File.dirname(apps_file)
|
131
|
+
FileUtils.chmod 0600, apps_file
|
132
|
+
end
|
133
|
+
|
134
|
+
def apps_file
|
135
|
+
"#{home_directory}/.pagoda/apps"
|
136
|
+
end
|
137
|
+
|
138
|
+
def extract_possible_name
|
139
|
+
cleanup_name(extract_git_clone_url.split(":")[1].split("/")[1].split(".")[0])
|
140
|
+
end
|
141
|
+
|
142
|
+
def cleanup_name(name)
|
143
|
+
name.gsub(/-/, '').gsub(/_/, '').gsub(/ /, '').downcase
|
144
|
+
end
|
145
|
+
|
146
|
+
def extract_git_clone_url(soft=false)
|
147
|
+
begin
|
148
|
+
url = IniParse.parse( File.read("#{locate_app_root}/.git/config") )['remote "origin"']["url"]
|
149
|
+
raise unless url.match(/^git@github.com:.+\.git$/)
|
150
|
+
url
|
151
|
+
rescue Exception => e
|
152
|
+
return false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def version
|
157
|
+
display Client.gem_version_string
|
158
|
+
end
|
159
|
+
|
160
|
+
def locate_app_root(dir=Dir.pwd)
|
161
|
+
return dir if File.exists? "#{dir}/.git/config"
|
162
|
+
parent = dir.split('/')[0..-2].join('/')
|
163
|
+
return false if parent.empty?
|
164
|
+
locate_app_root(parent)
|
165
|
+
end
|
166
|
+
|
167
|
+
def extract_option(options, default=true)
|
168
|
+
values = options.is_a?(Array) ? options : [options]
|
169
|
+
return unless opt_index = args.select { |a| values.include? a }.first
|
170
|
+
opt_position = args.index(opt_index) + 1
|
171
|
+
if args.size > opt_position && opt_value = args[opt_position]
|
172
|
+
if opt_value.include?('--')
|
173
|
+
opt_value = nil
|
174
|
+
else
|
175
|
+
args.delete_at(opt_position)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
opt_value ||= default
|
179
|
+
args.delete(opt_index)
|
180
|
+
block_given? ? yield(opt_value) : opt_value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Pagoda::Command
|
2
|
+
class Help < Base
|
3
|
+
def index
|
4
|
+
display %{
|
5
|
+
Pagoda
|
6
|
+
|
7
|
+
NAME
|
8
|
+
pagoda -- command line utility for pagodabox.com
|
9
|
+
|
10
|
+
SYNOPSIS
|
11
|
+
pagoda [command] [parameters]
|
12
|
+
|
13
|
+
DESCRIPTION
|
14
|
+
|
15
|
+
If no operands are given, we will attempt to pull data from the current
|
16
|
+
directory. If more than one operand is given, non-directory operands are
|
17
|
+
displayed first.
|
18
|
+
|
19
|
+
The following options are available:
|
20
|
+
|
21
|
+
COMMANDS
|
22
|
+
|
23
|
+
list # list your apps
|
24
|
+
deploy # Deploy your current state to pagoda
|
25
|
+
launch <name> # create (register) a new app
|
26
|
+
info # display info about an app
|
27
|
+
destroy # remove app
|
28
|
+
rollback # rollback app
|
29
|
+
tunnel # create a tunnel to your database on pagoda
|
30
|
+
|
31
|
+
|
32
|
+
PARAMETERS
|
33
|
+
|
34
|
+
---------------------------
|
35
|
+
GLOBAL :
|
36
|
+
---------------------------
|
37
|
+
-a <name> | --app=<name>
|
38
|
+
Set the application name (Only necessary when not in repo dir).
|
39
|
+
|
40
|
+
-u <username> | --username=<username>
|
41
|
+
When set, will not attempt to save your username. Also over-rides
|
42
|
+
any saved username.
|
43
|
+
|
44
|
+
-p <password> | --password=<password>
|
45
|
+
When set, will not attempt to save your password. Also over-rides
|
46
|
+
any saved password.
|
47
|
+
|
48
|
+
-f
|
49
|
+
Executes all commands without confirmation request.
|
50
|
+
|
51
|
+
---------------------------
|
52
|
+
DEPLOYING - pagoda deploy :
|
53
|
+
---------------------------
|
54
|
+
-b <branch> | --branch=<branch>
|
55
|
+
Specify the branch name. By default uses the branch
|
56
|
+
your local repo is on.
|
57
|
+
|
58
|
+
-c <commit> | --commit=<commit>
|
59
|
+
Specify the commit id. By default uses the commit HEAD is set to.
|
60
|
+
|
61
|
+
--latest
|
62
|
+
Will attempt to deploy to the latest commit on github rather than
|
63
|
+
your local repo's current commit.
|
64
|
+
|
65
|
+
---------------------------
|
66
|
+
TUNNELING - pagoda tunnel :
|
67
|
+
---------------------------
|
68
|
+
|
69
|
+
-t <type> | --type=<type>
|
70
|
+
Specify the tunnel type. (ex:mysql)
|
71
|
+
|
72
|
+
-n <instance> | --name=<instance>
|
73
|
+
Specify the instance name you want to operate on used for
|
74
|
+
database instance
|
75
|
+
|
76
|
+
|
77
|
+
EXAMPLES
|
78
|
+
launch an application on pagoda from inside the clone folder:
|
79
|
+
(must be done inside your repo folder)
|
80
|
+
pagoda launch <app name>
|
81
|
+
|
82
|
+
list your applications:
|
83
|
+
|
84
|
+
pagoda list
|
85
|
+
|
86
|
+
create tunnel to your database:
|
87
|
+
(must be inside your repo folder or specify app)
|
88
|
+
|
89
|
+
pagoda tunnel -a <app name> -t mysql -n <database name>
|
90
|
+
|
91
|
+
destroy an application:
|
92
|
+
(must be inside your repo folder or specify app)
|
93
|
+
|
94
|
+
pagoda destroy
|
95
|
+
|
96
|
+
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Pagoda::Command
|
2
|
+
class Tunnel < Auth
|
3
|
+
|
4
|
+
def index
|
5
|
+
app
|
6
|
+
type = option_value("-t", "--type") || 'mysql'
|
7
|
+
Pagoda::Command.run_internal("tunnel:#{type}", args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mysql
|
11
|
+
instance_name = option_value("-n", "--name")
|
12
|
+
unless instance_name
|
13
|
+
# try to find mysql instances here
|
14
|
+
dbs = client.app_databases(app)
|
15
|
+
if dbs.length == 0
|
16
|
+
errors = []
|
17
|
+
errors << "It looks like you don't have any MySQL instances for #{app}"
|
18
|
+
errors << "Feel free to add one in the admin panel (10 MB Free)"
|
19
|
+
error errors
|
20
|
+
elsif dbs.length == 1
|
21
|
+
instance_name = dbs.first[:name]
|
22
|
+
else
|
23
|
+
errors = []
|
24
|
+
errors << "Multiple MySQL instances found"
|
25
|
+
errors << ""
|
26
|
+
dbs.each do |instance|
|
27
|
+
errors << "-> #{instance[:name]}"
|
28
|
+
end
|
29
|
+
errors << ""
|
30
|
+
errors << "Please specify which instance you would like to use."
|
31
|
+
errors << ""
|
32
|
+
errors << "ex: pagoda tunnel -n #{dbs[0][:name]}"
|
33
|
+
error errors
|
34
|
+
end
|
35
|
+
end
|
36
|
+
display
|
37
|
+
display "+> Authenticating Database Ownership"
|
38
|
+
|
39
|
+
if client.database_exists?(app, instance_name)
|
40
|
+
Pagoda::TunnelProxy.new(:mysql, user, password, app, instance_name).start
|
41
|
+
else
|
42
|
+
errors = []
|
43
|
+
errors << "Security exception -"
|
44
|
+
errors << "Either the MySQL instance doesn't exist or you are unauthorized"
|
45
|
+
error errors
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'crack'
|
2
|
+
|
3
|
+
module Pagoda
|
4
|
+
module Helpers
|
5
|
+
INDENT = " "
|
6
|
+
|
7
|
+
def home_directory
|
8
|
+
running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
|
9
|
+
end
|
10
|
+
|
11
|
+
def running_on_windows?
|
12
|
+
RUBY_PLATFORM =~ /mswin32|mingw32/
|
13
|
+
end
|
14
|
+
|
15
|
+
def running_on_a_mac?
|
16
|
+
RUBY_PLATFORM =~ /-darwin\d/
|
17
|
+
end
|
18
|
+
|
19
|
+
def display(msg="", newline=true, level=1)
|
20
|
+
indent = build_indent(level)
|
21
|
+
if newline
|
22
|
+
puts("#{indent}#{msg}")
|
23
|
+
else
|
24
|
+
print("#{indent}#{msg}")
|
25
|
+
STDOUT.flush
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def option_value(short_hand = nil, long_hand = nil)
|
30
|
+
match = false
|
31
|
+
value = nil
|
32
|
+
|
33
|
+
if short_hand
|
34
|
+
if args.include?(short_hand)
|
35
|
+
value = args[args.index(short_hand) + 1]
|
36
|
+
match = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
if long_hand && !match
|
40
|
+
if match = args.grep(/#{long_hand}.*/).first
|
41
|
+
if match.include? "="
|
42
|
+
value = match.split("=").last
|
43
|
+
else
|
44
|
+
value = true
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
value
|
50
|
+
end
|
51
|
+
|
52
|
+
def format_date(date)
|
53
|
+
date = Time.parse(date) if date.is_a?(String)
|
54
|
+
date.strftime("%Y-%m-%d %H:%M %Z")
|
55
|
+
end
|
56
|
+
|
57
|
+
def ask(message=nil, level=1)
|
58
|
+
display("#{message}", false, level) if message
|
59
|
+
gets.strip
|
60
|
+
end
|
61
|
+
|
62
|
+
def confirm(message="Are you sure you wish to continue? (y/n)?", level=1)
|
63
|
+
return true if args.include? "-f"
|
64
|
+
case message
|
65
|
+
when Array
|
66
|
+
count = message.length
|
67
|
+
iteration = 0
|
68
|
+
message.each do |m|
|
69
|
+
if iteration == count - 1
|
70
|
+
display("#{m} ", false, level)
|
71
|
+
else
|
72
|
+
display("#{m} ", true, level)
|
73
|
+
end
|
74
|
+
iteration += 1
|
75
|
+
end
|
76
|
+
when String
|
77
|
+
display("#{message} ", false, level)
|
78
|
+
end
|
79
|
+
ask.downcase == 'y'
|
80
|
+
end
|
81
|
+
|
82
|
+
def error(msg, exit=true, level=1)
|
83
|
+
indent = build_indent(level)
|
84
|
+
STDERR.puts
|
85
|
+
case msg
|
86
|
+
when Array
|
87
|
+
STDERR.puts("#{indent}** Error:")
|
88
|
+
msg.each do |m|
|
89
|
+
STDERR.puts("#{indent}** #{m}")
|
90
|
+
end
|
91
|
+
when String
|
92
|
+
STDERR.puts("#{indent}** Error: #{msg}")
|
93
|
+
end
|
94
|
+
STDERR.puts
|
95
|
+
exit 1 if exit
|
96
|
+
end
|
97
|
+
|
98
|
+
def loop_transaction(app_name = nil)
|
99
|
+
finished = false
|
100
|
+
until finished
|
101
|
+
display ".", false, 0
|
102
|
+
sleep 1
|
103
|
+
if client.app_info(app_name || app)[:transactions].count < 1
|
104
|
+
finished = true
|
105
|
+
display
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_indent(level=1)
|
111
|
+
indent = ""
|
112
|
+
level.times do
|
113
|
+
indent += INDENT
|
114
|
+
end
|
115
|
+
indent
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
unless String.method_defined?(:shellescape)
|
122
|
+
class String
|
123
|
+
def shellescape
|
124
|
+
empty? ? "''" : gsub(/([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\\\\1').gsub(/\n/, "'\n'")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|