Making a CLI command using an SH script [closed] - shell

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 9 years ago.
Improve this question
I want to make a Pathogen helper script using a .sh file. I know if you make it executable it can be run as a command, but I have no idea how to do -o --options or arguments or anything like that.
Basically that's what I want answered, really all I need to know is how to do something like:
pathogen install git://...
Or something along those lines. Any help is appreciated. :)

The bash builtin getopts does not handle long arg parsing mechanism as far as I know.
getopt(1) is the tool you are looking for.
Not a program entirely, but you'll get the idea
PARSED_OPTIONS=$(getopt -n "$0" -o h123: --long "help,one,two,three:" -- "$#")
while true;
do
case "$1" in
-h|--help)
echo "usage $0 -h -1 -2 -3 or $0 --help --one --two --three"
shift;;
-1|--one)
echo "One"
shift;;
--)
shift
break;;
esac
done
Take a look at code example and explanation given here.

Passing arguments is the easiest of the two (see "What are special dollar sign shell variables?" on SO):
#!/bin/sh
echo "$#"; # total number of arguments
echo "$0"; # name of the shell script
echo "$1"; # first argument
Assuming the file is named "stuff" (sans an extension) and the result of running ./stuff hello world:
3
stuff
hello
To pass in single letter switches (w/ optional associated params), e.g. ./stuff -v -s hello you'll want to use getopts. See "How do you use getopts" on SO and this great tutorial. Here is an example:
#!/bin/sh
verbose=1
string=
while getopts ":vs:" OPT; do
case "$OPT" in
v) verbose=0;;
s) string="$OPTARG";;
esac;
done;
if verbose; then
echo "verbose is on";
fi;
echo "$string";
The line having getopts coupled with while needs further explanation:
while - start the while loop, going through everything getopts returns back after it processes
getopts :vs: OPT; - the program getopts with 2 arguments :vs: and OPT
getopts - returns something while can iterate over
:vs: - the first argument, this describes what switches getopts will look for while it parses the shell line
: - the first colon takes getopts out of debug mode, omit this to make getopts verbose
v - find the switch -v, this will not have an argument after it, just a simple switch
s: - find the option -s with an argument after it
OPT - will store the character used (the name of the switch), e.g. "v" or "s"
OPTARG - the variable to load the value into during each of while's iterations. For v, $OPTARG will not have a value, but for s it will.
The colon : tells getopts to look for an argument after the switch. The only exception is if the sequence of characters starts with : then it toggles getopts in/out of debug/verbose mode. For example:
getopts :q:r:stu:v will take getopts out of debug mode, will tell it that switches q, r, and u will expects args, while s, t, and u won't. This would be applicable for something like: stuff -q hello -r world -s -t -u 123 -v
getopts tuv will only tell getopts to search for switches t, u and v with no arguments, e.g. stuff -t -u -v, and to be verbose

Related

How to getopts after $1 in bash [closed]

Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
I want to run the script as ./script speed -a some_value -b some_value also ./script accuracy -a some_value -b some_value
What I tried is
while [ -n "$1" ]; do
case "$1" in
speed)
for i in "${#:2}"
do while getopts "a:b:" opt; do
case "${opt}" in
a) list=$OPTARG
echo $list
;;
b) list2=$OPTARG
echo $list2
;;
esac
done
done
echo "speed option passed"
break ;;
accuracy) echo "similar to above function"
break ;;
*) echo "Option $1 not recognized" ;; # In case you typed a different option other than a,b,c
esac
shift
done
getting output as when ran ./script speed -a some_value
this is something
speed option passed
I don't know if this is possible or not or is there any way to do something like this?
I don't think you want the outer loop (while [ -n "$1" ]; do), unless you want to be able to process multiple subcommands in a single run. That is, do you want this:
./script speed -a some_value -b some_value accuracy -a some_value -b some_value
To be roughly equivalent to this:
./script speed -a some_value -b some_value
./script accuracy -a some_value -b some_value
If not, remove that loop because you'll only be processing one subcommand per run. If you do want to process more than one subcommand per run, then you need to take some extra steps to remove or skip over the arguments relating to one subcommand before running the next one.
You do want to remove the for i in "${#:2}" loop -- that just doesn't mix with the way getopts works. What you do need to do is skip over the subcommand name before processing the options. You could either use shift to remove the subcommand name, something like this:
case "$1" in
speed)
shift # Remove the first argument ("speed")
while getopts "a:b:" opt; do
...
If you're going to allow multiple subcommands, add shift $((OPTIND-1)) sfter the getopts loop, to get it ready for the next subcommand.
Or you could modify OPTIND to tell getopts that it's already processed the first argument and it can go to work on the second:
case "$1" in
speed)
OPTIND=2 # Tell getopts to start processing at arg #2
while getopts "a:b:" opt; do
...
If you're going to handle multiple subcommands with this method... well, it's a bit more complicated and I think I'll duck the question.
Yet another option is to put the code for each subcommand in a function, and call it with all but the first argument:
speed_subcommand() {
local OPTIND
while getopts "a:b:" opt; do
...
}
case "$1" in
speed)
speed_subcommand "${#:2}" ;;
accuracy)
accuracy_subcommand "${#:2}" ;;
...
This method doesn't really mix with handling multiple subcommands per run.

