10

This is a Bash function that returns a value, via echo:

#!/bin/bash
get_hello_name() {
  echo 'Hello $1!'
}
msg=$(get_hello_name "x")
echo $msg

Output:

$ bash ./initial_script5.sh
Hello $1!

I then incorrectly thought that the last echo was returned (I come from Java and Python), and was trying to use echo to debug the rest of the function.

And then I was wondering why the heck I could not print out newlines in my echo statements, despite trying every single suggestion in this question.

This script demonstrates the problem:

#!/bin/bash
a_function() {
  echo "---In function"

  printf %"s\n" hello world
  printf "Hello\nworld"
  echo $'hello\nworld'
  echo -e 'hello\nworld'
}

echo "---Pre function"
printf %"s\n" hello world
printf "Hello\nworld"
echo $'hello\nworld'
echo -e 'hello\nworld'

x=$(a_function "x")
echo $x

echo "---Post function"
printf %"s\n" hello world
printf "Hello\nworld"
echo $'hello\nworld'
echo -e 'hello\nworld'

$ bash ./initial_script5.sh
---Pre function
hello
world
Hello
worldhello
world
hello
world
---In function hello world Hello worldhello world hello world
---Post function
hello
world
Hello
worldhello
world
hello
world

The problem is that all of the echos in the function are concatenated together, after being individually trimmed, and then returned as a whole.

So this leads me to two questions: How do you debug a function that returns a value, and how do you append newlines onto a variable (not that I even want to do the latter, necessarily, but I would like to understand it)?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
aliteralmind
  • 19,847
  • 17
  • 77
  • 108

3 Answers3

14

Why the newlines seemed to disappear from the variable

The newlines are actually retained in the variable. They do not display because the variable in the echo statement is not enclosed in double-quotes. From the code:

echo $x

When using a variable without double-quotes, word splitting is performed. Under the default $IFS (wiki entry on IFS), this means that all collections of whitespace, including newlines and tabs, are replaced with single space.

To avoid that, simply use double quotes as in:

echo "$x"

With that single change, the output of your script becomes:

$ bash a,sh 
---Pre function
hello
world
Hello
worldhello
world
hello
world
---In function
hello
world
Hello
worldhello
world
hello
world
---Post function
hello
world
Hello
worldhello
world
hello
world

The newlines that were always in the variable x are now displayed.

Aside: the two words that remain strung together

Note that the combination worldhello appears on one line because because that is what the code asked for:

printf "Hello\nworld"
echo $'hello\nworld'

The printf does not print a newline after world. Hence, that world appears on the same line as the hello which follows.

Documentation of the Details

man bash explains that double-quotes inhibit word splitting:

If the substitution appears within double quotes, word splitting and pathname expansion are not performed on the results.

Word-splitting happens after variable expansion, command substitution, and arithmetic expansion:

The shell scans the results of parameter expansion, command substitution, and arithmetic expansion that did not occur within double quotes for word splitting.

Another subtlety is that word splitting is performed only if some substitution took place:

Note that if no expansion occurs, no splitting is performed.

Normally, when word splitting is performed, all strings of spaces, tabs, and newlines are replaced by a single space. This default behavior can be changed by changing the value of the IFS variable:

The shell treats each character of IFS as a delimiter, and splits the results of the other expansions into words on these characters. If IFS is unset, or its value is exactly , the default, then sequences of space, tab, and newline at the beginning and end of the results of the previous expansions are ignored, and any sequence of IFS characters not at the beginning or end serves to delimit words. If IFS has a value other than the default, then sequences of the whitespace characters space and tab are ignored at the beginning and end of the word, as long as the whitespace character is in the value of IFS (an IFS whitespace character). Any character in IFS that is not IFS whitespace, along with any adjacent IFS whitespace characters, delimits a field. A sequence of IFS whitespace characters is also treated as a delimiter. If the value of IFS is null, no word splitting occurs.

