forked from thoughtworks/cruisecontrol.rb
-
Notifications
You must be signed in to change notification settings - Fork 1
/
command_line.rb
213 lines (188 loc) · 7.17 KB
/
command_line.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
require 'English'
# borrowed (with modifications) from the RSCM project
module CommandLine
QUOTE_REPLACEMENT = (Platform.family == "mswin32") ? '"' : '\\"'
LESS_THAN_REPLACEMENT = (Platform.family == "mswin32") ? '<' : '\\<'
class OptionError < StandardError; end
class ExecutionError < StandardError
attr_reader :cmd, :dir, :exitstatus, :stderr
def initialize(cmd, full_cmd, dir, exitstatus, stderr)
@cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
end
def to_s
"\ndir : #{@dir}\n" +
"command : #{@full_cmd}\n" +
"exitstatus: #{@exitstatus}\n" +
(@stderr ? "STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n" : '')
end
end
# Executes +cmd+.
# If the +:stdout+ and +:stderr+ options are specified, a line consisting
# of a prompt (including +cmd+) will be appended to the respective output streams will be appended
# to those files, followed by the output itself. Example:
#
# CommandLine.execute("echo hello world", {:stdout => "stdout.log", :stderr => "stderr.log"})
#
# will result in the following being written to stdout.log:
#
# /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
# hello world
#
# -and to stderr.log:
# /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
#
# If a block is passed, the stdout io will be yielded to it (as with IO.popen). In this case the output
# will not be written to the stdout file (even if it's specified):
#
# /Users/aslakhellesoy/scm/buildpatterns/repos/damagecontrol/trunk aslakhellesoy$ echo hello world
# [output captured and therefore not logged]
#
# If the exitstatus of the command is different from the value specified by the +:exitstatus+ option
# (which defaults to 0) then an ExecutionError is raised, its message containing the last 400 bytes of stderr
# (provided +:stderr+ was specified)
#
# You can also specify the +:dir+ option, which will cause the command to be executed in that directory
# (default is current directory).
#
# You can also specify a hash of environment variables in +:env+, which will add additional environment variables
# to the default environment.
#
# Finally, you can specify several commands within one by separating them with '&&' (as you would in a shell).
# This will result in several lines to be appended to the log (as if you had executed the commands separately).
#
# See the unit test for more examples.
def execute(cmd, options={}, &proc)
raise "Can't have newline in cmd" if cmd =~ /\n/
options = {
:dir => Dir.pwd,
:env => {},
:mode => 'r',
:exitstatus => 0 }.merge(options)
options[:stdout] = escape(File.expand_path(options[:stdout])) if options[:stdout]
options[:stderr] = escape(File.expand_path(options[:stderr])) if options[:stderr]
Dir.chdir(options[:dir]) do
return e(cmd, options, &proc)
end
end
module_function :execute
private
def e(cmd, options, &proc)
full_cmd = full_cmd(cmd, options, &proc)
options[:env].each{|k,v| ENV[k]=v}
begin
CruiseControl::Log.debug "#{Platform.prompt} #{format_for_printing(cmd)}" if options[:stdout].nil?
result = IO.popen(full_cmd, options[:mode]) do |io|
if proc
proc.call(io)
else
io.each_line do |line|
STDOUT.puts line if options[:stdout].nil?
end
end
end
exit_status = $CHILD_STATUS
raise "$CHILD_STATUS is nil" unless exit_status
verify_exit_code(exit_status, cmd, full_cmd, options)
return result
rescue Errno::ENOENT => e
if options[:stderr]
File.open(options[:stderr], "a") {|io| io.write(e.message)}
else
STDERR.puts e.message
STDERR.puts e.backtrace.map { |line| " #{line}" }
end
raise ExecutionError.new(cmd, full_cmd, options[:dir], nil, e.message)
end
end
module_function :e
def full_cmd(cmd, options, &proc)
stdout_opt, stderr_opt = redirects(options)
capture_info_command = (block_given? && options[:stdout]) ?
"echo [output captured and therefore not logged] >> #{options[:stdout]} && " :
''
stdout_prompt_command = options[:stdout] ?
"echo #{Platform.prompt} #{escape_and_concatenate(cmd)} >> #{options[:stdout]} && " :
''
stderr_prompt_command = options[:stderr] && options[:stderr] != options[:stdout] ?
"echo #{Platform.prompt} #{escape_and_concatenate(cmd)} >> #{options[:stderr]} && " :
''
cmd = escape_and_concatenate(cmd) unless cmd.is_a? String
redirected_command = block_given? ? "#{cmd} #{stderr_opt}" : "#{cmd} #{stdout_opt} #{stderr_opt}"
stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
end
module_function :full_cmd
def verify_exit_code(exit_status, cmd, full_cmd, options)
if exit_status.exitstatus != options[:exitstatus]
if options[:stderr]
if File.exist?(options[:stderr])
File.open(options[:stderr]) do |errio|
begin
errio.seek(-1200, IO::SEEK_END)
rescue Errno::EINVAL
# ignore - it just means we didn't have 400 bytes.
end
error_message = errio.read
end
else
error_message = "#{options[:stderr]} doesn't exist"
end
else
error_message = nil
end
raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, exit_status.exitstatus, error_message)
end
end
module_function :verify_exit_code
def redirects(options)
stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""
# redirecting stderr to stdout if they are the same file avoids a file lock conflict
stderr_opt =
case(options[:stderr])
when nil then ''
when options[:stdout] then '2>&1'
else "2>> #{options[:stderr]}"
end
# let's hope that nobody has slashes in directory names on their win32 file system
if Platform.family == 'mswin32'
stdout_opt.gsub!('/', '\\')
stderr_opt.gsub!('/', '\\')
end
[stdout_opt, stderr_opt]
end
module_function :redirects
def escape_and_concatenate(cmd)
if cmd.is_a?(String)
escape(cmd)
else
cmd.map { |item| escape(item) }.join(' ')
end
end
module_function :escape_and_concatenate
def escape(item)
if Platform.family == 'mswin32'
escaped_characters = /\\|&|\||>|<|\^/
escape_symbol = '^'
quote_argument = (item =~ /\s/)
else
escaped_characters = /"|'|<|>| |&|\||\(|\)|\\|;/
escape_symbol = '\\'
quote_argument_with_spaces = false
end
escaped_value = item.to_s.gsub(escaped_characters) { |match| "#{escape_symbol}#{match}" }
if quote_argument
'"' + escaped_value + '"'
else
escaped_value
end
end
module_function :escape
# command can be a string or an array
def format_for_printing(command)
if command.is_a? String
command
else
command.join(' ')
end
end
module_function :format_for_printing
end