267

I want to do something like this:

foo=( )
foo[0]="bar"
foo[35]="baz"
for((i=0;i<${#foo[@]};i++))
do
    echo "$i: ${foo[$i]}"
done
# Output:
# 0: bar
# 1: 

Then i tried to loop through it using for in:

foo=( )
foo[0]="bar"
foo[35]="baz"
for i in ${foo[@]}
do
    echo "?: $i"
done
# Output:
# ?: bar
# ?: naz

but here I don't know the index value.

I know you could something like

foo=( )
foo[0]="bar"
foo[35]="baz"
declare -p foo
# Output:
# declare -a foo='([0]="bar" [35]="baz")'

but, can't you do it in another way?

Mat
  • 202,337
  • 40
  • 393
  • 406
Tyilo
  • 28,998
  • 40
  • 113
  • 198

6 Answers6

460

You would find the array keys with "${!foo[@]}" (reference), so:

for i in "${!foo[@]}"; do 
  printf "%s\t%s\n" "$i" "${foo[$i]}"
done

Which means that indices will be in $i while the elements themselves have to be accessed via ${foo[$i]}

Community
  • 1
  • 1
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
  • 7
    Important note, while iterable, a space separated list of words is _not_ an array. Wrap it like so `(a b c)` to convert it to an array. – Breedly Aug 19 '16 at 18:59
  • 5
    The use of `[@]` and double quotes means it's not a "space separated list of words". You get the list of actual array keys, even if the individual keys contain whitespace. – glenn jackman Aug 21 '16 at 01:21
  • @glennjackman can you explain this more `The use of [@] and double quotes means it's not a "space separated list of words"` – Kasun Siyambalapitiya Dec 02 '16 at 10:27
  • 1
    `"${foo[@]}"` takes the (array) variable `foo` and expands it as an array, maintaining the identity of its elements, i.e., not splitting them on whitespace. If `foo=(x 'y z')`, then `f "${foo[@]}"` calls `f` with two arguments, `x` and `'y z'`. Metadata queries like `"${!foo[@]}"` and `"${#foo[@]}"` similarly act on `foo` as an array. – BallpointBen Sep 05 '18 at 01:23
  • Right answer, but that [reference](http://www.gnu.org/software/bash/manual/bashref.html#Shell-Parameter-Expansion) is inscrutable. [This answer](https://stackoverflow.com/a/43979315/1230197) helpfully just explains: "`"${!foo[@]}"` is the list of all the indexes set in the array". – pjhsea Feb 18 '19 at 00:31
  • Does this also work if the array is `$@`, i.e. the argument array passed to any function or script? – Johannes Sep 09 '19 at 05:58
  • Did you try it? What did you see? – glenn jackman Sep 09 '19 at 10:24
  • @glennjackman `echo "${!@}"` just prints nothing. `echo "${![@]}"` says "${![@]}: bad substitution". `echo "$!@"` prints `@`. I don't know what else to try. – Johannes Sep 11 '19 at 09:18
  • So that's your answee: you need to use `for ((i=1; i <= $#; i++)) ; do echo "${!i}" ;done` to use the positional parameter indices. Or simply `for param; do echo "$param" ; done` – glenn jackman Sep 11 '19 at 10:35
  • What if I have a function and want to print both index and value in an array. Since, my function is generic, I can not use array name. How to solve this case? Thanks – Jaraws Nov 11 '20 at 08:22
  • Please ask a new question – glenn jackman Nov 11 '20 at 11:43
  • This works better than doing a for loop against ${arrray[@]} on its own as I am finding this splits up strings contained in the array – John Sohn Oct 06 '21 at 16:56
  • @JohnSohn, you need to quote the array expansion for just this reason: `for item in "${array[@]}"` -- and then quote `"$item"` where you use it. – glenn jackman Oct 06 '21 at 17:12
  • @glennjackman I really really hate the bash shell lol Thank you. – John Sohn Oct 06 '21 at 21:48
38

you can always use iteration param:

ITER=0
for I in ${FOO[@]}
do  
    echo ${I} ${ITER}
    ITER=$(expr $ITER + 1)
done
aymericbeaumet
  • 6,853
  • 2
  • 37
  • 50
Eyal Ch
  • 9,552
  • 5
  • 44
  • 54
  • 13
    `((ITER++))` in modern bash – David Tonhofer Jan 18 '18 at 12:11
  • Why post-increment ? You only want the value incremented, hence ((++ITER)) is more directly a statement of what you want done .... – MikeW Feb 20 '18 at 15:14
  • 4
    No, not "ALWAYS". A hash can have "holes", which means not all numbers are an index. In your example the ${ITER} is not always the index of ${I}. – Marco Sep 04 '18 at 09:00
28
INDEX=0
for i in $list; do 
    echo ${INDEX}_$i
    let INDEX=${INDEX}+1
done
Aruy Aruy
  • 494
  • 5
  • 7
  • 1
    I liked the conciseness of the answer and strategy. For anyone wondering this is bash and the strat is you just keep track of the array index on your own. – Llama D'Attore Oct 11 '20 at 00:25
11

Simple one line trick for dumping array

I've added one value with spaces:

foo=([12]="bar" [42]="foo bar baz" [35]="baz")

For a quick dump of arrays or associative arrays I use

This one line command:

paste <(printf "%s\n" "${!foo[@]}") <(printf "%s\n" "${foo[@]}")

Will render:

12  bar
35  baz
42  foo bar baz

Explained

  • printf "%s\n" "${!foo[@]}" will print all keys separated by a newline,
  • printf "%s\n" "${foo[@]}" will print all values separated by a newline,
  • paste <(cmd1) <(cmd2) will merge output of cmd1 and cmd2 line by line.

Tunning

This could be tunned by -d switch:

paste -d : <(printf "%s\n" "${!foo[@]}") <(printf "%s\n" "${foo[@]}")
12:bar
35:baz
42:foo bar baz

or even:

paste -d = <(printf "foo[%s]\n" "${!foo[@]}") <(printf "'%s'\n" "${foo[@]}")
foo[12]='bar'
foo[35]='baz'
foo[42]='foo bar baz'

Associative array will work same:

declare -A bar=([foo]=snoopy [bar]=nice [baz]=cool [foo bar]='Hello world!')
paste -d = <(printf "bar[%s]\n" "${!bar[@]}") <(printf '"%s"\n' "${bar[@]}")
bar[foo bar]="Hello world!"
bar[foo]="snoopy"
bar[bar]="nice"
bar[baz]="cool"

Issue with newlines or special chars

Unfortunely, there is at least one condition making this not work anymore: when variable do contain newline:

foo[17]=$'There is one\nnewline'

Command paste will merge line-by-line, so output will become wrong:

paste -d = <(printf "foo[%s]\n" "${!foo[@]}") <(printf "'%s'\n" "${foo[@]}")
foo[12]='bar'
foo[17]='There is one
foo[35]=newline'
foo[42]='baz'
='foo bar baz'

For this work, you could use %q instead of %s in second printf command (and whipe quoting):

paste -d = <(printf "foo[%s]\n" "${!foo[@]}") <(printf "%q\n" "${foo[@]}")

Will render perfect ( and reusable! ):

foo[12]=bar
foo[17]=$'There is one\nnewline'
foo[35]=baz
foo[42]=foo\ bar\ baz

From man bash:

          %q     causes  printf  to output the corresponding argument in a
                 format that can be reused as shell input.

By using a function:

dumpArray() {
    local -n _ary=$1
    local _idx
    local -i _idlen=0
    for _idx in "${!_ary[@]}"; do
        _idlen=" ${#_idx} >_idlen ? ${#_idx} : _idlen "
    done
    for _idx in "${!_ary[@]}"; do
        printf "%-*s: %s\n" "$_idlen" "$_idx" \
            "${_ary["$_idx"]//$'\n'/$'\n\e['${_idlen}C  }"
    done
}

Then now:

dumpArray foo
12: bar
17: There is one
    newline
35: baz
42: foo bar baz

dumpArray bar
foo    : snoopy
bar    : nice
baz    : cool
foo bar: Hello world!

About UTF-8 format output

From UTF-8 string length, adding:

strU8DiffLen() { local chLen=${#1} LANG=C LC_ALL=C;return $((${#1}-chLen));}

Then

dumpArray() {
    local -n _ary=$1
    local _idx
    local -i _idlen=0
    for _idx in "${!_ary[@]}"; do
        _idlen=" ${#_idx} >_idlen ? ${#_idx} : _idlen "
    done
    for _idx in "${!_ary[@]}"; do
        strU8DiffLen "$_idx"
        printf "%-*s: %s\n" $(($?+$_idlen)) "$_idx" \
            "${_ary["$_idx"]//$'\n'/$'\n\e['${_idlen}C  }"
    done
}

Demo:

foo=([12]="bar" [42]="foo bar baz" [35]="baz")
declare -A bar=([foo]=snoopy [bar]=nice [baz]=cool [foo bar]='Hello world!')

foo[17]=$'There is one\nnewline'
LANG=fr.UTF-8 printf -v bar[déjà]  $'%(%a %d %b\n%Y\n%T)T' -1

dumpArray bar
déjà   : ven 24 déc
         2021
         08:36:05
foo    : snoopy
bar    : nice
baz    : cool
foo bar: Hello world!

dumpArray foo
12: bar
17: There is one
    newline
35: baz
42: foo bar baz
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
7

In bash 4, you can use associative arrays:

declare -A foo
foo[0]="bar"
foo[35]="baz"

# for Zsh, change this to: for key in "${(k)foo[@]}"
for key in "${!foo[@]}"
do
    echo "key: $key, value: ${foo[$key]}"
done

# output
# $ key: 0, value bar.
# $ key: 35, value baz.

In bash 3, this works (also works in zsh):

map=( )
map+=("0:bar")
map+=("35:baz")

for keyvalue in "${map[@]}" ; do
    key=${keyvalue%%:*}
    value=${keyvalue#*:}
    echo "key: $key, value $value."
done
mattmc3
  • 17,595
  • 7
  • 83
  • 103
  • trying the bash 3 version (in zsh), it echoes out the `value` for both key and value.. – cannyboy Dec 30 '21 at 20:49
  • 1
    I verified the second version works fine for me in Zsh 5.8, however, the first version is preferable if you're using Zsh. Replace `for key in "${!foo[@]}"` with `for key in "${(k)foo[@]}"` for Zsh. – mattmc3 Dec 30 '21 at 23:17
1
users=("kamal" "jamal" "rahim" "karim" "sadia")
index=()
t=-1

for i in ${users[@]}; do
  t=$(( t + 1 ))
  if [ $t -eq 0 ]; then
    for j in ${!users[@]}; do
      index[$j]=$j
    done
  fi
  echo "${index[$t]} is $i"
done
code4mk
  • 85
  • 3