3

I am a bit new to the bash scripting. So please bear with me. I am trying to create a table and assign the values in for loop like this:

packages=("foo" "bar" "foobar")
packageMap=()
function test() {
    i=0;
    for package in "${packages[@]}"
    do
        echo $i
        packageMap[$package]=$i
        i=$(expr $i + 1)
    done
}

test
echo the first value is ${packageMap["foo"]}

The output for this is:

0
1
2
the first value is 2

While my expected output is:

0
1
2
the first value is 0

So basically the variable's reference is being assigned to this rather than the value. How to solve this?

My bash version:

GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)

TIA

3
  • 1
    BTW, i=$(( i + 1 )) is much more efficient than i=$(expr $i + 1), as $( ) needs to fork off a child process, and expr is implemented with an external command -- /bin/expr -- not part of the shell. The $(( )) form has been present in the POSIX sh standard since its initial publication in 1991, so there's also no concern about portability. Commented Jul 30, 2018 at 20:17
  • @CharlesDuffy : Apart of POSIX compatibility, is there an advantage of using i=$(( i + 1 )) over ((i++))? I find the latter easier to read and to type. Commented Jul 31, 2018 at 7:06
  • 1
    @user1934428, see BashFAQ #105 -- ((i++)) can (depending on the interpreter version) cause your script to exit when you're using set -e. ((++i)) is less prone to that issue (would only happen when i is initially negative). Commented Jul 31, 2018 at 11:11

2 Answers 2

6

bash 3.2 only has indexed arrays, so packageMap[$package] only works as intended if $package is an integer, not an arbitrary string.

(What you are observing is $package being evaluated in an arithmetic context, where foo, bar, and foobar are recursively expanded until you get an integer value. Undefined variables expand to 0, so packageMap[foo] is equivalent to packageMap[0].)

If you were using bash 4 or later, you could use an associative array:

packages=("foo" "bar" "foobar")
declare -A packageMap
test () {
    i=0
    for package in "${packages[@]}"
    do
        echo $i
        packageMap[$package]=$i
        i=$(($i + 1))
    done
}

Given that i is the same as the index of each element of packages, you could also write

test () {
    for i in "${!packages[@]}"; do
        package=${packages[i]}
        packageMap[$package]=$i
    done
}

instead of explicitly incrementing i.

Sign up to request clarification or add additional context in comments.

4 Comments

could you change i=$(($i+1)) to ((i++)) so that psykid learns the shortcut?
@jeremysprofile Better to suggest (( ++i )) so it doesn't trigger failures with set -e on first invocation (as the postincrement will evaluate to the initial value, and (( 0 )) is falsey). See the first exercise in BashFAQ #105 (granted, some versions of bash exempt arithmetic results from set -e behavior, but relying on that is creating a nonobvious version dependency).
@chepner, I tried to use this in bash 3.2 (before posting here), of course it didn't work. Unfortunately I cant install bash 4 since I don't have admin rights on that machine.
You don't need admin rights; you can install anything you want in your own home directory.
0

As chep says, there are no associative arrays in bash 3. That said, if you don't mind wasting a bit of CPU, you can use functions to similar effect:

#!/bin/bash

packages=("foo" "bar" "foobar")

function packagemap () {
    local i
    for i in "${!packages[@]}"; do
        [[ ${packages[$i]} = $1 ]] && echo "$i" && return
    done
    echo "unknown"
}

echo "the first value is $(packagemap "foo")"

The ${!array[@]} construct expands to the set of indices for the array, which for a normally non-associative array consist of incrementing integers starting at 0. But array members can be removed without the indices being renumbered (i.e. unset packages[1]), so it's important to be able to refer to actual indices rather than assuming they're sequential with for loop that simply counts.

And I note that you're using Darwin. Remember that you really need it, you can install bash 4 using Homebrew or MacPorts.

1 Comment

Yes, I dont mind using up a bit of cpu since this script is for triggering the build command for my gradle packages. Since I cant install bash 4 on the machine I am using, I can only go in this way. Thanks for the solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.