Access associative arrays with variables - bash

Let's say we declared two associative arrays:
#!/bin/bash
declare -A first
declare -A second
first=([ele]=value [elem]=valuee [element]=valueee)
second=([ele]=foo [elem]=fooo [element]=foooo)
# echo ${$1[$2]}
I want to echo the given hashmap and element from script inputs. For example, if I run sh.sh second elem, the script should echo fooo.

An inelegant but bullet-proof solution would be to white-list $1 with the allowed values:
#!/bin/bash
# ...
[[ $2 ]] || exit 1
unset result
case $1 in
first) [[ ${first["$2"]+X} ]] && result=${first["$2"]} ;;
second) [[ ${second["$2"]+X} ]] && result=${second["$2"]} ;;
*) exit 1 ;;
esac
[[ ${result+X} ]] && printf '%s\n' "$result"
notes:
[[ $2 ]] || exit 1 because bash doesn't allow empty keys
[[ ${var+X} ]] checks that the variable var is defined; with this expansion you can also check that an index or key is defined in an array.

A couple ideas come to mind:
Variable indirection expansion
Per this answer:
arr="$1[$2]" # build array reference from input fields
echo "${!arr}" # indirect reference via the ! character
For the sample call sh.sh second elem this generates:
fooo
Nameref (declare -n) (requires bash 4.3+)
declare -n arr="$1"
echo "${arr[$2]}"
For the sample call sh.sh second elem this generates:
fooo

Related

Bash script using getopts to store strings as an array

I am working on a Bash script that needs to take zero to multiple strings as an input but I am unsure how to do this because of the lack of a flag before the list.
The script usage:
script [ list ] [ -t <secs> ] [ -n <count> ]
The list takes zero, one, or multiple strings as input. When a space is encountered, that acts as the break between the strings in a case of two or more. These strings will eventually be input for a grep command, so my idea is to save them in an array of some kind. I currently have the -t and -n working correctly. I have tried looking up examples but have been unable to find anything that is similar to what I want to do. My other concern is how to ignore string input after a flag is set so no other strings are accepted.
My current script:
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
;;
:)
echo "$0: Must supply an argument to -$OPTARG" >&2
exit
;;
?)
echo "Invalid option: -${OPTARG}"
exit
;;
esac
done
Edit: This is for a homework assignment and am unsure if the order of arguments can change
Edit 2: Options can be in any order
Would you please try the following:
#!/bin/bash
# parse the arguments before getopts
for i in "$#"; do
if [[ $i = "-"* ]]; then
break
else # append the arguments to "list" as long as it does not start with "-"
list+=("$1")
shift
fi
done
while getopts :t:n: arg; do
: your "case" code here
done
# see if the variables are properly assigned
echo "seconds=$seconds" "count=$count"
echo "list=${list[#]}"
Try:
#! /bin/bash -p
# Set defaults
count=10
seconds=20
args=( "$#" )
end_idx=$(($#-1))
# Check for '-n' option at the end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -n ]]; then
count=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Check for '-t' option at the (possibly new) end
if [[ end_idx -gt 0 && ${args[end_idx-1]} == -t ]]; then
seconds=${args[end_idx]}
end_idx=$((end_idx-2))
fi
# Take remaining arguments up to the (possibly new) end as the list of strings
strings=( "${args[#]:0:end_idx+1}" )
declare -p strings seconds count
The basic idea is to process the arguments right-to-left instead of left-to-right.
The code assumes that the only acceptable order of arguments is the one given in the question. In particular, it assumes that the -t and -n options must be at the end if they are present, and they must be in that order if both are present.
It makes no attempt to handle option arguments combined with options (e.g. -t5 instead of -t 5). That could be done fairly easily if required.
It's OK for strings in the list to begin with -.
My shorter version
Some remarks:
Instead of loop over all argument**, then break if argument begin by -, I simply use a while loop.
From How do I test if a variable is a number in Bash?, added efficient is_int test function
As any output (echo) done in while getopts ... loop would be an error, redirection do STDERR (>&2) could be addressed to the whole loop instead of repeated on each echo line.
** Note doing a loop over all argument could be written for varname ;do. as $# stand for default arguments, in "$#" are implicit in for loop.
#!/bin/bash
is_int() { case ${1#[-+]} in
'' | *[!0-9]* ) echo "Argument '$1' is not a number"; exit 3;;
esac ;}
while [[ ${1%%-*} ]];do
args+=("$1")
shift
done
while getopts :t:n: arg; do
case ${arg} in
t ) is_int "${OPTARG}" ; seconds=${OPTARG} ;;
n ) is_int "${OPTARG}" ; count=${OPTARG} ;;
: ) echo "$0: Must supply an argument to -$OPTARG" ; exit 2;;
? ) echo "Invalid option: -${OPTARG}" ; exit 1;;
esac
done >&2
declare -p seconds count args
Standard practice is to place option arguments before any non-option arguments or variable arguments.
getopts natively recognizes -- as the end of option switches delimiter.
If you need to pass arguments that starts with a dash -, you use the -- delimiter, so getopts stops trying to intercept option arguments.
Here is an implementation:
#!/usr/bin/env bash
# SYNOPSIS
# script [-t<secs>] [-n<count>] [string]...
# Counter of option arguments
declare -i opt_arg_count=0
while getopts :t:n: arg; do
case ${arg} in
t)
seconds=${OPTARG}
if ! [[ $seconds =~ ^[1-9][0-9]*$ ]] ; then
exit
fi
opt_arg_count+=1
;;
n)
count=${OPTARG}
if ! [[ $count =~ ^[1-9][0-9]*$ ]] ; then
exit 1
fi
opt_arg_count+=1
;;
?)
printf 'Invalid option: -%s\n' "${OPTARG}" >&2
exit 1
;;
esac
done
shift "$opt_arg_count" # Skip all option arguments
[[ "$1" == -- ]] && shift # Skip option argument delimiter if any
# Variable arguments strings are all remaining arguments
strings=("$#")
declare -p count seconds strings
Example usages
With strings not starting with a dash:
$ ./script -t45 -n10 foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="foo" [1]="bar" [2]="baz" [3]="qux")
With string starting with a dash, need -- delimiter:
$ ./script -t45 -n10 -- '-dashed string' foo bar baz qux
declare -- count="10"
declare -- seconds="45"
declare -a strings=([0]="-dashed string" [1]="foo" [2]="bar" [3]="baz" [4]="qux")

