9

In a script I'm writing, I want to create something temporary on my filesystem, but - it's not in /tmp but elsewhere, and may not be a file nor a directory (e.g. maybe it's a named pipe or a symbolic link). The point is - I'll have do the creation myself. Now, I want to use a unique filename for my temporary, so that future invocations of the utility plus any other running code won't try to use the same name.

If I were just creating a temporary file or directory in /tmp, I could use mktemp. But - what do I do when I only want to generate the name?

1
  • 5
    I don’t know how this must be fitted into your existing project but you could use mktemp to create a directory. That would allow you to create whatever pathnames you want beneath that directory path later.
    – Kusalananda
    Commentedyesterday

7 Answers 7

16

Bit of a conflicting requirement! Idiomatically, you put unique files either into $TMPDIR (or /tmp by default), or they are state-carrying files, in which case they belong in $XDG_STATE_HOME/yourapplicationname/ (which defaults to $HOME/.local/state/yourapplicationname/).

The point is - I'll do the creation myself.

Now, that's a bad idea! Just let mktemp create the file, and then overwrite it with what you want. Letting mktemp create it makes sure you're actually using a unique thing that hasn't been used before¹, e.g. by an interrupted execution of your script, or a second script.

In either case, you do use mktemp, with the -p option, e.g., something like

filename="$(mktemp -p "${XDG_STATE_HOME:-$HOME/.local/state}/einpoklumsscript/")" 

But that's a safe temporary file name, and that's not really the same as a unique filename. For that, you'd usually want a UUID (universally unique identifier) and you can generate one using uuidgen. But: unlike mktemp, that's not race-safe. It seems you wantmktemp, really!


¹ For your technical interest why this is the probably only way to create a filename that's safe against collisions even when running multiple instances of your script:

mktemp creates that temporary file by calling openat(AT_FDCWD, "/path/to/random/filename", O_RDWR|O_CREAT|O_EXCL).

And the exciting part here is O_RDWR|O_CREAT|O_EXCL:

  • O_RDWR ensures the file is only created if the result can be both read and written to.
  • O_CREATallows creation of the file by opening it,
  • O_EXCLenforces that this file is created; if it existed before, openat fails, and mktemp picks a different file name.

The UNIX filesystem API guarantees that two tasks, even running exactly synchronously, cannot O_EXCL open the same file name – thus that's the operating system-supplied only way to ensure a file is created only once. Anything else is a race condition!

So, both correctlyandidiomatically: use mktemp to create a temporary file that you then write into. That's the UNIX way of doing it! Don't create it yourself.

(a symlink in my case, or a named pipe, or something similar) in my filesystem, […] I'll have do the creation myself.

Luckily, that's not strictly true. Aside from openat(… , O_CREAT|O_EXCL), the renameat syscall is also atomic.

So, the race-condition-safe dance becomes, if you're in a shell script:

  1. filename="$(mktemp -p "${directory}")" to "reserve" the filename, safely
  2. replace that file with the thing you want to create, atomically:
    • if the thing you want to replace it with is a symlink, ln -f -s -- whatever "${filename}" does this correctly atomically (by creating a randomly-named symlink, and renameat-ing it to ${filename}). I suspect that's what you want here.
    • if the thing you want to replace it with is a named pipe (or anything else for which the creation tool doesn't allow forcing replacement):
      First, you create a local temporary directory safely on the same filesystem, localtmp="$(mktemp -d -p "${directory}")",
      then you create a named pipe in there, mkfifo -- "${localtmp}/fifo",
      then you rename that to the target mv -- "${localtmp}/fifo" "${filename}", and
      finally, you remove the empty temporary directory rmdir -- "${directory}"

That's it! Admittedly, a bit more work, but that's the way you can have safely randomly named anything in a shell script, so it was worth writing down :)

4
  • 1
    Actually, the file is not a file. It's a symlink in my case, or could have been a named pipe, or something like that.
    – einpoklum
    Commentedyesterday
  • 3
    then use mktemp, then make a symlink and rename it to the name of the file generated by mktemp. Again, mktemp is safe against collision, you generating a file from a random filename is not.Commented23 hours ago
  • 1
    @einpoklum anyways, because it is pretty significant here, added the info to your question!Commented22 hours ago
  • 2
    @einpoklum added info how to safely work around that problem :)Commented22 hours ago
7

Well, do you want a guaranteed unique name, or do you want a probabilistically unique name? If the latter, then yes, by all means just generate a random name using what ever means, use that and get on with the job. But that leaves the possibility, however slight, that another copy of the same tool (or even an unrelated one) might end up using the same file, if by chance it generated the exact same name.

To guarantee uniqueness, you need to actually create the object and verify it got created anew. The OS can do that safely, provided you ask for it, e.g. the open() system call with the O_CREAT and O_EXCL flags fails if a file of the same name already exists. The same is true for mkfifo() and mkdir().

So, if you want a FIFO, you could generate a name, run mkfifo check if it succeeds, and retry with a new name if it fails. But doing that manually is a bit weary. For regular files, mktemp does that for you, but it can't create FIFOs.

It can, however, create a unique directory for you, and then you can create what ever files / symlinks / FIFOs within that directory, knowing the whole directory is uniquely named.

So:

dir=/path/to/worktree/ d=$(mktemp -d -- "$dir/tmp.XXXXXX") || exit fifo="$d/myfifo" if ! mkfifo -- "$fifo"; then # this shouldn't happen printf >&2 '%s\n' "mkfifo '$fifo' failed??" exit 1 fi # ... rm -f -- "$fifo" && rmdir -- "$d" 

