tr Command in Linux: Complete Guide to Character Translation, Deletion, and Parameters

tr command in Linux and it perimeters

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:

  1. Explicit lists: tr 'abc' 'xyz' translates a→x, b→y, c→z.
  2. Ranges: tr 'a-z' 'A-Z' — the whole lowercase range mapped to uppercase.
  3. 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:

TaskBest Tool
Simple 1-to-1 character substitutiontr
Deleting specific characterstr
Pattern-based substitution (regex)sed
Line-based editing, multi-command scriptssed
Field/column-based processingawk
Multi-line context-aware replacementsed 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 localestr '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 correctlytr 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)

Total
0
Shares

Leave a Reply

Previous Post
tail command in Linux and it perimeters

tail Command in Linux: Complete Guide to Viewing End of Files, Following Logs, and Parameters

Next Post
uniq command in Linux and it perimeters

uniq Command in Linux: Complete Guide to Finding Unique Lines, Duplicates, and Parameters

Related Posts