189

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?

Benjamin W.
  • 46,058
  • 19
  • 106
  • 116
abelenky
  • 63,815
  • 23
  • 109
  • 159
  • Have you tried adding the files the old-fashioned way? Like `FILES[0] = ...`? (Edit: I just did; doesn't work. Interesting). – Dan Fego Jan 31 '12 at 17:43
  • POSIX: https://stackoverflow.com/questions/2936922/posix-sh-build-loop-variable-with-elements-containing-spaces – Ciro Santilli OurBigBook.com Apr 21 '18 at 08: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 May 01 '20 at 04:20

14 Answers14

160

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"
Dan Fego
  • 13,644
  • 6
  • 48
  • 59
  • 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. – Gordon Davisson Jan 31 '12 at 19:35
  • 34
    It's not necessary to loop over the indexes when you can loop over the elements with `for f in "${FILES[@]}"`. – Mark Edgar Feb 01 '12 at 02: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 – Michael Shaw Sep 07 '16 at 12:24
  • 1
    Whats does the sharp (`#`) symbol do in `for ((i = 0; i < ${#FILES[@]}; i++))` statement? – Michal Vician Dec 11 '18 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 Dec 11 '18 at 18:08
  • `${FILES[@]}` returns the contents of the array. `${#FILES[@]}` returns the count of elements in the array. (self-contained answer). – mcint Jul 08 '19 at 19:46
  • bash array index is 1-based – zhy2002 Mar 11 '22 at 03:24
122

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
user123444555621
  • 148,182
  • 27
  • 114
  • 126
  • 3
    Cleanest, most elegant solution in this bunch, though should re-iterate that each element defined in the array should be quoted. – maverick Oct 24 '12 at 04:01
  • While Dan Fego's answer is effective, this is the more idiomatic way to handle spaces in the elements. – Daniel Zhang Jul 11 '16 at 04: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 Oct 06 '16 at 09: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 May 26 '17 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 Apr 17 '19 at 01:47
  • IFS=$'\n' IMPORTANT - IFS= Internal Field Separator, changes break point for arrays!!! no break on spaces – user2718593 Apr 16 '21 at 17:36
59

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

Khushneet
  • 854
  • 9
  • 10
  • 5
    I had to move the IFS="" to before the array assignment but this is the correct answer. – rob Apr 02 '15 at 07: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? – Paulo Pedroso Oct 09 '15 at 14:37
  • Paulo, see another answer here which may be better for your case: https://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 May 26 '17 at 18:05
  • It also worked for me on bash. Thanks @Khushneet I was searching it for half an hour... – csonuryilmaz Oct 21 '17 at 19:42
  • Great, only answer on this page that worked. But I also had to move the `IFS=""` **before the array construction**. – pkamb Oct 24 '18 at 05:36
  • What is this Satanic black magic?! (Apparently it is [this](https://unix.stackexchange.com/a/184867)) – Code Commander Aug 16 '23 at 22:50
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
Dean Hall
  • 627
  • 6
  • 6
11

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"
Alcamtar
  • 1,478
  • 13
  • 19
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
Jonni2016aa
  • 144
  • 1
  • 3
4

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"
ychz
  • 533
  • 2
  • 7
  • 15
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).

TNT
  • 3,392
  • 1
  • 24
  • 27
2
#! /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

`

  • Is this answer different/better from the ones already given? – SiKing Aug 12 '21 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. – subham prasad Aug 16 '21 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.

Chris Seymour
  • 83,387
  • 30
  • 160
  • 202
1

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)
Manabu Nakazawa
  • 1,983
  • 22
  • 23
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
Javier Salas
  • 1,064
  • 5
  • 15
  • 38
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
Mark Stosberg
  • 12,961
  • 6
  • 44
  • 49
-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

Community
  • 1
  • 1
Madan Sapkota
  • 25,047
  • 11
  • 113
  • 117