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