0

The problem is as follows (Here I don't use find since it doesn't support double-asterisk wildcard **):

$ FILES=(foo/**/*.suffix bar/**/*.suffix2)
$ grep baz "${FILES[@]}" # works
# I use this to create one local var with local in one function
$ SUBFILES="${FILES[@]}"
$ grep baz "${SUBFILES[@]}" # doesn't work

I used od to check them but they are same at least have the same number of "\n" and the same length (I didn't check character by character. But at a glance they are same).

$ echo ${SUBFILES[@]} | od -c
$ echo ${FILES[@]} | od -c

Why does the variable assignment not create one object still work for grep?

5
  • Can you please confirm that you are using the bash shell and not zsh?
    – Kusalananda
    Commented Jan 2 at 8:54
  • @Kusalananda Sorry. I have fixed that.
    – An5Drama
    Commented Jan 2 at 8:56
  • 1
    echo $anything (without quotes) is almost always a bug that should have been written echo "$anything". The 2 statements are not equivalent, see mywiki.wooledge.org/Quotes.
    – Ed Morton
    Commented Jan 4 at 14:00
  • 1
    @EdMorton Yes. Sorry for my lacking rigorousness. I used double-quote somewhere while not elsewhere. Here double-quote is needed if some filename contain "whitespace or wildcard characters" as the well-known wiki says.
    – An5Drama
    Commented Jan 5 at 5:02
  • 1
    Regarding "Here I don't use find since it doesn't support double-asterisk wildcard **" - find doesn't need to support double-asterisk wildcard to look in sub-directories since it does that by default. By the way, you might want to add shopt -s nullglob to the top of your script so FILES doesn't literally contain foo/**/*.suffix if/when there are no .suffix files under foo.
    – Ed Morton
    Commented Jan 26 at 13:29

2 Answers 2

6

You're right, b="${a[@]}" doesn't create an array in Bash. Without the parenthesis, that's a scalar assignment, regardless of what there is on the right-hand side. So even though you used the @ indexing to ask for all the array items separately the assignment forces them to a single string. Meaning b="${a[@]}" is quite like b="${a[*]}" except that it always uses a space as a joiner, instead of the first character of $IFS.

E.g.

$ a=("foo bar" doo);
$ IFS=:;
$ b="${a[@]}"
$ c="${a[*]}"
$ typeset -p b c
declare -- b="foo bar doo"
declare -- c="foo bar:doo"

To get an array, you need to add the parenthesis, i.e. b=( "${a[@]}" ) would copy a simple array like the one you have. I say "simple", since array indexes can be non-contiguous, and the expansion there would lose the indexes, with the assignment creating new, contiguous ones starting from 0.

$ d[5]=xxx d[8]=yyzz
$ e=( "${d[@]}" )
$ typeset -p d e
declare -a d='([5]="xxx" [8]="yyzz")'
declare -a e='([0]="xxx" [1]="yyzz")'

If you want a copy that keeps the indexes, it's likely safest to just use a loop. In general, variables also have attributes like the export and integer attributes, and you'll have to re-set them manually too. There's no simple and straightforward way to copy a variable in full.


There is the "${var@A}" expansion in newer versions of Bash that gives the declare command to recreate the variable, but it contains the original name, so can't immediately be used for copying. I suppose you could do something like this to copy d into f...

$ d[5]=xxx d[8]=yyzz
$ d_repr="${d[@]@A}"
$ eval "declare -a f=${d_repr#declare -a d=}"
$ typeset -p d f
declare -a d=([5]="xxx" [8]="yyzz")
declare -a f=([5]="xxx" [8]="yyzz")

...but I would be wary of doing that in real life and in any case, that requires taking into account the attributes of the original variable. E.g. an integer array g would print as declare -ai g=....


Note that echo joins the arguments it gets with spaces, so echo ${a[@]} or echo "${a[@]}" hides where the separation between the array items actually is. And when the scalar assignment joins with spaces, and echo joins with spaces, the result is that the output is the same in both these cases.

That makes echo really bad for looking into things like this. It's better to use e.g. printf here, e.g. this makes it clearer that b is just a single element.

$ unset a b
$ a=("foo bar" doo)
$ b="${a[@]}"
$ printf "<%s>\n" "${a[@]}"
<foo bar>
<doo>
$ printf "<%s>\n" "${b[@]}"
<foo bar doo>

Or use typeset -p / declare -p.


Even in zsh, where $a expands it as an array (with distinct items separated), b=$a still joins to a string, so you need to know what type of a variable you're handling. (b=$a[@] is the same, and zsh uses the first character of IFS for both @ and *, a minor difference from Bash.)

2
  • 2
    Note that in zsh b=$a and b=$a[@] like b=$a[*] or b="${a[*]}" joins the elements with the first character of $IFS, not space (unless $IFS starts with space of course, which it does by default). Commented Jan 2 at 17:00
  • Such one good explanation! Thanks. References for future readers: 1. "scalar assignment" is shown in zsh doc man ZSHPARAM (related contents in info bash is not found.) "In scalar assignment, value is expanded as a single string, in which the elements of arrays are joined together". 2. "contiguous ones starting from 0" is also shown in info bash "the index of the element assigned is the last index assigned to by the statement plus one. Indexing starts at zero.". That is not same as zsh.
    – An5Drama
    Commented Jan 3 at 11:59
2

Array assignments in bash are written

array=( elements )

In your case,

SUBFILES=( "${FILES[@]}" )

This would assign all elements of the FILES array to the array SUBFILES.

With SUBFILES="${FILES[@]}" you assign a single string to the variable SUBFILES. This string will comprise the elements of FILES separated by single spaces.

See also Are there naming conventions for variables in shell scripts?

1

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.