Bash use getopts but enforce the options

Is possible to use getopts to force the user that run the script, to add the options?
I am not asking how to make an option to require a parameter (done with the : after the option), but how to actually tell the user that he need to add the -something when running the script.
Something like myscript.sh -f FILENAME; and if the user run the script as myscript.sh FILENAME he will get an error because he didn't add the -f option.
As now I check if $1 is empty or not, to print the usage message; and another statement to check if the -f option is in what the user passed; although if you have 10 options, you add 10 conditional statements? That feels a bit off and not efficient.
Once again, I am not asking how to handle getopts parameters, but the options themselves. I think the question is pretty clear to show that this has nothing to do with the answer mentioned as possible duplicate of this question.
Sounds like you want something like tar, which (usually) requires the user to specify an operation mode followed (usually) by the file name arguments. If that is the case, then no getopts alone will not help you. However, you can still use getopts to manage the arguments:
#!/bin/bash
help() {
cat <<EOTXT
${1:-This program does something.}
USAGE:
${0##*/} <-abcde> <thing>
WHERE:
-a Sets mode a
...
EOTXT
}
mode=0
OPTIND=1
while getopts abc opt; do
case "${opt}" in
a|b|c) mode=${opt};;
?) help "Unrecognized option"; exit 1;
esac
done
shift "$((OPTIND-1))"
[[ 0 == ${mode} ]] && { help "Missing mode: use one of -a, -b, or -c"; exit 1; }
[[ 0 == $# ]] && { help "Missing thing argument"; exit 1; }
Here you use the typical while getopts construct to process your arguments. When it detects one of your required flags, you set the bookkeeping variable ($mode here) to the value. Then at the end, you check that you have both mode and an extra argument.
Thus all of these test cases fail:
my-program # no mode or argument
my-program -a # mode, but no argument
my-program foo # argument, but no mode
my-program -d # bad mode, no argument
my-program -d foo # bad mode with argument
Only this passes:
my-program -a foo

Can getopts parse a subset of a bash script's arguments and leave the rest intact?

I am using getopts to parse arguments in a bash script. I want to do two things:
remove processed options from "$#"
leave unprocessed options in "$#"
consider the command-line
$ foo -a val_a -b val_b -c -d -e -f val_f positional_l positional_2 ...
Where foo uses getopts to parse options defined by a optstring of 'b:c' and afterwards needs to leave "$#" as
`-a val_a -d -e -f val_f positional_l positional_2 ...`
I need to do two things:
parse a subset of options that may be given
leave all other opptions intact
The reason for this is because foo must use the options it recognises to determine another script bar to which it must pass the remaining "#".
Normally getopts stops when it encounters an unrecognised option but I need it to continue (up to any --). I need it to proceess and remove the options it recognises and leave alone those that it doesn't.
I did try to work around my problem using -- between the foo options and the bar options but getopts seems to baulk if the text following -- begins with a - (I tried but could not escape the hyphen).
Anyway I would prefer not to have to use -- because I want the existence of bar to be effectively transparent to the caller of foo, and I'd like the caller of foo to be able to present the options in any order.
I also tried listing all baroptions in foo (i.e. using 'a:b:cdef:'for the optstring) without processing them but I need to delete the processed ones from "$#" as they occur. I could not work out how to do that (shift doesn't allow a position to be specified).
I can manually reconstruct a new options list (see my own answer) but I wondered if there was a better way to do it.
Try the following, which only requires the script's own options to be known in advance:
#!/usr/bin/env bash
passThru=() # init. pass-through array
while getopts ':cb:' opt; do # look only for *own* options
case "$opt" in
b)
file="$OPTARG";;
c) ;;
*) # pass-thru option, possibly followed by an argument
passThru+=( "-$OPTARG" ) # add to pass-through array
# see if the next arg is an option, and, if not,
# add it to the pass-through array and skip it
if [[ ${#: OPTIND:1} != -* ]]; then
passThru+=( "${#: OPTIND:1}" )
(( ++OPTIND ))
fi
;;
esac
done
shift $((OPTIND - 1))
passThru+=( "$#" ) # append remaining args. (operands), if any
./"$file" "${passThru[#]}"
Caveats: There are two types of ambiguities that cannot be resolved this way:
For pass-thru options with option-arguments, this approach only works if the argument isn't directly appended to the option.
E.g., -a val_a works, but -aval_a wouldn't (in the absence of a: in the getopts argument, this would be interpreted as an option group and turn it into multiple options -a, -v, -a, -l, -_, -a).
As chepner points out in a comment on the question, -a -b could be option -a with option-argument -b (that just happens to look like an option itself), or it could be distinct options -a and -b; the above approach will do the latter.
To resolve these ambiguities, you must stick with your own approach, which has the down-side of requiring knowledge of all possible pass-thru options in advance.
You can manually rebuild the options list like this example which processes the -b and -c options and passes anything left intact.
#!/bin/bash
while getopts ":a:b:cdef:" opt
do
case "${opt}" in
b) file="$OPTARG" ;;
c) ;;
*) opts+=("-${opt}"); [[ -n "$OPTARG" ]] && opts+=("$OPTARG") ;;
esac
done
shift "$((OPTIND-1))"
./$file "${opts[#]}" "$#"
So
./foo -a 'foo bar' -b bar -c -d -e -f baz one two 'three and four' five
would invoke bar, given as the argument to option b, as
./bar -a 'foo bar' -d -e -f baz one two 'three and four' five
This solution suffers the disadvantage that the optstring must include the pass-through options (i.e. ":a:b:cdef:" instead of the preferable ":b:c").
Replacing the argument list with the reconstructed one can be done like this:
set -- "${opts[#]}" "$#"
which would leave "$#" containing the unprocessed arguments as specified in the question.

