1

I'm new to shell. I want to create a hashmap-style array.

I already took a look at this How to define hash tables in Bash?

It's not exactly what I want. This is what I expect:

myArray=(emma=paris, london, ny john=tokyo, LA)

#names are my keys and cities are my values

My actual code is like:

 declare -A myArray=( ["$names"]="$cities" )

It doesn't work because each new city overwrites the last one.

user1934428
  • 19,864
  • 7
  • 42
  • 87
danaso
  • 115
  • 1
  • 1
  • 9
  • 4
    `declare -A array=( [key]=value )` isn't expected to add new values to an existing array, it just replaces the whole thing. Use `array[$key]=$value` to modify an array (one that was *previously* declared associative with `declare -A array` if necessary) rather than replacing it outright. – Charles Duffy Jun 30 '19 at 22:42

3 Answers3

0

danaso, did you quote the list of cities for each person, or did you quote each city individually? Things seem to work fine tfor me when I do the former. Here's a (slightly edited) transcript of my Bash session:

$  declare -A myArray=([emma]="paris london ny" [john]="tokyo LA")

Or, to create your associative array on the fly, as in your "actual code", from the regular arrays called names and cities, you would first create these arrays, like this:

$ declare -a names=(emma john)                     # Notice: small '-a' for inexed array.
$ declare -a cities=("paris london ny" "tokyo LA") # Notice: Quotes around whole lists of cities!

Then I would start with an empty myArray and build it up in a loop, like this:

$ declare myArray=()

for i in ${!names[@]}; do
      name=${names[$i]}
      city=${cities[$i]}
     myArray+=([$name]=$city)
done

As an aside: Sometime in the future, you may want to populate myArray in this manner, but with different entries in your cities and names. If and when you do, it will be a good iea to set -o noglob before the for-loop. This disables pathname expansion by the shell and guards against trouble when you accidentally have a name or city called * in one of your arrays. You can re-enable it with set +o noglob after the for-loop.

Anyway, no matter which way you populate myArray, the new cities no longer overwrite the previous, as you can see:

$  echo ${myArray[emma]}
paris london ny    

$ echo ${myArray[john]}
tokyo LA

And you can process the cities individually, such as in this for-loop:

$ for city in ${myArray[emma]}; do echo "hello, $city!";  done
hello, paris!
hello, london!
hello, ny!

How does that work for you?

