FIELD NOTES / UNIX & LINUXNew tools on weekdays. Beyond the man page.
$tr

Tr: change bytes without losing the records

Normalize an ASCII export while learning where translation, deletion, and squeezing erase meaning.

THE DEEP DIVE12 MIN
01

Translate one byte at a time

An inventory export arrives with inconsistent case, extra separators, and Windows line endings. You could normalize it into a convenient view. You could also erase the difference between two names or join records that were supposed to remain separate. `tr` is useful when the transformation really belongs at the character level; it needs a clear boundary around that job.

`tr` reads standard input, transforms it, and writes standard output. It does not take a filename operand. Here Bash's `printf` supplies the synthetic text; `%s` prints its following argument and `\n` emits the final newline. Single quotes protect the `printf` format from the shell. The plain `abc` and `ABC` operands contain no shell metacharacters. A pipe, `|`, passes the printed bytes to `tr`.

The two operands describe matching positions: `a` becomes `A`, `b` becomes `B`, and `c` becomes `C`. The `x` remains unchanged. The examples use GNU `tr` from Coreutils 9.4 and Bash 5.2.21 on Linux. GNU `tr` is not a Unicode text parser. Later sections make its byte boundaries explicit.

TERMINALRun in Bash
printf '%s\n' abc-x | tr abc ABC
TERMINALExpected standard output
ABC-x
02

Give the inventory a disposable fixture

We will reuse two synthetic records. `\t` in `printf`'s format writes a tab; `\r` writes a carriage return; `\n` writes a newline. Each record ends with carriage-return/newline, commonly abbreviated CRLF. Its embedded spaces and tabs are intentionally inconsistent.

Run these commands in a disposable directory. `>` writes output to the named file, creating it or replacing its previous contents. The first command therefore creates or overwrites `inventory.raw`. Every later example reads this same unchanged file; transformations go to standard output unless a different destination is explicitly named.

The `<` operator makes a file the command's standard input. `od` displays its bytes: `-An` removes address offsets, `-tx1` selects hexadecimal byte values, and `-v` displays repeated lines instead of abbreviating them. Hexadecimal `09` is a tab, `0d` is a carriage return, and `0a` is a newline. Reading this view first establishes what the export actually contains.

TERMINALRun in Bash
printf 'node07\t\t4  GPUs\r\nnode12\t8 GPUs\r\n' > inventory.raw
od -An -tx1 -v < inventory.raw
TERMINALExpected standard output
 6e 6f 64 65 30 37 09 09 34 20 20 47 50 55 73 0d
 0a 6e 6f 64 65 31 32 09 38 20 47 50 55 73 0d 0a
03

Use a locale contract for case conversion

`[:lower:]` and `[:upper:]` are character classes understood by `tr`: lowercase and uppercase letters. Their paired use requests case conversion. They are not regular expressions, despite the brackets. A single-quoted operand keeps the shell from treating those brackets as a filename pattern.

`LC_ALL=C` before a command sets its locale for that command only. A locale controls character categories and other language conventions. Here the C locale gives GNU `tr` a predictable single-byte environment; the supplied ASCII inventory names become uppercase. The tab counts and CRLF endings remain untouched.

This is normalization for the inventory system's chosen ASCII naming convention, not a claim that hostnames, usernames, or application keys are always case-insensitive. Do not merge distinct identifiers merely because a display convention would look tidier. GNU's manual explicitly limits reliable `tr` support to safe single-byte locales; this approach does not turn it into Unicode case conversion.

TERMINALRun in Bash
LC_ALL=C tr '[:lower:]' '[:upper:]' < inventory.raw | od -An -tx1 -v
TERMINALExpected standard output
 4e 4f 44 45 30 37 09 09 34 20 20 47 50 55 53 0d
 0a 4e 4f 44 45 31 32 09 38 20 47 50 55 53 0d 0a
04

Delete a known carriage return

The `-d` flag deletes every input character in its operand rather than translating characters. `\r` names a carriage return in `tr`'s operand grammar. This removes the `0d` bytes while preserving the `0a` newlines, tabs, spaces, and letters.

For this fixture, every carriage return is part of a CRLF line ending, so the operation produces an LF-ended view. But `tr -d '\r'` deletes all carriage returns anywhere in the stream. It does not verify that each one immediately precedes a newline. A file containing meaningful carriage returns inside a record needs a narrower operation.

