bash Script Arguments grouping - bash

I am having problems with expansion of command-line options containing spaces. They are not getting grouped as I expect them to be. How can I modify following code(below) to get the desired output(below).
function myFunction {
while getopts "a:b:A:" optionName; do
echo "$optionName::$OPTARG"
done
}
#dynamic variable, cannot be hardcoded into $MY_ARGS
MY_VAR="X1=162356374 X2=432876 X3=342724"
#$MY_ARGS is useful and will be used more than once,
#so we don't want to eliminate it and replace it's usage with its value everywhere
MY_ARGS="-a 24765437643 -b '$MY_VAR' -A jeeywewueuye"
myFunction $MY_ARGS
Actual Output:
a::24765437643
b::'X1=162356374
Desired Output:
a::24765437643
b::X1=162356374 X2=432876 X3=342724
A::jeeywewueuye

The best way to store a list of arguments is in an array. An array can handle arguments with whitespace without problem, and you don't have to figure out how to get the quotes and backslashes just right.
MY_ARGS=(-a 24765437643 -b "$MY_VAR" -A jeeywewueuye)
myFunction "${MY_ARGS[#]}"
The only unnatural part about arrays is the weird syntax to expand them: "${array[#]}". The quotes, curly braces, and [#] notation are all important.

I agree that arrays answer the question in the best way.
Perhaps you don't want to use arrays (colleagues will not understand), or you must obey the Google Guidelines for bash (nice work, I agree with for over 90%). that claims: "If you find you need to use arrays for anything more than assignment of ${PIPESTATUS}, you should use Python. ".
When you must look for another solutions:
An ugly solution is changing the IFS:
function myFunction {
while getopts "a:b:A:" optionName; do
echo "$optionName::$OPTARG"
done
}
#dynamic variable, cannot be hardcoded into $MY_ARGS
MY_VAR="X1=162356374 X2=432876 X3=342724"
MY_ARGS="-a/24765437643/-b/"$MY_VAR"/-A/jeeywewueuye"
IFS=/
myFunction ${MY_ARGS}
Perhaps you want to do something with myFunction
function myFunction {
while getopts "a:bA:" optionName; do
case "${optionName}" in
b) echo "${optionName}::${MY_VAR}" ;;
*) echo "${optionName}::${OPTARG}" ;;
esac
done
}
or you could tr the spaces into another character before calling myFunction and tr the characters back to spaces in myFunction().

Related

Is it possible to combine bash variable search and replace with substring?

