0

I cannot figure out (with my limited bash scripting skills) how solve this, i want somehow to combine variables in a bash script. I'm trying the following:

#!/bin/bash
basearch=x86_64
ol7_channels="ol UEKR4 UEKR3"
ol6_channels="ol UEKR4 UEKR3 UEK"
ol5_channels="ol UEK"

for version in 7 6 5
do
  for channel in ${ol${version}_channels}}
  do
    printf "Oracle Linux $version $channel $basearch"
  done
done

The desirable output would be:

OracleLinux 7 ol x86_64

OracleLinux 7 UEKR4 x86_64

OracleLinux 7 UEKR3 x86_64

OracleLinux 6 ol x86_64

OracleLinux 6 UEKR4 x86_64

OracleLinux 6 UEKR3 x86_64

OracleLinux 5 ol x86_64

OracleLinux 5 UEKR4 x86_64

OracleLinux 5 UEKR3 x86_64

I understand putting a variable inside a variable like i have done doesn't work. Can anyone show me a way how to acheive this?

Ichundu
  • 173
  • 1
  • 10
  • There isn't a single array anywhere in your question. – Charles Duffy Mar 27 '17 at 17:11
  • 1
    By the way -- which version of bash? The right way to do this in bash 4.3 or later would be to use namevars to point to actual arrays. – Charles Duffy Mar 27 '17 at 17:13
  • ...btw, consider making a habit of running your code through http://shellcheck.net/ -- that would also point out that using `printf` with a non-constant format string is bad form. (While this isn't the case in bash, in C and sufficiently C-like languages, this is sometimes not just "bad form" but "a cause of exploitable security bugs"). – Charles Duffy Mar 27 '17 at 17:19

3 Answers3

2

The below actually uses real arrays (as opposed to treating strings as if they were arrays), and uses the bash 4.3 feature namerefs to create an alias to those arrays.

#!/bin/bash
basearch=x86_64
channels_ol7=( ol UEKR4 UEKR3 )
channels_ol6=( ol UEKR4 UEKR3 UEK )
channels_ol5=( ol UEK )

for varname in "${!channels_ol@}"; do # iterate over variables starting with channels_ol
  version=${varname#channels_ol}      # trim prefix to get version number
  declare -n channels=$varname        # point channels nameref at our array
  for channel in "${channels[@]}"; do # iterate over that array
    printf 'Oracle Linux %s %s %s\n' "$version" "$channel" "$basearch"
  done
  unset -n channels                   # clear the nameref before proceeding
done
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
  • Unfortunately this doesn't work in a CentOS 7 box (bash version is 4.2). It doesn't recognize the "-n" option in "declare". Works perfectly though on my laptop running Arch Linux which has a newer version of bash. – Ichundu Mar 27 '17 at 17:27
  • BTW, I've extended this a little to no longer need `7`, `6` and `5` to be hardcoded. The version dependency is still present, though. – Charles Duffy Mar 27 '17 at 19:56
2

Another method:

#!/bin/bash

#bash 4.0+

basearch=x86_64
declare -A channels=(
        [7]="ol UEKR4 UEKR3"
        [6]="ol UEKR4 UEKR3 UEK"
        [5]="ol UEK"
)

#or for version in 5 6 7 #if you need ordered
for version in "${!channels[@]}"
do
        read -a chanlist <<<"${channels[$version]}"
        for channel in "${chanlist[@]}"
        do
            echo "$version $channel $basearch"
        done
        #or replace the whole "for channel" loop with the following line
        #printf "$version %s $basearch\n" "${chanlist[@]}"
        #but read charles's comment
done

output

7 ol x86_64
7 UEKR4 x86_64
7 UEKR3 x86_64
6 ol x86_64
6 UEKR4 x86_64
6 UEKR3 x86_64
6 UEK x86_64
5 ol x86_64
5 UEK x86_64
clt60
  • 62,119
  • 17
  • 107
  • 194
  • 1
    Outstanding. If someone needed to use this in a place where they wanted spaces to separate list elements, they could just use a different character and then set it in `IFS` for the `read`. Thus, it's very nearly as flexible as the array+nameref approach, and compatible back through bash 4.0. – Charles Duffy Mar 27 '17 at 17:55
  • ...yeeeah, that particular change (non-constant format strings) I'm not quite as sure I'm on board with, since it's liable to cause problems if your `version` or `basearch` values were to contain `%` signs or backslashes. – Charles Duffy Mar 27 '17 at 17:59
  • BTW, `for version in "${!channels[@]}"` would be another way to make this code a bit more DRY, not needing to have the list of versions hardcoded anywhere except in their definition (at the cost of losing any guarantee of ordering). – Charles Duffy Mar 27 '17 at 18:05
  • Great, this also works! In this particular case I don't care much about ordering, I just needed to iterate the $version and $channel variables correctly. Thanks! – Ichundu Mar 27 '17 at 19:09
1

You can compose your variable before hand, and then use variable indirect reference

#!/bin/bash
basearch=x86_64
ol7_channels="ol UEKR4 UEKR3"
ol6_channels="ol UEKR4 UEKR3 UEK"
ol5_channels="ol UEK"

for version in 7 6 5
do
  varname="ol${version}_channels";
  for channel in "${!varname}"
  do
    echo "Oracle Linux $version $channel $basearch"
  done
done

Nested variable names do not work in bash.

Community
  • 1
  • 1
jraynal
  • 507
  • 3
  • 10
  • 2
    `${!varname}` is not just "maybe" preferable, but **strongly** preferable. If your `version` contained user-controllable text, you could have arbitrary command execution with the `eval` version. – Charles Duffy Mar 27 '17 at 17:12
  • Thanks this works!! I removed eval and went with ${!varname} instead because it was also printing one line with one line with "eval" like: "Oracle Linux 7 eval x86_64" – Ichundu Mar 27 '17 at 17:15
  • 1
    BTW, if you somehow had `*` as a standalone word in one of your "arrays", you'd see it expanded as a glob (replaced with a list of filenames in the current directory). Similarly, channel names with spaces won't work in this form (whereas they're perfectly valid in legit arrays). – Charles Duffy Mar 27 '17 at 17:17