getopts to get multiple values for same argument - bash

I am looking to get multiple values from same argument using getopts. I want to use this script to ssh and run commands on a list of hosts provided through a file.
Usage: .\ssh.sh -f file_of_hosts.txt -c "Command1" "command2"
Expected output:
ssh userid#server1:command1
ssh userid#server1:command2
ssh userid#server2:commnand1
ssh userid#server2:commnand2
Sample Code I used but failed to get expected results
id="rm08397"
while getopts ":i:d:s:f:" opt
do
case $opt in
f ) file=$OPTARG;;
c ) cmd=$OPTARG;;
esac
done
shift "$(($OPTIND -1))"
# serv=$(for host in `cat $file`
# do
# echo -e "$host#"
# done
# )
# for names in $serv
# do
# ssh $id#$serv:
for hosts in $file;do
for cmds in $cmd;do
o1=$id#$hosts $cmds
echo $o1
done
done

You can achieve the effect by repeating -c :
declare -a cmds
id="rm08397"
while getopts ":c:f:" opt
do
case $opt in
f ) file="$OPTARG";;
c ) cmds+=("$OPTARG");;
esac
done
shift $((OPTIND -1))
for host in $(<$file);do
for cmd in "${cmds[#]}";do
echo ssh "$id#$host" "$cmd"
done
done
# Usage: ./ssh.sh -f file_of_hosts.txt -c "Command1" -c "command2"

Related

Why do shortened versions of long options work with getopt?

In the following script:
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF \
USAGE: ${0} \
EOF
}
## Defining_Version
version=1.0
## Defining_Input
options=$(getopt -o "t:" -l "h,help,v,version,taxonomy:" -a -- "$#")
eval set -- "$options"
while true;do
case $1 in
-h|--h|-help|--help)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
esac
shift
done
## Defining Taxonomy Default Value (in case is not provided)
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The commands:
./script.sh -v
./script.sh --v
./script.sh -version
./script.sh --version
Work as expected. But what I do not understand is why the commands:
./script.sh -ver
./script.sh --ver
work at all. An equivalent unexpected behavior is also observed for the commands:
./script.sh -tax 22
./script.sh --tax 22
I would be grateful to get an explanation and/or a way to correct this unexpected behavior.
Note that getopt is an external utility unrelated to Bash.
what I do not understand is why the commands: .. work at all.
Because getopt was designed to support it, there is no other explanation. From man getopt:
[...] Long options may be abbreviated, as long as the abbreviation is not ambiguous.
Unambiguous abbreviations of long options are converted to long options.
Based on the comments I have received, specially from #CharlesDuffy, I have modified my code to what I believe is a more robust and compatible version. Importantly, the code below addresses the pitfalls of the original code
#!/usr/bin/env bash
func_usage ()
{
cat <<EOF
USAGE: ${0}
EOF
## Defining_Version
version=1.0
## Defining_Input
while true;do
case $1 in
-h|--h|-help|--help|-\?|--\?)
func_usage
exit 0
;;
-v|--v|-version|--version)
echo $version
;;
-t|--t|-taxonomy|--taxonomy)
echo "Option t = $2 ";
Taxonomy_ID=$2
echo $Taxonomy_ID
shift
;;
--)
shift
break;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
;;
*)
break
esac
shift
done
TaxonomyID=${Taxonomy_ID:=9606};
echo $TaxonomyID
exit 0
The code above behaves as expected in that the commands:
./script -tax 22
Gives the warning:
WARN: Unknown option (ignored): -tax
9606
As expected

It is possible to mix options and arguments?

