ANSI escapes don't work in `printf` - bash

When trying to use ANSI color escapes from a shell script, I was completely stuck, as the escape sequences (\e) were printed verbatim to the output.
#!/bin/sh
GREEN="\e[32m"
RED="\e[31m"
CLEAR="\e[0m"
printf "${GREEN}test passed${CLEAR}\n"
printf "${RED}test failed${CLEAR}\n"
Produces
\e[32mtest passed\e[0m
\e[31mtest failed\e[0m

\e is not recognized by POSIX sh (as mentioned by honzasp), but \033 is.
GREEN='\033[32m'
CLEAR='\033[0m'
printf "${GREEN}testpassed${CLEAR}\n"
Generally, it's safer to not expand parameters inside the first argument to printf (consider, for example FOO="hello %s"; printf "$FOO bar \n" baz;). However, this requires you to embed an actual escape character in your parameters, rather than a string that printf interprets as an escape character.
GREEN=$(printf '\033[32m')
CLEAR=$(printf '\033[0m')
printf '%stest passed%s' "$GREEN" "$CLEAR"

The solution is to use #!/bin/bash instead of #!/bin/sh in the first line, because raw sh's printf doesn't understand the escapes.

Related

Capture command's output without translating "\n" to literal newline

cat file.json gives me what I want to capture inside of $JSON:
{
key: "value\nwith\nnewline\nchars"
}
I can't do JSON=$(cat file.json) though because then the newline characters are translated and I get after echo $JSON or echo -e $JSON.
{
key: "value
with
newline
chars"
}.
How can I preserve the newline characters inside of $JSON?
Capture using command substitution doesn't perform the translation you're worried about here, but using echo (or misusing printf by substituting into the format string rather than a separate parameter) will.
To emit a variable with backslash sequences intact, use:
printf '%s\n' "$JSON"
This avoids behavior that echo can have (either explicitly with bash's noncompliant extension for echo -e, or implicitly when the xpg_echo flag is enabled in bash, or as default out-of-the-box behavior with other, POSIX+XSI-compatible /bin/sh implementations) wherein escape sequences are replaced by echo, even if the variable passed as an argument had a multi-character backslash sequence.

How to "unescape" the output of bash's `printf "%q"`

In bash, you can use printf "%q" to escape the special characters in a string. I've added a line break in the following examples for clarity's sake:
$ printf "%q\n" "foo"
foo
$ printf "%q\n" 'foo$bar'
foo\$bar
$ printf "%q\n" "foo bar" # Tab entered with Ctrl+V Tab
$'foo\tbar'
You can supply the -v option to printf to stick the output into a variable, rather than echoing to stdout.
Now what if I want to echo the original, unescaped string back to stdout? If I just do a simple echo, it includes all the meta/control characters; echo -e gets me slightly further, but not to a fully unescaped state.
Use printf and eval to "unescape" the string (it is perfectly safe, since it is quoted by bash already). echo is, in general, dangerous with arbitrary user input.
eval printf '%s\\n' "$var"
The argument to printf is %s\\n with double backslashes even within the single quotes, since eval will take it as one argument and then strip it (evaluate it) as is, so it effectively becomes something like: printf %s\\n $'12\n34 56' which is a perfectly safe and valid command (the variable even includes a newline and space, as you can see).

possible bashism: echo -e

I am using following shebang in my script:
#!/bin/sh
To check whether I haven't used any bash syntax, I checked my script with checkbashisms script and it says
possible bashism in my_script.sh line 3 (echo -e):
echo -e "hello world"
And I am running this script on embedded board with busybox shell.
How to resolve this?
POSIX allows echo -e as the default behavior. You may do
var="string with escape\nsequence"
printf "%s\n" "$var" # not interpreting backslashes
printf "%b\n" "$var" # interpreting backslashes
This should pass the checkbashisms test
help echo
-e enable interpretation of the following backslash escapes
...
If your code doesn't need to interpret backslash escapes then you can safely remove the -e flag.
Sometimes, people tend to use -e just for interpreting \n as newline. If that's the case, know that you can have multiline strings just by quoting them and writing as it is. An example:
var="Hi,
I am
a multiline
string"
But if you do need to interpret backslash escapes, then using printf will be your best bet.
Though I would recommend using printf whether you need backslash interpretation or not.
With printf:
printf "%s\n" "$var" # no backslash interpretation
printf "$var" # backslash interpretation occurs

bash: variable expansion inside var=$' '

I'm almost certain the code I have here worked before. Here's a simplified version and what it produces:
a="atext"
b="btext"
var=$'${a}\n${b}\n'
printf "var=$var"
Which produces output:
var=${a}
${b}
The real code outputs var to file, but the variable expansions aren't happening for some reason.
If this can't work, can you suggest a nice alternative way, and why one uses $' '? Thanks.
GNU bash, version 4.3.42
$'' is a quoting type used to allow backslash escape sequences to describe literal strings with nonprintable characters and other such oddities. Thus, $'\n' evaluates to a single character -- a newline -- whereas '\n' and "\n" both evaluate to two characters, the first being a backslash and the second being an n.
If you want to have the exact behavior of your original code -- putting a literal newline between the results of two different expansions -- you can switch quote types partway through a string:
a="atext"
b="btext"
var="$a"$'\n'"$b"
printf '%s' "var=$var"
That is, right next to each other, with no spaces between:
"$a"
$'\n'
"$b"
This gives you $a and $b expanded, with a literal newline between them.
Why does this matter? Try the following:
$ a=atext
$ b=btext
$ var1="$a\n$b" # Assign with literal "\" and "n" characters
$ printf "$var1" # Here, printf changes the "\n" into the newline
atext
btext
$ printf '%s' "$var1" # ...but this form shows that the "\n" are really there
atext\nbtext
$ var2="$a"$'\n'"$b" # now, we put a single newline in the string
$ printf '%s' "$var2" # and now even accurate use of printf shows that newline
atext
btext
Just replace the single quotes with double quotes.
$ cat test
a="atext"
b="btext"
var=$"${a}\n${b}\n"
printf "var=$var"
$ sh test
var=atext
btext
For variable expansion you either need to use double quotes or no quotes. Single quotes negate expansion.

How can I do ANSI C quoting of an existing bash variable?

I have looked at this question, but it does not cover my use case.
Suppose I have the variable foo which holds the four-character literal \x60.
I want to perform ANSI C Quoting on the contents of this variable and store it into another variable bar.
I tried the following, but none of them achieved the desired effect.
bar=$'$foo'
echo $bar
bar=$"$foo"
echo $bar
Output:
$foo
\x61
Desired output (actual value of \x61):
a
How might I achieve this in the general case, including non-printable characters? Note that in this case a was used just as an example to make it easier to test whether the method worked.
By far the simplest solution, if you are using bash:
printf %b "$foo"
Or, to save it in another variable name bar:
printf -v bar %b "$foo"
From help printf:
In addition to the standard format specifications described in printf(1)
and printf(3), printf interprets:
%b expand backslash escape sequences in the corresponding argument
%q quote the argument in a way that can be reused as shell input
%(fmt)T output the date-time string resulting from using FMT as a format
string for strftime(3)
There are edge cases, though:
\c terminates output, backslashes in \', \", and \? are not removed,
and octal escapes beginning with \0 may contain up to four digits
The following works:
eval bar=\$\'$x\'
The command bar=$'\x61' has to be constructed first, then eval evaluates the newly built command.
I just found out that I can do this. Edited based on comments.
bar=$( echo -ne "$foo" )
The best method I know is
y=$(printf $(echo "$foo"|sed 's/%/%%/g'))
As mentioned in the comments, this trims trailing newlines from $foo. To overcome this:
moo=$(echo "${foo}:end"|sed 's/%/%%/g')
moo=$(printf "$moo")
moo=${moo%:end}
# the escaped string is in $moo
echo "+++${moo}---"
Since bash 4.4 there is a variable expansion to do exactly that:
$ foo='\x61'; echo "$foo" "${foo#E}"
\x61 a
To set another variable use:
$ printf -v bar "${foo#E}"; echo "$bar"
a
Sample of conversion via shell. Problem, the code is octal using \0nnn and hexdecimal (not on all shell) using \xnn (where n are [hexa]digit)
foo="\65"
print "$( echo "$foo" | sed 's/\\/&0/' )"
5
with awk, you could certainly convert it directly

Resources