123

I am creating temporary files from a bash script. I am deleting them at the end of the processing, but since the script is running for quite a long time, if I kill it or simply CTRL-C during the run, the temp files are not deleted.
Is there a way I can catch those events and clean-up the files before the execution ends?

Also, is there some kind of best practice for the naming and location of those temp files?
I'm currently not sure between using:

TMP1=`mktemp -p /tmp`
TMP2=`mktemp -p /tmp`
...

and

TMP1=/tmp/`basename $0`1.$$
TMP2=/tmp/`basename $0`2.$$
...

Or maybe is there some better solutions?

codeforester
  • 39,467
  • 16
  • 112
  • 140
skinp
  • 4,157
  • 4
  • 27
  • 20

8 Answers8

159

I usually create a directory in which to place all my temporary files, and then immediately after, create an EXIT handler to clean up this directory when the script exits.

MYTMPDIR="$(mktemp -d)"
trap 'rm -rf -- "$MYTMPDIR"' EXIT

If you put all your temporary files under $MYTMPDIR, then they will all be deleted when your script exits in most circumstances. Killing a process with SIGKILL (kill -9) kills the process right away though, so your EXIT handler won't run in that case.

Tometzky
  • 22,573
  • 5
  • 59
  • 73
Chris AtLee
  • 7,798
  • 3
  • 28
  • 27
  • 32
    +1 Definately use a trap on EXIT, not silly TERM/INT/HUP/whatever else you can think of. Though, remember to **quote** your parameter expansions and I would *also* recommend you *single* quote your trap: trap 'rm -rf "$TMPDIR"' EXIT – lhunath Mar 26 '09 at 20:05
  • 9
    Single quotes, because then your trap will still work if later on in your script you decide to clean up and change TMPDIR because of circomstances. – lhunath Mar 26 '09 at 20:06
  • 2
    @AaronDigulla Why is $() vs backticks important? – Ogre Psalm33 Feb 27 '13 at 21:39
  • 4
    @OgrePsalm33: http://stackoverflow.com/questions/4708549/shell-programming-whats-the-difference-between-command-and-command – Aaron Digulla Feb 28 '13 at 07:59
  • @lhunath: What does the single quote trap syntax do? Quite unfamiliar with the double quotes inside single quotes. Thought the double quotes would have no special meaning with this syntax. Does you proposed syntax send the string un-evaluated to trap, which will eval it on EXIT? – Alexander Torstling Jan 28 '15 at 12:08
  • 3
    @AlexanderTorstling code should always be single-quoted to prevent injection resulting in arbitrary code execution. If you expand data into a bash code STRING then that data can now do anything code does which results in innocent bugs wrt white space but also destructive bugs such as clearing your homedir for bizarre reasons or introducing security holes. Note that trap takes a string of bash code which will be evaluated as-is later on. So later on when the trap fires, the single quotes will be gone and there will be just the syntactical double quotes. – lhunath Jan 29 '15 at 14:36
116

You could set a "trap" to execute on exit or on a control-c to clean up.

trap '{ rm -f -- "$LOCKFILE"; }' EXIT

Alternatively, one of my favourite unix-isms is to open a file, and then delete it while you still have it open. The file stays on the file system and you can read and write it, but as soon as your program exits, the file goes away. Not sure how you'd do that in bash, though.

BTW: One argument I'll give in favour of mktemp instead of using your own solution: if the user anticipates your program is going to create huge temporary files, he might want set TMPDIR to somewhere bigger, like /var/tmp. mktemp recognizes that, your hand-rolled solution (second option) doesn't. I frequently use TMPDIR=/var/tmp gvim -d foo bar, for instance.

Tometzky
  • 22,573
  • 5
  • 59
  • 73
