2

For as long as I can remember, I've personally used the following method for changing the color of output text when using bash:

red=$'\033[1;31m'                                                                        
nc=$'\033[0m'

echo "${red}This is red text$nc"

It's always worked for me no matter if used on macOS or Linux. Out of curiosity, to better understand why it works, I've done a bit of research and have found some answers/examples that don't include $ (red='\033[1;31m'). After some testing, if I don't use $, the output color doesn't change to red, rather it just prints the string. Though, when executing it using sh instead of bash, the text does change color. Can someone help me understand why this is?

I've additionally tested it with double quotes instead of single quotes, and it only seems to work when using sh. I'll post examples below.

This is the script and code I used:

#!/bin/bash

red=$'\033[1;31m'
nc=$'\033[0m'

echo "${red}Test 1$nc"
echo ""

##########################

red=$"\033[1;31m"
nc=$"\033[0m"

echo "${red}Test 2$nc"
echo ""

##########################

red='\033[1;31m'
nc='\033[0m'

echo "${red}Test 3$nc"
echo ""

##########################

red="\033[1;31m"
nc="\033[0m"

echo "${red}Test 4$nc"
echo ""

Output on macOS with both bash and sh:

enter image description here

Output on Linux with both bash and sh:

enter image description here

Hunter T.
  • 603
  • Regardless of your quoting issues, you really shouldn't be outputting terminal-specific escape codes if your TERM isn't one of the ones that accepts those codes (or if your output is going to a non-terminal destination such as a pipe). The portable way is red=$(tput setaf 1). That will produce the empty string when used on a dumb terminal, for example. – Toby Speight Jul 12 '22 at 13:30

2 Answers2

10

The main thing here is that for the terminal to recognize the control sequence, the output sent the terminal must contain the ESC character, then [1;31m. That ESC is a control character, and we don't usually present it as-is in a shell script, but instead we use the backslash escape \033 to represent that character using its octal character code (it's 27 decimal, 033 octal, or 0x1b hex). The rest are just regular characters.

Now, that backslash-escape needs to be changed to the raw ESC character at some point. And there's two places where this could happen: the assignment to the variable and the command that prints it.


In shells that support it, the $'...' type of quotes interpret the escape already, so after red=$'\033[...', the variable itself contains the raw ESC character. That's not a standard POSIX feature, and in shells that don't support it, that would assign the raw dollar sign, then backslash, 033[ and so on. On the other hand, red='\033[...' would just assign backslash, 033[ etc., never interpreting the escape. See

($"...", and "..." never interpret the escape, and the one with the dollar is also non-standard so might leave the literal dollar sign in shells not supporting it.)


Then, the second step is the printing. In some shells echo expands backslash escapes. In others it doesn't. Some of those have echo -e that again does. Bash's echo by default doesn't do it, but with the xpg_echo setting set... it does. Zsh's echo does interpret them. See


So, in a usual Bash on your usual Linux desktop distribution, where xpg_echo isn't set, red=$'\033[1;31m'; echo "${red}text" produces the ESC on the assignment and prints red text, but red='\033[1;31m'; echo "${red}text" doesn't produce it in either the assignment or the echo and just prints \033[1;31mtext in normal color.

On Debian/Ubuntu, sh is Dash, where the $'...' quotes aren't supported but echo does interpret backslashes, so red=$'\033[1;31m'; echo "${red}text" prints a normal-colored $, and then the red text. This is what your run with sh and Linux looks like. You might get a different result in some other Linux system.

And on a Mac, the shell installed as sh is actually either Bash with xpg_echo set (the one installed as bash has it disabled), or zsh in newer macOS releases. Even though starting either as sh tries to make them more POSIX-compatible, they still support the $'...' quotes, so both red=$'\033[1;31m'; echo "${red}text" and red='\033[1;31m'; echo "${red}text" print red text.


Confused yet? red='\033[1;31m'; printf "%b\n" "${red}text"; should work in all POSIX-like shells. The %b tells it to process backslash escapes in the argument, so any in "text" would get processed.

ilkkachu
  • 138,973
  • 1
    In the closing, we might want to use printf '%b\n' "${red}text" so any % sequences in the text don't try to be interpreted. – Charles Duffy Jul 11 '22 at 23:23
  • On current macOS: $ echo $SHELL /bin/zsh – David Cowden Jul 12 '22 at 02:08
  • @CharlesDuffy, or printf "${red}%s\n" "text" if we didn't want to expand backslashes in "text". No way to get this really simple in the do what I want this time sense... – ilkkachu Jul 12 '22 at 08:55
  • @DavidCowden, oh, right! Remind me, do they have zsh as sh too? Or just as the default interactive shell, and something else as sh? Looks like zsh's sh-mode still works the same, i.e. it supports $'...'. – ilkkachu Jul 12 '22 at 09:05
  • @ilkkachu I also found that if I replaced echo with printf in all the tests, it printed out red text. I didn't even have to include %b, just straight red='\033[1;31m'; printf "${red}text";. I'm assuming that's because printf by default interprets back slashes. – Hunter T. Jul 12 '22 at 10:53
  • 1
    @StrangeRanger, yes, it does, and it doing that in the sameway everywhere is what makes it better than echo. But printf also interprets percent-signs as conversion specifiers, meaning fields to fill. E.g. value is %d would mean to print those two words and then look in the arguments for an integer number to print. If your text contains percent signs that should print as-is, it wouldn't work. E.g. printf "${red}value is over 90 %!" would give an error since %! is an invalid specifier. You'd need to use %% for a single % instead. Passing the text through %b/%s avoids that. – ilkkachu Jul 12 '22 at 10:58
  • @ilkkachu I see. Thank you for the help and detailed explanation! – Hunter T. Jul 12 '22 at 11:00
  • 1
    So to complete that example I showed, one could use printf "${red}value is over %d%%!\n" "$pct" to print that percentage. But with shell variables being strings already, the conversion isn't that necessary and printf "${red}value is over ${pct}%%\n", or printf "%b\n" "${red}value is over ${pct}%" would work too. Also strings given with %b do process backslashes, strings given with %s don't, so you could use the latter for any parts of the message that might contain those. – ilkkachu Jul 12 '22 at 11:14
  • echo $'\302\2331;31mred\302\2330m' works too, actually (at least in some UTF-8-based terminals) – user3840170 Jul 12 '22 at 15:41
  • @toby, Thanks for pointing out the error with "its". But, I wrote "using the octal character code" on purpose, since that's what the backslash escape takes. The hex escape \x1b works in a lot of shells, but e.g. not in Dash, and the decimal can't be used in those in any system I know. – ilkkachu Jul 12 '22 at 15:53
  • 1
    Ah yes - sorry for screwing that up - I misinterpreted. – Toby Speight Jul 13 '22 at 12:18
3

I can answer for Linux. and I think the crucial point is not how to use the ANSI escape sequences, but how to declare a variable.

red=$'\033[1;31m'  # a dollar sign and an ANSI escape sequence

red=$"\033[1;31m" # a dollar sign and an ANSI escape sequence

red='\033[1;31m' # only an ANSI escape sequence

red="\033[1;31m" # only an ANSI escape sequence

In sh, when you print with

echo "${red}some text"

you print the value of the variable red and then some text. In the first two cases there is a dollar sign before the ANSI escape sequence, so it is not affected by the colour change.

In bash, when you print with

echo -e "${red}some text"

the ANSI escape sequence is interpreted, but with

echo "${red}some text"

the ANSI escape sequence is not interpreted, instead the characters are printed literally, but might be converted 'before' the echo statement.

In your sh there is another built-in function echo which obviously interprets the ANSI escape sequence without the option -e.

The difference for you in bash between the first and other examples can be explained if you look at the hexadecimal value of red. See my edited script,

#!/bin/bash

red=$'\033[1;31m' nc=$'\033[0m' hexdump <<< "$red" echo $1 "${red}Test 1$nc" echo ""

##########################

red=$"\033[1;31m" nc=$"\033[0m" hexdump <<< "$red" echo $1 "${red}Test 2$nc" echo ""

##########################

red='\033[1;31m' nc='\033[0m' hexdump <<< "$red" echo $1 "${red}Test 3$nc" echo ""

##########################

red="\033[1;31m" nc="\033[0m" hexdump <<< "$red" echo $1 "${red}Test 4$nc" echo ""

where I also made it convenient to look at echo -e by adding that option to the script, so try both

bash box2

and

bash box2 -e

I suggest that you declare your 'colouring' variables without any dollar sign, for example

red='\033[1;31m'

and use the dollar sign only when you use a variable for example in an echo statement.

sudodus
  • 6,421