0

How can I get unmodified output from $() in bash?

I need to have bash return the results of command substitution (via $(command) or `command`), but bash treats the left square bracket as the test builtin and evaluates the text.

> echo "This shows an IP in brackets [127.0.0.1] when not in the command substitution context."
This shows an IP in brackets [127.0.0.1] when not in the command substitution context.

> echo $(echo "This should show an IP in brackets [127.0.0.1] but instead evaluates to '1'.")
This should show an IP in brackets 1 but instead evaluates to '1'.

> echo $(echo "The subtle addition of a space causes [127.0.0.1 ] to behave differently.")
The subtle addition of a space causes [127.0.0.1 ] to behave differently.

Background

Please note that the use of echo above to provide a minimal example is just that; my ultimate use will be including output from ping or tracert to show the IP of a host as seen by that host (i.e. respecting any hosts entries). In this usage, I have little control over the string being output by the command.

> grep -E "^[^#].*stackoverflow" /c/Windows/System32/drivers/etc/hosts
127.0.0.1 stackoverflow.com

> tracert -h 1 -w 1 stackoverflow.com | awk '/Tracing/'
Tracing route to stackoverflow.com [127.0.0.1]

> ping -n 1 -w 1 stackoverflow.com | awk '/Pinging/'
Pinging stackoverflow.com [127.0.0.1] with 32 bytes of data:

# Using dig or nslookup would show a real IP rather than the forced IP from the hosts entry.
> dig @ns1 +nocmd stackoverflow.com +noall +answer
stackoverflow.com.      118     IN      A       151.101.129.69
stackoverflow.com.      118     IN      A       151.101.193.69
stackoverflow.com.      118     IN      A       151.101.1.69
stackoverflow.com.      118     IN      A       151.101.65.69

> echo $(ping -n 1 -w 1 stackoverflow.com | awk '/Pinging/')
Pinging stackoverflow.com 1 with 32 bytes of data:

> echo $(ping -n 1 -w 1 stackoverflow.com | awk '/Pinging/ {print $3}')
1

> ping -n 1 -w 1 stackoverflow.com | awk '/Pinging/ {print $3}'
[127.0.0.1]
BQ.
  • 103
  • 3
    I don't think it's anything to do with the [ test operator; [127.0.0.1] is a shell glob that's matching a file named 1 that happens to exist in the current directory (and would also match files named 0, 2, 7 if such existed) – steeldriver Sep 06 '18 at 23:27
  • 1
    I don't entirely understand the background section, but the first three commands work as expected to me. I never see an output of 1 in bash 4.4.23(1). I also agree that it's probably not the test operator, because you need a space after the [ to use that. Finally, I'm not sure if relevant, but you can always use printf '%s\n' $(...). – Sparhawk Sep 06 '18 at 23:34
  • 1
    @steeldriver Nice catch. That was exactly the issue. I'd attempted to redirect stderr to stdout for curl (so I could grep the --verbose output) months ago and created the file accidentally when I'd run it with 2>1 rather than 2>&1. If you re-post this as an answer, I'll mark it as accepted. Thank you! – BQ. Sep 06 '18 at 23:37
  • 2
    Having a file named 1 is the symptom; the cause is insufficient quoting. – Jeff Schaller Sep 06 '18 at 23:46
  • 1
    @BQ. my observation isn't really an answer - as @JeffSchaller points out, you should fix the root cause (i.e. make sure [...] isn't treated as a filename glob) rather than relying on there being no files that happen to match it – steeldriver Sep 06 '18 at 23:55

1 Answers1

3

The problem is that the command substitution is not itself quoted. When command substitution uses the output of the echo command as the argument for the second echo command, the command results are subjected to both word splitting and file globbing. As pointed out in the comments, the characters within square brackets, [127.0.0.1] will match any files whose name is exactly one character and is one of either 1, 2, 7, or 0 (the pattern would match . only the shell ignores both the . and .. directory links). When I created a file called 1 I could replicate your result.

To avoid the results of the command substitution being subjected to globbing, you should wrap it in double quotes:

echo "$(echo "This will show an IP address in square brackets: [127.0.0.1]")"

See also the following related articles from Greg‘s Wiki (particularly the first one which covers the need for double-quoting both within and outside the command substitution):

  • 1
    Note that unlike what the syntax highlighting on SE seems to imply, the "$(echo " part is not a single quoted string. The inside of a command substitution is a separate context from the outside, and the quotes don't mix between them. Both sets of quotes are needed here, since there are two places where the globbing can happen. – ilkkachu Sep 07 '18 at 00:28
  • Thanks @ilkkachu The syntax highlighting was confusing, alright but I figured out how to disable it: https://meta.stackexchange.com/questions/184108/what-is-syntax-highlighting-and-how-does-it-work I also emphasised the first related link which covers this issue. – Anthony Geoghegan Sep 07 '18 at 00:50