tr

Translate, delete, or squeeze characters one at a time

Updated 2026-08-14

tr translates characters one-to-one: tr 'a-z' 'A-Z' maps every lowercase letter in SET1 to the matching position in SET2. It has no concept of a "line" or a "field": it works on the raw character stream, which makes it fast and simple, but also the wrong tool the moment you need context (use sed for that instead).

-d deletes every character in SET1 instead of translating it. -s squeezes runs of repeated characters down to one. -c complements SET1, operating on everything not listed, which is how you keep only digits (tr -cd '[:digit:]') rather than deleting them.

One mistake trips up almost everyone coming from grep or sed: tr doesn't understand [^...] as "not this." Brackets aren't special in tr's SET syntax at all: [^0-9] is parsed as the literal characters [, ^, ], plus the range 0-9, not "anything except a digit." Use -c for negation; never [^...].

Character classes ([:upper:], [:lower:], [:digit:], [:space:], [:punct:], [:alnum:], [:print:]) work inside either SET and are usually clearer than spelling out a range by hand.

Sample files used on this page

Every example below was run against these files. Recreate them to follow along.

users.csv a header row plus 6 records

name,age,department
Alice,34,Engineering
Bob,29,Sales
Carol,41,Engineering
Dave,25,Marketing
Erin,38,Sales
Frank,31,Engineering

crlf.txt two lines with Windows CRLF endings - 14 bytes, not 12

line1␍
line2␍
33 outputs, collapsed by default

Translating characters

SET1 and SET2 map position-for-position: the Nth character of SET1 becomes the Nth character of SET2.

Convert to uppercase

echo 'Hello, World!' | tr 'a-z' 'A-Z'

Every character in the a-z range maps to the matching position in A-Z. Characters not in SET1 (the comma, the space, the !) pass through untouched.

Show output
HELLO, WORLD!

Convert to uppercase using a character class

echo 'Hello, World!' | tr '[:lower:]' '[:upper:]'

[:lower:] and [:upper:] are clearer than spelling out a-z/A-Z by hand, and behave the same for plain ASCII text.

Show output
HELLO, WORLD!

Convert to lowercase

echo 'Hello, World!' | tr 'A-Z' 'a-z'

Same idea, reversed direction.

Show output
hello, world!

Swap case in a single pass

echo 'Hello World' | tr 'a-zA-Z' 'A-Za-z'

Each SET is a pair of concatenated ranges, so lowercase maps to uppercase and uppercase to lowercase simultaneously, in one command.

Show output
hELLO wORLD

ROT13-encode text

echo 'Attack at dawn' | tr 'A-Za-z' 'N-ZA-Mn-za-m'

Each letter maps to the one 13 places later in the alphabet, wrapping around. Running the exact same command on the output decodes it back - ROT13 is its own inverse.

Show output
Nggnpx ng qnja

Deleting characters with -d

-d removes every character listed in SET1. No SET2 is given - there's nothing to translate them to.

Delete specific characters

echo 'Hello, World!' | tr -d ','

Every comma is removed outright, not replaced with anything.

Show output
Hello World!

Delete an entire character class

echo 'Room 42, Floor 7' | tr -d '[:digit:]'

Removes every digit anywhere in the input, regardless of how many or where.

Show output
Room , Floor

Strip all punctuation

echo 'Hello, World! Nice.' | tr -d '[:punct:]'

[:punct:] covers the standard ASCII punctuation set in one class, instead of listing each symbol by hand.

Show output
Hello World Nice

The mistake: trying [^...] for 'not this'

echo 'Hello123' | tr -d '[^0-9]'

Brackets have no special meaning in tr's SET syntax - this is parsed as the literal characters [, ^, ] plus the range 0-9, not "everything except a digit." Verified: this deletes the digits and keeps the letters, the exact opposite of what a regex habit would predict.

The correct way to delete everything except a set

echo 'Hello123' | tr -cd '0-9'

-c complements the set first, then -d deletes what's left after complementing - so this keeps only digits. This is the actual tr equivalent of grep's [^...].

Show output
123

Squeezing repeats with -s

-s collapses any run of the same character, however long, down to a single instance.

Fix column extraction on padded text

printf 'a   b     c\n' | tr -s ' ' | cut -d' ' -f2

Squeeze first, then cut works correctly - this is the standard fix for cut's one-character-delimiter limitation on space-padded output like ps or ls -l. See the cut page.

Show output
b

Squeeze repeated letters generically

echo 'aabbccdd' | tr -s 'a-z'

Applies to any character in the given set, not just spaces - every run of the same letter collapses to one.

Show output
abcd

