8
\$\begingroup\$

I wrote the following atexit implementation for Bash

#! /bin/bash

set -e

ATEXIT=()

function atexit_handler
{
  local EXPR
  for EXPR in "${ATEXIT[@]}"; do
    echo "evaluating $EXPR"
    eval "$EXPR" || true
  done
}

trap atexit_handler EXIT

function atexit ()
{
  local EXPR
  for EXPR in "$@"; do
    ATEXIT+=("$EXPR")
  done
}

atexit true
atexit false
atexit "echo bye"

false

I am wondering, if this is possible without the use of eval.

\$\endgroup\$

3 Answers 3

7
\$\begingroup\$

I don't think it's possible without the eval. A close candidate might be:

atexit_handler() {
  local EXPR
  for EXPR in "${ATEXIT[@]}"; do
    echo "evaluating $EXPR"
    $EXPR || true
  done
}

But this won't work with non-trivial expressions like this:

atexit 'for i in "a b" c; do echo $i; done'

Using the function keyword in function declaration like this is an outdated practice:

function atexit_handler
{
  local EXPR
  for EXPR in "${ATEXIT[@]}"; do
    echo "evaluating $EXPR"
    eval "$EXPR" || true
  done
}

Use the more modern style I wrote in the previous example above.


Instead of this:

  for EXPR in "$@"; do
    ATEXIT+=("$EXPR")
  done

A simpler way to iterate over $@:

  for EXPR; do
    ATEXIT+=("$EXPR")
  done

But actually, as @etan-reisner pointed out in a comment, it's silly to loop here when you can add the entire arg list in one swift move:

ATEXIT+=("$@")
\$\endgroup\$
5
  • 2
    \$\begingroup\$ Why bother iterating over "$@" at all? Why not just splat it into the () directly? ATEXIT+=("$@") \$\endgroup\$ Commented Oct 23, 2014 at 18:45
  • 1
    \$\begingroup\$ Using set -e means it works even when the script is run "manually" (i.e. bash myscript.sh) which the shebang does not. \$\endgroup\$ Commented Oct 23, 2014 at 18:46
  • \$\begingroup\$ @EtanReisner spot on! Thanks for that. I added the first point to my answer and removed the second. \$\endgroup\$
    – janos
    Commented Oct 23, 2014 at 20:34
  • \$\begingroup\$ Emacs does not color the function names in sh-mode. Using the function keyword adds some syntax highlighting to functions. \$\endgroup\$
    – ceving
    Commented Oct 27, 2014 at 9:51
  • \$\begingroup\$ At the time I posted this, I didn't know what EXIT really meant. Thanks @TobySpeight for pointing this out, dropped that point. (I fear I may have said things like this in other old answers too. I should probably search and verify all :-/) \$\endgroup\$
    – janos
    Commented Nov 27, 2018 at 21:57
2
\$\begingroup\$

Still uses eval, but it seems cleaner to do:

atexit() {
        cmd="$1"
        eval set -- $(trap -p EXIT)
        trap "$cmd; $3" EXIT
}

To allow semi-colons (eg, atexit "echo foo;") and aesthetic purity, you might like:

atexit() {
        cmd="${1%;}"
        eval set -- $(trap -p EXIT)
        trap "$cmd${3:+; }$3" EXIT
}
\$\endgroup\$
2
  • \$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please edit to show what aspects of the question code prompted you to write this version, and in what ways it's an improvement over the original. It may be worth (re-)reading How to Answer. \$\endgroup\$ Commented Nov 27, 2018 at 15:10
  • \$\begingroup\$ Nice idea. You should use quotes in case the trap commands contain special characters: eval "set -- $(trap -p EXIT)". (Aside: trap -p <sigspec> is specific to bash. POSIX only specifies a way to display all the traps, by running trap with no arguments.) \$\endgroup\$
    – tom
    Commented Jul 17, 2022 at 9:23
1
\$\begingroup\$

Sorry for excavating this, but just found it and wanted to try.

Will offer what I can.

#! /bin/bash

This is no crime, but it's a tad more portable (if that matters at all to you) if you use

#! /bin/env bash

env itself is virtually always in /bin, though some installations move the interpreter binaries.

I don't personally like or use set -e. It doesn't give me as much control over what happens. Personally, I prefer traps, something like this:

trap 'echo "ERROR $? at $0:$LINENO - [$BASH_COMMAND]"' err

Then I get useful feedback.

$: bogus
bash: bogus: command not found
ERROR 127 at /usr/bin/bash:73 - [bogus]

As for the eval, how about creating a controlled file to source at exit time?

atexit_handler() {
  tmp=$(mktemp)
  printf "%s\n" "${ATEXIT[@]}" >| $tmp
  trap 'echo "Error $? in exit handler when processing [$BASH_COMMAND]"' err
  . $tmp
}

This would allow for more arbitrarily complex possibilities, without really worsening the security risk of the eval, which isn't catastrophic if your code registers all the commands going into the handler anyway. It also leaves you the option of scheduling the cleanup of the file with the handler, so that if it succeeds it leaves no mess, but if it fails will abort and let you debug the file.

atexit true
atexit "echo bye"
atexit 'sing() { for i in do re me;do echo "$i"; done; }'
atexit sing
atexit false
atexit 'rm $tmp' # defer evaluation 

I love this idea, btw. I may start using it. :)

\$\endgroup\$
4
  • 1
    \$\begingroup\$ I've never seen a /bin/env - on Debian-based systems at least, it's always in /usr/bin. \$\endgroup\$ Commented Nov 27, 2018 at 16:48
  • \$\begingroup\$ As long as it's working, I wouldn't sweat it. YMMV; even where I work, across a lot of systems, they are mostly all RHEL 6 or 7 and vaguely similar in structure, so it has rarely been an issue. It's just something I have been told to cultivate as a habit, and I have (anecdotally) had fewer issues since I started doing it. shrug :D \$\endgroup\$ Commented Nov 27, 2018 at 16:58
  • \$\begingroup\$ So RHEL have /bin/env instead of /usr/bin/env? Or as well as? If it has both, then the latter would seem to be the safer choice. \$\endgroup\$ Commented Nov 27, 2018 at 18:21
  • \$\begingroup\$ Common to have it linked in both locations just in case. :) \$\endgroup\$ Commented Nov 27, 2018 at 18:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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