Paul Tomblin
  • 179,021
  • 58
  • 319
  • 408
  • 8
    With Bash, `exec 5<>$TMPFILE` ties file descriptor 5 to $TMPFILE as read-write, and you can use `<&5`, `>&5`, and `/proc/$$/fd/5` (Linux) thereafter. The only problem is that Bash lacks `seek` function... – ephemient Mar 27 '09 at 02:16
  • 6
    A couple of notes on `trap`: there's no way to trap `SIGKILL` (by design, as it immediately terminates execution). So, if that might happen, have a fallback plan (such as `tmpreaper`). Secondly, traps are not cumulative - if you have more than one action to perform, they must all be in the `trap` command. One way to cope with multiple cleanup actions is to define a function (and you can redefine it as your program proceeds, if necessary) and reference that: `trap cleanup_function EXIT`. – Toby Speight Sep 28 '16 at 13:40
  • 1
    I had to use `trap "rm -f $LOCKFILE" EXIT` or I would get a unexpected end of file error. – Jaakko Nov 22 '16 at 10:51
  • 4
    Shellcheck gave warning to use single quotes, that the expression would be expanded 'now' with double quotes rather than later when the trap is invoked. – LaFayette Sep 03 '18 at 08:55
  • 1
    Yes, better to use single quotes and put the `trap` statement at the very beginning of the script (after shebang). `trap 'rm -f -- "$lockfile"' EXIT` to be very precise. – codeforester Mar 20 '19 at 21:57
  • 1
    Why are you using curly braces here? – CMCDragonkai Jul 03 '22 at 06:58
  • 1
    @CMCDragonkai copied from the linked man page. Probably so you can have multiple commands in the trap – Paul Tomblin Jul 04 '22 at 10:09
  • I am using this technique to prevent multiple running instance of a shell script. The problem is when the second instance exits, the trap is triggered and the lock file created by first instance gets deleted! So there is a possibility another instance might start executing after that. Any ideas to fix this? – gkns Aug 10 '23 at 11:59
30

You want to use the trap command to handle exiting the script or signals like CTRL-C. See the Greg's Wiki for details.

For your tempfiles, using basename $0 is a good idea, as well as providing a template that provides room for enough temp files:

tempfile() {
    tempprefix=$(basename "$0")
    mktemp /tmp/${tempprefix}.XXXXXX
}

TMP1=$(tempfile)
TMP2=$(tempfile)

trap 'rm -f $TMP1 $TMP2' EXIT
Brian Campbell
  • 322,767
  • 57
  • 360
  • 340
  • 1
    Do not trap on TERM/INT. Trap on EXIT. Trying to predict the exit condition based on signals received is silly and definately not a catchall. – lhunath Mar 26 '09 at 20:03
  • 3
    Minor point: Use $() instead of single backticks. And put double quotes around $0 because it could contain spaces. – Aaron Digulla Nov 18 '09 at 09:18
  • Well, the backticks work fine in this comment, but that's a fair point, it is good to be in the habit of using `$()`. Added the double quotes as well. – Brian Campbell Nov 18 '09 at 14:37
  • 1
    You may replace your whole subroutine with just TMP1=$(tempfile -s "XXXXXX") – Ruslan Kabalin Jul 21 '11 at 09:37
  • 4
    @RuslanKabalin Not all systems have a `tempfile` command, while all reasonable modern systems that I know of have a `mktemp` command. – Brian Campbell Nov 29 '11 at 01:56
14

Just keep in mind that choosen answer is bashism, which means solution as

trap "{ rm -f $LOCKFILE }" EXIT

would work only in bash (it will not catch Ctrl+c if shell is dash or classic sh), but if you want compatibility then you still need to enumerate all signals that you want to trap.

Also keep in mind that when script exits the trap for signal "0"(aka EXIT) is always performed resulting in double execution of trap command.

That the reason not to stack all signals in one line if there is EXIT signal.

To better understand it look at following script that will work across different systems without changes:

#!/bin/sh

on_exit() {
  echo 'Cleaning up...(remove tmp files, etc)'
}