When the rule is positional—remove carriage return only at line end—use a line-aware tool such as the sed article's addressed edits, with its implementation and input format qualified. `tr` is the smaller tool when you already know the global deletion rule is appropriate. Preserve `inventory.raw`; an inspection copy should not overwrite the evidence it explains.

TERMINALRun in Bash
LC_ALL=C tr -d '\r' < inventory.raw | od -An -tx1 -v
TERMINALExpected standard output
 6e 6f 64 65 30 37 09 09 34 20 20 47 50 55 73 0a
 6e 6f 64 65 31 32 09 38 20 47 50 55 73 0a
05

Squeezing preserves one occurrence

The `-s` flag squeezes adjacent repetitions of characters listed in the last operand. With one operand and no translation, two tabs become one tab, and two spaces become one space. Tab followed by space does not form a repetition of the same character, so it is not collapsed as one combined whitespace run.

`[:blank:]` denotes horizontal whitespace—spaces and tabs in the C locale. That is different from `[:space:]`, which includes line-breaking whitespace. Selecting the narrower class documents which boundaries you intend to preserve.

The result is still a byte stream with CRLF endings. Squeezing is not column parsing and does not understand quoted values. If a field deliberately contains repeated spaces, it changes that field too. Use the awk article's field-processing model when the action depends on which field contains the whitespace.

TERMINALRun in Bash
LC_ALL=C tr -s '[:blank:]' < inventory.raw | od -An -tx1 -v
TERMINALExpected standard output
 6e 6f 64 65 30 37 09 34 20 47 50 55 73 0d 0a 6e
 6f 64 65 31 32 09 38 20 47 50 55 73 0d 0a
06

Translate first, then squeeze

To create a plain display, translate tabs into spaces before squeezing repeated spaces. When `-s` is combined with two translation operands, GNU `tr` translates first, then squeezes characters from the second operand. This ordering matters: tabs and spaces become the same character before repeated spaces are collapsed.

The first command in the pipeline removes known carriage returns. The second translates `\t` to a literal space and squeezes repeated spaces. The raw file remains unchanged. Only the displayed view has consistent separators and LF endings.

This is a useful way to read a rough ASCII export, but it remains a lossy view. A space-bearing field could become indistinguishable from two separate fields. Human-friendly output and trustworthy data transport are separate goals. Decide which goal you have before feeding a normalized view into an inventory comparison.

TERMINALRun in Bash
LC_ALL=C tr -d '\r' < inventory.raw | LC_ALL=C tr -s '\t' ' '
TERMINALExpected standard output
node07 4 GPUs
node12 8 GPUs
07

A short destination changes the contract

Translation pairs are easiest to reason about when their expanded arrays have equal lengths. If the second array is shorter, GNU `tr` normally extends it by repeating its last character. Thus translating `abc` to `X` maps all three letters to `X`.

GNU's `-t` flag instead truncates the first array to the length of the second. Only `a` is translated in the second command; `b` and `c` remain unchanged. This flag changes the mapping, not the input length. Other implementations have historically made different choices for unequal arrays, so implicit padding is a portability trap.

Use equal-length explicit arrays when that is your intended operation. If you intentionally want a many-to-one conversion, document it instead of letting a short operand hide the decision. These tiny experiments explain why a recipe can appear to work on one host and transform different bytes on another.

TERMINALRun in Bash
printf '%s\n' abc-x | LC_ALL=C tr abc X
printf '%s\n' abc-x | LC_ALL=C tr -t abc X
TERMINALExpected standard output
XXX-x
Xbc-x
08

A complement can join unrelated records

The `-c` flag selects the complement of the first operand: every byte not in that array in this C-locale example. Together `-c -d` therefore deletes everything except the listed bytes. The range `0-9` expands to the ASCII digits in the C locale.

The first pipeline keeps only digits. It loses both letters and line boundaries, producing one uninterrupted sequence from two records. The following `printf` adds a newline only so that this deliberate bad view ends cleanly on the display. The joined digits do not mean anything useful about the inventory.

The second pipeline includes `\n` in the retained set, preserving record separators. It still merges the node number with the GPU count within each record. An allowlist of bytes is not a parser, and a successful `tr` exit does not mean a field has been validated. For counts tied to named records, preserve fields and validate their structure before choosing a value.

TERMINALRun in Bash
LC_ALL=C tr -c -d '0-9' < inventory.raw
printf '\n'
LC_ALL=C tr -c -d '0-9\n' < inventory.raw
TERMINALExpected standard output
074128
074
128
09

