tr
Translate, delete, or squeeze characters one at a time
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␍
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.
Collapse repeated spaces into one
printf 'a b c\n' | tr -s ' '
Whatever the run length - 3 spaces or 30 - -s reduces it to exactly one.
Show output
a b c
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
Squeezing newlines removes blank lines entirely, not just extras
printf 'a\n\nb\n\n\n\nc\n' | tr -s '\n'
A blank line is just two adjacent newlines, and -s squeezes any run of them down to one, which eliminates the blank line completely rather than leaving a single blank line the way cat -s does.
Show output
a
b
c
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
Strip non-printable characters - but watch what counts as printable
printf 'Hello\aWorld\n' | tr -cd '[:print:]'
[:print:] doesn't include the newline character. Verified: this strips the bell character as intended, but also strips every line break in the input, joining everything onto one line with no separators.
Strip non-printable characters correctly, keeping line breaks
printf 'Hello\aWorld\n' | tr -cd '[:print:]\n'
Add \n explicitly to the keep-set alongside [:print:] so line structure survives the cleanup.
Show output
HelloWorld
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
Replace spaces with underscores for safer filenames
echo 'my file name.txt' | tr ' ' '_'
A common preprocessing step before using a filename in a context that handles spaces awkwardly (some scripts, some URLs).
Show output
my_file_name.txt
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
Confirm an explicit character list and a range behave identically
echo 'abc' | tr 'abc' 'xyz'
For three specific characters, spelling them out and using the range a-c produce the exact same mapping - ranges are just shorthand for the equivalent list.
Show output
xyz