203

I'm trying to construct an array in bash of the filenames from my camera:

FILES=(2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg)

As you can see, there is a space in the middle of each filename.

I've tried wrapping each name in quotes, and escaping the space with a backslash, neither of which works.

When I try to access the array elements, it continues to treat the space as the element delimiter.

How can I properly capture the filenames with a space inside the name?

4
  • Have you tried adding the files the old-fashioned way? Like FILES[0] = ...? (Edit: I just did; doesn't work. Interesting). Commented Jan 31, 2012 at 17:43
  • POSIX: stackoverflow.com/questions/2936922/… Commented Apr 21, 2018 at 8:28
  • All of the answers here break down for me using Cygwin. It does weird things if there are spaces in file names, period. I work around it by creating an "array" in a text file listing of all elements I want to work with, and iterating over lines in the file: Formatting is mucking with intended backticks here surrounding the command in parenthesis: IFS=""; array=(find . -maxdepth 1 -type f -iname \*.$1 -printf '%f\n'); for element in ${array[@]}; do echo $element; done Commented May 1, 2020 at 4:20
  • Related: When to wrap quotes around a shell variable? Commented Sep 26, 2024 at 16:51

14 Answers 14

168

I think the issue might be partly with how you're accessing the elements. If I do a simple for elem in $FILES, I experience the same issue as you. However, if I access the array through its indices, like so, it works if I add the elements either numerically or with escapes:

for ((i = 0; i < ${#FILES[@]}; i++))
do
    echo "${FILES[$i]}"
done

Any of these declarations of $FILES should work:

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

or

FILES=("2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg")

or

FILES[0]="2011-09-04 21.43.02.jpg"
FILES[1]="2011-09-05 10.23.14.jpg"
FILES[2]="2011-09-09 12.31.16.jpg"
FILES[3]="2011-09-11 08.43.12.jpg"
Sign up to request clarification or add additional context in comments.

7 Comments

Note that you should use double-quotes when you use the array elements (e.g. echo "${FILES[$i]}"). It doesn't matter for echo, but it will for anything that uses it as a filename.
It's not necessary to loop over the indexes when you can loop over the elements with for f in "${FILES[@]}".
@MarkEdgar i experiencing problems with for f in ${FILES[@]} when the array members have spaces. It seems that the whole array is reinterpreted again, with the spaces spitting your existing members into two or more elements. It seems the " " are very important
Whats does the sharp (#) symbol do in for ((i = 0; i < ${#FILES[@]}; i++)) statement?
I answered this six years ago but I believe it's to get the count of the number of elements in the array FILES.
|
134

There must be something wrong with the way you access the array's items. Here's how it's done:

for elem in "${files[@]}"
...

From the bash manpage:

Any element of an array may be referenced using ${name[subscript]}. ... If subscript is @ or *, the word expands to all members of name. These subscripts differ only when the word appears within double quotes. If the word is double-quoted, ${name[*]} expands to a single word with the value of each array member separated by the first character of the IFS special variable, and ${name[@]} expands each element of name to a separate word.

Of course, you should also use double quotes when accessing a single member

cp "${files[0]}" /tmp

6 Comments

Cleanest, most elegant solution in this bunch, though should re-iterate that each element defined in the array should be quoted.
While Dan Fego's answer is effective, this is the more idiomatic way to handle spaces in the elements.
Coming from other programming languages, the terminology from that excerpt is really hard to understand. Plus the syntax is baffling. I'd be extremely grateful if you could go into it a bit more? Particularly expands to a single word with the value of each array member separated by the first character of the IFS special variable
Yes, agree the double quotes are solving it and this is better than other solutions. To further explain - most others are just lacking the double quotes. You got the correct: for elem in "${files[@]}", while they have for elem in ${files[@]} - so the spaces confuse the expansion and for tries running on the individual words.
This does not work for me in macOS 10.14.4, which uses "GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin18)". Maybe a bug in the older version of bash?
|
61

You need to use IFS to stop space as element delimiter.

FILES=("2011-09-04 21.43.02.jpg"
       "2011-09-05 10.23.14.jpg"
       "2011-09-09 12.31.16.jpg"
       "2011-09-11 08.43.12.jpg")
IFS=""
for jpg in ${FILES[*]}
do
    echo "${jpg}"
done

If you want to separate on basis of . then just do IFS="." Hope it helps you:)

7 Comments

I had to move the IFS="" to before the array assignment but this is the correct answer.
I am using several arrays to parse info and I shall have the effect of IFS="" working in only one of them. Once I use IFS="" all other arrays stop parsing accordingly. Any hints about this?
Paulo, see another answer here which may be better for your case: stackoverflow.com/a/9089186/1041319. Have not tried IFS="", and seems it does solve it elegantly - but your example shows why one may encounter issues in some cases. It may be possible to set the IFS="" on a single line, but it may still be more confusing than the other solution.
It also worked for me on bash. Thanks @Khushneet I was searching it for half an hour...
Great, only answer on this page that worked. But I also had to move the IFS="" before the array construction.
|
17

I agree with others that it's likely how you're accessing the elements that is the problem. Quoting the file names in the array assignment is correct:

FILES=(
  "2011-09-04 21.43.02.jpg"
  "2011-09-05 10.23.14.jpg"
  "2011-09-09 12.31.16.jpg"
  "2011-09-11 08.43.12.jpg"
)

for f in "${FILES[@]}"
do
  echo "$f"
done

Using double quotes around any array of the form "${FILES[@]}" splits the array into one word per array element. It doesn't do any word-splitting beyond that.

Using "${FILES[*]}" also has a special meaning, but it joins the array elements with the first character of $IFS, resulting in one word, which is probably not what you want.

Using a bare ${array[@]} or ${array[*]} subjects the result of that expansion to further word-splitting, so you'll end up with words split on spaces (and anything else in $IFS) instead of one word per array element.

Using a C-style for loop is also fine and avoids worrying about word-splitting if you're not clear on it:

for (( i = 0; i < ${#FILES[@]}; i++ ))
do
  echo "${FILES[$i]}"
done

Comments

17

This was already answered above, but that answer was a bit terse and the man page excerpt is a bit cryptic. I wanted to provide a fully worked example to demonstrate how this works in practice.

If not quoted, an array just expands to strings separated by spaces, so that

for file in ${FILES[@]}; do

expands to

for file in 2011-09-04 21.43.02.jpg 2011-09-05 10.23.14.jpg 2011-09-09 12.31.16.jpg 2011-09-11 08.43.12.jpg ; do

But if you quote the expansion, bash adds double quotes around each term, so that:

for file in "${FILES[@]}"; do

expands to

for file in "2011-09-04 21.43.02.jpg" "2011-09-05 10.23.14.jpg" "2011-09-09 12.31.16.jpg" "2011-09-11 08.43.12.jpg" ; do

The simple rule of thumb is to always use [@] instead of [*] and quote array expansions if you want spaces preserved.

To elaborate on this a little further, the man page in the other answer is explaining that if unquoted, $* an $@ behave the same way, but they are different when quoted. So, given

array=(a b c)

Then $* and $@ both expand to

a b c

and "$*" expands to

"a b c"

and "$@" expands to

"a" "b" "c"

Comments

10

If you had your array like this: #!/bin/bash

Unix[0]='Debian'
Unix[1]="Red Hat"
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${Unix[@]});
    do echo $i;
done

You would get:

Debian
Red
Hat
Ubuntu
Suse

I don't know why but the loop breaks down the spaces and puts them as an individual item, even you surround it with quotes.

To get around this, instead of calling the elements in the array, you call the indexes, which takes the full string thats wrapped in quotes. It must be wrapped in quotes!

#!/bin/bash

Unix[0]='Debian'
Unix[1]='Red Hat'
Unix[2]='Ubuntu'
Unix[3]='Suse'

for i in $(echo ${!Unix[@]});
    do echo ${Unix[$i]};
done

Then you'll get:

Debian
Red Hat
Ubuntu
Suse

Comments

8

For those who prefer set array in oneline mode, instead of using for loop

Changing IFS temporarily to new line could save you from escaping.

OLD_IFS="$IFS"
IFS=$'\n'

array=( $(ls *.jpg) )  #save the hassle to construct filename

IFS="$OLD_IFS"

Comments

4

Not exactly an answer to the quoting/escaping problem of the original question but probably something that would actually have been more useful for the op:

unset FILES
for f in 2011-*.jpg; do FILES+=("$f"); done
echo "${FILES[@]}"

Where of course the expression would have to be adopted to the specific requirement (e.g. *.jpg for all or 2001-09-11*.jpg for only the pictures of a certain day).

Comments

4
#! /bin/bash

renditions=(
"640x360    80k     60k"
"1280x720   320k    128k"
"1280x720   320k    128k"
)

for z in "${renditions[@]}"; do
    echo "$z"
    
done

OUTPUT

640x360 80k 60k

1280x720 320k 128k

1280x720 320k 128k

`

2 Comments

Is this answer different/better from the ones already given?
yes, as you can see the output, each element inside renditions array is a string with spaces, and we loop it through without quotes around ${renditions[@]} then space will be treated as element delimiter, so here I am wrapping double quotes around ${renditions[@]}, which gives me the above output.
2

Escaping works.

#!/bin/bash

FILES=(2011-09-04\ 21.43.02.jpg
2011-09-05\ 10.23.14.jpg
2011-09-09\ 12.31.16.jpg
2011-09-11\ 08.43.12.jpg)

echo ${FILES[0]}
echo ${FILES[1]}
echo ${FILES[2]}
echo ${FILES[3]}

Output:

$ ./test.sh
2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg

Quoting the strings also produces the same output.

Comments

2

If the elements of FILES come from another file whose file names are line-separated like this:

2011-09-04 21.43.02.jpg
2011-09-05 10.23.14.jpg
2011-09-09 12.31.16.jpg
2011-09-11 08.43.12.jpg

then try this so that the whitespaces in the file names aren't regarded as delimiters:

while read -r line; do
    FILES+=("$line")
done < ./files.txt

If they come from another command, you need to rewrite the last line like this:

while read -r line; do
    FILES+=("$line")
done < <(./output-files.sh)

Comments

0

Another solution is using a "while" loop instead a "for" loop:

index=0
while [ ${index} -lt ${#Array[@]} ]
  do
     echo ${Array[${index}]}
     index=$(( $index + 1 ))
  done

Comments

0

If you aren't stuck on using bash, different handling of spaces in file names is one of the benefits of the fish shell. Consider a directory which contains two files: "a b.txt" and "b c.txt". Here's a reasonable guess at processing a list of files generated from another command with bash, but it fails due to spaces in file names you experienced:

# bash
$ for f in $(ls *.txt); { echo $f; }
a
b.txt
b
c.txt

With fish, the syntax is nearly identical, but the result is what you'd expect:

# fish
for f in (ls *.txt); echo $f; end
a b.txt
b c.txt

It works differently because fish splits the output of commands on newlines, not spaces.

If you have a case where you do want to split on spaces instead of newlines, fish has a very readable syntax for that:

for f in (ls *.txt | string split " "); echo $f; end

Comments

-1

I used to reset the IFS value and rollback when done.

# backup IFS value
O_IFS=$IFS

# reset IFS value
IFS=""

FILES=(
"2011-09-04 21.43.02.jpg"
"2011-09-05 10.23.14.jpg"
"2011-09-09 12.31.16.jpg"
"2011-09-11 08.43.12.jpg"
)

for file in ${FILES[@]}; do
    echo ${file}
done

# rollback IFS value
IFS=${O_IFS}

Possible output from the loop:

2011-09-04 21.43.02.jpg

2011-09-05 10.23.14.jpg

2011-09-09 12.31.16.jpg

2011-09-11 08.43.12.jpg

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.