I have this function:
#! /usr/bin/env bash
function underline() {
U="${1//?/${2:--}}"
echo -e "\n$1\n${U:0:${#1}}\n"
}
underline "$1" "^-v-"
will work as expected:
$ ./u.sh "This is all you're going to see"
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v
It does what you expect. Originally it assumed that the underline character was just that, 1 character.
For "fun", I extended it to move from a single character underline to a repeating string underline (why? ... well ... because I can I suppose ... almost zero practical value in this exercise!).
And so, being the "let's make this so difficult that you need to write 2 pages of documentation to explain what's going on" sort of guy, I was wondering if the function could be written as a single line. And I don't mean:
function underline() { U="${1//?/${2:--}}"; echo -e "\n$1\n${U:0:${#1}}\n"; }
I don't think you can combine bash's variable search/replace with substring which is what is required.
I'm aware that this will work happily in zsh:
#! /usr/bin/env zsh
function underline() {
echo -e "\n$1\n${${1//?/${2:--}}:0:${#1}}\n"
}
underline $1 "^-v-"
e.g.
$ ./u.sh "This is all you're going to see"
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v
but not capable of this in bash (it seems).
No, you cannot combine those in a single parameter expansion in bash. But this single printf (a builtin in bash) should do the trick:
underline() { printf '%s\n%.*s\n' "$1" ${#1} "${1//?/${2:--}}"; }
underline "This is all you're going to see" "^-v-"
outputs
This is all you're going to see
^-v-^-v-^-v-^-v-^-v-^-v-^-v-^-v

Parameter expansion with replacement, avoid additional variable

I'm trying to join input $* which is one parameter consisting of all the parameters added together.
This works.
#!/bin/bash
foo() {
params="${*}"
echo "${params//[[:space:]]/-}"
}
foo 1 2 3 4
1-2-3-4
However, is it possible to skip the assignment of variable?
"${"${*}"//[[:space:]]/-}"
I'm getting bad substitution error.
I can also do
: "${*}"
echo "${_//[[:space:]]/-}"
But it feels hacky.
One option could be to set bash's internal field separator, IFS, to - locally and just echo "$*":
foo() {
local IFS=$'-'
echo "$*"
}
To answer your question, you can do global pattern substitutions on the positional parameters like this:
${*//pat/sub}
${#//pat/sub}
And also arrays like this:
${arr[*]//pat/sub}
${arr[#]//pat/sub}
This won’t join the parameters, but substitute inside them.
Setting IFS to dash adds a dash in between each parameter for echo "$*", or p=$*, but won’t replace anything inside a parameter.
Eg:
$ set -- aa bb 'cc cc'
$ IFS=-
$ echo "$*"
aa-bb-cc cc
To remove all whitespace, including inside a parameter, you can combine them:
IFS=-
echo "${*//[[:space:]]/-}"
Or just assign to a name first, like you were doing:
no_spaces=$*
echo "${no_spaces//[[:space:]]/-}"

Creating and populating dynamically named arrays in bash

I am creating dynamic arrays which all have different letters in their name. For the purpose of this question, my initial array of letters has been set at a fixed length. However, in my final implementation this letter array will be any length.
For each letter, I construct a string
I declare a new array with that string, making use of eval to evaluate the variable's value within the declare command.
I add some values to the array, again using eval to evaluate any variable values.
Here is the code:
declare -a LETTER_ARRAY=( "A" "B" "C" "D" "E" )
for i in "${LETTER_ARRAY[#]}"
do
name_string="apple${i}"
color0="red"
color1="green"
eval "declare -a ${name_string}_array"
eval "${name_string}_array[0]=$color0"
eval "${name_string}_array[1]=$color1"
done
So, how can I iterate through these dynamic arrays and echo what is in them? I have tried the following:
for i in "${LETTER_ARRAY[#]}"
do
eval "array_name='apple${i}_array'"
echo ${array_name[0]}
done
This has not worked for me. I can confirm that my dynamic arrays were successfully created and populated, as when I echo out a value manually, I get a result:
echo ${appleA_array[0]}
red
A perfect place to use a namereference:
letter_array=(A B C D E)
for i in "${letter_array[#]}"; do
declare -n var="apple${i}_array"
var[0]=red
var[1]=green
done
declare -p appleA_array
would output:
declare -a appleA_array=([0]="red" [1]="green")
how can I iterate through these dynamic arrays and echo what is in them?
With the above:
for i in "${letter_array[#]}"; do
declare -n var="apple${i}_array"
printf "%s\n" "${var[0]}"
done
Notes:
Do not use eval. Eval is evil.
Do not use upper case variables, by convention they are used for exported variables, like COLUMNS, PWD, UID, EUID, LINES. Use lower case variables in your scripts.
Check your scripts with http://shellcheck.net for most common mistakes
But if you are creating a 2d array, then an associative array might be better:
declare -A apple_arrays
letter_array=(A B C D E)
for i in "${letter_array[#]}"; do
apple_arrays[$i,0]=red
apple_arrays[$i,1]=green
done
for i in "${letter_array[#]}"; do
printf "one=%s two=%s\n" "${apple_arrays[$i,0]}" "${apple_arrays[$i,1]}"
done
how can I iterate through these dynamic arrays
echo ${array_name[0]} does not work because array_name is not the name of an array; $array_name is. Therefore, eval "echo \"\${${array_name}[0]}\"" would to the trick.
However, I'd recommend namerefs.
By The way: declare works without eval and is more reliable that way.
#! /usr/bin/env bash
letters=({A..E})
for i in "${letters[#]}"; do
declare -a "apple${i}_array=(red green)"
done
for i in "${letters[#]}"; do
declare -n array="apple${i}_array"
# now you can use `array` as if it was `appleA_array`, `appleB_array`, ...
echo "${array[0]}"
done
Your first line is not bash syntax. If I try the line
declare -a LETTER_ARRAY = [ "A" "B" "C" "D" "E" ]
I get:
bash: declare: `=': not a valid identifier
bash: declare: `[': not a valid identifier
bash: declare: `]': not a valid identifier
I think, you get similar error messages, but you ignored them
More errors:
Forgotten do
case mismatch: suffix on definition: _array, and for output: _ARRAY
Use always double quotes when using [#]
One correct syntax is:
declare -a LETTER_ARRAY=( "A" "B" "C" "D" "E" )
for i in "${LETTER_ARRAY[#]}"
do
name_string="apple${i}"
color0="red"
color1="green"
eval "declare -a ${name_string}_array"
echo "${name_string}_array[0]=$color0"
eval "${name_string}_array[0]=$color0"
eval "${name_string}_array[1]=$color1"
done
echo ${appleA_array[0]}
Your eval "array_name='AZ${i}_ARRAY'" makes array_name a scalar, not an array. Arrays in bash are usually created like this
arr=( your elements go here )
If you want to assign one array to another, you have to interpolate the elements between those parenthesis, for instance:
arr=( ${other_array[#]} )
Since you are using bash, this would perform word splitting the elements of other_array, if they contain spaces. Hence you would usually write it for the safe side as
arr=( "${other_array[#]}" )
Hence, for your case, you could do a
eval "array_name=( \${AZ${i}_ARRAY[#]} )"
This causes an array array_name to be created, with the elements of the respective AZi_ARRAY.
I omitted here for simplicity the prevention against word splitting, because in your example, the array elements contain single words only.

How to parse a string into variables?

I know how to parse a string into variables in the manner of this SO question, e.g.
ABCDE-123456
becomes:
var1=ABCDE
var2=123456
via, say, cut. I can do that in one script, no problem.
But I have a few dozen scripts which parse strings/arguments all in the same fashion (same arguments & variables, i.e. same parsing strategy).
And sometimes I need to make a change or add a variable to the parsing mechanism.
Of course, I could go through every one of my dozens of scripts and change the parsing manually (even if just copy & paste), but that would be tedious and more error-prone to bugs/mistakes.
Is there a modular way to do parse strings/arguments as such?
I thought of writing a script which parses the string/args into variables and then exports, but the export command does not work form child-to-parent, (only vice-versa).
Something like this might work:
parse_it () {
SEP=${SEP--}
string=$1
names=${#:2}
IFS="$SEP" read $names <<< "$string"
}
$ parse_it ABCDE-123456 var1 var2
$ echo "$var1"
ABCDE
$ echo "$var2"
123456
$ SEP=: parse_it "foo:bar:baz" id1 id2 id3
$ echo $id2
bar
The first argument is the string to parse, the remaining arguments are names of variables that get passed to read as the variables to set. (Not quoting $names here is intentional, as we will let the shell split the string into multiple words, one per variable. Valid variable names consist of only _, letters, and numbers, so there are no worries about undesired word splitting or pathname generation by not quoting $names). The function assumes the string uses a single separator of "-", which can be overridden via the environment.
For more complex parsing, you may want to use a custom regular expression (bash 4 or later required for the -g flag to declare):
parse_it () {
reg_ex=$1
string=$2
shift 2
[[ $string =~ $reg_ex ]] || return
i=1
for name; do
declare -g "$name=${BASH_REMATCH[i++]}"
done
}
$ parse_it '(.*)-(.*):(.*)' "abc-123:xyz" id1 id2 id3
$ echo "$id2"
123
I think what you really want is to write your function in one script and include it in all of your other scripts.
You can include other shell scripts by the source or . command.
For example, you can define your parse function in parseString.sh
function parseString {
...
}
And then in any of your other script, do
source parseString.sh
# now we can call parseString function
parseString abcde-12345

Create associative array in bash 3

After thoroughly searching for a way to create an associative array in bash, I found that declare -A array will do the trick. But the problem is, it is only for bash version 4 and the bash version the server has in our system is 3.2.16.
How can I achieve some sort of associative array-like hack in bash 3? The values will be passed to a script like
ARG=array[key];
./script.sh ${ARG}
EDIT: I know that I can do this in awk, or other tools but strict bash is needed for the scenario I am trying to solve.
Bash 3 has no associative arrays, so you're going to have to use some other language feature(s) for your purpose. Note that even under bash 4, the code you wrote doesn't do what you claim it does: ./script.sh ${ARG} does not pass the associative array to the child script, because ${ARG} expands to nothing when ARG is an associative array. You cannot pass an associative array to a child process, you need to encode it anyway.
You need to define some argument passing protocol between the parent script and the child script. A common one is to pass arguments in the form key=value. This assumes that the character = does not appear in keys.
You also need to figure out how to represent the associative array in the parent script and in the child script. They need not use the same representation.
A common method to represent an associative array is to use separate variables for each element, with a common naming prefix. This requires that the key name only consists of ASCII letters (of either case), digits and underscores. For example, instead of ${myarray[key]}, write ${myarray__key}. If the key is determined at run time, you need a round of expansion first: instead of ${myarray[$key]}, write
n=myarray__${key}; echo ${!n}
For an assignment, use printf -v. Note the %s format to printf to use the specified value. Do not write printf -v "myarray__${key}" %s "$value" since that would treat $value as a format and perform printf % expansion on it.
printf -v "myarray__${key}" %s "$value"
If you need to pass an associative array represented like this to a child process with the key=value argument representation, you can use ${!myarray__*} to enumerate over all the variables whose name begins with myarray__.
args=()
for k in ${!myarray__*}; do
n=$k
args+=("$k=${!n}")
done
In the child process, to convert arguments of the form key=value to separate variables with a prefix:
for x; do
if [[ $x != *=* ]]; then echo 1>&2 "KEY=VALUE expected, but got $x"; exit 120; fi
printf -v "myarray__${x%%=*}" %s "${x#*=}"
done
By the way, are you sure that this is what you need? Instead of calling a bash script from another bash script, you might want to run the child script in a subshell instead. That way it would inherit from all the variables of the parent.
Here is another post/explanation on associative arrays in bash 3 and older using parameter expansion:
https://stackoverflow.com/a/4444841
Gilles' method has a nice if statement to catch delimiter issues, sanitize oddball input ...etc. Use that.
If you are somewhat familiar with parameter expansion:
http://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
To use in your scenario [ as stated: sending to script ]:
Script 1:
sending_array.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
bash ./receive_arr.sh "${ARRAY[#]}"
Script 2: receive_arr.sh
argAry1=("$#")
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
process_arr argAry1[#]
exit 0
Method 2, sourcing the second script:
Script 1:
sending_array.sh
source ./receive_arr.sh
# A pretend Python dictionary with bash 3
ARRAY=( "cow:moo"
"dinosaur:roar"
"bird:chirp"
"bash:rock" )
process_arr ARRAY[#]
Script 2: receive_arr.sh
function process_arr () {
declare -a hash=("${!1}")
for animal in "${hash[#]}"; do
echo "Key: ${animal%%:*}"
echo "Value: ${animal#*:}"
done
}
References:
Passing arrays as parameters in bash
If you don't want to handle a lot of variables, or keys are simply invalid variable identifiers, and your array is guaranteed to have less than 256 items, you can abuse function return values. This solution does not require any subshell as the value is readily available as a variable, nor any iteration so that performance screams. Also it's very readable, almost like the Bash 4 version.
Here's the most basic version:
hash_index() {
case $1 in
'foo') return 0;;
'bar') return 1;;
'baz') return 2;;
esac
}
hash_vals=("foo_val"
"bar_val"
"baz_val");
hash_index "foo"
echo ${hash_vals[$?]}
More details and variants in this answer
You can write the key-value pairs to a file and then grep by key. If you use a pattern like
key=value
then you can egrep for ^key= which makes this pretty safe.
To "overwrite" a value, just append the new value at the end of the file and use tail -1 to get just the last result of egrep
Alternatively, you can put this information into a normal array using key=value as value for the array and then iterator over the array to find the value.
This turns out to be ridiculously easy. I had to convert a bash 4 script that used a bunch of associative arrays to bash 3. These two helper functions did it all:
array_exp() {
exp=${#//[/__}
eval "${exp//]}"
}
array_clear() {
unset $(array_exp "echo \${!$1__*}")
}
I'm flabbergasted that this actually works, but that's the beauty of bash.
E.g.
((all[ping_lo] += counts[ping_lo]))
becomes
array_exp '((all[ping_lo] += counts[ping_lo]))'
Or this print statement:
printf "%3d" ${counts[ping_lo]} >> $return
becomes
array_exp 'printf "%3d" ${counts[ping_lo]}' >> $return
The only syntax that changes is clearing. This:
counts=()
becomes
array_clear counts
and you're set. You could easily tell array_exp to recognize expressions like "=()" and handle them by rewriting them as array_clear expressions, but I prefer the simplicity of the above two functions.

Resources