on_preExit() {
  echo
  echo 'Exiting...' # Runs just before actual exit,
                    # shell will execute EXIT(0) after finishing this function
                    # that we hook also in on_exit function
  exit 2
}


trap on_exit EXIT                           # EXIT = 0
trap on_preExit HUP INT QUIT TERM STOP PWR  # 1 2 3 15 30


sleep 3 # some actual code...

exit 

This solution will give you more control since you can run some of your code on occurrence of actual signal just before final exit (preExit function) and if it needed you can run some code at actual EXIT signal (final stage of exit)

Alex
  • 785
  • 6
  • 9
8

GOOD HABITS ARE BEAUTIFUL

  • Avoid assuming the value of a variable is never going to be changed at some super distant time (especially if such a bug would raise an error).

  • Do cause trap to expand the value of a variable immediately if applicable to your code. Any variable name passed to trap in single quotes will delay the expansion of its value until after the catch.

  • Avoid the assumption that a file name will not contain any spaces.

  • Do use Bash ${VAR@Q} or $(printf '%q' "$VAR") to overcome issues caused by spaces and other special characters like quotes and carriage returns in file names.

    zTemp=$(mktemp --tmpdir "$(basename "$0")-XXX.ps")
    trap "rm -f ${zTemp@Q}" EXIT
Paul
  • 442
  • 4
  • 9
  • 2
    It should be noted that the `${parameter@operator}` expansions were added in Bash 4.4 (released September 2016). – Robin A. Meade Jul 31 '20 at 19:48
  • 2
    Equivalent for Bash < v4.4: `trap "rm -f $(printf %q "$zTemp")" EXIT` – Robin A. Meade Nov 01 '20 at 02:47
  • This is the best answer because there's no need to worry about the value of `zTemp` being altered later in the script. Also, `zTemp` can be declared `local` to a function; it need not be a global script variable. – Robin A. Meade Nov 01 '20 at 02:48
5

The alternative of using a predictable file name with $$ is a gaping security hole and you should never, ever, ever think about using it. Even if it is just a simple personal script on your single user PC. It is a very bad habit you should not obtain. BugTraq is full of "insecure temp file" incidents. See here, here and here for more information on the security aspect of temp files.

I was initially thinking of quoting the insecure TMP1 and TMP2 assignments, but on second thought that would probably not be a good idea.

hlovdal
  • 26,565
  • 10
  • 94
  • 165
  • 1
    I'd give if I could: +1 for the security advice and another +1 for not quoting bad idea and the reference – TMG Sep 23 '19 at 05:51
2

I prefer using tempfile which creates a file in /tmp in the safe manner and you do not have to worry about its naming:

tmp=$(tempfile -s "your_sufix")
trap "rm -f '$tmp'" exit
Ruslan Kabalin
  • 6,580
  • 2
  • 28
  • 20
  • tempfile is sadly very unportable although safer, so it is often better to avoid it or at least emulate it. – lericson Mar 18 '13 at 15:59
-5

You don't have to bother removing those tmp files created with mktemp. They will be deleted anyway later.

Use mktemp if you can as it generates more unique files then '$$' prefix. And it looks like more cross platform way to create temp files then explicitly put them into /tmp.

Mykola Golubyev
  • 57,943
  • 15
  • 89
  • 102
  • Deleted by operation|file system itself after some period of time – Mykola Golubyev Mar 26 '09 at 21:05
  • 4
    Magic? A cronjob? Or a rebooted Solaris machine? – innaM Mar 26 '09 at 21:52
  • Probably one of them. If temp file wasn't removed by some interruption ( it won't be too often ) someday tmp files will be removed - that's why they called temp. – Mykola Golubyev Mar 27 '09 at 08:45
  • 25
    You can not, should not, must not assume that something put in /tmp will remain there forever; at the same time, you should not assume that it will magically go away. – innaM Mar 27 '09 at 09:14