Character arrays are not search patterns

The bracket syntax from a regular expression does not transfer directly to `tr`. In a `tr` operand, `[0-9]` includes literal brackets as well as the digit range. Deleting it therefore removes the brackets too. Bare `0-9` retains the brackets while removing the digits.

The synthetic label makes the distinction visible. Single quotes protect the entire operand from the shell; they do not change how `tr` interprets it. Quoting solves shell interpretation, while choosing the correct operand solves utility interpretation.

The grep article uses patterns to select matching lines. `tr` arrays instead identify characters for mapping or deletion wherever they appear. A substring replacement such as changing `node` into `host` belongs in a different tool. `tr node host` would pair individual letters, including occurrences outside that word.

TERMINALRun in Bash
printf '%s\n' '[node07]' | LC_ALL=C tr -d '[0-9]'
printf '%s\n' '[node07]' | LC_ALL=C tr -d '0-9'
TERMINALExpected standard output
node
[node]
10

Keep invisible bytes visible during inspection

A record can contain NUL, the zero-valued byte, even though a terminal does not make it obvious. This controlled fixture inserts it through `printf`'s `\0` escape. `od` reveals `00` between `node` and `07`.

`tr -d '\000'` deletes that byte; `\000` is a three-digit octal byte notation in the operand. The second byte view confirms the deletion. This is useful when a specified downstream format prohibits NUL and a derived copy needs normalization. It is also a way to demonstrate exactly what a suspicious or corrupted export contains.

Do not interpret deletion as a security verdict. The output now resembles an ordinary name, but it was not the original name. NUL can also be a deliberate record separator, as the find article explains. Removing it from such a stream destroys the framing. Preserve the original and decide what NUL means in that particular format before changing it.

TERMINALRun in Bash
printf 'node\00007\n' | od -An -tx1
printf 'node\00007\n' | LC_ALL=C tr -d '\000' | od -An -tx1
TERMINALExpected standard output
 6e 6f 64 65 00 30 37 0a
 6e 6f 64 65 30 37 0a
11

The C locale does not make UTF-8 single-byte text

UTF-8 represents some characters using several bytes. Here octal escapes `\303\251` construct the two UTF-8 bytes for é without depending on the source editor's encoding. The first view shows both. `tr -d '\303'` removes just the first byte, leaving `a9` as an invalid standalone UTF-8 byte.

Setting `LC_ALL=C` makes GNU `tr`'s byte operation predictable; it does not make the transformation Unicode-aware. A character-oriented replacement, case conversion, or truncation in multilingual text needs an implementation that actually supports the encoding and operation you want.

This limitation is useful knowledge during incident inspection: odd rendering can indicate altered bytes, not merely a font problem. Inspect bytes before normalizing them. A derived ASCII-only view can aid searching while discarding exactly the material you may need to explain an encoding failure. Keep such a view labeled as a transformation, not a faithful transcript.

TERMINALRun in Bash
printf '\303\251\n' | od -An -tx1
printf '\303\251\n' | LC_ALL=C tr -d '\303' | od -An -tx1
TERMINALExpected standard output
 c3 a9 0a
 a9 0a
12

Choose the smallest transformation that preserves meaning

The inventory exercise has three different outcomes: faithful byte inspection, a readable normalized view, and a digits-only stream that has lost its structure. All can be produced by commands that run successfully. Their usefulness comes from the contract you choose, not the exit status alone.

Use `tr` for a global character mapping, a deliberate deletion, or adjacent repetitions of a specified character. Use a record-aware tool when the meaning depends on a field, a word, a line ending's position, or a quoting convention. These are practical boundaries rather than competing tool loyalties.

For external input, normalization is not generic sanitization. Collapsing separators or removing control bytes can change identifiers, conceal evidence, and make distinct records look alike. This article demonstrates interpretation and data-loss risks; it does not assert a CVE or an observed exploit in GNU `tr`. Retain the original, constrain the transformation, and verify the resulting bytes before relying on the view.

HELP SHAPE THE NEXT ISSUE

What belongs in your toolbox?

Choose a command you want to know better. We start with familiar tools and work toward the less obvious ones.

READER’S CHOICE

What should we cover next?

Vote for a future deep dive, or suggest a command we missed.

One command name, up to 32 characters. Suggestions are reviewed by a person. A new article starts a fresh poll.

Loading poll…