Deleting and squeezing together

Delete one set, then squeeze what's left

printf 'aabbccdd11\n' | tr -ds '0-9' 'a-z'

-ds takes two sets: SET1 is deleted first, then runs of characters in SET2 are squeezed in what remains. Here, digits are removed, then the repeated letters collapse.

Show output
abcd

When SET1 and SET2 are different lengths

A shorter SET2 repeats its last character

echo 'abcdef' | tr 'a-f' 'XY'

By default, if SET2 is shorter than SET1, its final character is repeated to fill out the mapping - a maps to X, but b through f all map to Y.

Show output
XYYYYY

Truncate SET1 to match SET2's length instead

echo 'abcdef' | tr -t 'a-f' 'XY'

-t changes that behaviour: SET1 is truncated to SET2's length instead of SET2 being extended, so only a and b are translated (to X and Y) and c through f pass through untouched.

Show output
XYcdef

Complementing a set with -c

-c operates on every character NOT in SET1 - the actual way to express 'everything except.'

Keep only digits

echo 'Hello123World' | tr -cd '[:digit:]'

Complement of digits is every letter; -d deletes that complement, leaving only the digits.

Show output
123

Keep only letters and digits

echo 'Hello, World! 123' | tr -cd '[:alnum:]'

[:alnum:] covers both letters and digits in one class - strips spaces and punctuation, keeps everything else.

Show output
HelloWorld123

Complementing a set includes the trailing newline

echo 'ABC' | tr --complement 'A' '_'

echo appends a trailing newline, and that newline is not 'A' either, so it's part of the complement too. That is four characters back, not three: the line's own newline got translated along with B and C.

Show output
A___

Practical text cleanup

Convert Windows line endings to Unix

tr -d '\r' < crlf.txt

CRLF line endings are a trailing carriage-return byte before each newline - deleting \r converts the file to plain Unix LF endings without touching anything else.

Convert tabs to spaces

printf 'a\tb\tc\n' | tr '\t' ' '

A direct one-to-one substitution, no class needed for a single character.

Show output
a b c

Mask digits for a redacted display

echo 'Card 4111 2222 3333' | tr '[:digit:]' '#'

Every digit becomes #, preserving the original grouping and spacing - useful for showing structure without showing the real numbers.

Show output
Card #### #### ####

Convert a European decimal comma to a dot

echo '3,14' | tr ',' '.'

A one-character fix for numeric data that uses a comma as the decimal separator, before feeding it to a tool that expects a dot.

Show output
3.14

Translate using an octal character code

printf 'ABA\n' | tr '\101' 'X'

\101 is the octal escape for 'A' - useful for a character that's awkward to type literally or paste cleanly into a script.

Show output
XBX

Convert a CSV file to tab-separated

tr ',' '\t' < users.csv | head -3

Works fine for simple CSV with no quoted fields or embedded commas - for anything with quoting rules, use a real CSV-aware tool instead.

Show output
name	age	department
Alice	34	Engineering
Bob	29	Sales

Long-form flags

Squeeze, spelled out

echo 'aabbcc' | tr --squeeze-repeats 'a-z'

--squeeze-repeats is the long-form spelling of -s.

Show output
abc

Delete, spelled out

echo 'a,b,c' | tr --delete ','

--delete is the long-form spelling of -d.

Show output
abc

Complement, spelled out

echo 'Hello123World' | tr --complement --delete '[:digit:]'

--complement is the long-form spelling of -c, and combines with --delete exactly like -cd does.

Show output
123

Building real pipelines

One word per line

printf 'the quick brown fox\n' | tr -cs '[:alpha:]' '\n'

-c takes the complement of letters (everything that isn't a letter - spaces, in this case), -s squeezes runs of it, giving one word per output line. The starting point for the classic word-frequency pipeline below.

Show output
the
quick
brown
fox

Count word frequency in a block of text

printf 'the quick brown fox jumps over the lazy dog the fox runs\n' | tr -cs '[:alpha:]' '\n' | tr '[:upper:]' '[:lower:]' | sort | uniq -c | sort -rn

A classic Unix pipeline: split into one word per line, lowercase everything so case doesn't fragment the count, then the standard sort | uniq -c | sort -rn frequency count from the sort and uniq pages.

Show output
      3 the
      2 fox
      1 runs
      1 quick
      1 over
      1 lazy
      1 jumps
      1 dog
      1 brown

Join multiple lines into one

printf 'one\ntwo\nthree\n' | tr '\n' ' '

Translating every newline to a space collapses a whole file onto a single line - the opposite of the one-word-per-line trick above.

Show output
one two three