1

I want to read into bash associative array the content of one yaml file, which is a simple key value mapping.

Example map.yaml

---

a: "2"
b: "3"
api_key: "somekey:thatcancontainany@chara$$ter"

the key can contain any characters excluding space the value can contain any characters without limitations $!:=@etc

What will always be constant is the separator between key and value is :

the script proceses.sh

#!/usr/bin/env bash

declare -A map
# how to read here into map variable, from map.yml file
#map=populatesomehowfrommap.yaml

for key in "${!map[@]}"
do
  echo "key  : $key"
  echo "value: ${map[$key]}"
done

I tried to play around with yq tool, similar to json tool jq but did not have success yet.

Kristi Jorgji
  • 1,154
  • 1
  • 14
  • 39
  • 3
    Since you didn't show how you used `yq` and how you filled `map`, we can't say what you did wrong. – user1934428 Apr 25 '22 at 14:31
  • I did not add that because don't want to limit solutions to only using yq. Feel free comment any solution with or without yq that achieves the goal. Meanwhile I will post my faulty yq solution update question – Kristi Jorgji Apr 25 '22 at 14:32
  • Is that your actual yaml? Nested items makes this a lot harder. – 0stone0 Apr 25 '22 at 14:34
  • Yes that is my actual yaml, it is and will be flat only key => value map where value is always a string – Kristi Jorgji Apr 25 '22 at 14:35
  • For reading a formatted file, you need to write a parser. For a **general** YAML file, you either have to re-invent the wheel and develop your own parser, or use an existing one. `yq` is just an example; you can use a different one. Things may be simpler, if you can ensure that your YAML input does not contain any possible valid YAML code, but is restricted to a certain YAML subset. In this case you need to define, how the data may look like (given a small example is, for sure, **not** a definition). – user1934428 Apr 25 '22 at 14:35
  • @user1934428 that is the definition of my yaml. I always expect key value map, both are string. Nothing is nested – Kristi Jorgji Apr 25 '22 at 14:36
  • 1
    Is every key/value association always on a single line? Can the values have double-quotes or colons embedded? You need to be more specific what can or cann not be between the delimiting quotes.... – user1934428 Apr 25 '22 at 14:37
  • yes it is a valid yaml file, every definition is in a new line and always surrounded by `"` like in the provided example map.yml – Kristi Jorgji Apr 25 '22 at 14:44
  • `bash` doesn't have nested arrays, so there's little reason to support decoding arbitrary YAML. Use a different language. – chepner Apr 25 '22 at 14:50
  • sorry! yes it is a typo, updating now and closing " – Kristi Jorgji Apr 25 '22 at 15:24

3 Answers3

1

One way, is by letting output each key/value pair on a single line, in the following syntax:

key@value

Then we can use bash's IFS to split those values.
The @ is just an example and can be replaced with any single char


