Pass argument or Ignore it if not passed BASH - bash

Got an interesting scripting problem working with a Cli tool
-a means argument in this cli tool
In a Put or Post case, I must pass two arguments. So:
--verb "$1" \
-a "$2" \
-a "$3"
Passes in script
"put" "[\"name\"]" "testing this"
Works fine!
Then In a Get case, I must pass one argument. So:
--verb "$1" \
-a "$2" \
-a "$3"
Passes in script
"get" "[\"name\"]"
Of course this would fail because I MUST pass one argument but I did pass two, now that favors only the PUT operation, how do you think I handle this to make both PUT and GET work?
This is all in bash

For more general-case handling (which will also work correctly with more than three arguments), construct an array:
#!/usr/bin/env bash
case $BASH_VERSION in '') echo "ERROR: This must be run with bash, not sh" >&2; exit 1;; esac
# unconditionally, we always have a verb; assign to a variable then shift away
args=( --verb "$1" ) # this creates an array
shift # makes old $2 be $1, old $3 be $2, etc
# iterate over remaining arguments and add each preceded by '-a'
for arg in "$#"; do # iterate over all args left after the shift
args+=( -a "$arg" ) # for each, add '-a' then that arg to our array
done
# use the constructed array
runYourProgramWith "${args[#]}"
To only correctly handle $3 being optional:
--verb "$1" \
-a "$2" \
${3+ -a "$3" }

Related

How to pass an argument passed to a bashrc function? [duplicate]

