7

I'm having an issue with the following piece of (G)AWK script:

do {
    ...
} while (system("sleep 10"))

My intention is to break the loop when the user presses ^C during the sleep, but it's not working.

I believe the problem is that Bash exits with 0 when interrupted with ^C, at least when it's executed by AWK's system():

$ awk 'BEGIN { print "\n" system("sleep 2") }'
(let the sleep complete)
0

 

$ awk 'BEGIN { print "\n" system("sleep 2") }'
^C
0

Why is it so?

Is this a bug in Bash or in (G)AWK?

Is there a simple solution that doesn't involve complicated Bash-specific syntax like trap?

The best I could come up with is this:

do {
    ...
} while (42 == system("sleep 10 && exit 42"))

which still looks like a kludge to me.

  • 1
    You might consider perl for this job, as you can be very specific about signal handling behavior (moreso than with ruby or python). – Otheus Mar 3 '16 at 17:01
6

What awk's system() should return is poorly specified.

What seems to be common among awk implementations is that upon a normal exit, it returns the exit code (the number passed to exit(3) modulo 256), but when the shell process is killed by a signal, there's a lot of different behaviour.

Also note that while the C function system(3) is meant to ignore SIGINT (and SIGQUIT) in the parent, it's not very clear (to me at least) that the requirement also applies to awk's system(). Some awk implementations (like mawk) will die upon that SIGINT (that's also the behaviour I'd like to see as I don't like my CTRL-C being ignored just because awk happens to be running the system() function), some (like gawk or traditional implementations) won't.

Also note that some shells can intercept some of those signals and eventually call exit() which would affect the behaviour (see the discussion in comments about the Bourne shell for instance), which is why I use exec in the examples below to remove the shell from the loop.

For the value returned by system() (there's even more variation if you consider close()1) upon a SIGINT, we see:

$ nawk 'BEGIN {print system("exec kill -s INT $$")}'
0.0078125
$ bwk-awk 'BEGIN {print system("exec kill -s INT $$")}'
0.0078125
$ mawk 'BEGIN {print system("exec kill -s INT $$")}'
130
$ gawk 'BEGIN {print system("exec kill -s INT $$")}'
0

0.0078125 being 2 / 256 (for a SEGV of 11, you'd get 0.542969 ((128+11)/256) if a core was dumped, 0.0429688 (11/256) otherwise), nawk being the nawk found on Solaris 10 or 11, or its Linux port in the Heirloom toolchest, bwk-awk being the awk maintained by Brian Kernighan himself (the K in awk) the basis for the awk found on some BSDs (here tested on Debian GNU/Linux). /usr/xpg4/bin/awk on Solaris 11 behaves like gawk.

So based on the value s returned by system(3) (an integer where bits 0 to 6 are the signal number, bit 7 the core-bit, and bits 8 to 15 the exit code), awk's system() above returns either:

  • s / 256 (traditional awk implementations),
  • int(s/256) (gawk),
  • or in mawk, the same transformation as done by shells like the Bourne or C-shell for instance ((s&127)+128 if killed, s>>8 otherwise), except that if a core is dumped, you get (s&127)+256 instead of (s&127)+128 (the value is (s&255)+128).

So, here, you could do instead:

awk 'BEGIN{print system("trap exit\\ 1 INT; sleep 10")}'

But it would still cause awk to be killed with some awk implementations like mawk. If your sh is bash or yash, you could do:

awk 'BEGIN{print system("set -m; sleep 10; exit")}'

So sleep is run in its own process group (and only it gets the SIGINT).

Another alternative could be to ignore SIGINT before calling awk. However, most shells (and that's a POSIX requirement) cannot change a signal handler if the signal was already ignored on start. So things like:

(
  trap '' INT
  awk 'BEGIN{print system("trap exit\\ 1 INT; sleep 10; exit")}'
)

Won't work. zsh doesn't have that (self-inflicted) limitation though, so if you know zsh is available, you could do:

(
  trap '' INT
  awk 'BEGIN{print system("exec zsh -c \"TRAPINT() exit 1; sleep 10\"")}'
)

Which would work whether awk is mawk, gawk or other and would avoid having to mess with job control. At this point though, it would be worth considering using perl/python/ruby... instead of awk where you can adapt the signal handling to your needs.

Notes

1 Upon close() of a pipeline, as in:

awk 'BEGIN {cmd = "kill -s INT $$"; cmd | getline; print close(cmd)}'

First, this time ^C interrupts awk in all implementations I've tried (there's not such requirement to ignore SIGINT/SIGQUIT for popen(3)/pclose(3) (a natural way to implement that getline) as there is for system(3)).

But when it comes to the exit status (where s is the value returned by pclose(3)/waitpid(2) like for system() above), we see:

  • Solaris nawk: doesn't work, you can't call close() like that in Solaris nawk.
  • /usr/xpg4/bin/awk on Solaris. Returns always 0, even upon a exit(1) done by the process. Clearly a conformance bug.
  • gawk and bwk-awk: gives s (exit 1 gives 256, killed by SIGINT gives 2, killed by a SIGSEGV of 11 with core gives 139).
  • mawk: same as for system(), looks like mawk is the only implementation that gave any thought into that.
  • Poorly specified or not, it looks like a bug in GAWK to me. Oh well. Thanks – Tobia Mar 3 '16 at 16:43
  • Also, set -m by itself seems to do the trick! system("set -m; sleep 10") is actually the cleanest solution of the bunch, and it works reliably across all AWKs. Awesome! – Tobia Mar 3 '16 at 17:02
  • @Tobia. Yes, happens to work with yash as well. Note that that usage of set -m is not portable (won't work with other implementations of sh that I know). And it will prevent you from running awk in background. – Stéphane Chazelas Mar 3 '16 at 17:07
  • You're right. It doesn't work with Dash, which is the default /bin/sh in many systems :-( – Tobia Mar 3 '16 at 17:32
  • Looks like you are not using the original nawk but something else. – schily Mar 4 '16 at 8:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.