This works, but please note the following limitations:

  • It does not expect nested values, only a flat list`
  • The field seperator (@ in the example) does not exist in the YAML key/value's

#!/bin/bash

declare -A arr
while IFS="@" read -r key value
do
    arr[$key]="$value"
done < <(yq e 'to_entries | .[] | (.key + "@" + .value)' input.yaml)

for key in "${!arr[@]}"
do
    echo "key  : $key"
    echo "value: ${arr[$key]}"
done
$ cat input.yaml
---
a: "bar"
b: "foo"

$
$
$ ./script.sh
key  : a
value: bar
key  : b
value: foo
$
0stone0
  • 34,288
  • 4
  • 39
  • 64
  • Thanks, unfortunately I have `=` in the value of the keys. Can be base64 and other values that contain = – Kristi Jorgji Apr 25 '22 at 14:50
  • You can use another separator, as long as it's not in the key/value. – 0stone0 Apr 25 '22 at 14:53
  • can IFS be more then one character long? I tried and only uses first char. My concern is that value can have any possible string (also credentials keys base64 etc) so might end up in bugs due to ifs of value `@` or something else being present in value as well... – Kristi Jorgji Apr 25 '22 at 15:05
  • You can make the IFS based on a regex. Please see [this question/answer](https://stackoverflow.com/questions/42635429/how-to-parse-a-string-with-multiple-characters-to-split-on-bash-scripting). Not sure if that will fix your problem tho since you're not sure what chars might be there. – 0stone0 Apr 25 '22 at 15:11
  • I am getting on bash mac m1, `bash: declare: -A: invalid option` ` echo $SHELL /bin/bash ` – Kristi Jorgji Apr 25 '22 at 15:12
1

With the following limitations:

  • simple YAML key: "value" in single lines
  • keys cannot contain :
  • values are always wrapped in "
#!/usr/bin/env bash

declare -A map
regex='^([^:]+):[[:space:]]+"(.*)"[[:space:]]*$'

while IFS='' read -r line
do
    if [[ $line =~ $regex ]]
    then
        printf -v map["${BASH_REMATCH[1]}"] '%b' "${BASH_REMATCH[2]}"
    else
        echo "skipping: $line" 1>&2
    fi
done < map.yaml

Update

Here's a robust solution using yq, which would be simpler if the builtin @tsv filter implemented the lossless TSV escaping rules instead of the CSV ones.

#!/usr/bin/env bash

declare -A map

while IFS=$'\t' read key value
do
    printf -v map["$key"] '%b' "$value"
done < <(
    yq e '
        to_entries | .[] |
        [
            (.key   | sub("\\","\\") | sub("\n","\n") | sub("\r","\r") | sub("\t","\t")),
            (.value | sub("\\","\\") | sub("\n","\n") | sub("\r","\r") | sub("\t","\t"))
        ] |
        join("  ")
    ' map.yaml
)

note: the join needs a literal Tab

Fravadona
  • 13,917
  • 1
  • 23
  • 35
  • Thanks. I tried it and got error like `process.sh: line 18: printf: `map[ssm_api_key]': not a valid identifier ` Line 18 in my modified script is `printf -v map["${BASH_REMATCH[1]}"] '%b' "${BASH_REMATCH[2]}"` and my map.yml has one extra key `ssm_api_key: "somesecretkey:2323"` – Kristi Jorgji Apr 25 '22 at 15:19
  • it feels like you're not using bash-4+; are you running it on macOS or with a `sh` shebang? – Fravadona Apr 25 '22 at 15:24
  • first line of bash is `#!/usr/bin/env bash` yes bash version seems not to be 4 `bash --version GNU bash, version 3.2.57(1)-release (arm64-apple-darwin21) Copyright (C) 2007 Free Software Foundation, Inc.` – Kristi Jorgji Apr 25 '22 at 15:25
  • @KristiJorgji: as user1934428 has pointed out in the comments, you need to provide more details on your actual data; you were asked if the data can contain embedded colons but you never responded, nor does your sample data demonstrate embedded colons but *now* ... you're telling Fravadona the data contains embedded colons; please update the question with a more respresentative set of data along with details on what characters may (not) exist in the data – markp-fuso Apr 25 '22 at 15:25
  • @markp-fuso I updated my question now with extra key value pair and mentioning that can contain any character – Kristi Jorgji Apr 25 '22 at 15:26
  • 2
    bash-3 doesn't have associative arrays, you can't load your data with it – Fravadona Apr 25 '22 at 15:27
  • I used `brew upgrade bash` and now got version 5. I runned again and worked correctly. This solution works. Can I assume that value can have any value ? The limitation of not containing `:` is only for the key right ?\ – Kristi Jorgji Apr 25 '22 at 15:33
  • There is no constraint that associative array keys can't contain `:` – Charles Duffy Apr 25 '22 at 15:34
0

I used @Fravadona s answer so will mark it as answer

After some modification to my use case, what worked for me looks like:

DEFS_PATH="definitions"

declare -A ssmMap
for file in ${DEFS_PATH}/*
do
    filename=$(basename -- "$file")
    projectName="${filename%.*}"

    regex='^([^:]+):[[:space:]]*"(.*)"[[:space:]]*$'
    while IFS='' read -r line
    do
        if [[ $line =~ $regex ]]
        then
            value="${BASH_REMATCH[2]}"
            value=${value//"{{ ssm_env }}"/$INFRA_ENV}
            value=${value//"{{ ssm_reg }}"/$SSM_REGION}
            value=${value//"{{ projectName }}"/$projectName}
            printf -v ssmMap["${BASH_REMATCH[1]}"] '%b' "$value"
        else
            echo "skipping: $line" 1>&2
        fi
    done < "$file"
done

Basically in real use case I have one folder where yaml definitions are located. I iterate over all of them to form the associative array ssmMap

Kristi Jorgji
  • 1,154
  • 1
  • 14
  • 39