4

I've a bash script with some file manipulations and I would like to process a loop until the end of the block after pressing CTRL+C. I've made an example:

#!/bin/bash

# Register signal handler
ABORT=0;
trap ABORT=1 SIGINT;

# Create temp dir
TEMPDIR=$(mktemp -d -t $0);

# Helper functions
function do_other_stuff {
    true;
}

# Process files
for ((COUNTER = 0; COUNTER < 3 && ABORT == 0; COUNTER++)); do
    FILE=/some/directory/$COUNTER.txt;
    BASE=$(basename $FILE);
    cp $FILE $TEMPDIR;
    > $FILE;
    do_other_stuff;
    cp $TEMPDIR/$BASE $FILE;
    rm $TEMPDIR/$BASE;
done;

rm -rf $TEMPDIR;

This seems to work quite well, but I noticed, that sometimes BASE in the statement

BASE=$(basename $FILE);

is not set, if the trap happens to occur during the basename command. This leads to errors in the cp and following commands.

Did I miss something there? How is the intention of bash to recover from traps? Is there another solution with the same effect?

Jolta
  • 2,620
  • 1
  • 29
  • 42
Urs Marti
  • 43
  • 2

1 Answers1

2

Instead of

BASE=$(basename $FILE);

Have this one instead:

BASE=${FILE##*/}

It's also a good idea to place your work functions on the background away from the interface that handles SIGINT. Just avoid asking for input within it. Also quote your variables properly always.

#!/bin/bash

# Register signal handler
ABORT=0;
trap ABORT=1 SIGINT;

# Create temp dir
TEMPDIR=$(mktemp -d -t $0);

# Helper functions
function do_other_stuff {
    true;
}

# Process files
for ((COUNTER = 0; COUNTER < 3 && ABORT == 0; COUNTER++)); do
    (
        FILE=/some/directory/$COUNTER.txt
        BASE=${FILE##*/}
        cp "$FILE" "$TEMPDIR"
        > "$FILE"
        do_other_stuff
        cp "$TEMPDIR/$BASE" "$FILE"
        rm "$TEMPDIR/$BASE"
    ) &
    CPID=$!
    # Handle SIGINT but don't end the loop until subprocess finishes its work.
    while kill -s 0 CPID &>/dev/null; do  ## Checks if subprocess is still there.
        # Wait if yes.
        wait "$CPID"
    done
done

rm -rf "$TEMPDIR"

This one will abort the operation quickly:

# Process files
for ((COUNTER = 0; COUNTER < 3 && ABORT == 0; COUNTER++)); do
    (
        FILE=/some/directory/$COUNTER.txt
        BASE=${FILE##*/}
        cp "$FILE" "$TEMPDIR"
        > "$FILE"
        do_other_stuff
        cp "$TEMPDIR/$BASE" "$FILE"
        rm "$TEMPDIR/$BASE"
    ) &
    CPID=$!
    while
        wait "$CPID"
        if [[ ABORT -eq 1 ]]; then
            kill -s ABRT "$CPID" &>/dev/null
            break
        fi
        kill -s 0 "$CPID" &>/dev/null
    do
        continue
    done
done

rm -rf "$TEMPDIR"
konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • That worked, thank you. There is only one question left, what the intention of bash in that matter is. – Urs Marti Sep 10 '13 at 19:42
  • @UrsMarti Since we know that SIGINT could interrupt other operations beside being handled by the trap, we just let the work commands be placed on a background under a subshell (`( commands; ...; ) &`). So by doing that SIGINT would no longer be able to reach them, and as intended, the shell would easily be able to process it. When in wait mode (`wait`) and a signal is received, the trap is triggered setting the flag to a variable, and after that, the operation continues on the next command after wait. That is the time where we go to a loop to keep checking the subshell until it finishes. – konsolebox Sep 10 '13 at 19:47