touch, : > and install are not three spellings of one command¶
Level: 201 · for anyone with a terminal
One line: Five of the usual seven ways to blank a file truncate the file that is already there — the mode survives and every other name for it sees zero bytes — while install unlinks the name and puts a new file in its place, so the mode becomes whatever -m said and the old bytes are still sitting under the other name.
Why a cheatsheet is the wrong shape for this¶
Every list of "ways to create a file in Linux" has the same seven or eight rows, and on an empty directory every row is correct: touch f, > f, : > f, truncate -s 0 f, cp /dev/null f, install -m 644 /dev/null f, dd if=/dev/null of=f. Seven commands, seven files of zero bytes, nothing to choose between them.
The list is not useful on an empty directory, though. It is useful on the file you already have, and there the seven rows stop agreeing — because they are not seven ways to do one thing, they are three different operations wearing one description:
- Truncate.
: >,truncate -s 0,cp /dev/null,dd if=/dev/nullandprintf '' >all set the length of the existing file to zero. Nothing is unlinked, so the mode, the owner, every hard link and every open file descriptor are looking at the same file, now empty. - Replace.
installunlinks the name and creates a new file there.-mis not a courtesy, it is the whole point of the tool — it installs a file, with the permissions you asked for. The consequence is that the old file is still on disk, reachable through any other name, until the last reference goes away. - Neither.
touchsets timestamps, and creating the file is what it does when there is nothing to set them on. Pointed at a file with content, it changes no bytes at all — which is why it is the right command for "make sure this exists" and the wrong one for "empty this".
The distinction is the same one behind log rotation, and behind a disk that stays full after you delete the thing filling it: a truncation happens to the file, a replacement happens to the name.
In the terminal¶
Verified output of creating_and_writing_files_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
1. SEVEN WAYS TO MAKE AN EMPTY FILE, AND SEVEN ZEROS
a.txt 0 bytes
b.txt 0 bytes
c.txt 0 bytes
d.txt 0 bytes
e.txt 0 bytes
f.txt 0 bytes
g.txt 0 bytes
Seven commands, seven empty files, and on a fresh name that really is
all there is to say. The list is worth having because the file you
want to empty usually already exists — and that is a different
question, which is section 2.
2. THE SAME SEVEN, POINTED AT A FILE THAT ALREADY HAS SOMETHING IN IT
command t.txt its 2nd name mode (was 600)
touch t.txt kept still 12b 600
: > t.txt emptied emptied 600
truncate -s 0 t.txt emptied emptied 600
cp /dev/null t.txt emptied emptied 600
install -m 644 /dev/null t.txt emptied still 12b 644
dd if=/dev/null of=t.txt emptied emptied 600
printf '' > t.txt emptied emptied 600
Row 1 is the one everybody knows: touch does not empty anything, it
sets timestamps, and creating the file is what it does when there is
nothing to timestamp. Rows 2, 3, 4, 6 and 7 all TRUNCATE the file that
is already there: the mode survives, and so does the second name,
because what changed is the file both names point at. Those five are
interchangeable and choosing between them is a matter of taste.
Row 5 is not in that family at all. install REPLACES: it unlinks the
name, puts a new file there, and -m decides the mode rather than
preserving it. The old file is still twelve bytes and still reachable
through the other name — and through any process that had it open.
That is install doing its job, and it is only a surprise when it is
being used as a seventh way to type ': >'.
3. THE COLUMN THAT LOOKS LIKE IT WOULD ANSWER THIS, AND DOES NOT
after install, link.txt still holds 12 bytes: not the same file
and the inode number of t.txt? that one is not printed here.
It is not printed because it is not stable enough to be an answer key,
and that is the lesson rather than an inconvenience. Measured on
2026-09-07: this same install came back with a DIFFERENT number six
times out of six on APFS and on overlayfs when run in a clean
directory, and with the SAME number inside this script, where the
earlier sections had already freed one for it to reuse. An inode
number is unique among the files that exist right now, not across
time: unlink a file and the number goes back in the pool, and the
very next create may be handed it. So a number that did not change
is not evidence that the file survived, and a number that did change
is not evidence that anything was lost. The second name is evidence.
When the question is whether this is still the same file, ask
something that holds it open — another link, or a descriptor — and
not a number that is only promised to be unique today.
4. WHAT EACH WAY OF WRITING A LINE ACTUALLY PUTS IN THE FILE
echo 'text' > f 746578740a
printf 'text' > f 74657874
printf 'line 1\nline 2\n' > f 6c696e6520310a6c696e6520320a
cat <<< 'text' > f 746578740a
echo 'text' | tee f 746578740a
echo -n text (a POSIX echo) 2d6e20746578740a
echo -n text (bash builtin) 74657874
74 65 78 74 is text. Four of the seven append an 0a you did not type:
echo, the here-string, and tee-behind-echo all end the line for you,
and that is usually right, because a text file's last byte is normally
a newline. printf writes what it is given and nothing else.
The last two rows are the same six characters twice. A POSIX echo has
no -n option, so it prints the flag as text AND ends the line anyway:
2d 6e 20 is '-n ', and the file is six bytes longer than the file you
asked for. bash's builtin, and zsh's, and dash's, all honour -n. This
is not a Linux-versus-Mac split; it is a which-shell split, and the
shell in question is whatever /bin/sh happens to be where the script
finally runs. printf is the way to write bytes without a newline.
5. THE HEREDOC DELIMITER IS A QUOTING DECISION
<< EOF -> hello Adam, and a command
<< 'EOF' -> hello $NAME, and $(echo a command)
Same three lines of typing, and the quotes around the word EOF are the
whole difference. Unquoted, the body is a double-quoted string: $NAME
expands and $( ) runs. Quoted, the body is literal. Neither is the
default you should assume — when the heredoc is a config file, a
Dockerfile or somebody else's script, quote the delimiter, and when it
is a template you are filling in, do not.
6. THE REDIRECT THAT REFUSES, AND THE ONE THAT CANNOT
$ bash -c "set -o noclobber; echo one > n.txt; echo two > n.txt"
exit=1
$ bash -c "set -o noclobber; echo one > n2.txt; : > n2.txt"
exit=1
$ bash -c "set -o noclobber; echo one > n3.txt; echo two >| n3.txt"
exit=0
$ bash -c "set -o noclobber; echo one > n4.txt; truncate -s 0 n4.txt"
exit=0
noclobber makes > refuse to overwrite an existing file, and ': >' is
not an exception to it: the ':' is a command that runs no risk, but
the redirection beside it is the same redirection. The override is
'>|'. And the last row is the reason to know which of these idioms is
a shell feature and which is a program — truncate is a program, so
noclobber has no opinion about it and the file is emptied regardless.
7. AN EMPTY FILE, AND A FILE FULL OF NOTHING
truncate -s 0 -> 0 bytes, hex: (nothing)
truncate -s 5 -> 5 bytes, hex: 0000000000
truncate sets a length, and growing is as much a length change as
shrinking. The five bytes it invents are NUL, which makes this the
fastest way to turn a text file into one that diff will call binary
and that most tools will stop reading at. An empty file holds no
bytes; this one holds five, and prints as nothing either way.
The half of the list that writes something¶
The second half of every such cheatsheet is "and here is how to put a line in it" — echo, printf, a here-document, a here-string, tee, and a one-liner in whichever language is at hand. Those differ from each other too, in the one way this library cares about: how many bytes end up in the file.
echo, the here-string and echo | tee all finish the line for you. That is usually right — a text file's last byte is normally 0a, and a file without one confuses wc -l, read, and every tool that counts lines. printf writes what it is given and nothing more, which is why it is the tool when the bytes matter.
echo -n is where a list like this earns a bug. It is presented everywhere as the way to write a line without a trailing newline, and whether it works depends on which shell runs the script — not on which machine. A POSIX echo has no options at all: it prints -n as text and then ends the line anyway, so the file is six bytes longer than the one you asked for and the newline you were avoiding is there regardless. bash, zsh and dash builtins all honour -n; bash --posix does not, and neither does the /bin/sh on a Mac, which is bash in exactly that mode. A script that runs correctly under bash script.sh can therefore write a different file under sh script.sh — the same file, on the same machine, two minutes apart.
That one cannot be recorded in this page's answer key, because /bin/sh is bash on macOS and dash on Ubuntu and they genuinely disagree; the example reproduces the POSIX branch portably instead, by turning the two options on by hand. The real per-platform run:
$ /bin/sh -c 'echo -n text' | xxd -p # macOS: bash 3.2 in posix mode
2d6e20746578740a # "-n text\n" — 8 bytes
$ dash -c 'echo -n text' | xxd -p # what /bin/sh is on Ubuntu
74657874 # "text" — 4 bytes
Two things that differ between BSD and GNU¶
install may or may not hand the inode number back, and neither answer means anything. Measured 2026-09-07: a clean directory gave a different number six times out of six on APFS and on overlayfs, while the same command inside this page's script — which has already created and deleted files — got the same number back on Ubuntu. An inode number is unique among the files that exist at this moment, not across time; unlinking one puts the number back in the pool. So the number is not the witness. The second hard link is, and it says the same thing on both platforms.
stat takes -f on both systems and means something different by it. On BSD -f is the format string; on GNU -f means display the filesystem, not the file. Both succeed, so the tempting fallback —
— never reaches the second branch on Linux and prints a block-count summary where you wanted an inode. It is the hexdump trap again in a different tool: a flag letter that is valid on both platforms and means two different things is far worse than one that errors. The example on this page probes once with stat -c '%i' . and then commits to an answer.
Four questions that all sound like "make me a file"¶
| What you meant | How to ask it | What it costs |
|---|---|---|
| Make sure it exists, leave it alone if it does | touch f |
nothing; and it empties nothing, which is the point |
| Make it zero bytes, same file, same mode | : > f — or truncate -s 0 f if noclobber is on |
every open descriptor and every hard link now sees an empty file, which is either what you wanted or a bug |
| Put a new file there with known permissions | install -m 644 /dev/null f |
the old file survives under any other name; ownership and mode are set, not inherited |
| Write exact bytes, no newline you did not type | printf '%s' "$text" > f |
none — this is the one that behaves the same in every shell |
The last row is the one to make a habit. printf has no -n question, no escape-interpretation question, and no which builtin is this question; echo has all three, and only ever saves you three characters.
If you are coming from Python or ABAP¶
Python. The three operations are three different calls, and the names line up better than the shell's do. open(p, 'w') is : > — it truncates in place, keeping the inode and the mode. open(p, 'a').close() is close to touch for the make sure it exists half, and Path(p).touch() is the whole of it, timestamps included. os.replace(src, p) is install — an atomic rename over the name, which is why "write a temp file, then os.replace" is the recipe for a config file a reader must never see half-written. And print(x, file=f) adds the newline while f.write(x) does not, which is the same echo/printf split one layer up. The one thing Python will not do for you is the mode: open(p, 'w') on a new file gives you 0o666 & ~umask, and os.open(p, os.O_CREAT | os.O_WRONLY, 0o600) is how you ask, which matters the moment the file holds a credential.
ABAP (Not machine-checked — CI cannot run ABAP.) OPEN DATASET f FOR OUTPUT truncates, which is the : > row, and IN TEXT MODE ENCODING UTF-8 versus IN BINARY MODE is the echo/printf split in the strongest form any of these languages has: text mode appends the platform's line terminator to every TRANSFER, binary mode transfers the bytes you handed it. So the same program writes a different number of bytes depending on a clause six lines earlier, and nothing in the TRANSFER statement says so. There is no touch: the closest thing is opening for output and closing, which is the destructive one — OPEN DATASET ... FOR APPENDING is the non-destructive create. And permissions are the operating system's, set outside the program, so the install -m row has no ABAP spelling at all.
Try it¶
printf 'old\n' > f; ln f g; : > f; wc -c f g. Then the same withinstall -m 644 /dev/null f. One of them leftgalone.- Put
echo -n done > fin a script, run it withbash script.shand then withsh script.sh, andxxdthe file both times. On a Mac those are two different files. truncate -s 5 fon an empty file, thenfile fanddiff f f2. You asked for a length and got five NUL bytes — the byte that makes tools stop reading.set -o noclobberin your shell for a day. It will refuse the>you did not mean;>|is how you say you meant it.
Practice¶
Predict, then run. Write down what each of these six lines puts in f — as hex, byte for byte — before you run any of them. Then run them and check.
echo hello > f
printf hello > f
printf 'hello\n' > f
cat <<< hello > f
cat << EOF > f
hello $USER
EOF
cat << 'EOF' > f
hello $USER
EOF
Then the second half, which is the one that catches people. Set up a file that somebody else is holding:
printf 'old content\n' > f
ln f g # a second name
chmod 600 f
exec 3< f # a descriptor, opened before anything happens
Now empty f twice — once with : > f and once with install -m 644 /dev/null f, rebuilding the setup in between — and after each one report three things: the size of g, the mode of f, and what cat <&3 prints. Then say which of the three you could have predicted from the inode number.
Answers
One row of the key cannot match your run, and that row is the answer to half the question: the unquoted heredoc expands $USER, so the key pins it to ada and yours will hold your own name.
Verified output of creating_and_writing_files_kata_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
PART ONE — THE SIX WRITES, AS BYTES
echo hello 68656c6c6f0a
printf hello 68656c6c6f
printf 'hello\n' 68656c6c6f0a
cat <<< hello 68656c6c6f0a
<< EOF ($USER=ada) 68656c6c6f206164610a
<< 'EOF' 68656c6c6f2024555345520a
68 65 6c 6c 6f is hello. Rows 1, 3 and 4 are the SAME FILE: echo is
printf with an 0a glued on and an argument parser you did not ask
for, and <<< supplies the newline too. Row 2 is the only one of the
four that stops where you stopped typing.
Rows 5 and 6 differ by the length of a username, and row 6 holds a
literal dollar sign — 24 55 53 45 52 is $USER, five characters that
are in the file rather than a name that was looked up.
The reader's row 5 will not match this one, and that is the answer:
an unquoted delimiter makes the heredoc a double-quoted string, so
what lands in the file depends on who ran it.
PART TWO — EMPTYING A FILE THAT SOMEBODY ELSE IS HOLDING
: > f g: 0 bytes mode: 600 fd 3 reads: (nothing)
install -m 644 /dev/null f g: 12 bytes mode: 644 fd 3 reads: old content
Three questions, and the two commands disagree on all three.
': >' TRUNCATES. There is one file, both names see it, and the
descriptor opened before the blanking is still on that same file —
which is now empty, so it reads nothing. The mode was never touched.
install REPLACES. The name f now points at a new file; g and fd 3
still hold the old one, twelve bytes of it, and will go on holding it
until the last reference goes away. -m set the mode because setting
the mode is what install is for. This is the shape of the classic
log-rotation bug: the daemon's descriptor keeps a nameless file
growing on a disk where nothing can be found to delete.
And the inode number predicted none of the three. Unlinking returns
it to the pool, so the replacement may be handed the same number
back — the lesson's own section 3 measures both outcomes. What
answers the question is something that HOLDS the file: the second
link, or the descriptor.
See also¶
- The trailing newline — why the byte
echoadds for you is usually the right one, and what breaks when it is missing printfwrites bytes — theecho -e/echo -nportability table, at lengthsplit,paste,lookandtee—teeas the tool with no opinion, which is why it is also the way to write a file you do not owndiffcompares lines,cmpcompares bytes — where a file of NUL bytes stops being text- The NUL byte — the five bytes
truncate -s 5invents - The POSIX utilities, in full ↗ —
installis not on that list: it is a BSD/GNU utility with no specification to agree on, which is why the two of them differ echoin POSIX ↗ — where "implementations shall not support any options" is written down