PS: I'd like to thank Charles Duffy for the improvements to my code that he suggested in the comments.

  • Consider `read -r -a cities <<<"${myArray[emma]}"`; it's not just faster than the `echo | tr` pipeline, it also avoids bugs like a city named `*` being replaced with a list of filenames. – Charles Duffy Jun 30 '19 at 22:43
  • (That said, `for city in $(...)` will work just as well -- or just as poorly, as it were -- without the `tr` if you have the default `IFS` value, as a space is just as much in `IFS`, and thus used for string-splitting, as a newline is). – Charles Duffy Jun 30 '19 at 22:44
  • Thanks for the tip invilving `read`! I have to go to bed now. but tomorrow morning I'll brush up on the read(1) manpage to make sure I know what I'm doing, and then update the post. – Thomas Blankenhorn Jun 30 '19 at 22:51
  • The `read` we want is a shell builtin, so `help read` (or its equivalent content in `man bash`) is a better fit than `man read`; see also [BashFAQ #1](http://mywiki.wooledge.org/BashFAQ/001) when you're back and able to take a look. – Charles Duffy Jun 30 '19 at 22:52
  • Charles, on reading the Bash documentation, I'll go with your `read` version. But I'll leave out the `-a` and `-r` options because `-a` yields the wrong result nd -r isn't relevant to the example. Also, `for city in $(...)` treats `london paris ny` as one chunk rather than three elements. But thanks for your suggestions! – Thomas Blankenhorn Jul 01 '19 at 09:38
  • If you don't use `-a`, you don't get an array! After `read -r -a arrayname`, `for item in "${arrayname[@]}"` iterates over the items **without** using string-splitting. – Charles Duffy Jul 01 '19 at 12:25
  • And if `for city in $(...)` treats `london paris ny` as one chunk, either you aren't using bash, or you changed your active `IFS` value to `$'\n'`. – Charles Duffy Jul 01 '19 at 12:25
  • Thanks for your answer. Actually, myArray is in a loop ie it's completed dynamically like : while [condition=true ]; do declare -A myArray=( ["$names"]="$cities" ) done – danaso Jul 01 '19 at 12:25
  • Im sorry, Charles, but that's the opposite of what I got when I actually tested. I did get an array I could loop over without the -a option, I only got the first element with it. – Thomas Blankenhorn Jul 01 '19 at 12:28
  • @ThomasBlankenhorn, that sounds like you used `$myArray`, not `"${myArray[@]}"` -- if you don't use array syntax for accessing arrays, you only get the first element. – Charles Duffy Jul 01 '19 at 12:28
  • `read cities <<<"${myArray[emma]}"` is just a buggy assignment; it doesn't do anything you'd actually *want* that `cities=${myArray[emma]}` doesn't. – Charles Duffy Jul 01 '19 at 12:31
  • @ThomasBlankenhorn, if you use `declare -p varname`, that'll tell you if the thing is really an array. If it says `declare -- cities="london paris nyc"`, that's not an array at all, it's just a string with spaces in it. If it says `declare -a cities=( [0]="london" [1]="paris" [2]="nyc" )`, **that's** an array. – Charles Duffy Jul 01 '19 at 12:33
  • You're right, Charles! After removing the superfluous string quotations, I could eliminate the whole `read` business altogether. My code examples are a lot cleaner now, and testing confirmed they still work. Thanks for your patience in helping me get it right! Much appreciated. – Thomas Blankenhorn Jul 01 '19 at 13:23
  • I wouldn't quite call that "right", exactly -- you still have a city named `*` replaced with a list of filenames. – Charles Duffy Jul 01 '19 at 14:46
  • I'm not fully convinced that makes it 'wrong' when the objective is to show a beginner how associative arrays work in Bash. But okay, I've added a side note about `setting -o noglob` before populating `myArray` and `set +o noglob` after. – Thomas Blankenhorn Jul 01 '19 at 17:24
  • @ThomasBlankenhorn, if you're showing a beginner a string and telling them it's an array? That's *very* wrong; it's teaching a complete misapprehension about what the datatypes actually are. – Charles Duffy Jul 01 '19 at 17:43
  • Bash is a dynamically typed language. Treating a string like an array may be frowned upon, but that doesn't make it wrong. – Thomas Blankenhorn Jul 01 '19 at 18:01
  • If you at least communicate that that's what you're doing, I may merely frown. Certainly, types change over time as assignments or explicit modifications take place, but at any given time, only a specific set of bits are set on the mask describing the type of a given variable. Something is an array or a string; it is not both at once. – Charles Duffy Jul 01 '19 at 19:32
0

Bash 4 supports associative arrays. But if using Bash 3 or lower, you can roll your own structure by using a regular array, then splitting each element on some separator such as =.

#!/bin/bash

array=(
  emma="paris london ny" 
  john="tokyo LA"
)

for item in "${array[@]}"; do 
  key=$(cut -d "=" -f 1 <<< "$item")
  value=$(cut -d "=" -f 2 <<< "$item")
  echo "$key=$value"
done 

Sample output:

emma=paris london ny
john=tokyo LA
zwbetz
  • 1,070
  • 7
  • 10
  • Why the `cut` usage? That's *very* slow compared to, say, `key=${item%%=*}; value=${item#*=}`. – Charles Duffy Jun 30 '19 at 22:40
  • That said, the OP here clearly *does* have bash 4 -- they're reporting their values being overwritten, not reporting `declare -A` itself failing with an error. That makes sense -- if they run `declare -A array=( [key]=value )` every time they want to add a new key, that replaces the whole array outright, so it's *expected* to remove all old keys. – Charles Duffy Jun 30 '19 at 22:46
  • You've made good points. They'll be useful for future readers. – zwbetz Jun 30 '19 at 22:48
  • (Granted, it's a related mistake to declare an array as indexed and have the key treated as a number rather than a string; since any string that maps to an undefined shell variable has the value 0 in a numeric context -- like keys of an indexed array -- one gets the same overwriting-the-value behavior, but distinguishable in that the key was never correctly assigned in the first place, and showed up as 0 in `declare -p array`). – Charles Duffy Jun 30 '19 at 22:50
0

The declare -A arrayName=( ["key"]="value" ) syntax should only be used when intending to replace the variable's contents outright, overwriting all preexisting values to leave only those described in the declaration.

When you want to add new values over time, use arrayName["$key"]="$value". Thus:

declare -A arrayName=( )   # Initialize an empty associative array
arrayName["key1"]=value1   # add key1
arrayName["key2"]=value2   # add key2
arrayName["key3"]=value3   # add key3
declare -p arrayName       # print resulting array

...has the same result as:

# initialize with all three values already present
declare -A arrayName=( ["key1"]="value1" ["key2"]="value2" ["key3"]="value3" )
declare -p arrayName       # print resulting array

...whereas the following discards all but the last:

declare -A arrayName=( ["key1"]="value1" )  # initialize with key1=value1
declare -A arrayName=( ["key2"]="value2" )  # discard and overwrite with key2=value2
declare -A arrayName=( ["key3"]="value3" )  # discard and overwrite with key3=value3
declare -p arrayName                        # output only has key3 and value3!
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441