How to search within a range of bash command-line parameters

Let's say I have the program test.sh, which has parameters, some optional and some not.
For example:
./test.sh --foo /path/to/file --baz --bar1 --bar2 --bar3
where foo and baz, as well as the path, are necessary and the bars are optional parameters.
Now, I want to be able to make the everything after the path order-insensitive.
I could use
if [[ "$3" == "--baz" ]] || [[ "$4" == "--baz" ]] || ... || [[ "${(n-1)}" == "--baz" ]] || [[ "${n}" == "--baz" ]]
but that's slow and messy, even for me.
Ideally I would have something along the lines of this:
if [[ ${n > 2} == "--baz" ]]; then
Violating standard utility syntax guidelines like this makes your program unpredictable and difficult to use, but you can still do so e.g. with a simple utility function:
hasArgAfter() {
n="$1"
word="$2"
shift "$((n+2))"
for arg
do
[ "$arg" = "$word" ] && return 0
done
return 1
}
if hasArgAfter 2 "--baz" "$#"
then
echo "Some argument strictly after #2 is --baz"
fi
Based on the answers in:
Check if a Bash array contains a value
How to slice an array in Bash
You could come up with the following answer:
[[ " ${#:3} " =~ " --baz " ]] && echo yes || echo no
This might fail if you have something like
command --foo /path/to/file --flag1 option1 --flag2 "foo --baz bar" --flag3
where foo --baz bar is an option to --flag2
Another way, a bit safer, would be:
for arg in "${#:3}"; do
[[ "${arg}" != "--baz" ]] && continue
# perform action here if --baz is found
set-of-commands
# need this line for early return
break
done
Create a function for handling inputs, then ordering won't matter.
What I would do is define global variables for your script using the typset command at the top of your script. Then I would handle the user input options using a function or just code it without a function. This way when input is not in order or it is missing it is properly handled.
The example below is using a case statement and it is using the build in shift option to go through all of the inputs. $1 is --option $2 is the value such as "/path/to/something", I have checks in there check if $2 is empty "-z" or || if it isn't, set it. When done items are either set or empty. In your code you will check if set or empty to determine if you are going to use that variable (not shown here.
# -- create globals --
typeset fooPath
typeset baz
typeset bar1
# -- get required commandline input --
get_user_input_options() {
while [[ $# -gt 0 ]] ;do
key="$1"
case ${key,,} in
--foo|--foo-path)
fooPath="${2}"
shift
;;
-b|--baz)
[[ -z "${2}" ]] || baz="${2}"
shift
;;
--bar1)
[[ -z "${2}" ]] || bar1="${2}"
shift
;;
*) echo "ERROR: Unknown option $key given."
exit 9
;;
esac
shift
done
}
# -- get inputs first in script logic --
get_user_input_options "$#"
echo $fooPath
echo $baz
echo $bar1
Example outputs:
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1
/some/thing
askdh
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test
/some/thing
askdh
test
[centos#ip-172-31-22-252 ~]$ ./t.sh --foo "/some/thing" --baz askdh --bar1 test --notvalid askdha
ERROR: Unknown option --notvalid given.

Nesting If Condition Inside A While Loop

I'm reading the contents of a file and storing them in 2 variables then simultaneously want to compare it with an array using if statement. Code is given below
#!/bin/bash
# Define File
datafile=./regions-with-keys
# Create Nodes File
cat << EOF > $datafile
region1 key1
region2 key2
region3 key3
EOF
# User Input
clear;
echo -ne "PLEASE SELECT REGIONS(s) :\n\033[37;40m[Minimum 1 Region Required]\033[0m"
read -ra a
echo "${a[#]}"
# Reading Regions & Keys
for i in "${a[#]}"
do
while read -r $b $c; do
if [ "${a[#]}" -eq "$b" ]; then
echo "$b" "$c"
fi
done < $datafile
done;
it gives command not found for if statement when executed..
Aim of the code is to match the array indexes of userinput with $a from $datafile, if match is successful print
$b and $c
Try this Shellcheck-clean code:
#!/bin/bash -p
# Define File
datafile=./regions-with-keys
# Create Nodes File
cat <<EOF >"$datafile"
region1 key1
region2 key2
region3 key3
EOF
# User Input
clear
echo 'PLEASE SELECT REGIONS(s) :'
echo -ne '\e[37;40m[Minimum 1 Region Required]\e[0m'
read -ra input_regions
declare -p input_regions
# Reading Regions & Keys
for input_rgn in "${input_regions[#]}" ; do
while read -r data_rgn key ; do
if [[ $data_rgn == "$input_rgn" ]] ; then
printf '%s %s\n' "$data_rgn" "$key"
fi
done <"$datafile"
done
Significant changes from the code in the question are:
Use meaningful variable names.
Use declare -p input_regions to print the contents of the array in an unambiguous way.
Use varname instead of $varname as arguments to read. That fixes a serious bug in the original code.
Use printf instead of echo for printing variable values. See Why is printf better than echo?.
Used [[ ... == ...]] instead of [ ... -eq ... ] for comparing the region names. [[ ... ]] is more powerful than [ ... ]. See Is double square brackets [[ ]] preferable over single square brackets [ ] in Bash?. Also, -eq is for comparing integers and == (or, equivalently, =) is for comparing strings.
Did various cleanups (removed some blank lines, removed unnecessary semicolons, ...).
The new code is Shellcheck-clean. Shellcheck identified several problems with the original code.
If you want to report incorrect input regions, try replacing the "Reading Regions & Keys" code with this:
for input_rgn in "${input_regions[#]}" ; do
# Find the key corresponding to $input_rgn
key=
while read -r data_rgn data_key ; do
[[ $data_rgn == "$input_rgn" ]] && key=$data_key && break
done <"$datafile"
if [[ -n $key ]] ; then
printf '%s %s\n' "$input_rgn" "$key"
else
printf "error: region '%s' not found\\n" "$input_rgn" >&2
fi
done

Bash substitution: log when variable is not set

I use bash substitutions to give neat one-line validation for input, e.g.:
#!/bin/bash
export PARAM1=${1?Error, please pass a value as the first argument"}
# do something...
In some cases though, I want to only log a message when something is unset and then continue as normal. Is this possible at all?
Maybe something along the lines of
test -n "$1" && export PARAM1="$1" || log "\$1 is empty!"
should do; here the test clause returns true if and only if $1 is non-empty.
For regular parameters (in bash 4 or later), you can use the -v operator to check if a parameter (or array element, as of version 4.3) is set:
[[ -v foo ]] || echo "foo not set"
bar=(1 2 3)
[[ -v bar[0] ]] || echo "bar[0] not set"
[[ -v bar[8] ]] || echo "bar[8] not set"
Unfortunately, -v does not work with the positional parameters, but you can use $# instead (since you can't set, say, $3 without setting $1).
(( $# >= 3 )) || echo "third argument not set"
Before -v became available, you would need to compare two default-value expansions to see if a parameter was unset.
[[ -z $foo && ${foo:-bar} == ${foo-bar} ]] && echo "foo is unset, not just empty"
There's nothing special about bar; it's just an arbitrary non-empty string.

How can I parse long-form arguments in shell?

Everything I see uses getopt or the slightly-fancier getopts which only supports one-character options (e.g., -h but not --help). I want to do fancy long options.
I've done something like this:
_setArgs(){
while [ "${1:-}" != "" ]; do
case "$1" in
"-c" | "--configFile")
shift
configFile=$1
;;
"-f" | "--forceUpdate")
forceUpdate=true
;;
"-r" | "--forceRetry")
forceRetry=true
;;
esac
shift
done
}
As you can see, this supports both the single-character and the longer options nicely. It allows for values to be associated with each argument, as in the case of --configFile. It's also quite extensible, with no artificial limitations as to what options can be configured, etc.
As included above, the "${1:-}" prevents an "unbound variable" error when running in bash "strict" mode (set -euo pipefail).
Assuming that you "want to do fancy long options" regardless of the tool, just go with getopt (getopts seems to be mainly used when portability is crucial). Here's an example of about the maximum complexity that you'll get:
params="$(getopt -o e:hv -l exclude:,help,verbose --name "$(basename "$0")" -- "$#")"
if [ $? -ne 0 ]
then
usage
fi
eval set -- "$params"
unset params
while true
do
case $1 in
-e|--exclude)
excludes+=("$2")
shift 2
;;
-h|--help)
usage
;;
-v|--verbose)
verbose='--verbose'
shift
;;
--)
shift
break
;;
*)
usage
;;
esac
done
With this code, you can specify -e/--exclude more than once, and ${excludes[#]} will contain all of the given excludes. After processing (-- is always present) anything remaining is stored in $#.
I have created a bash function that is the easiest to use and needs no customization. Just use the function and pass all long options with or without arguments and the function will set them as variables with the corresponding option arguments as values in your script.
function get_longOpt {
## Pass all the script's long options to this function.
## It will parse all long options with its arguments,
## will convert the option name to a variable and
## convert its option value to the variable's value.
## If the option does not have an argument, the
## resulting variable's value will be set to true.
## Works properly when providing long options, only.
## Arguments to options may not start with two dashes.
##
#### Usage
##
## get_longOpt $#
##
## May expand to:
##
## get_longOpt --myOption optimopti --longNumber 1000 --enableMe --hexNumber 0x16
##
### Results in the bash interpretation of:
## myOption=optimopti
## longNumber=1000
## enableMe=true
## hexNumber=0x16
##
local -a opt_list=( $# )
local -A opt_map
local -i index=0
local next_item
for item in ${opt_list[#]}; do
# Convert arg list to map.
let index++
next_item="${opt_list[$index]}"
if [[ "${item}" == --* ]] \
&& [[ "${next_item}" != --* ]] \
&& [[ ! -z "${next_item}" ]]
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]="${next_item}"
elif [[ "${item}" == --* ]] \
&& { [[ "${next_item}" == --* ]] \
|| [[ -z "${next_item}" ]]; }
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]=true
fi
done
for item in ${!opt_map[#]}; do
# Convert map keys to shell vars.
value="${opt_map[$item]}"
[[ ! -z "${value}" ]] && \
printf -v "$item" '%s' "$value"
done
}
The up to date original source code is available here:
https://github.com/theAkito/akito-libbash/blob/master/bishy.bash

Resources