197

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 elementdelimiter.

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

3
  • Have you tried adding the files the old-fashioned way? Like FILES[0] = ...? (Edit: I just did; doesn't work. Interesting).
    – Dan Fego
    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
    – Alex Hall
    Commented May 1, 2020 at 4:20

14 Answers 14

163

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"
7
  • 10
    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. Commented Jan 31, 2012 at 19:35
  • 36
    It's not necessary to loop over the indexes when you can loop over the elements with for f in "${FILES[@]}".
    – Mark Edgar
    Commented Feb 1, 2012 at 2:44
  • 12
    @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 Commented Sep 7, 2016 at 12:24
  • 1
    Whats does the sharp (#) symbol do in for ((i = 0; i < ${#FILES[@]}; i++)) statement? Commented Dec 11, 2018 at 13:28
  • 4
    I answered this six years ago but I believe it's to get the count of the number of elements in the array FILES.
    – Dan Fego
    Commented Dec 11, 2018 at 18:08
128

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
  • 3
    Cleanest, most elegant solution in this bunch, though should re-iterate that each element defined in the array should be quoted.
    – maverick
    Commented Oct 24, 2012 at 4:01
  • While Dan Fego's answer is effective, this is the more idiomatic way to handle spaces in the elements. Commented Jul 11, 2016 at 4:38
  • 3
    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
    – Jodes
    Commented Oct 6, 2016 at 9:52
  • 2
    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.
    – arntg
    Commented May 26, 2017 at 17:55
  • 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?
    – Mark
    Commented Apr 17, 2019 at 1:47
60

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
  • 5
    I had to move the IFS="" to before the array assignment but this is the correct answer.
    – rob
    Commented Apr 2, 2015 at 7:52
  • 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? Commented Oct 9, 2015 at 14:37
  • 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.
    – arntg
    Commented May 26, 2017 at 18:05
  • It also worked for me on bash. Thanks @Khushneet I was searching it for half an hour... Commented Oct 21, 2017 at 19:42
  • Great, only answer on this page that worked. But I also had to move the IFS="" before the array construction.
    – pkamb
    Commented Oct 24, 2018 at 5:36
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
0
14

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"
9

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
6

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"
3

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).

3
#! /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
  • Is this answer different/better from the ones already given?
    – SiKing
    Commented Aug 12, 2021 at 22:36
  • 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. Commented Aug 16, 2021 at 15:38
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.

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)
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
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
-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

Not the answer you're looking for? Browse other questions tagged or ask your own question.