I am trying to search how to pass parameters in a Bash function, but what comes up is always how to pass parameter from the command line.
I would like to pass parameters within my script. I tried:
myBackupFunction("..", "...", "xx")
function myBackupFunction($directory, $options, $rootPassword) {
...
}
But the syntax is not correct. How can I pass a parameter to my function?
There are two typical ways of declaring a function. I prefer the second approach.
function function_name {
command...
}
or
function_name () {
command...
}
To call a function with arguments:
function_name "$arg1" "$arg2"
The function refers to passed arguments by their position (not by name), that is $1, $2, and so forth. $0 is the name of the script itself.
Example:
function_name () {
echo "Parameter #1 is $1"
}
Also, you need to call your function after it is declared.
#!/usr/bin/env sh
foo 1 # this will fail because foo has not been declared yet.
foo() {
echo "Parameter #1 is $1"
}
foo 2 # this will work.
Output:
./myScript.sh: line 2: foo: command not found
Parameter #1 is 2
Reference: Advanced Bash-Scripting Guide.
Knowledge of high level programming languages (C/C++, Java, PHP, Python, Perl, etc.) would suggest to the layman that Bourne Again Shell (Bash) functions should work like they do in those other languages.
Instead, Bash functions work like shell commands and expect arguments to be passed to them in the same way one might pass an option to a shell command (e.g. ls -l). In effect, function arguments in Bash are treated as positional parameters ($1, $2..$9, ${10}, ${11}, and so on). This is no surprise considering how getopts works. Do not use parentheses to call a function in Bash.
(Note: I happen to be working on OpenSolaris at the moment.)
# Bash style declaration for all you PHP/JavaScript junkies. :-)
# $1 is the directory to archive
# $2 is the name of the tar and zipped file when all is done.
function backupWebRoot ()
{
tar -cvf - "$1" | zip -n .jpg:.gif:.png "$2" - 2>> $errorlog &&
echo -e "\nTarball created!\n"
}
# sh style declaration for the purist in you. ;-)
# $1 is the directory to archive
# $2 is the name of the tar and zipped file when all is done.
backupWebRoot ()
{
tar -cvf - "$1" | zip -n .jpg:.gif:.png "$2" - 2>> $errorlog &&
echo -e "\nTarball created!\n"
}
# In the actual shell script
# $0 $1 $2
backupWebRoot ~/public/www/ webSite.tar.zip
Want to use names for variables? Just do something this.
local filename=$1 # The keyword declare can be used, but local is semantically more specific.
Be careful, though. If an argument to a function has a space in it, you may want to do this instead! Otherwise, $1 might not be what you think it is.
local filename="$1" # Just to be on the safe side. Although, if $1 was an integer, then what? Is that even possible? Humm.
Want to pass an array to a function by value?
callingSomeFunction "${someArray[#]}" # Expands to all array elements.
Inside the function, handle the arguments like this.
function callingSomeFunction ()
{
for value in "$#" # You want to use "$#" here, not "$*" !!!!!
do
:
done
}
Need to pass a value and an array, but still use "$#" inside the function?
function linearSearch ()
{
local myVar="$1"
shift 1 # Removes $1 from the parameter list
for value in "$#" # Represents the remaining parameters.
do
if [[ $value == $myVar ]]
then
echo -e "Found it!\t... after a while."
return 0
fi
done
return 1
}
linearSearch $someStringValue "${someArray[#]}"
In Bash 4.3 and above, you can pass an array to a function by reference by defining the parameter of a function with the -n option.
function callingSomeFunction ()
{
local -n someArray=$1 # also ${1:?} to make the parameter mandatory.
for value in "${someArray[#]}" # Nice!
do
:
done
}
callingSomeFunction myArray # No $ in front of the argument. You pass by name, not expansion / value.
If you prefer named parameters, it's possible (with a few tricks) to actually pass named parameters to functions (also makes it possible to pass arrays and references).
The method I developed allows you to define named parameters passed to a function like this:
function example { args : string firstName , string lastName , integer age } {
echo "My name is ${firstName} ${lastName} and I am ${age} years old."
}
You can also annotate arguments as #required or #readonly, create ...rest arguments, create arrays from sequential arguments (using e.g. string[4]) and optionally list the arguments in multiple lines:
function example {
args
: #required string firstName
: string lastName
: integer age
: string[] ...favoriteHobbies
echo "My name is ${firstName} ${lastName} and I am ${age} years old."
echo "My favorite hobbies include: ${favoriteHobbies[*]}"
}
In other words, not only you can call your parameters by their names (which makes up for a more readable core), you can actually pass arrays (and references to variables - this feature works only in Bash 4.3 though)! Plus, the mapped variables are all in the local scope, just as $1 (and others).
The code that makes this work is pretty light and works both in Bash 3 and Bash 4 (these are the only versions I've tested it with). If you're interested in more tricks like this that make developing with bash much nicer and easier, you can take a look at my Bash Infinity Framework, the code below is available as one of its functionalities.
shopt -s expand_aliases
function assignTrap {
local evalString
local -i paramIndex=${__paramIndex-0}
local initialCommand="${1-}"
if [[ "$initialCommand" != ":" ]]
then
echo "trap - DEBUG; eval \"${__previousTrap}\"; unset __previousTrap; unset __paramIndex;"
return
fi
while [[ "${1-}" == "," || "${1-}" == "${initialCommand}" ]] || [[ "${##}" -gt 0 && "$paramIndex" -eq 0 ]]
do
shift # First colon ":" or next parameter's comma ","
paramIndex+=1
local -a decorators=()
while [[ "${1-}" == "#"* ]]
do
decorators+=( "$1" )
shift
done
local declaration=
local wrapLeft='"'
local wrapRight='"'
local nextType="$1"
local length=1
case ${nextType} in
string | boolean) declaration="local " ;;
integer) declaration="local -i" ;;
reference) declaration="local -n" ;;
arrayDeclaration) declaration="local -a"; wrapLeft= ; wrapRight= ;;
assocDeclaration) declaration="local -A"; wrapLeft= ; wrapRight= ;;
"string["*"]") declaration="local -a"; length="${nextType//[a-z\[\]]}" ;;
"integer["*"]") declaration="local -ai"; length="${nextType//[a-z\[\]]}" ;;
esac
if [[ "${declaration}" != "" ]]
then
shift
local nextName="$1"
for decorator in "${decorators[#]}"
do
case ${decorator} in
#readonly) declaration+="r" ;;
#required) evalString+="[[ ! -z \$${paramIndex} ]] || echo \"Parameter '$nextName' ($nextType) is marked as required by '${FUNCNAME[1]}' function.\"; " >&2 ;;
#global) declaration+="g" ;;
esac
done
local paramRange="$paramIndex"
if [[ -z "$length" ]]
then
# ...rest
paramRange="{#:$paramIndex}"
# trim leading ...
nextName="${nextName//\./}"
if [[ "${##}" -gt 1 ]]
then
echo "Unexpected arguments after a rest array ($nextName) in '${FUNCNAME[1]}' function." >&2
fi
elif [[ "$length" -gt 1 ]]
then
paramRange="{#:$paramIndex:$length}"
paramIndex+=$((length - 1))
fi
evalString+="${declaration} ${nextName}=${wrapLeft}\$${paramRange}${wrapRight}; "
# Continue to the next parameter:
shift
fi
done
echo "${evalString} local -i __paramIndex=${paramIndex};"
}
alias args='local __previousTrap=$(trap -p DEBUG); trap "eval \"\$(assignTrap \$BASH_COMMAND)\";" DEBUG;'
Drop the parentheses and commas:
myBackupFunction ".." "..." "xx"
And the function should look like this:
function myBackupFunction() {
# Here $1 is the first parameter, $2 the second, etc.
}
A simple example that will clear both during executing script or inside script while calling a function.
#!/bin/bash
echo "parameterized function example"
function print_param_value(){
value1="${1}" # $1 represent first argument
value2="${2}" # $2 represent second argument
echo "param 1 is ${value1}" # As string
echo "param 2 is ${value2}"
sum=$(($value1+$value2)) # Process them as number
echo "The sum of two value is ${sum}"
}
print_param_value "6" "4" # Space-separated value
# You can also pass parameters during executing the script
print_param_value "$1" "$2" # Parameter $1 and $2 during execution
# Suppose our script name is "param_example".
# Call it like this:
#
# ./param_example 5 5
#
# Now the parameters will be $1=5 and $2=5
It takes two numbers from the user, feeds them to the function called add (in the very last line of the code), and add will sum them up and print them.
#!/bin/bash
read -p "Enter the first value: " x
read -p "Enter the second value: " y
add(){
arg1=$1 # arg1 gets to be the first assigned argument (note there are no spaces)
arg2=$2 # arg2 gets to be the second assigned argument (note there are no spaces)
echo $(($arg1 + $arg2))
}
add x y # Feeding the arguments
Another way to pass named parameters to Bash... is passing by reference. This is supported as of Bash 4.0
#!/bin/bash
function myBackupFunction(){ # directory options destination filename
local directory="$1" options="$2" destination="$3" filename="$4";
echo "tar cz ${!options} ${!directory} | ssh root#backupserver \"cat > /mnt/${!destination}/${!filename}.tgz\"";
}
declare -A backup=([directory]=".." [options]="..." [destination]="backups" [filename]="backup" );
myBackupFunction backup[directory] backup[options] backup[destination] backup[filename];
An alternative syntax for Bash 4.3 is using a nameref.
Although the nameref is a lot more convenient in that it seamlessly dereferences, some older supported distros still ship an older version, so I won't recommend it quite yet.

