strings has a printable set, not an encoding¶
Level: 201 · for anyone with a terminal
One line: strings does not look for text — it prints every run of four or more bytes from a set it calls printable, and that set is ASCII, so an accent ends a word and a UTF-16 file prints nothing at all. Which part of the file it reads, whether a tab counts, and on a Mac whether the file arrived as an argument or on stdin all change what comes back — and the one build that asks the locale uses it to cut UTF-8 characters in half.
strings is the third tool in the usual kit for poking at a file you do not recognise. file guesses what it is, xxd shows every byte, and strings promises the part in between: only the readable bits. It never asks whether the file is text at all — the question every other reader answers with a test of its own — it prints the runs it finds in any file whatever. This page is about what readable means to it, which turns out to be narrower than its name and different on each of the two machines that ran it.
What it is looking for¶
Walk the file one byte at a time. A byte in the printable set extends the current run; any other byte ends it. A run of four or more bytes is printed, followed by a newline, and a shorter one vanishes. That is the whole program — section 1 of the Python example writes it as one regular expression — and since it decodes nothing, there is nothing in a file it can fail on.
Everything interesting is in the word printable. POSIX, which specifies strings, hands that word to the locale: LC_CTYPE is to decide "single-byte as opposed to multi-byte characters" and "to identify printable strings" (POSIX, strings ↗). By default, neither build measured here reads a multi-byte character as one character. GNU ignores the locale altogether. Apple consults it one byte at a time, which is worse than ignoring it, for a reason section 4 of the Python example makes concrete.
The chapter's three questions, answered for this tool:
| The question | strings |
|
|---|---|---|
| 1 | Bytes or characters? | Bytes, always. Neither build decodes anything unless a GNU-only flag asks it to |
| 2 | Who decided what counts? | The build. GNU fixes the set at ASCII plus tab. Apple fixes it at ASCII plus form feed for a file named on the command line, and asks the locale — one byte at a time — for a file on stdin |
| 3 | What happens to text that is not valid? | Nothing, because nothing is invalid to it. A byte outside the set ends a run. It is never printed, never counted and never mentioned, and the exit status is 0 |
In the terminal¶
Verified output of strings_printable_runs_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
1. FOUR PRINTABLE BYTES IN A ROW, OR NOTHING
$ strings < zoo.bin
hello
world
Two runs, two lines. The three bytes between them — 00 01 02 — are
not printed, not replaced and not mentioned: a byte outside the set
simply ends the run it interrupted. That is the whole algorithm.
$ printf 'ab\000cde\000fghi\n' | strings
fghi
Runs of two and three bytes vanish without a word, because the
default minimum is four. -n moves the minimum and changes nothing else:
$ printf 'ab\000cde\000fghi\n' | strings -n 2
ab
cde
fghi
2. WHICH BYTES KEEP A RUN GOING
Each byte value in turn, between 'abcd' and 'efgh': one line back means
strings counted the byte as printable, two lines means it ended the run.
keeps the run going 20-7e 95 byte values
ends the run 00-08 0b-1f 7f-ff 159 byte values
Ninety-five bytes, and they are exactly printable ASCII: space through
tilde. Every byte from 80 to ff ends a run — and those are the only
bytes UTF-8 uses for anything that is not ASCII. Two values were left
out on purpose: newline, which ends a line everywhere, and tab, the one
byte these two builds disagree about. The page has that table.
3. AN ACCENT ENDS A WORD
$ printf 'caf\303\251 bar\n' | strings | cat -vet
bar$
café is c a f, then c3 a9. The run 'caf' is three bytes and is dropped;
c3 and a9 are outside the set; ' bar' is four bytes, so it survives —
with its leading space, which cat -vet makes visible. The word you were
looking for is exactly the part that is missing.
$ printf 'caf\351 bar\n' | strings | cat -vet
bar$
The same words in Latin-1 are one byte shorter and get the same answer:
strings is not refusing UTF-8, it is refusing everything above 7e.
$ printf '\305\274\303\263\305\202w\n' | strings | wc -c | tr -d ' '
0
żółw: four letters in seven bytes, and nothing comes back. Three of the
letters take two bytes each, all six of those bytes are above 7e, and
the w left over is a run of one.
4. UTF-16 IS INVISIBLE TO IT
$ xxd u16.txt
00000000: 4800 6500 6c00 6c00 6f00 2c00 2000 5700 H.e.l.l.o.,. .W.
00000010: 6f00 7200 6c00 6400 2100 0a00 o.r.l.d.!...
Every ASCII letter in UTF-16LE is followed by a 00 byte, so no two
printable bytes are ever next to each other:
$ strings < u16.txt | wc -c | tr -d ' '
0
Twenty-eight bytes of readable text, and nothing. Decode first, and
strings has runs to find:
$ iconv -f UTF-16LE -t UTF-8 u16.txt | strings
Hello, World!
5. WHAT IT THREW AWAY, AND HOW TO GET IT BACK
$ strings -t d < zoo.bin | sed 's/^ *//'
0 hello
8 world
-t d puts each run's byte offset in front of it. (The sed removes a
padding difference: one build right-aligns the number and the other
does not.) The number is the part of each line that strings made up,
and it is the part xxd needs to show you what surrounded the run:
$ xxd -s 5 -l 3 zoo.bin
00000005: 0001 02 ...
The three bytes between the runs, which strings never mentioned.
Section 2 is the whole printable set, measured rather than recalled. Ninety-five byte values keep a run going, and they are exactly 20 to 7e, space to tilde. Everything else ends one — the control characters, DEL, and all 128 bytes from 80 up. In UTF-8, and in every ASCII-based 8-bit table, a character that is not ASCII is made only of bytes from that top half, which is why section 3 loses the first three letters of café and every letter of żółw, and why the Latin-1 spelling does no better. This is not a UTF-8 problem; it is an above-7e problem. The sweep leaves tab out on purpose: it is the one byte the two builds disagree about, in the table below.
Section 4 is the one to remember when a file came from Windows. UTF-16 puts a 00 byte after every ASCII letter, so no two printable bytes are ever adjacent, and twenty-eight bytes of readable text produce nothing at all — no warning, exit 0. UTF-16 is Windows' native string type (why it stayed), which is where you will meet this: text a Windows program kept in its own string type is invisible to the default. iconv first and strings second works on both platforms; GNU also has -e l, below.
Section 5 is how to use it anyway. strings discards everything between runs without saying how much, so a line of its output is only good as a pointer. -t d prints the pointer, a byte offset, and xxd -s turns the offset back into bytes. That pair is the honest workflow: strings to find where the text is, xxd to see what is actually around it.
In Python¶
Verified output of strings_printable_runs_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. THE WHOLE TOOL IS ONE REGULAR EXPRESSION
zoo = b'hello\x00\x01\x02world\n'
re.findall(rb'[\x20-\x7e]{4,}', zoo) -> [b'hello', b'world']
Runs of four or more bytes from space to tilde; every other byte ends
a run and is thrown away. That is strings. The rest of this program is
about the one thing builds disagree on: which bytes count as printable.
2. FOUR TABLES, ONE PER BUILD AND INPUT PATH
GNU strings 96 bytes 09 20-7e
Apple, file argument 96 bytes 0c 20-7e
Apple, stdin, C 95 bytes 20-7e
Apple, stdin, UTF-8 190 bytes 20-7e a0-ac ae-ff
Three are ASCII plus at most one control character. The fourth is what
Apple's strings uses when the file arrives on STDIN in a UTF-8 locale:
it also keeps every byte from a0 to ff except ad (the soft hyphen),
each judged as if it were a Latin-1 character on its own.
3. FIVE INPUTS THROUGH FOUR TABLES
tab = b'abcd\tefgh\n'
GNU strings [b'abcd\tefgh']
Apple, file argument [b'abcd', b'efgh']
Apple, stdin, C [b'abcd', b'efgh']
Apple, stdin, UTF-8 [b'abcd', b'efgh']
café, UTF-8 = b'caf\xc3\xa9 bar\n'
GNU strings [b' bar']
Apple, file argument [b' bar']
Apple, stdin, C [b' bar']
Apple, stdin, UTF-8 [b'caf\xc3\xa9 bar']
café, Latin-1 = b'caf\xe9 bar\n'
GNU strings [b' bar']
Apple, file argument [b' bar']
Apple, stdin, C [b' bar']
Apple, stdin, UTF-8 [b'caf\xe9 bar']
żółw, UTF-8 = b'\xc5\xbc\xc3\xb3\xc5\x82w\n'
GNU strings []
Apple, file argument []
Apple, stdin, C []
Apple, stdin, UTF-8 [b'\xc5\xbc\xc3\xb3\xc5']
…, UTF-8 = b'Wait\xe2\x80\xa6 what\n'
GNU strings [b'Wait', b' what']
Apple, file argument [b'Wait', b' what']
Apple, stdin, C [b'Wait', b' what']
Apple, stdin, UTF-8 [b'Wait\xe2', b'\xa6 what']
On these five inputs the first three tables differ only over the tab.
The fourth keeps café whole, cuts żółw after five bytes, and splits the
ellipsis in two.
4. A RUN OF PRINTABLE BYTES IS NOT NECESSARILY TEXT
Every UTF-8 input above, through the fourth table, then decoded:
café, UTF-8 b'caf\xc3\xa9 bar' valid UTF-8
żółw, UTF-8 b'\xc5\xbc\xc3\xb3\xc5' NOT valid UTF-8 (UnicodeDecodeError)
…, UTF-8 b'Wait\xe2' NOT valid UTF-8 (UnicodeDecodeError)
…, UTF-8 b'\xa6 what' NOT valid UTF-8 (UnicodeDecodeError)
All three inputs were valid UTF-8. Two came out broken: the ł cut after
its first byte, and the ellipsis spread over two runs with its middle
byte gone. The one that came out whole was kept for the wrong reason —
c3 and a9 are à and © in Latin-1, and this table is Latin-1's.
A tool that never decodes cannot fail to decode; it can still hand you
bytes that no longer do.
5. WHAT A STRINGS FOR UTF-8 WOULD HAVE TO DO
tab 'abcd' | 'efgh'
café, UTF-8 'café bar'
café, Latin-1 ' bar'
żółw, UTF-8 'żółw'
…, UTF-8 'Wait… what'
Decode, then count characters instead of bytes: a character counts
unless it is a control character or a byte that did not decode (which
surrogateescape keeps as a marker, so it still ends a run). Now żółw is
four characters and a run, the ellipsis stays in one piece, and the
Latin-1 file gets exactly the answer strings gave it. That decode step
is the part strings leaves to you.
Section 1 is the tool. re.findall(rb'[\x20-\x7e]{4,}', data) is strings on every input on this page where the two builds agree, which is every input without a tab in it. It has to be a bytes pattern, and that is a small lesson of its own: bytes has no isprintable() at all — only isascii() and an ASCII-only isalpha() family — which fits, since printable is a property of a character, and a byte is not a character until something has decoded it.
Section 2 writes down the four tables the builds actually use, from the sweeps below. Three are ASCII plus at most one control character. The fourth is Apple's strings reading stdin in a UTF-8 locale, and it also keeps every byte from a0 to ff except ad, the soft hyphen — as if each byte were a Latin-1 character on its own. A byte of UTF-8 is not a Latin-1 character, and section 4 is what happens when a tool treats it as one.
Section 4 is why "consults the locale" does not mean "understands UTF-8". Three valid UTF-8 inputs go in. One comes out with its last letter cut after its lead byte — żó and half an ł. One has its ellipsis dealt across two lines with the middle byte missing. The third comes out whole, and for the wrong reason: c3 and a9 happen to be à and ©. The Mac transcript below is the real tool doing all three. The ellipsis is there for more than its three bytes: in Windows-1252 it is the single byte 85, which file guesses shows is the one byte above 127 that file calls ASCII.
Section 5 is the part strings leaves to you. Decode first — with surrogateescape, so a byte that is not UTF-8 survives as a marker instead of raising — and then look for runs of characters. żółw is four characters and a run, the ellipsis stays in one piece, and the Latin-1 file gets exactly the answer strings gave it, because its e9 did not decode and so ends the run like any other non-text. The regular expression names only the controls and the undecodable bytes, never a Unicode property, so its answer does not depend on which Unicode version the interpreter shipped with.
Where the builds part¶
Nothing in this section can be an answer key. CI runs Apple's strings on one runner and GNU's on the other, and everything below is somewhere they differ, so each result is a dated measurement naming the build it came from. On a Mac, /usr/bin/strings is one of 78 hard links to the same small launcher (stat -f %l says so), which runs the copy that ships with the Command Line Tools (xcrun --find strings names it). It is Apple's own program, and it rejects --version as an unknown flag, which is the quickest way to tell which strings you have.
Which bytes count¶
Each byte value placed between abcd and efgh, and strings asked whether that made one run or two — the sweep in section 2 above, repeated with the input path and the locale changed.
| Build, input path, locale | Bytes that keep a run going | Count |
|---|---|---|
| GNU 2.42 and 2.44, argument or stdin, any locale | 09 20–7e |
96 |
| Apple, file argument, any locale | 0c 20–7e |
96 |
Apple, stdin, LC_ALL=C |
20–7e |
95 |
| Apple, stdin, UTF-8 locale | 20–7e a0–ac ae–ff |
190 |
Measured 2026-09-10: Apple strings from the Command Line Tools on macOS 26.6.2; GNU binutils 2.42 (ubuntu:24.04) and 2.44 (Debian). Newline is left out of the table because it ends a line for every build. "Any locale" means that C and a UTF-8 locale gave identical sweeps.
The Mac has two strings¶
Apple's strings reads a named file and a stream with different code, and the two disagree in three ways: whether form feed is printable, whether the locale matters, and — next section — what happens at the end of the file. So strings f and cat f | strings, which every tutorial treats as one command, are two commands on a Mac.
$ printf 'caf\303\251 bar\n' > c.txt
$ LC_ALL=en_US.UTF-8 strings c.txt | xxd
00000000: 2062 6172 0a bar.
$ LC_ALL=en_US.UTF-8 strings < c.txt | xxd
00000000: 6361 66c3 a920 6261 720a caf.. bar.
$ printf '\305\274\303\263\305\202w\n' | LC_ALL=en_US.UTF-8 strings | xxd
00000000: c5bc c3b3 c50a ......
$ printf 'Wait\342\200\246 what\n' | LC_ALL=en_US.UTF-8 strings | xxd
00000000: 5761 6974 e20a a620 7768 6174 0a Wait... what.
$ printf 'abcd\014efgh\n' > ff.txt
$ strings ff.txt | cat -vet
abcd^Lefgh$
$ strings < ff.txt | cat -vet
abcd$
efgh$
The first pair is the same file, the same locale and the same machine, with two answers: given the name, strings drops caf as it does everywhere else; given the stream, it keeps café whole. The next two commands are why the stream's answer cannot be trusted even when it looks right. żółw comes back as c5 bc c3 b3 c5 — żó and the first byte of ł, which is not UTF-8 — and the ellipsis's three bytes are dealt across two lines with the middle one gone, exactly as section 4 of the Python example predicted from the table alone. Form feed runs the other way: printable to the file path, a separator to the stream.
The last byte of the file¶
$ printf 'hello\000\001\002world\377' > test_binary.bin
$ strings test_binary.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 64ff 0a hello.world..
$ strings < test_binary.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 640a hello.world.
$ printf 'hello\000\001\002world\377\n' > with_newline.bin
$ strings with_newline.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 640a hello.world.
ff is printable to no table on this page, and Apple's strings printed it anyway — only for a named file, only when it was the file's very last byte, and only after a run long enough to print on its own (wor\377 prints nothing at all). One more byte after it and it disappears. GNU prints hello and world for all three commands. That first file is precisely what a first experiment builds — some text, some junk, no trailing newline — so on a Mac the experiment's output ends in a byte no UTF-8 terminal can draw.
$ printf 'hello\000\001\002world\377' > test_binary.bin
$ strings test_binary.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 640a hello.world.
$ strings < test_binary.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 640a hello.world.
$ printf 'hello\000\001\002world\377\n' > with_newline.bin
$ strings with_newline.bin | xxd
00000000: 6865 6c6c 6f0a 776f 726c 640a hello.world.
Which part of the file¶
POSIX leaves the default to the implementation — "it is implementation-defined what portion of each file is scanned" — and defines one flag for the question: -a, "Scan files in their entirety." The two builds took opposite defaults, and Apple's -a is not the standard's.
$ printf 'int main(void) { return 0; }\n' > dummy.c && cc dummy.c -o test_executable
$ file -b test_executable
Mach-O 64-bit executable x86_64
$ strings test_executable | wc -l
0
$ strings -a test_executable | wc -l
0
$ strings - test_executable
__PAGEZERO
__TEXT
__text
__TEXT
__unwind_info
__TEXT
__LINKEDIT
/usr/lib/dyld
/usr/lib/libSystem.B.dylib
_mh_execute_header
main
__mh_execute_header
_main
$ printf 'int main(void) { return 0; }\n' > dummy.c && cc dummy.c -o test_executable
$ strings test_executable | wc -l
60
$ strings -d test_executable | wc -l
6
$ strings -a test_executable | wc -l
60
$ strings test_executable | head -n 10
/lib64/ld-linux-x86-64.so.2
__libc_start_main
libc.so.6
GLIBC_2.34
__gmon_start__
PTE1
GCC: (GNU) 14.4.0
crt1.o
__abi_tag
crtstuff.c
Apple's default reads the sections of an object file except (__TEXT,__text) — its man page says so — and a program this small has nothing readable in any other section, so the answer is no lines at all, exit 0. -a adds that one section and still finds nothing, because everything readable in this file is in its load commands, which are not in any section. Only - reads every byte, and there it all is: the segment names, the dynamic linker, the one library, the symbol _main. GNU has read the whole file by default since binutils 2.25 — the release notes say "Change the default behaviour to be --all" — and keeps -d for the old sections-only scan, so the same one-line program shows its dynamic linker, its C library and its compiler banner without being asked. On a Mac, strings - is the command the tutorials mean.
Past ASCII: GNU only¶
$ printf 'caf\303\251 bar\n' > c.txt
$ strings c.txt
bar
$ strings -e S c.txt
café bar
$ strings -U escape c.txt
caf\u00e9 bar
$ strings -U hex c.txt
caf<0xc3a9> bar
$ strings -U show c.txt | xxd
strings: invalid argument to -U/--unicode: show
$ strings -U locale c.txt | xxd
00000000: 6361 66c3 2062 6172 0a caf. bar.
$ printf 'Hello, World!\n' | iconv -f UTF-8 -t UTF-16LE | strings -e l
Hello, World!
$ strings -e S c.txt 2>&1; echo "exit $?"
error: /Library/Developer/CommandLineTools/usr/bin/strings: unknown flag: -e
Usage: /Library/Developer/CommandLineTools/usr/bin/strings [-] [-a] [-o] [-t format] [-number] [-n number] [[-arch <arch_flag>] ...] [--] [file ...]
exit 1
GNU has two ways past ASCII and Apple has none. -e sets the character width. S adds every byte from 80 to ff to the set, which returns café bar whole — and would return Latin-1, or any other 8-bit table, just as whole, because it has stopped asking which one. l and b read 16-bit little- and big-endian, which is the one-flag answer to section 4. -U is about UTF-8 specifically (binutils 2.38 and later). escape and hex decode each character and print it in a form you can paste back somewhere, which makes them the only modes on this page that know where one UTF-8 character ends and the next begins. locale — "display them according to the current locale", in the release notes — prints the first byte of every character and drops the rest: caf, c3, a space, bar, which is not UTF-8. It does that on 2.40, 2.42 and 2.44, and in the C locale exactly as in C.UTF-8. And 2.42's own --help offers the mode as show, a word its parser rejects (2.44's help says locale) — a page has a date in two lines. Apple refuses -e outright, exit 1, which is the good kind of failure: a script that needs UTF-16 on both platforms has to convert first, and finds that out immediately.
If you are coming from Python or ABAP¶
Python. The whole tool is section 1's one line, and what it teaches is where the line has to be drawn: strings is a question you can only ask of bytes, and Python's bytes has no isprintable(). The moment you want characters you have made the decision strings never makes — which encoding — and section 5 is what the program looks like once you have made it. Two things transfer directly. re.findall over bytes with {4,} is the fastest way to pull ASCII islands out of a binary in a script, without any of the platform differences above. And errors="surrogateescape" is the tool for the half-decoded middle ground, because it keeps the bytes that did not decode as markers you can see, instead of an exception you have to catch.
ABAP. (Not machine-checked — CI cannot run ABAP.) There is no strings, and the two ways of opening a file split its job the same way this page does. OPEN DATASET … IN BINARY MODE gives you an xstring, which has no notion of printable at all — any scan for readable runs is yours to write, byte by byte against hex 20 to 7E, and it inherits every limit above. IN TEXT MODE ENCODING … makes the conversion decide instead, before your code sees a character, which is section 5 of the Python example done by the runtime. The second is nearly always the question you meant; the first is only for what strings is genuinely good at, finding the ASCII islands in data that is not text.
Try it¶
Then, on your own files:
- On a Mac,
strings /bin/ls | wc -land thenstrings - /bin/ls | wc -l. The difference is everything the default never read. - Take a file you know has accented text in it — an export, a mail, a subtitle file — and compare
head -3 filewithstrings file | head -3. Every word that lost letters had a byte above7e. - On a Mac, in your usual terminal:
strings file > a.txt, thenstrings < file > b.txt, thencmp a.txt b.txt. If they differ, you are looking at the Mac's twostrings. - If you have a registry export from Windows (
.reg— regedit writes UTF-16), comparestrings file.reg | wc -lwithiconv -f UTF-16 -t UTF-8 file.reg | strings | wc -l. strings -t da binary you care about, pick an offset that looks interesting, andxxd -s <offset> -l 64the file there. What hadstringsleft out on either side?
See also¶
xxdis the dump you can put back — the other half of section 5's pair: whatstringsleft out, byte for bytefileguesses — the first of the three tools, and the one byte above 127 it calls ASCII- Binary is a verdict, not a property — the question
stringsnever asks, and the testsfile,grepandgiteach bring to it - Inspecting a file — the same kit in a workflow, and which column of each tool is the file
- The NUL byte — the byte that ends every run on this page, and the one most other tools call binary
- UTF-16 and surrogates — why section 4's file is two bytes a letter
- Locale and
LC_CTYPE— the setting POSIX says decides what is printable, and which tools actually listen to it - The first two bytes — what the
cf fa ed feat the start of the Mac'stest_executableis - A page has a date — documentation that describes a different build from the one installed