In my bash script I am using echo to pipe a string containing c style comments into the mail command as follows:
echo -e $EMAIL_TXT | mail -s "$SUBJECT" $RECIPIENT where $EMAIL_TXT contains
"/* some text */". In the email I am getting a list of all directories in the root since it is actually evalutating /* as all directories in root. How do I get my script to evaluate this as a string and not the actual command?
Put quotes around $EMAIL_TXT
echo "$EMAIL_TXT"
A note about -e:
-e causes escape sequences (such as \n and \t) in the arguments to echo to be converted to their corresponding characters (newline and tab, in those cases). It is not necessary if the arguments contain actual newline and tab characters, and it is probably not desirable either. Suppose, for example, that this answer were the text of the email. In that case, echo -e would convert the \n's into newlines, completely destroying the sense of the message.
bash does not convert \n into a newline, except in one specific case. So if you type:
echo "foo\nbar"
You'll see
foo\nbar
Because that is what you typed; bash does not convert the escape sequence. However, you can get bash to convert the escape sequence by using a different quotation form:
$ echo $'foo\nbar'
foo
bar
$
Of course, you can also just include a newline:
$ echo "foo
> bar"
foo
bar
$
Put quotes around the variable. I.e.
echo -e "$EMAIL_TXT" | mail -s "$SUBJECT" $RECIPIENT
Run just the echo command with and without the quotes to see the difference.
Related
This question already has answers here:
When to wrap quotes around a shell variable?
(5 answers)
Closed 1 year ago.
I'm fetching an event description from an API using curl and assigning the results to a variable in bash like this:
Event=$( curl -s -X GET https://api.vendor.com/v1/events/ev_$API_ID\
-H 'Accept: application/json' \
-u 'mykey:' )
EVTITLE=$(echo $Event | jq -r '.name')
DESC=$(echo $Event | jq -r '.description')
This is working well so far. But sometimes the EVTITLE or DESC strings have shell special chars in the strings like &, ! and sometimes quotes.
So, later, when I go to pass the variable to a sed command like this:
(to replace values in a template file)
ti_sed="s/EVTITLE/"$EVTITLE"/"
sed -i -e "$ti_sed" filename
Where the value in $EVTITLE is something like
Amy does Q&A for you and "other things" !
I'd like to avoid having bash interpret those strings before sed goes to work.
Is there a way to groom the strings so the final sed output looks like the input?
For example can I get the string value of $EVTITLE between single quotes?
Is there a way to groom the strings so the final sed output looks
like the input?
Here's a bash demo script which reads strings from a temporary JSON
file into an indexed array and has GNU sed write its own conversion
script to edit a template.
Note that \n, \r, \t, \u etc. in the JSON source will be converted
by jq -r before bash and sed see them. The bash script reads
newline-delimited lines and does not work for JSON strings containing \n.
More comments below.
#!/bin/bash
jsonfile="$(mktemp)" templatefile="$(mktemp)"
# shellcheck disable=SC2064
trap "rm -f -- '${jsonfile}' '${templatefile}'" INT EXIT
cat << 'HERE' > "${jsonfile}"
{
"Name":"A1",
"Desc":"*A* \\1 /does/ 'Q&A' for you\tand \"other things\" \\# $HOME !"
}
HERE
printf '%s\n' '---EVTITLE---' > "${templatefile}"
mapfile -t vars < <(
jq -r '.Name, .Desc' < "${jsonfile}"
)
wait "$!" || exit ## abort if jq failed
# shellcheck disable=SC2034
name="${vars[0]}" desc="${vars[1]}"
printf '%s\n' "${desc}" |
tee /dev/stderr |
sed -e 's/[\\/&\n]/\\&/g' -e 's/.*/s\/EVTITLE\/&\//' |
tee /dev/stderr |
sed -f /dev/stdin "${templatefile}"
These are the 3 lines output by the script (with tabs expanding to
different lengths) showing the contents of:
the shell variable desc
the generated sed script
the edited template file
*A* \1 /does/ 'Q&A' for you and "other things" \# $HOME !
s/EVTITLE/*A* \\1 \/does\/ 'Q\&A' for you and "other things" \\# $HOME !/
---*A* \1 /does/ 'Q&A' for you and "other things" \# $HOME !---
bash stores the string it reads and passes it on without modification
using printf to sed which in turn adds escapes as needed for a
replacement string to be inserted between s/EVTITLE/ and /, i.e.
the sed script required to edit the template file.
In the replacement section of a sed substitute command the
following have a special meaning according to
POSIX
\ (backslash) the escape character itself
the s command delimiter, default is / but it may be anything
other than backslash and newline
& (ampersand) referencing the entire matched portion
\𝛂 (𝛂 is one of digits 1 through 9 ) referencing a matched group
a literal newline
but several seds recognize other escapes as replacements. For example,
GNU sed will replace \f, \n, \t, \v etc. as in C, and (unless
--posix option) its extensions \L, \l, \U, \u, and \E act
on the replacement.
(More on this by info sed -n 'The "s" Command', info sed -n Escapes,
info sed --index-search POSIXLY_CORRECT.)
What this means is that all backslash, command delimiter, ampersand,
and newline characters in the input must be escaped, i.e. prefixed with
a backslash, if they are to represent themselves when used in a
replacement section. This is done by asking sed to s/[\\/&\n]/\\&/g.
Recall that most of the meta characters used in regular expressions
(and the shell, for that matter), such as ^$.*[]{}(), have no special
meaning when appearing in the replacement section of sed's s
command and so should not be escaped there. Contrariwise, & is not
a regex meta character.
Here are a series of cases where echo $var can show a different value than what was just assigned. This happens regardless of whether the assigned value was "double quoted", 'single quoted' or unquoted.
How do I get the shell to set my variable correctly?
Asterisks
The expected output is /* Foobar is free software */, but instead I get a list of filenames:
$ var="/* Foobar is free software */"
$ echo $var
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...
Square brackets
The expected value is [a-z], but sometimes I get a single letter instead!
$ var=[a-z]
$ echo $var
c
Line feeds (newlines)
The expected value is a a list of separate lines, but instead all the values are on one line!
$ cat file
foo
bar
baz
$ var=$(cat file)
$ echo $var
foo bar baz
Multiple spaces
I expected a carefully aligned table header, but instead multiple spaces either disappear or are collapsed into one!
$ var=" title | count"
$ echo $var
title | count
Tabs
I expected two tab separated values, but instead I get two space separated values!
$ var=$'key\tvalue'
$ echo $var
key value
In all of the cases above, the variable is correctly set, but not correctly read! The right way is to use double quotes when referencing:
echo "$var"
This gives the expected value in all the examples given. Always quote variable references!
Why?
When a variable is unquoted, it will:
Undergo field splitting where the value is split into multiple words on whitespace (by default):
Before: /* Foobar is free software */
After: /*, Foobar, is, free, software, */
Each of these words will undergo pathname expansion, where patterns are expanded into matching files:
Before: /*
After: /bin, /boot, /dev, /etc, /home, ...
Finally, all the arguments are passed to echo, which writes them out separated by single spaces, giving
/bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/
instead of the variable's value.
When the variable is quoted it will:
Be substituted for its value.
There is no step 2.
This is why you should always quote all variable references, unless you specifically require word splitting and pathname expansion. Tools like shellcheck are there to help, and will warn about missing quotes in all the cases above.
You may want to know why this is happening. Together with the great explanation by that other guy, find a reference of Why does my shell script choke on whitespace or other special characters? written by Gilles in Unix & Linux:
Why do I need to write "$foo"? What happens without the quotes?
$foo does not mean “take the value of the variable foo”. It means
something much more complex:
First, take the value of the variable.
Field splitting: treat that value as a whitespace-separated list of fields, and build the resulting list. For example, if the variable
contains foo * bar then the result of this step is the 3-element
list foo, *, bar.
Filename generation: treat each field as a glob, i.e. as a wildcard pattern, and replace it by the list of file names that match this
pattern. If the pattern doesn't match any files, it is left
unmodified. In our example, this results in the list containing foo,
following by the list of files in the current directory, and finally
bar. If the current directory is empty, the result is foo, *,
bar.
Note that the result is a list of strings. There are two contexts in
shell syntax: list context and string context. Field splitting and
filename generation only happen in list context, but that's most of
the time. Double quotes delimit a string context: the whole
double-quoted string is a single string, not to be split. (Exception:
"$#" to expand to the list of positional parameters, e.g. "$#" is
equivalent to "$1" "$2" "$3" if there are three positional
parameters. See What is the difference between $* and $#?)
The same happens to command substitution with $(foo) or with
`foo`. On a side note, don't use `foo`: its quoting rules are
weird and non-portable, and all modern shells support $(foo) which
is absolutely equivalent except for having intuitive quoting rules.
The output of arithmetic substitution also undergoes the same
expansions, but that isn't normally a concern as it only contains
non-expandable characters (assuming IFS doesn't contain digits or
-).
See When is double-quoting necessary? for more details about the
cases when you can leave out the quotes.
Unless you mean for all this rigmarole to happen, just remember to
always use double quotes around variable and command substitutions. Do
take care: leaving out the quotes can lead not just to errors but to
security
holes.
In addition to other issues caused by failing to quote, -n and -e can be consumed by echo as arguments. (Only the former is legal per the POSIX spec for echo, but several common implementations violate the spec and consume -e as well).
To avoid this, use printf instead of echo when details matter.
Thus:
$ vars="-e -n -a"
$ echo $vars # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a
However, correct quoting won't always save you when using echo:
$ vars="-n"
$ echo "$vars"
$ ## not even an empty line was printed
...whereas it will save you with printf:
$ vars="-n"
$ printf '%s\n' "$vars"
-n
user double quote to get the exact value. like this:
echo "${var}"
and it will read your value correctly.
echo $var output highly depends on the value of IFS variable. By default it contains space, tab, and newline characters:
[ks#localhost ~]$ echo -n "$IFS" | cat -vte
^I$
This means that when shell is doing field splitting (or word splitting) it uses all these characters as word separators. This is what happens when referencing a variable without double quotes to echo it ($var) and thus expected output is altered.
One way to prevent word splitting (besides using double quotes) is to set IFS to null. See http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :
If the value of IFS is null, no field splitting shall be performed.
Setting to null means setting to empty
value:
IFS=
Test:
[ks#localhost ~]$ echo -n "$IFS" | cat -vte
^I$
[ks#localhost ~]$ var=$'key\nvalue'
[ks#localhost ~]$ echo $var
key value
[ks#localhost ~]$ IFS=
[ks#localhost ~]$ echo $var
key
value
[ks#localhost ~]$
The answer from ks1322 helped me to identify the issue while using docker-compose exec:
If you omit the -T flag, docker-compose exec add a special character that break output, we see b instead of 1b:
$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$
With -T flag, docker-compose exec works as expected:
$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b
Additional to putting the variable in quotation, one could also translate the output of the variable using tr and converting spaces to newlines.
$ echo $var | tr " " "\n"
foo
bar
baz
Although this is a little more convoluted, it does add more diversity with the output as you can substitute any character as the separator between array variables.
I know there is a duplicate for this question already at: How to trim whitespace from a Bash variable?.
I read all the answers there but I have a question about another solution in my mind and I want to know if this works.
This is the solution I think works.
a=$(printf "%s" $a)
Here is a demonstration.
$ a=" foo "
$ a=$(printf "%s" $a)
$ echo "$a"
foo
Is there any scenario in which this solution may fail?
If there is such a scenario in which this solution may fail, can we modify this solution to handle that scenario without compromising the simplicity of the solution too much?
If the variable a is set with something like "-e", "-n" in the begining, depending on how you process later your result, a user might crash your script:
-e option allows echo to interpret things backslashed.
Even in the case you only want to display the variable a, -n would screw your layout.
You could think about using regex to check if your variable starts with '-' and is followed by one of the available echo options (-n, -e, -E, --help, --version).
It fails when the input contains spaces between non-whitespace characters.
$ a=" foo bar "
$ a=$(printf "%s" $a)
$ echo "$a"
foobar
The expected output was the following instead.
foo bar
You could use Bash's builtin pattern substitution.
Note: Bash pattern substitution uses 'Pathname Expansion' (glob) pattern matching, not regular expressions. My solution requires enabling the optional shell behaviour extglob (shopt -s extglob).
$shopt -s extglob
$ a=" foo bar "
$ echo "Remove trailing spaces: '${a/%*([[:space:]])}'"
Remove trailing spaces: ' foo bar'
$ echo "Remove leading spaces: '${a/#*([[:space:]])}'"
Remove leading spaces: 'foo bar '
$ echo "Remove all spaces anywhere: '${a//[[:space:]]}'"
Remove all spaces anywhere: 'foobar'
For reference, refer to the 'Parameter Expansion' (Pattern substitution) and 'Pathname Expansion' subsections of the EXPANSION section of the Bash man page.
The definition of a here-document is here:
http://en.wikipedia.org/wiki/Here_document
How can you type a tab in a here-document? Such as this:
cat > prices.txt << EOF
coffee\t$1.50
tea\t$1.50
burger\t$5.00
EOF
UPDATE:
Issues dealt with in this question:
Expanding the tab character
While not expanding the dollar sign
Embedding a here-doc in a file such as a script
TAB="$(printf '\t')"
cat > prices.txt << EOF
coffee${TAB}\$1.50
tea${TAB}\$1.50
burger${TAB}\$5.00
EOF
You can embed your here doc in your script and assign it to a variable without using a separate file at all:
#!/bin/bash
read -r -d '' var<<"EOF"
coffee\t$1.50
tea\t$1.50
burger\t$5.00
EOF
Then printf or echo -e will expand the \t characters into tabs. You can output it to a file:
printf "%s\n" "$var" > prices.txt
Or assign the variable's value to itself using printf -v:
printf -v var "%s\n" "$var"
Now var or the file prices.txt contains actual tabs instead of \t.
You could process your here doc as it's read instead of storing it in a variable or writing it to a file:
while read -r item price
do
printf "The price of %s is %s.\n" $item $price # as a sentence
printf "%s\t%s\n" $item $price # as a tab-delimited line
done <<- "EOF"
coffee $1.50 # I'm using spaces between fields in this case
tea $1.50
burger $5.00
EOF
Note that I used <<- for the here doc operator in this case. This allows me to indent the lines of the here doc for readability. The indentation must consist of tabs only (no spaces).
For me, I type ctrl-V followed by ctrl-I to insert a tab in the bash shell. This gets around the shell intercepting the tab, which otherwise has a 'special' meaning. Ctrl-V followed by a tab should work too.
When embedding dollar signs in a here document you need to disable interpolation of shell variables, or else prefix each one with a backslash to escape (i.e. \$).
Using your example text I ended up with this content in prices.txt:
coffee\t.50
tea\t.50
burger\t.00
because $1 and $5 are not set. Interpolation can be switched off by quoting the terminator, for example:
cat > prices.txt <<"EOF"
As others have said, you can type CTRL-V then tab to insert a single tab when typing.
You can also disable bash tab-completion temporarily, for example if you want to paste text or if you want to type a long here-doc with lots of tabs:
bind '\C-i:self-insert' # disable tab-completion
# paste some text or type your here-doc
# note you don't use "\t" you just press tab
bind '\C-i:complete' # reenable tab-completion
EDIT: If you're on a Mac and use iTerm 2, there is now a "Paste with literal tabs" option that allows pasting code with tabs onto the bash command line.
I note that the correct answer has already been given, but I am attempting to summarize into a more succinct answer.
1. There is nothing to prevent you from having literal tab characters in a here document.
To type a literal tab at the Bash prompt, you need to escape it. The escape character is ctrl-V (unless you have custom bindings to override this).
$ echo -n 't<ctrl-v><tab>ab' | hexdump -C
00000000 74 09 61 62 |t.ab|
00000004
In most any programmer's editor, it should be trivial to insert a literal tab character (although some editors might want escaping, too. In Emacs, ctrl-Q TAB inserts a literal tab).
For legibility, it might be better to use some sort of escape instead of a literal tab character. In Bash, the $'...' string syntax is convenient for this.
2. To prevent variable expansion, quote all dollar signs, or put the here doc terminator in quotes.
$ hexdump -C <<HERE
> t<ctrl-v><tab>\$ab
HERE
00000000 74 09 24 61 62 0a |t.$ab.|
00000006
$ hexdump -C <<'HERE'
> t<ctrl-v><tab>$ab
HERE
00000000 74 09 24 61 62 0a |t.$ab.|
00000006
In this context, it doesn't matter whether you use single or double quotes.
3. I am not sure I understand this subquestion. The purpose of a here document is to embed it in a script. The previous example illustrates how to pass a here document to hexdump in a script, or at the command line.
If you want to use the same here document multiple times, there is no straightforward way to do that directly. The script could write a here document to a temporary file, then pass that temporary file to multiple commands, then erase the temporary file. (Take care to use trap to remove the temporary file also in case the script is interrupted.)
You could also put the contents of the here document in a variable, and interpolate that.
# Note embedded newlines inside the single quotes,
# and the use of $'...\t...' to encode tabs
data=$'coffee\t$1.50
tea\t$1.50
burger\t$5.00'
# Run Word Count on the data using a here document
wc <<HERE
$data
HERE
# Count number of tab characters using another here document with the same data
grep -c $'\t' <<HERE
$data
HERE
You could equivalently use echo -E "$data" | wc; echo -E "$data" | grep -c $'\t' but using echo is not very elegant and might be less portable (though if your target is bash, all echos should be the same. If your target is Bourne shell in general, you might also spend an external process for each echo).
Here's a shorter version of Dennis Williamson's answer. Inspiration from here: http://tldp.org/LDP/abs/html/here-docs.html.
#!/bin/bash
var=$(echo -e "$(cat <<"EOF"
coffee\t$1.50
tea\t$1.50
burger\t$5.00
EOF
)")
echo "$var"
Tabs that are pasted into a heredoc vanish, because bash is still interpreting them as special characters marking the start of an auto-completion sequence/request.
If you want to quickly paste a heredoc in the current shell, you can do this by disabling auto-completion for the life of the current shell.
Here's what happens with normal auto-completion if you paste a heredoc containing tabs:
$ cat <<EOF
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
EOF
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
TABAFTERTABBEFORE
Run this command:
bind "set disable-completion on"
Paste again and your tabs are retained:
$ cat <<EOF
> TABAFTER TABBEFORE
> TABAFTER TABBEFORE
> TABAFTER TABBEFORE
> TABAFTER TABBEFORE
> TABAFTER TABBEFORE
> EOF
TABAFTER TABBEFORE
TABAFTER TABBEFORE
TABAFTER TABBEFORE
TABAFTER TABBEFORE
TABAFTER TABBEFORE
Re: subquestion #3, I read this question as:
"[W]hat if I wanted to[...] store the here-doc commented in the same
file as the script for later reference?"
Use the script name as the output of the here doc, appending rather than replacing, assuming the executor also has write permissions. Shell comments are not interpreted during a here doc block.
_thisline=${LINENO}
cat <<EOF >>$0
#====== $(date) =========
#On this run, these variable values were used as of line ${_thisline}: A=${A}, B=${B}, B=${C}
EOF
In a similar way you can use a here doc to write out a new script expanding variables to values, exec it, and then you have an exact record of what was run rather having to trace the code.
If you want to use tabs for the file indentation and for the heredoc:
You just need to separate the tabs of the indentation, from the tabs of the document with a whitespace:
try_me() {
# #LinuxGuru's snippet; thanks!
sed 's/^ //g' >> tmp.conf <<-EOF
/var/log/nginx/*log {
daily
rotate 10
missingok
notifempty
compress
sharedscripts
postrotate
/bin/kill -USR1 $(cat /var/run/nginx.pid 2>/dev/null) 2>/dev/null || :
endscript
}
EOF
}
try_me
The only drawback is that the not-indented lines will look a little weird; they have a leading whitespace char on the script
/var/log/nginx/*log
}
However, that won't be there on the resulting file (sed 's/^ //g' instead of cat)
One simple and direct solution to the original problem is to use the $(echo $'...') idiom:
cat > prices.txt << EOF
$(echo $'coffee\t$1.50')
$(echo $'tea\t$1.50')
$(echo $'burger\t$5.00')
EOF
If you use a tool like sed and quote the delimiter (EOF), things can get simpler:
sed 's/\\t/\t/g' > prices.txt << 'EOF'
coffee\t$1.50
tea\t$1.50
burger\t$5.00
EOF
Quoting EOF prevents parameter (dollar sign) expansion.
sed converts '\t' into tab characters.
If you have patters like \\t in your here doc, then you would need a more complex sed invocation such as: sed -e 's/\\t/\t/g' -e 's/\\\t/\\t/g'.
Use #EOF and it will preserve tabs.
cat >> /etc/logrotate.d/nginx <<#EOF
/var/log/nginx/*log {
daily
rotate 10
missingok
notifempty
compress
sharedscripts
postrotate
/bin/kill -USR1 $(cat /var/run/nginx.pid 2>/dev/null) 2>/dev/null || :
endscript
}
#EOF
How do I echo one or more tab characters using a bash script?
When I run this code
res=' 'x # res = "\t\tx"
echo '['$res']' # expect [\t\tx]
I get this
res=[ x] # that is [<space>x]
echo -e ' \t '
will echo 'space tab space newline' (-e means 'enable interpretation of backslash escapes'):
$ echo -e ' \t ' | hexdump -C
00000000 20 09 20 0a | . .|
Use printf, not echo.
There are multiple different versions of the echo command. There's /bin/echo (which may or may not be the GNU Coreutils version, depending on the system), and the echo command is built into most shells. Different versions have different ways (or no way) to specify or disable escapes for control characters.
printf, on the other hand, has much less variation. It can exist as a command, typically /bin/printf, and it's built into some shells (bash and zsh have it, tcsh and ksh don't), but the various versions are much more similar to each other than the different versions of echo are. And you don't have to remember command-line options (with a few exceptions; GNU Coreutils printf accepts --version and --help, and the built-in bash printf accepts -v var to store the output in a variable).
For your example:
res=' 'x # res = "\t\tx"
printf '%s\n' "[$res]"
And now it's time for me to admit that echo will work just as well for the example you're asking about; you just need to put double quotes around the argument:
echo "[$res]"
as kmkaplan wrote (two and a half years ago, I just noticed!). The problem with your original commands:
res=' 'x # res = "\t\tx"
echo '['$res']' # expect [\t\tx]
isn't with echo; it's that the shell replaced the tab with a space before echo ever saw it.
echo is fine for simple output, like echo hello world, but you should use printf whenever you want to do something more complex. You can get echo to work, but the resulting code is likely to fail when you run it with a different echo implementation or a different shell.
You can also try:
echo Hello$'\t'world.
Put your string between double quotes:
echo "[$res]"
you need to use -e flag for echo then you can
echo -e "\t\t x"
From the bash man page:
Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C standard.
So you can do this:
echo $'hello\tworld'
Use the verbatim keystroke, ^V (CTRL+V, C-v, whatever).
When you type ^V into the terminal (or in most Unix editors), the following character is taken verbatim. You can use this to type a literal tab character inside a string you are echoing.
Something like the following works:
echo "^V<tab>" # CTRL+V, TAB
Bash docs (q.v., "quoted-insert")
quoted-insert (C-q, C-v)
Add the next character that you type to the line verbatim. This is how to insert key sequences like C-q, for example.
side note: according to this, ALT+TAB should do the same thing, but we've all bound that sequence to window switching so we can't use it
tab-insert (M-TAB)
Insert a tab character.
--
Note: you can use this strategy with all sorts of unusual characters. Like a carriage return:
echo "^V^M" # CTRL+V, CTRL+M
This is because carriage return is ASCII 13, and M is the 13th letter of the alphabet, so when you type ^M, you get the 13th ASCII character. You can see it in action using ls^M, at an empty prompt, which will insert a carriage return, causing the prompt to act just like you hit return. When these characters are normally interpreted, verbatim gets you get the literal character.
Using echo to print values of variables is a common Bash pitfall.
Reference link:
http://mywiki.wooledge.org/BashPitfalls#echo_.24foo
If you want to use echo "a\tb" in a script, you run the script as:
# sh -e myscript.sh
Alternatively, you can give to myscript.sh the execution permission, and then run the script.
# chmod +x myscript.sh
# ./myscript.sh
res="\t\tx"
echo -e "[${res}]"