Pass an array via command-line to be handled with getopts()

I try to pass some parameters to my .bash file.
terminal:
arr=("E1" "E2" "E3")
param1=("foo")
param2=("bar")
now I want to call my execute.bash file.
execute.bash -a ${arr[#]} -p $param1 -c param2
this is my file:
execute.bash:
while getopts ":a:p:c:" opt; do
case $opt in
a) ARRAY=${OPTARG};;
p) PARAM1=${OPTARG};;
c) PARAM2=${OPTARG};;
\?) exit "Invalid option -$OPTARG";;
esac
done
for a in "${ARRAY[#]}"; do
echo "$a"
done
echo "$PARAM1"
echo "$PARAM2"
But my file only prints:
E1
foo
bar
Whats the problem with my script?
Expanding all values in the array using ${arr[#]} expands each value as a separate command-line argument, so getopt only sees the first value as the parameter to the "-a" option.
If you expand using ${arr[*]} then all of the array values are expanded into a single command-line argument, so getopt can see all of the values in the array as a single argument to the "-a" option.
There are a couple of other issues: you need to quote the values on the command line:
< execute.bash -a ${arr[#]} -p $param1 -c param2
> execute.bash -a "${arr[*]}" -p $param1 -c $param2
and use braces around ${OPTARG} in the getopt processing to make it an array assignment:
< a) ARRAY=${OPTARG};;
> a) ARRAY=(${OPTARG});;
after making these changes, I get this output:
E1
E2
E3
foo
bar
which I think is what you are expecting.
You have a problem with passing the array as one of the parameter for the -a flag. Arrays in bash get expanded in command line before the actual script is invoked. The "${array[#]}" expansions outputs words separated by white-space
So your script is passed as
-a "E1" "E2" "E3" -p foo -c bar
So with the getopts() call to the argument OPTARG for -a won't be populated with not more than the first value, i.e. only E1. One would way to achieve this is to use the array expansion of type "${array[*]}" which concatenates the string with the default IFS (white-space), so that -a now sees one string with the words of the array concatenated, i.e. as if passed as
-a "E1 E2 E3" -p foo -c bar
I've emphasized the quote to show arg for -a will be received in getopts()
#!/usr/bin/env bash
while getopts ":a:p:c:" opt; do
case $opt in
a) ARRAY="${OPTARG}";;
p) PARAM1="${OPTARG}";;
c) PARAM2="${OPTARG}";;
\?) exit "Invalid option -$OPTARG";;
esac
done
# From the received string ARRAY we are basically re-constructing another
# array splitting on the default IFS character which can be iterated over
# as in your input example
read -r -a splitArray <<<"$ARRAY"
for a in "${splitArray[#]}"; do
echo "$a"
done
echo "$PARAM1"
echo "$PARAM2"
and now call the script with args as. Note that you are using param1 and param2 are variables but your definition seems to show it as an array. Your initialization should just look like
arr=("E1" "E2" "E3")
param1="foo"
param2="bar"
and invoked as
-a "${arr[*]}" -p "$param1" -c "$param2"
A word of caution would be to ensure that the words in the array arr don't already contain words that contain spaces. Reading them back as above in that case would have a problem of having those words split because the nature of IFS handling in bash. In that case though use a different de-limiter say |, # while passing the array expansion.
If I want to export the array MY_ARRAY, I use at caller side:
[[ $MY_ARRAY ]] && export A_MY_ARRAY=$(declare -p MY_ARRAY)
... and at at sub script side:
[[ $A_MY_ARRAY =~ ^declare ]] && eval $A_MY_ARRAY
The concept works for parameters too. At caller side:
SUB_SCRIPT "$(declare -p MY_ARRAY)"
... and at at sub script side:
[[ $1 =~ ^declare ]] && eval $1
The only issue of both solution is, that the variable names are the same at both sides. This can be changed if replacing the variable name before expanding it.

