0

This bash script

i=0
var='bonjour ? hello' 
for v in $var
do ((i++))
echo -e "$i)$v"
done

will output:

1)bonjour
2)-
3)hello

not the expected:

1)bonjour
2)?
3)hello

Why ?

programmer
  • 41
  • 6
  • 3
    `for v in $var` expands each word in `$var` as a glob. `?` is a glob that matches any filename with only one character. – Charles Duffy Nov 05 '21 at 17:05
  • 3
    @axiac, you can reproduce it if you run `touch -` first. – Charles Duffy Nov 05 '21 at 17:05
  • 2
    BTW, `echo -e` is best avoided altogether. _Certainly_ don't use it if you don't have a specific reason to believe the `-e` will be useful (and even if you _do_ have a specific reason to want `echo -e ...`, you're better off using `printf '%b\n' ...`, as described by [the POSIX standard for `echo`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html), a standard that doesn't allow a compliant `echo -e` to print anything but the exact string`-e` on output -- bash's implementation is _sometimes_ compliant, depending on the runtime flags that are active at any given time). – Charles Duffy Nov 05 '21 at 17:09
  • 1
    For a more in-depth discussion, see [Why is printf better than echo?](https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo) over at [unix.se]. – Charles Duffy Nov 05 '21 at 17:11
  • Is there any way to prevent "for" from expanding the variable. (If I use quotes around $var then for will not parse the "$var" in different words and treat it only as a single word) – programmer Nov 05 '21 at 17:18
  • Only by completely turning off glob expansion (which then needs to be turned back on later for your shell to operate normally). Better to avoid the problem -- _really_, use an array. (If you have a string as input, `read -a` can turn that string into an array for you). – Charles Duffy Nov 05 '21 at 17:19
  • what was the rational of this strange behavior: is there a useful purpose in applying glob to "for" ? – programmer Nov 05 '21 at 17:24
  • It's not specific to `for`. **All** unquoted expansions undergo string-splitting and globbing. All of them (except where in a specific context where it's suppressed, like the right-hand side of an assignment). There would need to be a good reason for something to _not_ undergo it. – Charles Duffy Nov 05 '21 at 17:26
  • The rationale _for bash_ is that the POSIX sh standard mandates it; bash has no choice if it wants to be a standards-compliant shell. – Charles Duffy Nov 05 '21 at 17:27
  • Thanks for this clear explanation – programmer Nov 05 '21 at 17:27
  • The rationale _for POSIX_ is that historical shells did it -- and there are reasons one could justify that historical behavior; `files='*.gif *.jpeg'; for file in $files; do ...` f/e – Charles Duffy Nov 05 '21 at 17:28
  • ...is it a bad decision in hindsight? Definitely. But if a shell goes and fixes all the standard's bad decisions, it ends up like zsh -- subtly incompatible with everyone else, and worse, where people writing code to be compatible with that shell end up in habits that make their code wildly buggy when they try to write code for any shell that hasn't made the same "corrections" to standard-mandated behavior. – Charles Duffy Nov 05 '21 at 17:29
  • In this example it seems my variable is always quoted. Where isn't it? i=0;var='bonjour ? hello' ;eval ay=("$var");for v in "${ay[@]}"; do ((i++)); echo -e $i"$v"; done – programmer Nov 05 '21 at 17:43
  • 1
    `eval` creates a second evaluation pass without your syntactic quotes. It's seriously bad juju; don't ever use it. – Charles Duffy Nov 05 '21 at 17:57
  • 1
    To explain what I mean by the above: When `eval ay=("$var")` is parsed, the quotes around `$var` are consumed _at that parse time_ to mean that the contents of `$var` shouldn't undergo string-splitting and glob expansion at that time (at the parse _before_ `eval` is actually run). So what's then _actually passed_ to the `eval` command is the exact string `ay=(bonjour ? hello)` -- the quotes were consumed before `eval` started so they're gone by the time the assignment actually runs. – Charles Duffy Nov 05 '21 at 18:00
  • See also [BashFAQ #48](https://mywiki.wooledge.org/BashFAQ/048) for more re: the general "don't ever use eval" assertion. – Charles Duffy Nov 05 '21 at 18:01

1 Answers1

3

This happens because you're expanding $var unquoted, which replaces ? with a list of single-character filenames in the current directory. You evidently ran this code in a directory containing a file named -.


Lists of things should only ever be stored in arrays, not strings.

This lets you quote the expansion to suppress globbing and word-splitting, while still iterating over list elements:

#!/usr/bin/env bash
i=0
varArray=( 'bonjour' '?' 'hello' ) 
for v in "${varArray[@]}"; do
  ((i++))
  echo "$i)$v"
done

If your input comes in as a string, you can use read -a to convert it into an array without performing glob expansion:

#!/usr/bin/env bash
i=0
var='bonjour ? hello'
read -r -a varArray <<<"$var"
for v in "${varArray[@]}"; do
  ((i++))
  echo "$i)$v"
done
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441