3

I have two strings of same number of substrings divided by a delimiter.

I need to create key-value pairs from substrings.

Short example: Input:

firstString='00011010:00011101:00100001'
secondString='H:K:O'
delimiter=':'

Desired result:

${translateMap['00011010']} -> 'H'
${translateMap['00011101']} -> 'K'
${translateMap['00100001']} -> 'O'

So, I wrote:

IFS="$delimiter" read -ra fromArray <<< "$firstString"
IFS="$delimiter" read -ra toArray <<< "$secondString"
declare -A translateMap

curIndex=0
for from in "${fromArray[@]}"; do
    translateMap["$from"]="${toArray[curIndex]}"
    ((curIndex++))
done

Is there any way to create the associative array directly from 2 strings without the unneeded arrays and loop? Something like:

IFS="$delimiter" read -rA translateMap["$(read -ra <<< "$firstString")"] <<< "$secondString"

Is it possible?

1
  • I'm afraid it isn't possible to assign everything without a loop (apart from creating a bash command using other tools and then using eval/declare on that). I checked all hits for the keyword associative in the bash manual and couldn't find anything useful. Commented Jan 4, 2020 at 13:08

4 Answers 4

2

A (somewhat convoluted) variation on @accdias's answer of assigning the values via the declare -A command, but will need a bit of explanation for each step ...

First we need to break the 2 variables into separate lines for each item:

$ echo "${firstString}" | tr "${delimiter}" '\n'
00011010
00011101
00100001

$ echo "${secondString}" | tr "${delimiter}" '\n'
H
K
O

What's nice about this is that we can now process these 2 sets of key/value pairs as separate files.

NOTE: For the rest off this discussion I'm going to replace "${delimiter}" with ':' to make this a tad bit (but not much) less convoluted.

Next we make use of the paste command to merge our 2 'files' into a single file; we'll also designate ']' as the delimiter between key/value mappings:

$ paste -d ']' <(echo "${firstString}" | tr ':' '\n') <(echo "${secondString}" | tr ':' '\n')
00011010]H
00011101]K
00100001]O

We'll now run these results through a couple sed patterns to build our array assignments:

$ paste -d ']' <(echo "${firstString}" | tr ':' '\n') <(echo "${secondString}" | tr ':' '\n') | sed 's/^/[/g;s/]/]=/g'
[00011010]=H
[00011101]=K
[00100001]=O

What we'd like to do now is use this output in the typeset -A command but unfortunately we need to build the entire command and then eval it:

$ evalstring="typeset -A kv=( "$(paste -d ']' <(echo "${firstString}" | tr ':' '\n') <(echo "${secondString}" | tr ':' '\n') | sed 's/^/[/g;s/]/]=/g')" )"

$ echo "$evalstring"
typeset -A kv=( [00011010]=H
[00011101]=K
[00100001]=O )

If we want to remove the carriage returns and put on a single line we append another tr at the output from the sed command:

$ evalstring="typeset -A kv=( "$(paste -d ']' <(echo "${firstString}" | tr ':' '\n') <(echo "${secondString}" | tr ':' '\n') | sed 's/^/[/g;s/]/]=/g' | tr '\n' ' ')" )"

$ cat "${evalstring}"
typeset -A kv=( [00011010]=H [00011101]=K [00100001]=O )

At this point we can eval our auto-generated typeset -A command:

$ eval "${evalstring}"

And now loop through our array displaying the key/value pairs:

$ for i in ${!kv[@]}; do echo "kv[${i}] = ${kv[${i}]}"; done
kv[00011010] = H
kv[00100001] = O
kv[00011101] = K

Hey, I did say this would be a bit convoluted! :-)

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

8 Comments

It eval works perfectly! Can we do that it was not ( [00011010]=H, [00100001]=O, [00011101]=K ) But ( [00011010]="H", [00100001]="O", [00011101]="K" ) Even better is ( ["00011010"]="H", ["00100001"]="O", ["00011101"]="K" )
sed "s/^/[\"/g; s/]/\"]/g; s/]/]=\"/g;" Mostly completed it. But don't know how to paste last \" properly
@kvantour sorry, wasn't copying your answer; you've edited your answer a few times until you came up with the answer that's similar to mine; it took me awhile to compose/post my message ... which appears to have occurred right about the time you (re)(re)edited your answer
Be advised that eval is evil and could lead to unexpected results.
and echo -e has some issues, too
|
1

It is probably not what you expect, but this works:

key_string="A:B:C:D"
val_string="1:2:3:4"
declare -A map
while [ -n "$key_string" ] && [ -n "$val_string" ]; do
    IFS=: read -r key key_string <<<"$key_string"
    IFS=: read -r val val_string <<<"$val_string"
    map[$key]="$val"
done

for key in "${!map[@]}"; do echo "$key => ${map[$key]}"; done

It uses recursion in the read function to reassign the string value. The downside of this method is that it destroys the original strings. The while-loop checks constantly if both strings have a non-zero length.

Next to the above in pure bash, you could any command to generate the associative array. See How do I populate a bash associative array with command output?

This generally looks like:

declare -A map="( $( magic_command ) )"

where the magic_command generates an output like

[key1]=val1
[key2]=val2
[key3]=val3

In this case we use the command:

paste -d "" <(echo "[${key_string//:/]=$'\n'[}]=") \
            <(echo "${val_string//:/$'\n'}")

where we use bash substitution to replace the delimiter with a newline. However, any other magic_command might do. For completion:

key_string="A:B:C:D"
val_string="1:2:3:4"
declare -A map="( $(paste -d "" <(echo "[${key_string//:/]=$'\n'[}]=") \
                                <(echo "${val_string//:/$'\n'}"))      )"
for key in "${!map[@]}"; do echo "$key => ${map[$key]}"; done

Both examples generate the following output

D => 4
C => 3
B => 2
A => 1

3 Comments

It is a nice idea. But it doesn't work or I'm doing something wrong. Can you explain how it works? How "hash" was declared? Can it be used like this ${hash[$currentKey]} ?
I renamed the variable hash into map. It is just the name of your associative array. So you declare it with declare -A map
Command version works, but I got error of array assign. At least, you done the most closer thing to what I'm asked. I'll be playing around with declare -A map="( $(paste -d "" <(echo -e "[${string1//:/]=\\n[}]=") \ <(echo -e "${string2//:/\\n}")) )" Thank you!
1

Not exactly the answer for what you asked but at least it is shorter:

key='00011010:00011101:00100001'
value='H:K:O'
ifs=':'

IFS="$ifs" read -ra keys <<< "$key"
IFS="$ifs" read -ra values <<< "$value"
declare -A kv

for ((i=0; i<${#keys[*]}; i++)); do
    kv[${keys[i]}]=${values[i]}
done

As a side note, you can initialize an associative array in one step with:

declare -A kv=([key1]=value1 [key2]=value2 [keyn]=valuen)

But I don't know how to use that in your case.

Comments

0

If values in your strings won't use spaces i would suggest this approach

firstString='00011010:00011101:00100001'
secondString='H:K:O'
delimiter=':'

declare -A translateMap

firstArray=( ${firstString//$delimiter/' '} )
secondArray=( ${secondString//$delimiter/' '} )

for i in ${!firstArray[@]}; {
    translateMap[firstArray[$i]}]=${secondArray[$i]}
}

Comments

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.