For Loop for Arguments Collects Spaces Instead of Arguments

I've created a shell script that has the SSH address as the first argument, then has one or more arguments afterwards. The script is capable of working with two arguments or less (the address and the single argument), but it is not able to work properly after. Below is my code to get a better idea.
ssh.sh $1 << EOF
$(typeset -f sr_single "$#")
if [ "$#" -eq 2 ]; then
echo $2
sr_single $2
elif [ "$#" -lt 2 ]; then
echo "Needs at least two arguments: serial number and argument(s)"
else
echo "${#:2}"
for i in "${#:2}"; do
echo "'" $i "'"
sr_single $i
done
fi
EOF
Below is what it returns if I call the function "sr.sh test#ssh.com -l"
-l
And below is what it returns when I call "sr.sh test#ssh.com -l -v"
-l -v
' '
My question is how is this function not getting the second variable and the ones after on this one, where it seems to be working properly in the rest of the program? Thanks
To do this with less hair loss, define all your code in functions, like so:
rmt_main() {
if (( $# == 2 )); then
printf 'Exactly arguments received; $2 is: %q\n' "$2" >&2
elif (( $# < 2 )); then
echo 'Error: Needs at least two arguments' >&2
else
otherfunc "${#:2}"
fi
}
otherfunc() {
echo "Otherfunc called with arguments:" >&2
printf '%q\n' "$#"
}
...after which, calling can look like:
# generate an eval-safe string containing your arguments
printf -v args_str '%q ' "$#"
# explicitly invoke bash, so we don't need to worry about whether our escaping is POSIX-y
ssh "$1" 'bash -s' <<EOF
$(typeset -f rmt_main otherfunc) # emit function definitions
rmt_main $args_str # and call rmt_main with the eval-safe argument list
EOF
Note that the only contents we're expanding inside the heredoc are generated by the local shell in a form guaranteed to be correctly escaped to parse as code (by the remote shell). We are not under any circumstances expanding data (like command-line arguments) into the heredoc in unescaped form, and all our functions use completely conventional quoting (which is to say that all parameter expansions inside the function definitions are quoted).

How to set an arbitrary positional argument, while still preserving the rest?

I would like to do something like this, but preserve every argument after $i:
for i in "$#"; do
if [[ $i == "--" ]]; then
set $i "-S --"
break
fi
done
ls "$#"
In this example, I want to make a simple wrapper over ls where -S is always the final option that is applied.
This is simple if the arguments do not have "--":
ls "$#" -S
However, this breaks whenever there is a "--" as an argument.
To work around this, I would like to find the first occurrence of -- and place an -S before it.
EDIT:
The reason why I do not use:
ls -S "$#"
is because I want the output to be sorted by size LAST. So if -t is passed into the arguments, the output should be sorted by modification time THEN by size. That use case fails here:
ls -S -t
Create a second array by iterating over the first one and inserting -S where needed.
#! /bin/bash
arr=()
for arg in "$#" ; do
if [[ $arg == -- ]] ; then
arr+=(-S --)
else
arr+=("$arg")
fi
done
ls "${arr[#]}"
You might need to insert it just once to be utterly correct:
#! /bin/bash
arr=()
inserted=
for arg in "$#" ; do
if [[ $arg == -- && ! $inserted ]] ; then
arr+=(-S --)
inserted=1
else
arr+=("$arg")
fi
done
If you really need to set the positional arguments, use
set "${arr[#]}"
to set positional arguments to the members of ${arr[#]}.

Bash: handling mass arguments

I'd like to be able to handle multiple arguments to a given flag no matter what the order of flags is. Do you guys think this is acceptable? Any improvements?
So:
$ ./script -c opt1 opt2 opt3 -b foo
opt1 opt2 opt3
foo
Code:
echo_args () {
echo "$#"
}
while (( $# > 0 )); do
case "$1" in
-b)
echo $2
;;
-c|--create)
c_args=()
# start looping from this flag
for arg in ${#:2}; do
[ "${arg:0:1}" == "-" ] && break
c_args+=("$arg")
done
echo_args "${c_args[#]}"
;;
*)
echo "huh?"
;;
esac
shift 1
done
The getopts utility shall retrieve options and option-arguments from a list of parameters.
$ cat script.sh
cflag=
bflag=
while getopts c:b: name
do
case $name in
b) bflag=1
bval="$OPTARG";;
c) cflag=1
cval="$OPTARG";;
?) printf "Usage: %s: [-c value] [-b value] args\n" $0
exit 2;;
esac
done
if [ ! -z "$bflag" ]; then
printf 'Option -b "%s" specified\n' "$bval"
fi
if [ ! -z "$cflag" ]; then
printf 'Option -c "%s" specified\n' "$cval"
fi
shift $(($OPTIND - 1))
printf "Remaining arguments are: %s\n" "$*"
Note the Guideline 8:
When multiple option-arguments are specified to follow a single option, they should be presented as a single argument, using commas within that argument or <blank>s within that argument to separate them.
$ ./script.sh -c "opt1 opt2 opt3" -b foo
Option -b "foo" specified
Option -c "opt1 opt2 opt3" specified
Remaining arguments are:
The standard links are listed below:
getopts - parse utility options
Section 12.2 Utility Syntax Guidelines
I noticed in the comments that you don't want to use any of these. What you could do is set all of the arguments as a string, then sort them using a loop, pulling out the ones you want to set as switched and sorting them using if statements. It is a little brutish, but it can be done.
#!/bin/bash
#set all of the arguments as a variable
ARGUMENTS=$#
# Look at each argument and determine what to do with it.
for i in $ARGUMENTS; do
# If the previous loop was -b then grab the value of this argument
if [[ "$bgrab" == "1" ]]; then
#adds the value of -b to the b string
bval="$bval $i"
bgrab="0"
else
# If this argument is -b, prepare to grab the next argument and assign it
if [[ "$i" == "-b" ]]; then
bgrab="1"
else
#Collect the remaining arguments into one list per your example
RemainingArgs="$RemainingArgs $i"
fi
fi
done
echo "Arguments: $RemainingArgs"
echo "B Value: $bval"
I use something similar in a lot of my scripts because there are a significant amount of arguments that can be fed into some of them, and the script needs to look at each one to figure out what to do. They can be out of order or not exist at all and the code still has to work.

Resources