1

For the sake of exercise, I've written the few lines below in a bash script to transform a HTTP post input into an associative array. Following through on my learning, I was wondering, if there are different, maybe more elegant, ways to do this. I mean, I know there are different ways. I'm wondering which ones exist and what the advantages and disadvantages of each methods are.

Note: I'm using post as input here. The exercise however is to build a array from a multiple name/value pair string. The input could be coming from any files or other input source.

Note2: the string I'm dealing with could look like this: name=myName&age=myAge. So there are 2 delimiters. One separating name/value pairs (&) and the other separating the value from its name (=).

#!/bin/bash
read post;
declare -A myArr;
IFS_bck=$IFS;
IFS="=";
while read name value; do
  myArr[$name]=$value;
done < <(sed -rn -e 's/&/\n/g p' <<<"$post");
IFS=$IFS_bck;

P.S. I'm not willing to start a religious war. I'm simply wondering how you would do this and why you would chose your proposal over mine.

  • You want just one key-value pair in the array? – heemayl Mar 9 '16 at 12:28
  • This isn't about Ubuntu - you'd be better asking on stackoverflow.com – Carl H Mar 9 '16 at 12:44
  • @CarlH This is of course on-topic here.. – heemayl Mar 9 '16 at 13:02
  • Small question though, you said multiple name/value pair string. The example shows two pairs, but could this also be name=myName&age=myAge&married=true ? If that's the case, the you need something other than associative array. Likey a linked list would be better. And probably a different language - bash isn't the right tool for this – Sergiy Kolodyazhnyy Mar 9 '16 at 15:24
  • 1
    "… how you would do this and why you would chose …" The problem here is that you have given us an XY problem. You have already decided on an associative array as the solution, and are asking us how to build one. Depending on the actual problem, the rest of us might not even use an associative array at all. When it comes to records and fields, awk sweeps the floor with other tools, which is why kos picked that. If you're really into learning this, start with the actual problem. – muru Mar 9 '16 at 15:30
2

I would do this just using bash itself:

#!/bin/bash
read -e -p 'Enter the string: ' post
declare -A myArr

while IFS='&' read name age; do
    myArr["${name##*=}"]="${age##*=}"
done <<<"$post"

printf 'NAME: %s, AGE: %s\n' "${!myArr[@]}" "${myArr[@]}"

Here is what i get in the output:

