I have this function:
sgrep ()
{
local OPTIND;
if getopts i o; then
grep --color=auto -P -in \"$1\" \"$2\";
shift $((OPTIND-1));
getopts
problems:As I said in a comment, you need to make OPTIND
and opt
local to the function, so it doesn't inherit values from previous runs of the function. To understand why this is, let me start with your original function (from the first version of your question), and add some instrumentation in the form of echo
commands to show how things change as it runs:
sgrep ()
{
echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
if getopts "i" i; then
opt="-i";
shift $((OPTIND-1));
echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
fi;
echo "Done parsing, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
# grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2";
}
...and try running that, first with no -i
flag:
$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='1', opt='', args=somesearch somefile
Done parsing, OPTIND='1', opt='', args=somesearch somefile
And it worked fine! After parsing, opt
is empty (as it should be), and both "somesearch" and "somefile" remain in the argument list to be passed to grep
.
I should explain a little about OPTIND
, though, before going on. getopts
is designed to be run repeatedly to iterate through the flag (aka option) arguments, and OPTIND
is part of how it keeps track of where it is in processing the argument list. In particular, it's the number of the next argument that it needs to examine to see if it's a flag (and process it if it is). In this case, it start off at 1 (i.e. $1
is the next arg to examine), and it stays there because $1
is a regular argument, not a flag.
BTW, if you'd done shift $((OPTIND-1))
after processing as usual, it'd do shift 0
, which would all zero flag arguments from the arg list. Just as it should. (On the other hand, if you had a loop and put shift
inside the loop, it'd change the arg list out from under getopts
, causing it to lose track of its place and get very confused. That's why you put the shift
after the loop.)
Ok, let's try it with an actual flag:
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='1', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
Again, it worked properly! It parsed the -i
, set opt
appropriately, incremented OPTIND
to 2 so if you'd had a loop it would've examined the second argument, found it's a regular argument, and stopped the loop. And then shift $((OPTIND-1))
shifted off the one flag argument, leaving the non-flag ones to be passed to grep
.
Let's try it again, with the same flag:
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=-i somesearch somefile
Oops, now it's all gone screwy, and it's because it inherited OPTIND
and opt
from the previous run. OPTIND
being 2 tells getopts
that it's already examined $1
and doesn't have to process it again; it looks at $2
, sees it doesn't start with -
so it isn't a flag, so it returns false, and the if
doesn't run and the flag argument doesn't get shifted away. Meanwhile, opt
is still set to "-i
" from the last run.
That is why getopts
hasn't been working right for you. To prove it, let's modify the function to make both variables local:
sgrep ()
{
local OPTIND opt # <- This is the only change here
echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
if getopts "i" i; then
opt="-i";
shift $((OPTIND-1));
echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
fi;
echo "Done parsing, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
# grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2";
}
And try it out:
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
Now it starts off a little weird because OPTIND
is blank instead of 1, but that's not actually a problem because getopts
assumes it should start at 1. So it parses the argument, sets opt
(which didn't inherit a bogus value from before), and shifts the flag out of the argument list.
There is a problem, though. Suppose we pass an illegal (/unsupported) flag:
$ sgrep -k 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-k somesearch somefile
-bash: illegal option -- k
Parsed -? flag, OPTIND='2', opt='-i', args=somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
Oops again. Since getopts
processed an argument starting with -
, it printed an error but went ahead and returned true with the variable i
set to "?" to indicate there was a problem. Your system didn't check that, it just assumed it must be -i
.
Now, let me show you the standard (recommended) version, with a while
loop and a case
on the flag, with an error handler. I've also taken the liberty of removing single semicolons from the end of lines, 'cause they're useless in shell:
sgrep ()
{
local OPTIND opt
echo "Starting sgrep, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
while getopts "i" i; do
case "$i" in
i )
opt="-$i"
echo "Parsed -$i flag, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
;;
* )
return 1 ;;
esac
done
shift $((OPTIND-1))
echo "Done parsing, OPTIND='$OPTIND', opt='$opt', args=$*" >&2
# grep --color=auto -P ${opt} "$1" "$2" || exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"
}
And run it:
$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=somesearch somefile
Done parsing, OPTIND='1', opt='', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
$ sgrep 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=somesearch somefile
Done parsing, OPTIND='1', opt='', args=somesearch somefile
$ sgrep -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i somesearch somefile
Done parsing, OPTIND='2', opt='-i', args=somesearch somefile
...Parsing works as expected, even with repeated runs. Check the error handling:
$ sgrep -k 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-k somesearch somefile
-bash: illegal option -- k
And since there's a loop, it handles multiple flags (even if there's only one defined flag):
$ sgrep -i -i -i -i 'somesearch' 'somefile'
Starting sgrep, OPTIND='', opt='', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='2', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='3', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='4', opt='-i', args=-i -i -i -i somesearch somefile
Parsed -i flag, OPTIND='5', opt='-i', args=-i -i -i -i somesearch somefile
Done parsing, OPTIND='5', opt='-i', args=somesearch somefile
Now, you might complain that that's a lot of code for such a simple task (just one possible flag!), and you'd be right. But it's basically boilerplate; you don't have to write that whole thing every time, just copy a standard example, fill in the options string and cases to handle them, and that's pretty much it. If it weren't in a function you wouldn't have the local
command, and you'd use exit 1
instead of return 1
to bail out, but that's about it.
If you really want it to be simple, just use if [ "$1" = "-i" ]
, and don't get involved with the complexities of using getopts
.
|| exit 1 |
does not exit before another pipe when grep fail?:There are actually three problems with that approach: first, to exit a function you use return
instead of exit
.
Second, the shell parses pipes with higher precedence than ||
so the command was treated as:
grep --color=auto -P ${opt} "$1" "$2"
Logical or'ed with:
exit 1 | sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"
Rather than
grep --color=auto -P ${opt} "$1" "$2" || exit 1
Piped to:
sed -E -n 's/^([0-9]+)/\1/p' | xargs -I{} vim +"{}" "$2"
Third, and most importantly, the elements of a pipeline run in subprocesses. For shell commands like exit
and return
, that means they run in subshells, and running exit
or return
(or break
or ...) in a subshell doesn't have that effect on the parent shell (i.e the one running the function). That means there's nothing you can do within the pipeline to make the function return directly.
In this case, I think your best option is something like:
grep ... | otherstuff
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
return 1
fi
I have changed the positional params to one bigger:
sgrep ()
{
local OPTIND;
if getopts i o; then
grep --color=auto -P -in "$2" "$3";
shift $((OPTIND-1));
else
grep --color=auto -P -n "$1" "$2";
fi | sed -E -n 's/^([0-9]+).*/\1/p' | xargs -I{} vim +"{}" "$2";
stty sane
}
Which works, but I don't like it. In one branch I have to use bigger positions, because of the option, but on the other branch without option used, positions do not change. It is messy
I have tried to put shift $((OPTIND-1))
immediately after then
but for no avail.