10

I have one particular server that is exhibiting strange behaviour when using tr. Here is an example from a working server:

-bash-3.2$ echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-z]
1234567890
-bash-3.2$

That makes perfect sense to me.

This, however, is from the 'special' server:

[root@host~]# echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-z]
abcdefghijklmnpqrstuvwxyz1234567890

As you can see, deleting all lower case characters fails. BUT, it has deleted the letter 'o'

The interesting part is the following two examples, which make no sense to me whatsoever:

[root@host~]# echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-n]
opqrstuvwxyz1234567890
[root@host~]# echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-o]
abcdefghijklmnpqrstuvwxyz1234567890
[root@host~]#

(again, the 'o' is deleted in the last example)

Does anyone have any idea what is going on here? I can't reproduce on any other linux box that I am using.

  • 5
    Tangentially related: tr ranges are written without the enclosing [...]. So tr -d '[a-z]' will kill a-z, and also characters [ and ]. Use tr -d a-z to kill only letters a-z. – Satō Katsura Jul 3 '18 at 11:21
24

you have a file named o in current directory

foo> ls
foo> echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-z]
1234567890
foo> touch o
foo> echo "abcdefghijklmnopqrstuvwxyz1234567890"|tr -d [a-z]
abcdefghijklmnpqrstuvwxyz1234567890

shell will expand [a-z] string if a match is found.

This is called pathname expansion, according to man bash

Pathname Expansion
After word splitting, unless the -f option has been set, bash scans each word for the characters *, ?, and [. ... (...)

bash will perform expansion.

[...] Matches any one of the enclosed characters.

  • @Chris You can check the shell's expansion using for example echo: touch o ; echo tr -d [a-z] gives this: tr -d o – pabouk Jul 3 '18 at 14:34
8

What is happening

The shell (bash) sees the argument [a-z]. That's a wildcard pattern (a glob), which matches any lowercase letter¹. Therefore the shell looks for a file name that matches this pattern. There are three cases:

  • No file in the current directory has a name that is a single lowercase letter. Then the shell leaves the wildcard pattern unchanged, and tr sees the arguments -d and [a-z]. This is what happens on most of your machines.
  • A single file in the current directory has a name that is a single lowercase letter. Then the shell expands the pattern to this file name, and tr sees the arguments -d and the file name. This happens on the server, and the matching file is called o since we can see that tr deleted the letter o.
  • Two or more files in the current directory have a name that is a single lowercase letter. Then the shell expands the pattern to the list of matching file names, and tr sees three or more arguments: -d and the file names. Since tr expects a single argument after -d, it will complain.

What you should have done

If there are special characters in the argument of a command, you must escape them. Put the argument in single quotes '…' (this is the simplest way, there are others). Inside single quotes, all characters stand for themselves except the single quote itself. If there is a single quote inside the argument, replace it by '\''.

tr -d '[a-z]'

However note that this is still probably not what you meant! This tells tr to delete lowercase letters and square brackets. It's equivalent to tr -d ']a-z[', tr '[]a-z', etc. To delete lowercase letters, use

tr -d a-z

The argument to tr is a character set. You put brackets around a character set in a regular expression or wildcard pattern to indicate that it's a character set. But tr works on a single character at a time. Its command line arguments are what you'd put inside the brackets.

You do need brackets to indicate character classes. In a regular expression, you use brackets inside brackets to indicate a character class, e.g. [[:lower:]]* matches any number of lowercase letters, [[:lower:]_]* matches any number of lowercase letters and underscores. In the argument of tr, you need the set without its surrounding brackets, so tr -d '[:lower:]' deletes lowercase letters, tr -d '[:lower:]_' deletes lowercase letters and underscores, etc.

¹ In some locales it may match other characters.

  • 1
    Note that on Solaris 10 (and other ancient SysV based Unices), you do need tr -d '[a-z]' with /usr/bin/tr. With /usr/xpg4/bin/tr, tr -d a-z works but tr -d '[a-z]' does not delete [ nor ]. – Stéphane Chazelas Jul 3 '18 at 20:45
  • 1
    /usr/xpg4/bin/tr -d '[a-z]' not deleting [ nor ] was apparently fixed in Solaris 11. – Stéphane Chazelas Jul 3 '18 at 21:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.