Is it possible to mix options (with getopts) and arguments ($1....$10)?
getopt (singular) can handle options and arguments intermixed, as well as short -s and long --long options and -- to end options processing.
See here how file1 and file2 are mixed with options and it separates them out:
$ args=(-ab opt file1 -c opt file2)
$ getopt -o ab:c: -- "${args[#]}"
-a -b 'opt' -c 'opt' -- 'file1' 'file2'
Typical usage looks like:
#!/bin/bash
options=$(getopt -o ab:c: -l alpha,bravo:,charlie: -- "$#") || exit
eval set -- "$options"
# Option variables.
alpha=0
bravo=
charlie=
# Parse each option until we hit `--`, which signals the end of options.
# Don't actually do anything yet; just save their values and check for errors.
while [[ $1 != -- ]]; do
case $1 in
-a|--alpha) alpha=1; shift 1;;
-b|--bravo) bravo=$2; shift 2;;
-c|--charlie) charlie=$2; shift 2;;
*) echo "bad option: $1" >&2; exit 1;;
esac
done
# Discard `--`.
shift
# Here's where you'd actually execute the options.
echo "alpha: $alpha"
echo "bravo: $bravo"
echo "charlie: $charlie"
# File names are available as $1, $2, etc., or in the "$#" array.
for file in "$#"; do
echo "file: $file"
done

Bash using optional parameters

I am trying to create a function that will take in $PASS, $USER, and $COMMAND as inputs the $USER and $PASS are optional, meaning it will use default username and passwords if non was supplied as parameters. Here is my function
function exec_ssh_command() {
local PASS=${1:-${ROOT_PW}}; # use argument supplied or default root pw
local USER=${2:-${ROOT_USER}};
shift 2
local COMMAND=$#
echo "Executing command: ${COMMAND}..."
sshpass -p ${PASS} ssh ${USER}#${ADDRESS} ${COMMAND}
}
If this is run without $1 and $2 arguments it breaks the function, so the output would be something like sshpass -p ls -ltr ssh {USER}#{ADDRESS} ls -ltr if my $COMMAND is ls -ltr
How can I get around this?
You can use getopts to parse positional parameters as below:
exec_ssh_command() {
local OPTIND=0
local OPTARG OPTION
local pass=${DEFAULT_PASS} user=${DEFAULT_USER}
while getopts "u:p:" OPTION; do
case "$OPTION" in
u) user=$OPTARG;;
p) pass=$OPTARG;;
esac
done
sshpass -p "$pass" ssh "${user}#${ADDRESS}" "${#:OPTIND}"
}
Sample usages:
exec_ssh_command -u my_user -p my_password ls -ltr
exec_ssh_command -p my_password ls -ltr
exec_ssh_command -p my_password ls -ltr
exec_ssh_command ls -ltr
Explanation:
See help getopts on a bash prompt for the complete info. (Any explanation I would have added here would have been just a snippet from the same output.)
If you don't provide at least two arguments, the shift 2 can fail because there are not enough arguments to shift. Try testing them before:
if [[ -n "$1" ]] ; then
PASS="$1"
shift
else
PASS="$ROOT_PW"
fi
if [[ -n "$1" ]] ; then
USER="$1"
shift
else
USER="$ROOT_USER"
fi
COMMAND=$#
...
If you want to make the convention that the function is to be called: exec_ssh_command [[user] password] command, you could implement it like:
exec_ssh_command() {
local pass user
case $# in
2) pass=${ROOT_PW}; user="$1"; shift 1;;
1) pass=${ROOT_PW}; user=${ROOT_USER};;
*) pass="$1"; user="$2"; shift 2;;
esac
sshpass -p "$pass" ssh "${user}#${ADDRESS?}" "$#"
}
or (basically the same thing, just a stylistic difference):
exec_ssh_command() {
local pass user
case $# in
2) user="$1"; shift 1;;
1) ;;
*) pass="$1"; user="$2"; shift 2;;
esac
sshpass -p "${pass:-$ROOT_PW}" ssh "${user:-$ROOT_USER}#${ADDRESS?}" "$#"
}

Executing a local script on a remote Machine