How to Debug

  1. Use set -x

    Place the line set -x at the beginning of the code that you wish to run. The results of evaluating each line will be displayed as the function is run, each preceded by PS4 (default is +, space) to distinguish it from normal output.

    The debug output can be turned off by including the line set +x.

    set -x and set +x both also work on the command line.

  2. Use stderr

    Send debug output to stderr (file descriptor 2) as follows:

    echo "My Debug Info" >&2
    

    By default, pipelines and command substitutions only operate on stderr. Consequently, information sent to stderr will, by default, appear on the terminal.

More on echo

By default, echo ignores escape characters and the sequence \n simply means a \ followed by an n:

$ echo "Hello\nworld 4"
Hello\nworld 4

To have \n interpreted as a newline, use -e:

$ echo -e "Hello\nworld 4"
Hello
world 4
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
John1024
  • 109,961
  • 14
  • 137
  • 171
  • Thank you for such a thorough answer. A follow up question is at the bottom of my question post. – aliteralmind Jan 10 '15 at 02:40
  • @aliteralmind Your welcome. I added a section to the end of the answer for your follow-up question on the issue of `echo` treating `\n` as a newline or not. – John1024 Jan 10 '15 at 02:50
  • 1
    Ohhhhh. I got it! I thought you were talking about quoting *every* echo line, with 'x' just serving as a placeholder/dummy value. You mean leaving my original code exactly as it is, with a single change: The line after the function call, put *that* x in double quotes. Now it works perfectly, including your updated `-e` comment. I wish I could give you more upvotes!! – aliteralmind Jan 10 '15 at 02:53
  • Added an answer containing a working version, based on your advice. – aliteralmind Jan 10 '15 at 02:58
  • Wrote about this adventure on my blog: https://aliteralmind.wordpress.com/2015/01/10/bashcrash/ – aliteralmind Jan 10 '15 at 06:22
  • @aliteralmind: It's not even just "every echo line", it's (almost) every variable reference. Basically, anytime you see `$something` in a shell script, there should be double-quotes around it (unless there's a specific reason to avoid them). (There are some cases where it doesn't matter, but it's easier and safer to just always double-quote than to figure out when it's safe to leave them off.) – Gordon Davisson Jan 10 '15 at 06:26
  • @GordonDavisson: Good to know. Will keep that in mind. – aliteralmind Jan 10 '15 at 06:27
  • Perhaps also cover `echo ${x}` (whether it is any different from `echo $x` or not) in your answer? – Peter Mortensen Nov 05 '21 at 14:35
2

The working code after following John1024's tips (note the newlines print in the "1" lines, but not the "4", because of the e option):

#!/bin/bash
a_function() {
    echo -e "Hello\nworld\n 1"
    echo "Hello"
    echo "world"
    echo "Hello\nworld 4"
}

echo -e "Hello\nworld\n 1"
echo "Hello"
echo "world"
echo "Hello\nworld 4"

x=$(a_function "x")
echo "x-no-quotes>"
echo $x                       # No new lines!
echo "<x-no-quotes"

echo "x-in-double-quotes>"
echo "$x"                     # Yes new lines!
echo "<x-in-double-quotes"

echo -e "Hello\nworld\n 1"
echo "Hello"
echo "world"
echo "Hello\nworld 4"

Output:

Hello
world
 1
Hello
world
Hello\nworld 4
x-no-quotes>
Hello world 1 Hello world Hello\nworld 4
<x-no-quotes
x-in-double-quotes>
Hello
world
 1
Hello
world
Hello\nworld 4
<x-in-double-quotes
Hello
world
 1
Hello
world
Hello\nworld 4
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
aliteralmind
  • 19,847
  • 17
  • 77
  • 108
0

You might want to call it with a double quote:

echo "$x"

However, if you would want to explicitly show what you typed inside, also known as literal expression, use a single quote:

echo '$x'
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131