← back to latest

Identify a file by what is inside it, not what it is called

file *

anatomy

file
Reads the beginning of each argument and reports what the contents look like. The name of the file plays no part in the answer. It runs three batteries of tests in a fixed order and stops at the first that produces a result: filesystem tests (is this a directory, a symlink, a device, an empty file), magic tests (do the leading bytes match a known signature), and language tests (if it is text, is it a script, source code, or prose). The magic tests are the interesting ones. They come from a database of byte patterns at known offsets, kept as editable text in /usr/share/misc/magic rather than compiled into the binary, which is why a tool from 1973 still recognises formats invented last year.
*
A shell glob, not an argument file understands. Bash expands it to every non-hidden name in the current directory before file runs, so the sorting is the shell's doing and dotfiles are excluded. Given more than one name, file pads the labels into a column, which turns a directory survey into something you can read down. Any path works in its place, and a single download is the more common case. What does not work is leaving it off: with no arguments at all, file prints its usage and exits 1 rather than reading standard input, which is what `-f -` is for.

Sample output

backup.zip:    gzip compressed data, from Unix, original size modulo 2^32 71680
deploy.sh:     Bourne-Again shell script, ASCII text executable, with CRLF line terminators
dump.bin:      OpenPGP Public Key
export.csv:    CSV text
latest:        symbolic link to export.csv
logo.png:      PNG image data, 16 x 16, 8-bit/color RGBA, non-interlaced
q3-report.pdf: JSON text data

Three of these are lying about themselves: backup.zip is a gzip stream that unzip will refuse and tar -xzf will open, and q3-report.pdf is JSON that no PDF reader will ever render. The line that saves the most time is deploy.sh, because “with CRLF line terminators” is the full explanation for bad interpreter: /bin/bash^M. Note that latest is reported as the link itself rather than as what it points at; file -L latest follows it and says CSV text.

When you would reach for it

A download that arrived with no extension, or with one you have no reason to trust: an export from a vendor, an attachment, an artifact pulled from a build server. It is also the first command to run when a parser rejects a file that looks fine, because the answer is usually that the bytes are not what the name promised, or that the file is text with the wrong line endings or the wrong encoding. Point it at a whole directory and you get a manifest of what you are actually holding.

Gotchas

  • The exit status is 0 even when a file cannot be opened. file missing.txt prints cannot open and still reports success, so a script that checks $? will sail past a typo in a path. Add -E to turn open failures into real errors and an exit status of 1.
  • Magic matching is a guess, and it is confident about guesses. The dump.bin line above is 4 KB of output from /dev/urandom, identified as an OpenPGP public key because a few random bytes at the front happened to match. file -k dump.bin keeps going past the first hit and shows every match, which is how you tell a real identification from a collision.
  • Only the head of the file is examined. The text-versus-binary decision scans the first 65536 bytes, so a log file that is clean text for 64 KB and then turns into garbage still reports ASCII text. Raise the window with file -P encoding=1000000 when you need certainty about a large file.
  • Symlinks are not followed by default, and POSIXLY_CORRECT in the environment silently reverses that, so the same command can answer differently on two machines. Be explicit: -L to follow, -h to report the link. On Alpine there is no file at all until you run apk add file, since busybox does not provide it. macOS ships its own build with its own copy of the magic database, so descriptions can differ word for word from Linux even though the flags are the same.

Variants

$ find . -type f | file -f -

Identify every file in a tree. -f - reads the list of names from standard input, one per line, so file starts once instead of once per file

$ file -z backup.zip

Look through the compression and report what is inside: POSIX tar archive (GNU) (gzip compressed data, from Unix), without unpacking anything

$ [ "$(file -b --mime-type upload.dat)" = image/png ] || echo "not a png"

-b drops the filename and --mime-type drops the prose, leaving one token that a script can compare against

lineage

A file command appeared in Version 4 AT&T Unix in 1973, which makes it older than the pipe symbol as most people know it. The version running on nearly every machine today is not that one. Ian F. Darwin rewrote it from scratch in 1986 and 1987 with no AT&T code in it, which is the reason it could be given away freely while the original could not, and Christos Zoulas has maintained and extended it since the early 1990s. Darwin's real contribution was not the program but the split he made inside it: the knowledge of what bytes mean was pulled out of the C source and put into a text database, so teaching the tool a new format became an edit to a data file rather than a patch to a binary. That database, and the libmagic library that reads it, long ago outgrew the command. It is what a web server consults to pick a Content-Type, what a desktop environment asks before choosing an icon, and what a mail client uses to label an attachment. The output you get from file is a thin front end on a lookup table that most of the software on the machine is quietly using too.