I have a script on my local machine, but need to run it on a remote machine without copying it over there (IE, I can't sftp it over and just run it there)
I currently have the following functioning command
echo 'cd /place/to/execute' | cat - test.sh | ssh -T user#hostname
However, I also need to provide a commandline argument to test.sh.
I tried just adding it after the .sh, like I would for local execution, but that didn't work:
echo 'cd /place/to/execute' | cat - test.sh "arg" | ssh -T user#hostname
"cat: arg: No such file or directory" is the resulting error
You need to override the arguments:
echo 'set -- arg; cd /place/to/execute' | cat - test.sh | ssh -T user#hostname
The above will set the first argument to arg.
Generally:
set -- arg1 arg2 arg3
will overwrite the $1, $2, $3 in bash.
This will basically make the result of cat - test.sh a standalone script that doesn't need any arguments`.
Depends on the complexity of the script that you have. You might want to rewrite it to be able to use rpcsh functionality to remotely execute shell functions from your script.
Using https://gist.github.com/Shadowfen/2b510e51da6915adedfb saved into /usr/local/include/rpcsh.inc (for example) you could have a script
#!/bin/sh
source /usr/local/include/rpcsh.inc
MASTER_ARG=""
function ahelper() {
# used by doremotely just to show that we can
echo "master arg $1 was passed in"
}
function doremotely() {
# this executes on the remote host
ahelper $MASTER_ARG > ~/sample_rpcsh.txt
}
# main
MASTER_ARG="newvalue"
# send the function(s) and variable to the remote host and then execute it
rpcsh -u user -h host -f "ahelper doremotely" -v MASTER_ARG -r doremotely
This will give you a ~/sample_rpcsh.txt file on the remote host that contains
master arg newvalue was passed in
Copy of rpcsh.inc (in case link goes bad):
#!/bin/sh
# create an inclusion guard (to prevent multiple inclusion)
if [ ! -z "${RPCSH_GUARD+xxx}" ]; then
# already sourced
return 0
fi
RPCSH_GUARD=0
# rpcsh -- Runs a function on a remote host
# This function pushes out a given set of variables and functions to
# another host via ssh, then runs a given function with optional arguments.
# Usage:
# rpcsh -h remote_host -u remote_login -v "variable list" \
# -f "function list" -r mainfunc [-- param1 [param2]* ]
#
# The "function list" is a list of shell functions to push to the remote host
# (including the main function to run, and any functions that it calls).
#
# Use the "variable list" to send a group of variables to the remote host.
#
# Finally "mainfunc" is the name of the function (from "function list")
# to execute on the remote side. Any additional parameters specified (after
# the --)gets passed along to mainfunc.
#
# You may specify multiple -v "variable list" and -f "function list" options.
#
# Requires that you setup passwordless access to the remote system for the script
# that will be running this.
rpcsh() {
if ! args=("$(getopt -l "host:,user:,pushvars:,pushfuncs:,run:" -o "h:u:v:f:r:A" -- "$#")")
then
echo getopt failed
logger -t ngp "rpcsh: getopt failed"
exit 1
fi
sshvars=( -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null )
eval set -- "${args[#]}"
pushvars=""
pushfuncs=""
while [ -n "$1" ]
do
case $1 in
-h|--host) host=$2;
shift; shift;;
-u|--user) user=$2;
shift; shift;;
-v|--pushvars) pushvars="$pushvars $2";
shift; shift;;
-f|--pushfuncs) pushfuncs="$pushfuncs $2";
shift; shift;;
-r|--run) run=$2;
shift; shift;;
-A) sshvars=( "${sshvars[#]}" -A );
shift;;
-i) sshvars=( "${sshvars[#]}" -i $2 );
shift; shift;;
--) shift; break;;
esac
done
remote_args=( "$#" )
vars=$([ -z "$pushvars" ] || declare -p $pushvars 2>/dev/null)
ssh ${sshvars[#]} ${user}#${host} "
#set -x
$(declare -p remote_args )
$vars
$(declare -f $pushfuncs )
$run ${remote_args[#]}
"
}

Execution sequence in shell script and passing the right arguments to functions

I have a shell script that I use to launch some ROS launcher file (not that important for my question). My requirement is to use some arguments and therefore I am using getopts.
Basically, I am getting one file or a few, playing them with ROS and simultaneously recording all the files back to a single file. After that, if I have provided an argument -r or -c, I would like to run two additional operations (reindex and compress) on the recorded file. But it is required that the other process are done, before I can run the -r and -c. I am using the wait keyword, but I am not sure I really understand the flow. In other words, the playing and recording should be done and only then -r and -c should be run if provided as arguments.
Second question is related to how do I get or pass the same file that was outputted to these two functions (reindex and compress)?
So my desired format is:
./myscript -i file1 -i file2 -o /home/output -r -c
#!/bin/bash
OUTPUT_FILE_NAME="output_$(date +%Y.%m.%d--%H_%M_%S)"
usage="
$(basename "$0") [-i] [-o] [-r] [-c] [-h] -- to be done"
# Reset is necessary if getopts was used previously in the script.
OPTIND=1
while getopts ":i:orch:" opt; do
case $opt in
i ) echo "INPUT file - argument = $OPTARG"; input_files+=($OPTARG) ;;
o ) echo "OUTPUT dir - argument = $OPTARG"; output_dir=($OPTARG) ;;
r ) echo "REINDEX"; reindex ;;
c ) echo "COMPRESS"; compress ;;
h ) echo "$usage"
graceful_exit ;;
* ) echo "$usage"
exit 1
esac
done
# Shift off the options
shift $((OPTIND-1))
roslaunch myLauncher.launch &
echo "Number of loaded files: ${#input_files[#]}"
echo -n "FILES are:"
rosbag play ${input_files[#]} &
rosbag record -o $output_dir/$OUTPUT_FILE_NAME -a &
wait
function reindex{
rosbag reindex $output_dir/$OUTPUT_FILE_NAME
}
function compress{
rosbag reindex $output_dir/$OUTPUT_FILE_NAME
}
Thank you in advance!
You're very close to where you need to be — and using getopts puts you firmly on the correct track, too. Note whether or not you need to reindex or compress in the option parsing loop. Then, after the music has been played and the output file written, run the code from the functions if you need to:
#!/bin/bash
OUTPUT_FILE_NAME="output_$(date +%Y.%m.%d--%H_%M_%S)"
usage="$(basename "$0") [-i file] [-o file] [-r] [-c] [-h]"
input_files=() # Empty list of files/tracks
output_dir="." # Default output directory
r_flag="no" # No reindex by default
c_flag="no" # No compress by default
while getopts ":i:orch:" opt; do
case $opt in
(i) echo "INPUT file - argument = $OPTARG"; input_files+=("$OPTARG");;
(o) echo "OUTPUT dir - argument = $OPTARG"; output_dir="$OPTARG";;
(r) echo "REINDEX"; r_flag="yes";;
(c) echo "COMPRESS"; c_flag="yes";;
(h) echo "$usage" >&2; exit 0;;
(*) echo "$usage" >&2; exit 1;;
esac
done
# Shift off the options
shift $((OPTIND-1))
if [ $# != 0 ] || [ "${#input_files[#]}" = 0 ] ||
then
echo "$usage" >&2
exit 1
fi
roslaunch myLauncher.launch &
echo "Number of loaded files: ${#input_files[#]}"
echo -n "FILES are:"
rosbag play "${input_files[#]}" &
rosbag record -o "$output_dir/$OUTPUT_FILE_NAME" -a &
wait
if [ "$r_flag" = "yes" ]
then
rosbag reindex "$output_dir/$OUTPUT_FILE_NAME"
fi
if [ "$c_flag" = "yes" ]
then
rosbag compress "$output_dir/$OUTPUT_FILE_NAME"
fi
I didn't keep the functions since they didn't provide any value in the rewritten code.

Resources