I still remember the first time I needed to clean up a messy CSV export at 2 AM because a cron job was about to choke on it. Grep couldn’t help me, sed felt like overkill, and awk was going to take five minutes to write correctly. What I actually needed was something that could just swap one set of characters for another, strip out junk bytes, and squeeze duplicate spaces — fast. That’s when I really started using tr, and it’s stayed in my daily toolkit ever since.
tr stands for “translate” (or “transliterate”), and it’s one of the oldest utilities in the Unix toolbox. It’s small, it’s fast, and it does exactly one job: it operates on a stream of characters, not lines or fields. That distinction is what makes it different from sed and awk, and it’s also why so many people misunderstand it at first.
In this guide I’m going to walk through everything I’ve learned about tr over the years — from the basic syntax to the internals of how it processes bytes, all the way to real automation scripts I’ve used on production servers.
What Exactly Is tr?
tr reads from standard input, translates or deletes characters, and writes the result to standard output. It cannot read files directly — no tr file.txt. It only works with STDIN and STDOUT, which is a common gotcha for beginners.
Here’s the basic syntax:
tr [OPTIONS] SET1 [SET2]
- SET1 is the set of characters to act on.
- SET2 is the set of characters to translate them into (when doing a translation).
Because tr only understands character streams, you’ll almost always see it used with a pipe:
cat file.txt | tr 'a-z' 'A-Z'
or with input redirection:
tr 'a-z' 'A-Z' < file.txt
I tested this directly:
$ echo "test line 1
test LINE 2
Test Line 3" | tr 'a-z' 'A-Z'
TEST LINE 1
TEST LINE 2
TEST LINE 3
Every lowercase letter became uppercase. Simple, predictable, and fast.
How tr Works Internally
Under the hood, tr builds two internal arrays (or a single array for deletion mode) mapping each byte in SET1 to the corresponding byte in SET2. It then streams input character by character, doing a constant-time lookup for each byte and writing out the translated byte. This is why tr is so much faster than sed or awk for pure character-level work — there’s no regex engine, no line buffering logic, just a lookup table and a tight loop.
Because it works on raw bytes (or in multibyte-aware locales, on characters), tr doesn’t understand “lines” or “words” the way grep, sed, and awk do. It has no concept of a record separator except for the fact that newlines are just another character it can translate or delete like anything else.
This single-purpose design is a deliberate part of the Unix philosophy: do one thing, do it well, and let the shell pipeline handle the rest.
Basic Syntax and Character Sets
You can specify character sets in a few ways:
- Explicit lists:
tr 'abc' 'xyz'translates a→x, b→y, c→z. - Ranges:
tr 'a-z' 'A-Z'— the whole lowercase range mapped to uppercase. - POSIX character classes:
[:upper:],[:lower:],[:digit:],[:space:],[:punct:],[:alpha:],[:alnum:],[:cntrl:].
Example using classes, which I find much more readable than raw ranges:
$ echo "Hello World 123" | tr '[:lower:]' '[:upper:]'
HELLO WORLD 123
If SET1 is longer than SET2, the last character of SET2 is repeated to match the length — unless you use -t (truncate) to change that behavior.
Core Options and Parameters
Here’s the full rundown of the options I actually use:
-d — Delete Characters
Removes every character in SET1 from the input, with no replacement.
$ echo "test line 1" | tr -d 'aeiou'
tst ln 1
I use this constantly to strip out unwanted characters — carriage returns being the most common case:
tr -d '\r' < windows_file.txt > unix_file.txt
That single command has saved me more debugging time than I’d like to admit, especially when a script written on Linux started throwing weird errors because a config file was edited on Windows and had \r\n line endings instead of \n.
-s — Squeeze Repeats
Collapses consecutive repeated characters from SET1 into a single instance.
$ echo "aaabbbccc" | tr -s 'a-c'
abc
This is great for cleaning up whitespace:
$ echo "too many spaces" | tr -s ' '
too many spaces
-c — Complement SET1
Inverts the character set — instead of acting on the characters you listed, tr acts on everything except those characters.
echo "Hello123World456" | tr -cd '[:digit:]'
This strips everything that ISN’T a digit, leaving 123456. I use this pattern a lot when I need to extract just the numeric content out of noisy log lines or extracted PDF text.
-t — Truncate SET1
When SET1 is longer than SET2, -t truncates SET1 to the length of SET2 instead of repeating the last character of SET2.
$ echo "abcdef" | tr -t 'abcdef' 'xyz'
xyzdef
Without -t, the same command would map a→x, b→y, c→z, and then d, e, f would all map to z (the last character of SET2 repeated):
$ echo "abcdef" | tr 'abcdef' 'xyz'
xyzzzz
That distinction trips people up all the time, so it’s worth testing before you rely on it in a script.
Practical, Real-World Examples
1. Case Conversion
tr '[:lower:]' '[:upper:]' < input.txt
tr '[:upper:]' '[:lower:]' < input.txt
2. Removing Non-Printable / Control Characters
Log files pulled from embedded devices or serial consoles often have stray control characters that break downstream tools:
tr -d '[:cntrl:]' < dirty.log > clean.log
3. Converting Line Endings (DOS to Unix)
tr -d '\r' < dos_file.txt > unix_file.txt
This is a lighter-weight alternative to dos2unix when that package isn’t installed.
4. Replacing Spaces with Underscores in Filenames
for f in *.txt; do
mv "$f" "$(echo "$f" | tr ' ' '_')"
done
I’ve used this exact pattern to batch-rename files exported from a Windows-based backup system before ingesting them into a Linux-based archive pipeline.
5. Generating a Random Password
tr pairs beautifully with /dev/urandom:
tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16; echo
I tested this and it reliably produces a 16-character alphanumeric string, which is handy for quick throwaway credentials in test environments (never for anything security-critical without a proper CSPRNG-backed tool).
6. Counting Words
Combine tr with wc to normalize whitespace before counting:
cat file.txt | tr -s ' \n' '\n' | wc -l
This squeezes all spaces and newlines into single newlines, effectively giving you one “word” per line, and wc -l counts them.
7. ROT13 Encoding
A classic party trick:
echo "Hello" | tr 'A-Za-z' 'N-ZA-Mn-za-m'
This shifts each letter 13 places, a simple substitution cipher still occasionally used for obscuring spoilers in text.
tr in Shell Scripting and Automation
Because tr is POSIX-compliant and available on essentially every Unix-like system (including BusyBox-based embedded Linux and macOS), it’s a safe choice for portable scripts where you can’t guarantee GNU sed or Perl are installed.
A common pattern I use in deployment scripts is sanitizing environment variable names or user input before using them as identifiers:
sanitize_name() {
echo "$1" | tr -c '[:alnum:]_' '_'
}
safe_name=$(sanitize_name "My Service #1")
echo "$safe_name"
# My_Service__1
This guarantees the output only contains alphanumeric characters and underscores — useful for generating safe systemd unit names, Docker container names, or temp file prefixes.
Another pattern: normalizing hostnames or usernames pulled from external APIs to lowercase before writing them into /etc/hosts or LDAP entries, since case-sensitivity bugs in DNS or auth systems are a genuinely painful category of bug to track down.
Comparing tr to sed and awk
People often ask me when to use tr versus sed versus awk. Here’s how I think about it:
| Task | Best Tool |
|---|---|
| Simple 1-to-1 character substitution | tr |
| Deleting specific characters | tr |
| Pattern-based substitution (regex) | sed |
| Line-based editing, multi-command scripts | sed |
| Field/column-based processing | awk |
| Multi-line context-aware replacement | sed or awk |
tr has no concept of regular expressions — it maps characters one-to-one. If you need to match a pattern like an email address or a phone number, you need sed or grep -E. But if you just need to swap characters, delete a specific byte, or normalize whitespace, tr is faster and the syntax is far simpler.
I’ve profiled this on large files before: for a straightforward character substitution across a 500MB text file, tr consistently finishes in a fraction of the time sed 's/a/A/g' takes, because it isn’t invoking a regex engine per character.
Troubleshooting Common tr Issues
“tr: extra operand” error — this almost always means you tried to pass a filename directly:
tr 'a' 'b' file.txt # WRONG
tr doesn’t accept a filename argument. Fix it with redirection:
tr 'a' 'b' < file.txt # CORRECT
Unexpected results with ranges in different locales — tr 'a-z' 'A-Z' can behave oddly in some locales where the collation order isn’t a straightforward alphabet. If you hit strange behavior, force the C locale:
LC_ALL=C tr 'a-z' 'A-Z' < file.txt
I make this a habit in scripts that need predictable, byte-oriented behavior regardless of the system’s configured locale.
SET2 being empty when using -d — you don’t provide SET2 when deleting; tr -d 'set1' is correct on its own.
Multibyte / UTF-8 characters not translating correctly — tr in some implementations is byte-oriented, not character-oriented, which causes corruption on multi-byte UTF-8 sequences (like accented characters or emoji) if you’re not careful with which bytes you target. Test carefully on multibyte input, and consider Perl or Python for heavy Unicode manipulation.
Performance Considerations
Because tr uses a simple lookup table rather than a regex engine, it’s extremely lightweight on CPU and memory, and it streams input rather than loading a whole file into memory, so it scales well to very large files. In pipelines processing gigabytes of log data, I’ll often reach for tr specifically because I know it won’t become the bottleneck the way an ill-optimized regex in sed or awk might.
Security Notes
tr itself doesn’t pose direct security risks, but be cautious in scripts where user-supplied input is passed as SET1 or SET2 — while tr doesn’t execute code, malformed or adversarial input combined with shell quoting mistakes can lead to unexpected behavior elsewhere in a pipeline. Always quote your variables:
tr -d "$user_supplied_chars" < file.txt # quote to prevent word-splitting
Compatibility Across Distributions
tr ships as part of GNU coreutils on virtually all major Linux distributions — Ubuntu, Debian, Fedora, RHEL, CentOS, Arch, openSUSE. The version I tested against here is GNU coreutils 9.4. Minor differences exist between GNU tr and BSD/macOS tr (particularly around POSIX character classes and multibyte handling), so if you’re writing scripts meant to run on both Linux and macOS, test on both or stick to LC_ALL=C behavior and basic ranges for maximum portability.
A Closer Look at How tr’s Internal Mapping Table Is Built
It’s worth walking through exactly what happens when tr parses its two operand sets, since understanding this clears up a lot of otherwise-confusing edge cases. When you invoke tr 'a-z' 'A-Z', tr‘s first job is to expand both SET1 and SET2 from their compact range/class notation into full, explicit lists of individual characters — a-z expands to the 26 lowercase letters in order, and A-Z expands similarly. Once both sets are fully expanded, tr builds a direct positional mapping: the first character of the expanded SET1 maps to the first character of expanded SET2, the second to the second, and so on. This is why the order of characters within a range matters — tr isn’t doing anything semantically aware of “uppercase” or “lowercase” as categories, it’s purely doing positional character-to-character substitution based on how the ranges expand.
This also explains a subtlety that catches people off guard with POSIX character classes: because [:upper:] and [:lower:] are guaranteed by POSIX to expand in a consistent, defined order matching the natural alphabet sequence, tr '[:lower:]' '[:upper:]' behaves predictably and portably. But hand-rolled ranges like a-z can, in principle, behave differently across locales where the collation order isn’t a simple A-through-Z sequence — which is exactly why forcing LC_ALL=C is the recommended safety net any time you need guaranteed, portable, byte-oriented range behavior rather than locale-dependent collation.
Building More Elaborate tr Pipelines
Beyond the individual examples already covered, tr composes naturally into longer pipelines for more elaborate one-off text transformations that would otherwise require writing a small script. A pattern I’ve used for quickly sanitizing a list of tags or labels pulled from an API response into something safe to use as shell variable names or filesystem-safe identifiers:
echo "Q3 Sales & Marketing Report!" | tr -c '[:alnum:]' '_' | tr -s '_'
The first tr replaces every character that isn’t alphanumeric with an underscore (using -c to complement the alphanumeric class), and the second tr -s '_' squeezes any resulting runs of consecutive underscores down to a single one — together producing a clean identifier like Q3_Sales_Marketing_Report_ from otherwise messy, punctuation-heavy input, all without invoking a heavier scripting language.
tr for Quick Data Format Conversion
tr is also a surprisingly effective tool for lightweight reformatting between simple delimiter-separated formats, when a full awk or sed script would be overkill:
echo "apple:banana:cherry" | tr ':' ','
This converts colon-separated values into comma-separated values in a single pass — genuinely useful when working with fields extracted from Unix system files like /etc/passwd, which traditionally uses colons as its field separator, and reformatting them for a CSV-consuming tool that expects commas instead.
Summary
tr is a small utility that rewards you for learning it well. It won’t replace sed or awk, and it isn’t trying to — its entire value comes from doing character-level translation, deletion, and squeezing faster and more simply than any regex-based tool could. Once you internalize that it works on a raw character stream and only accepts STDIN, the rest of its behavior becomes predictable, and you’ll find yourself reaching for it constantly in shell scripts, log cleanup, and quick one-liners.
References
- GNU Coreutils Manual —
tr: https://www.gnu.org/software/coreutils/manual/html_node/tr-invocation.html - POSIX Specification for
tr: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/tr.html man tr(local manual page)