Enter the string: name=Foo Bar&age=40
NAME: Foo Bar, AGE: 40
  • The input string is spitted on & using IFS environmental variable

  • The name and age values are parsed using parameter expansion pattern ${var##*=}

  • You can get all keys using ${!myArr[@]} and all values using ${!myArr[@]}

In practice, I don't think you would just make an associative array of one element. If you have multiple elements, replace the last printf line with a simple for construct to loop over the keys:

for key in "${!myArr[@]}"; do
    printf 'NAME: %s, AGE: %s\n' "$key" "${myArr["$key"]}"
done
3

What you have doesn't look bad, but neither Bash nor sed play too well with splitting on multiple delimiters; personally I'd use AWK and simplify the script a bit (using AWK changing IFS becomes redundant; notice that you don't need semicolons at the end of statements in Bash):

#!/bin/bash
read post
declare -A myArr
while read name value; do
    myArr[$name]=$value
done < <(<<<"$post" awk -F= '{print $1,$2}' RS='&|\n')

The AWK command:

  • Reads $post;
  • Splits records on sequences of amperstands / newlines (splitting on newlines is a trick to prevent the while loop from failing on the final empty record);
  • Splits fields on sequences of equal signs;
  • Prints the fields separated by the default separator, a space.
% cat script.sh 
#!/bin/bash
read post
declare -A myArr
while read name value; do
    myArr[$name]=$value
done < <(<<<"$post" awk -F= '{print $1,$2}' RS='&|\n')
printf '%s %s\n' "${myArr[name]}" "${myArr[age]}"
% bash script.sh 
name=myName&age=myAge
myName myAge
% 
  • 1
    kossy, why not just while IFS=\= .... and get rid of unnecessary parsing and process substitution ..also OP should know that they should quote their variables..anyway, you have my +1 always.. – heemayl Mar 9 '16 at 13:44
  • @heemayl I remember seeing a comment here mentioning read's -d option. How come neither of you have used it, and the comment's vanished? – muru Mar 9 '16 at 15:26
  • @muru duh..need coffee..kos, go ahead..i am out.. – heemayl Mar 9 '16 at 15:30
  • @muru It was mine, but it's not viable since they're reading with read; if I use & as the delimiter I also force them to type & at the end. – kos Mar 9 '16 at 15:32
  • (if they copy-paste. Otherwise I force them ro read a single key/value pair). – kos Mar 9 '16 at 15:52
2

Yet another way, using just IFS:

#!/bin/bash
declare -A myArr
IFS='&=' read -a post
for ((i = 0; i < ${#post[@]}; i += 2))
do
    myArr[${post[i]}]=${post[i + 1]}
done
for key in "${!myArr[@]}"
do
    printf "%s: %s\n" "$key" "${myArr[$key]}"
done

read splits the incoming line into words on all characters in IFS, so you can use both & and = in IFS to split on both. Given that the POST input always has a value for a key, this will work.

However, this method can't check if there's a strict alternation between & and =. So, for example, age&myAge=name=myName will be parsed so that age=myAge and name=myName.


A note about IFS

You have backed up IFS and restored it. But you only need IFS for read, so apply IFS only for READ:

IFS='...' read ... # or
while IFS='...' read ...; ...

Restoring IFS is tricky, since an unset IFS and an empty IFS affect the shell differently, but are the same when you take the value of IFS itself. That is:

IFS=
IFS_BAK="$IFS"

will give the same value for IFS_BAK as:

unset IFS
IFS_BAK="$IFS"

An empty IFS is exactly that: an empty string. However, an unset IFS causes the shell to behave as if the default IFS (space, tab, newline) was used:

$ foo='a  b  c'
$ printf "|%s|\n" $foo
|a|
|b|
|c|
$ IFS=; printf "|%s|\n" $foo
|a  b  c|
$ unset IFS; printf "|%s|\n" $foo
|a|
|b|
|c|

So, if you'd found yourself with an unset IFS, and then tried the old backup-and-restore-IFS trick, you might get surprising results. It's better to restrict changes to IFS to be only for those commands that need it.

1

You have said:

So there are 2 delimiters. One separating name/value pairs (&) and the other separating the value form its name (=).

Well, we can split name/value pairs using & as IFS into variables, and use suffix/prefix removal to free the actual name and age values.

$> cat post-parse.sh                                                           
#!/bin/bash
IFS='&' read PAIR1 PAIR2
# if necessary use these as well
# name_key=${PAIR1%%=*}
# age_key=${PAIR2%%=*}
name_val=${PAIR1##*=}
age_val=${PAIR2##*=}
echo $name_val $age_val
$> ./post-parse.sh
name=Serg&age=25
Serg 25
$> 

You've also said:

The exercise however is to build a array from a multiple name/value pair string. The input could be coming from any files or other input source.

If we want to store multiple key-value pairs, we can read input line by line (so no need to use sed to get rid of \n there), and apply same concept I showed above:

#!/bin/bash
declare -A myArray
while read input_line ; # read input line by line
do
echo $input_line
  IFS='&' read PAIR1 PAIR2 <<< $input_line # split in two with &
  # name_key=${PAIR1%%=*}
  # age_key=${PAIR2%%=*}
  name_val=${PAIR1##*=}
  age_val=${PAIR2##*=}
  myArray[$name_val]=$age_val
done
# print out the array
for key in "${!myArray[@]}" 
do
   echo ${myArray[$key]} is $key
done

Sample run bellow uses here document but it could be anything, even a pipe. Point is, read command doesn't care where it gets input.

$> ./post-parse.sh << EOF                                                      
> name=John&age=25                                                             
> name=Jane&age=35                                                             
> EOF
name=John&age=25
name=Jane&age=35
25 is John
35 is Jane

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.