How to quote bash flag arguments to pass through one getopts call and be interpreted by a second?

Script nerf calls script herd, which calls script er. nerf uses a flag on herd that explicitly takes arguments needing to be passed to er.
This was not a problem before nerf existed - when herd was just called from the command line, we could single-quote the arguments to the -p flag, and they would never be interpreted by herd's getopts, but instead they would be interpreted by er's getopts.
But now we have generated values in the flags that eventually need to go to er, so I need to expand the variable $file_contents in nerf, but not let them be interpreted by getopts until they get to er.
Any of these three scripts can be modified.
$ cat nerf
#!/bin/bash
file_contents="`cat one_liner_file`"
er_args="-jkl -m $file_contents"
./herd -p "$er_args" # <-- the problem
$ cat herd
#!/bin/bash
passthru_args=""
while getopts "p:a:b:cde" opt
do
case $opt in
p) passthru_args="$OPTARGS" ;;
...
esac
done
./er "$passthru_args"
$ cat er
#!/bin/bash
while getopts "jklm:" opt
do
case $opt in
...
esac
done
If I use single quotes on the marked line above, I get the literal string "$er_args" passed through. Using double quotes, the flags are directly interpreted by herd. Using single inside double quotes, the flags aren't interpreted by ANY getopts.
I'm thinking there's no elegant solution here, but please let me know if I'm wrong. The only solutions I can think of are crappy:
Expose all of er's flags explicitly through herd.
Remove the er call from herd and place it directly into nerf.
???
What many tools do is when passed -p "-jkl -m something", they split up the string using pseudo-shell syntax. This is a bad idea because it makes space and quote handling unpredictable.
Instead, the better way is to have a way to pass individual words to the command. This is what find -exec does -- all arguments after -exec and up until + or ; are passed literally as separate arguments.
Here's a simple example of a herd with the same semantics:
#!/bin/bash
passthru_args=()
while getopts "pa:b:cde" opt
do
case $opt in
p)
while [[ ${!OPTIND} != ';' ]]
do
passthru_args+=("${!OPTIND}")
let OPTIND++
done
let OPTIND++
;;
*) echo "herd: $opt is $OPTARG"
;;
esac
done
./er "${passthru_args[#]}"
You can now run ./herd -p -jkl -m "some stuff" \; -a foo
This will run ./er -jkl -m "some stuff" safely without any space issues (but you'll have a hard time nesting multiple calls that use ; as an argument terminator).

How to read arguments from bash [duplicate]

This question already has answers here:
Creating bash scripts that take arguments
(3 answers)
Closed 8 years ago.
I am curious as to how to pass in arguments via terminal to the bash script and read them and process the script functions based on the arguments.
So if I did something like:
./scriptname.sh install
#or
./scriptname.sh assets install
How would I say, ok the first argument installs something, while the second sais to do something else based on the first argument.
$0 is the name of the command
$1 first parameter
$2 second parameter
$3 third parameter etc. etc
$# total number of parameters
for args in $*
blah blah
One great way to pass arguments to a script is to use the bash builtin functionality getopts
you can use it like this:
# a script that accepts -h -a <argument> -b
while getopts "ha:b" OPTION
do
case $OPTION in
h)
# if -h, print help function and exit
helpFunction
exit 0
;;
a)
# -a requires an argument (because of ":" in the definition) so:
myScriptVariable=$OPTARG
;;
b)
# do something special
doSomeThingSpecial
;;
?)
echo "ERROR: unknonw options!! ABORT!!"
helpFunction
exit -1
;;
esac
done
You can access a particular argument with $1, $2, ... See eg What does "$1/*" mean in "for file in $1/*"
You can also use "$#" to loop on your arguments. Ex : https://github.com/gturri/dotfiles/blob/master/bootstrap.sh#L64

Resources