If you can't create the intermediate directory either, then you need to either accept the chance of a collision, or do the checking manually:

dir=/path/to/worktree/ until fifo=$(mktemp -u -- "$dir/myfifo.XXXXXX") || exit mkfifo -- "$fifo" do echo >&2 "oops, collision, retrying..." done printf '%s\n' "created '$fifo'" 
1
  • 1
    Using a unique directory is probably the safest way to go, yes; but I might stick to the simpler suggestions, since my script is not required to be that robust.
    – einpoklum
    Commentedyesterday
6

From the mktemp man page...

-u, --dry-run do not create anything; merely print a name (unsafe) 

And you cannot guarantee the future.

    6

    mktemp does have the following option:

     -u, --dry-run do not create anything; merely print a name (unsafe) 

    E.g. got just a temporary pathname without creation using:

    [mr_halfword@skylake-alma eclipse_project]$ mktemp --dry-run --tmpdir=$HOME /home/mr_halfword/tmp.luj0friP22 [mr_halfword@skylake-alma eclipse_project]$ file /home/mr_halfword/tmp.luj0friP22 /home/mr_halfword/tmp.luj0friP22: cannot open `/home/mr_halfword/tmp.luj0friP22' (No such file or directory) 

    The point is - I'll do the creation myself.

    The reason that the --dry-run option is unsafe is that it doesn't guarantee that two processes may not try and use the same temporary pathname.

    If I were just creating a temporary file or directory in /tmp, I could use mktemp.

    Given that the --tmpdir option specifies the temporary directory, overriding the default of /tmp, it may be safer to use --tmpdir to allow mktemp to create a temporary pathname of your choice in a safe way.

      3

      The true idomatic way (in Unix) to generate a unique temporary filename is to use the current process ID to form the name.

      For example in a shell script:

      touch tmpfile.$$ 

      There can only ever be one process with a given ID running at any one time so the process ID is unique for the lifetime of that process.

      In general this is usually considered to have sufficient "uniqueness" for any temporary file, as most such files will only have a lifetime equal to that of the process creating and using them.

      Indeed if your file is truly temporary and is used only while your process (script) is running then even if there's a stale leftover with the same name you can just use it as if it were created by your process since it is impossible for the previous process that did create it to still be using it. That process is no longer running as your current process now has that same process ID and so that file, even if previously existing, is still "unique", though perhaps it needs some cleanup, such as truncation if it is an ordinary file.

      Of course if a given user can only ever run one instance of your script at any time, and they will run it in such a way that the temporary file is created in a private directory only they can write to, then even including the process ID in the temporary filename is pointless, but people will probably look at your code as suspect if you don't include the process ID.

      The rest of this is a digression about possible security issues that may or may not affect your script:

      Using a "predictable" name, e.g. one relying on the process ID for uniqueness, is not always considered secure, especially if the directory where the file is created has more permissions than maybe it should (or it is a shared directory with intentionally non-private permissions, e.g. /tmp) since some attacker could potentially have created the target file with slightly different permissions than you intend, or even as a symlink pointing to a sensitive file, etc., etc., etc.

      Since you are asking about shell scripts, well the safest way to make sure a file of some arbitrary type is unique, and is securely created as unique, is to create it in a uniquely named directory, since directories can safely be created with unique names from a shell script (and presumably with private permissions, given a safe umask is set). This way your script can also safely avoid potential "Time of Check(creation) vs. Time Of Use" (TOCTOU) attacks. Just add an "edition" number to the path if the mkdir(1) fails and try again. There's still some risk an attacker can perform a denial of service attack against your script though...

      So, of course the modern idiom is indeed to use mktemp(1), with it's -d option for your purposes since that will give you a safe, secure, and of course unique and private (even with an unsafe umask) playground in which to create any type of file you wish. You can avoid /tmp by using the -p dir option, where of course dir can be just "." if that's what you need. mktemp(1) takes care of making sure the result is unique and doesn't clash with any stale leftovers.

      2
      • s/is not always considered secure/is considered very insecure/ and should not be done. s/is to use/was to use/. This kind of approach should really be discouraged.Commented7 hours ago
      • The "true idiomatic way" is really, really not a good way. I'm one of the biggest traditionalists I know, and even I would not recommend this (let alone in the first sentence of an answer!). I always used to do it that way, and I've still got some old shell scripts lying around that occasionally generate weird errors because they (a) forget to remove their tmpfiles and (b) happen to run under the same PID next time. It's not a just a theoretical concern. And it's really no harder to type tf=$(mktemp) than it is to type tf=/tmp/tf$$.Commented1 hour ago
      1

      I don't understand why a temporary file would not be in /tmp or the system's temporary directory, but your script could look like:

      #! /usr/bin/env bash r_dir=`mktemp -d` file="${r_dir}/filename.ext" # ... Use the file variable and create your file... # 

      If you don't create the file, but a separate command does, pass $file as an argument or:

      start_dir=${PWD} cd ${r_dir} # Run the command # cd ${start_dir} 

      For a bash one liner:

      command --arg "`mktemp -d`/filename.ext" 
      New contributor
      user7215 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
        0

        No bash one liners?? unbelievable!

        echo $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c6) 

        -c6 6 letters long or 6^62 possibilities.

        1
        • 1
          If you just want something random and are using bash, with no checks for collisions, you might as well just use $RANDOM.
          – terdon
          Commented19 hours ago

        You must log in to answer this question.

        Start asking to get answers

        Find the answer to your question by asking.

        Ask question

        Explore related questions

        See similar questions with these tags.