1 -- $Id: CmdChain.lua,v 1.33 2005/03/26 23:36:13 cpressey Exp $
9 -- Global "class" variable:
12 -- Some 'symbolic constants':
13 CmdChain.LOG_SILENT = {}
14 CmdChain.LOG_QUIET = {}
15 CmdChain.LOG_VERBOSE = {}
17 CmdChain.FAILURE_IGNORE = {}
18 CmdChain.FAILURE_WARN = {}
19 CmdChain.FAILURE_ABORT = {}
21 CmdChain.RESULT_NEVER_EXECUTED = {}
22 CmdChain.RESULT_POPEN_ERROR = {}
23 CmdChain.RESULT_SELECT_ERROR = {}
24 CmdChain.RESULT_CANCELLED = {}
25 CmdChain.RESULT_SKIPPED = {}
27 -- Create a new command chain object instance.
28 CmdChain.new = function(...)
32 local replacements = {}
38 -- Fix up a command descriptor. If it is just a string, turn
39 -- it into a table; fill out any missing default values in the
40 -- table; and expand the string as appropriate.
41 local fix_cmd = function(cmd)
42 if type(cmd) == "string" then
43 cmd = { cmdline = cmd }
45 assert(type(cmd) == "table")
47 if cmd.cmdline == nil then
50 assert(type(cmd.cmdline) == "string")
52 if cmd.log_mode == nil then
53 cmd.log_mode = CmdChain.LOG_VERBOSE
55 assert(cmd.log_mode == CmdChain.LOG_SILENT or
56 cmd.log_mode == CmdChain.LOG_QUIET or
57 cmd.log_mode == CmdChain.LOG_VERBOSE)
59 if cmd.failure_mode == nil then
60 cmd.failure_mode = CmdChain.FAILURE_ABORT
62 assert(cmd.failure_mode == CmdChain.FAILURE_IGNORE or
63 cmd.failure_mode == CmdChain.FAILURE_WARN or
64 cmd.failure_mode == CmdChain.FAILURE_ABORT)
66 if cmd.replacements == nil then
69 assert(type(cmd.replacements) == "table")
71 cmd.cmdline = App.expand(cmd.cmdline,
72 cmd.replacements, replacements)
74 cmd.display_cmdline = cmd.cmdline
76 if cmd.on_executed ~= nil then
77 assert(type(cmd.on_executed) == "function")
80 -- cmd.desc, cmd.tag, cmd.sensitive, cmd.on_executed,
81 -- and cmd.input are left as nil if not specified.
86 -- Open a stream to a command to be executed, read from it,
87 -- and update the progress bar as data comes and (and/or as the
88 -- read from the stream times out.)
91 -- - the return value of the command on the other end of the stream;
92 -- - a boolean indicating whether it was cancelled by the user; and
93 -- - the output of the command, if requested (cmd.capture.)
95 local stream_loop = function(pr, cmd)
97 local cancelled = false
102 local escape = function(pat)
103 return string.gsub(pat, "([^%w])", "%%%1")
106 cmdline = "(" .. cmd.cmdline .. ") 2>&1"
107 if not cmd.input then
108 cmdline = cmdline .. " </dev/null"
110 pty = Pty.open(cmdline)
112 App.log("! could not open pty to: " .. cmd.cmdline)
113 return CmdChain.RESULT_POPEN_ERROR, false, output
121 if cancelled then break end
122 line, err = pty:readline(1000)
125 if cmd.sensitive ~= nil then
126 assert(type(cmd.sensitive) == "string")
128 line, escape(cmd.sensitive), "***not*shown***"
131 cancelled = not pr:update()
132 if cmd.log_mode == CmdChain.LOG_VERBOSE then
133 App.log("| " .. line)
134 elseif cmd.log_mode == CmdChain.LOG_QUIET then
135 io.stderr:write("| " .. line .. "\n")
136 else -- cmd.log_mode == CmdChain.LOG_SILENT
140 table.insert(output, line)
143 if err == Pty.TIMEOUT then
144 cancelled = not pr:update()
145 elseif err == Pty.EOF then
148 App.log("! pty:read() failed, err=%d", err)
150 return CmdChain.RESULT_SELECT_ERROR,
157 pty:signal(Pty.SIGTERM)
160 return pty:close(), cancelled, output
163 local interruption_dialog = function(cmd, cancelled, result)
164 local done_interruption = false
165 local done_command = false
169 msg = "was cancelled."
171 msg = "FAILED with a return code of " .. tostring(result) .. "."
174 while not done_interruption do
178 short_desc = "Execution of the command\n\n" ..
179 cmd.display_cmdline .. "\n\n" .. msg,
184 short_desc = "View the command output that led up to this",
192 short_desc = "Try executing this command again",
194 done_interruption = true
200 short_desc = "Abort this sequence of commands",
202 result = CmdChain.RESULT_CANCELLED
203 done_interruption = true
210 short_desc = "Skip this particular command and resume with the next one",
212 result = CmdChain.RESULT_SKIPPED
213 done_interruption = true
221 return done_command, result
224 -- Execute a single command.
225 -- Return values are:
226 -- - a COMMAND_RESULT_* constant, or a value from 0 to 255
227 -- to indicate the exit code from the utility; and
228 -- - the output of the command, if requested (cmd.capture.)
230 local command_execute = function(pr, cmd)
232 local cancelled = false
233 local done_command = false
234 local result, output = CmdChain.RESULT_NEVER_EXECUTED, ""
237 pr:set_short_desc(cmd.desc)
239 pr:set_short_desc(cmd.display_cmdline)
241 cancelled = not pr:update()
243 if App.option.confirm_execution then
244 done_command = not App.ui:confirm(
245 "About to execute:\n\n" .. cmd.display_cmdline ..
246 "\n\nIs this acceptable?"
250 while not done_command do
252 if cmd.log_mode ~= CmdChain.LOG_SILENT then
253 App.log(",-<<< Executing `" .. cmd.display_cmdline .. "'")
255 if App.option.fake_execution then
256 if cmd.log_mode ~= CmdChain.LOG_SILENT then
257 App.log("| (not actually executed)")
261 result, cancelled, output = stream_loop(pr, cmd)
263 if cmd.log_mode ~= CmdChain.LOG_SILENT then
264 App.log("`->>> Exit status: " .. tostring(result))
269 done_command, result = interruption_dialog(cmd, cancelled, result)
271 elseif cmd.failure_mode == CmdChain.FAILURE_IGNORE then
274 elseif (result ~= 0 and cmd.failure_mode ~= CmdChain.FAILURE_WARN) then
276 done_command, result = interruption_dialog(cmd, cancelled, result)
282 if cmd.on_executed ~= nil then
283 cmd.on_executed(cmd, result, output)
286 return result, output
294 -- Set the global replacements for this command chain.
295 -- These will be applied to each command that is
296 -- subsequently added (although local replacements will
297 -- be applied first.)
299 chain.set_replacements = function(cmd, new_replacements)
300 App.merge_tables(replacements, new_replacements,
301 function(key, dest_val, src_val)
307 -- Get captured output by its id.
309 chain.get_output = function(cmd, cap_id)
310 return capture[cap_id]
314 -- Add one or more commands to this command chain.
316 chain.add = function(chain, ...)
319 if table.getn(arg) == 0 then
323 for cmd_no, cmd in ipairs(arg) do
324 table.insert(cmds, fix_cmd(cmd))
328 -- Execute a series of external utility programs.
329 -- Returns 1 if everything executed OK, 0 if one of the
330 -- critical commands failed or if the user cancelled.
331 chain.execute = function(chain)
334 local i, n, result = 0, 0, 0
335 local return_val = true
340 pr = App.ui:new_progress_bar{
341 title = "Executing Commands"
347 result, output = command_execute(pr, cmds[i])
348 if result == CmdChain.RESULT_CANCELLED then
352 if type(result) == "number" and result > 0 and result < 256 then
354 if cmd.failure_mode == CmdChain.FAILURE_ABORT then
358 if cmds[i].capture then
359 capture[cmds[i].capture] = output
361 pr:set_amount((i * 100) / n)
369 -- Show the commands that have been added to this
370 -- command chain to the user in a dialog box.
371 chain.preview = function(chain)
375 for i, cmd in cmds do
376 contents = contents .. cmd.cmdline .. "\n"
381 name = "Command Preview",
382 short_desc = contents,
383 role = "informative",
384 minimum_width = "72",
387 { id = "ok", name = "OK" }
392 -- Record these commands in a shell script file.
393 chain.record = function(chain, file)
397 local gen_rand_string = function(len)
400 local A = string.byte("A")
404 s = s .. string.char(math.random(A, A + 25))
411 local get_marker_for = function(text)
414 s = gen_rand_string(5)
415 while string.find(text, s, 1, true) do
416 s = gen_rand_string(5)
421 for i, cmd in cmds do
423 -- Write lines apropos to the cmdline being
424 -- executed, taking in account the failure mode.
426 if cmd.failure_mode == CmdChain.FAILURE_IGNORE then
427 file:write(cmd.cmdline)
428 elseif cmd.failure_mode == CmdChain.FAILURE_WARN then
429 file:write("(" .. cmd.cmdline .. " || " ..
430 "echo \"WARNING: " .. cmd.cmdline ..
431 " failed with exit code $?\")")
432 elseif cmd.failure_mode == CmdChain.FAILURE_ABORT then
433 file:write(cmd.cmdline)
437 -- If the command has input, include that as a heredoc.
440 local marker = get_marker_for(cmd.input)
441 file:write(" <<" .. marker .. "\n" .. cmd.input .. marker)
444 if cmd.failure_mode == CmdChain.FAILURE_WARN or
445 cmd.failure_mode == CmdChain.FAILURE_ABORT then
450 -- Write the description as a comment following.
453 file:write(" # " .. cmd.desc)
460 -- ``Constructor'' - initialize our instance data.
462 if table.getn(arg) > 0 then
463 chain:add(unpack(arg))