shellunixksh

ksh conditional execution of timeout


On some systems, certain commands like df and prtdiag (Sun only) can hang indefinitely due to various causes. I'm trying to harden my ksh scripts so that should they encounter a hang on these, they would fail rather than stall indefinitely. To this end, I'm planning on using the timeout command, but it isn't universally available on 100% of our footprint, so I must conditionally use timeout only if it exists, and the bare command if it doesn't. I would like to find a compact one-liner for this as I'll need to plug this into numerous existing command chains with minimal increase of complexity.

For most commands, this is simple. But timeout returns a non-zero status code when it times out (124, 143 or 271), not just when it fails to run because it doesn't exist (1 or 127). Therefore, the simple || operator cannot differentiate between "no such command" and "the operation timed out". As a result, this won't work:

(timeout 5 df || df) # will execute bare df if there is no timeout *or* if the first df times out, thus hanging

Another option that tests for the timeout command without trying execute it also doesn't work for the same reason. The timeout itself on a time-out condition will look to || the same as if the timeout command doesn't exist:

(command -v timeout >/dev/null && timeout 5 df || df) # same result

I need a compact if-then-else for return codes where the return code of the last piece (else) is driven solely by the first piece (if) and not influenced by the middle one (then). If the timeout command exists, use it, otherwise, execute the bare command. I do not want a time-out condition to result in executing the bare command.

Here's one way that works correctly, by testing twice:

(command -v timeout >/dev/null || df; command -v timeout >/dev/null && timeout 5 df)

But this doesn't feel very elegant; I'm having to repeat the command -v timeout >/dev/null twice. Another option that works with only one test:

(command -v timeout >/dev/null; if [ $? -eq 0 ]; then timeout 5 df; else df; fi)

But the explicit if-then-else-fi with the semi-colons and such feels heavy-handed and not as compact as it should be.

Any suggestions for a cleaner, more compact way of doing this? It would need to work in as old as ksh88 on AIX, Sun, HP-UX and Linux.


Solution

  • Note that A && B || C executes C if A or B fails. You need to use if to separate B and C properly.

    Sounds like you'll want to create a function, and likely in a library file: if this is in file my_timeout.ksh

    timeout() {
      if command -v timeout >/dev/null; then
        command timeout "$@"
        # possibly capture $? and add a case statement 
        # if you need to do special things upon actual timeout
      else
        "$@"
      fi
    }
    

    Then in your scripts the minimal change is:

    . my_timeout.ksh
    timeout df --with args
    

    The ksh . will look in the PATH for files to source (at least in ksh93, not 100% sure about ksh88). Otherwise, source it by absolute path.