Re: Bash-specific performance by avoiding sed

Pavel Raiskup <[email protected]>
Newsgroups gmane.comp.gnu.libtool.general
Message-ID <[email protected]>
On Monday 05 of October 2015 09:47:05 Pavel Raiskup wrote:
> On Monday 05 of October 2015 01:25:24 Pavel Raiskup wrote:
> > On Monday 05 of October 2015 00:45:50 Pavel Raiskup wrote:
> > > > should we test the size of the string first ?  i've written such raw shell 
> > > > string parsing functions before, and once you hit a certain size (like 1k+ 
> > > > iirc), forking out to sed is way faster, especially when running in multibyte 
> > > > locales (like UTF8) which most people are doing nowadays.
> > > > -mike
> > > 
> > > Well, that optimization would require (fast) strlen()-like construct.
> > > Anyway, the vast majority of calls to func_quote () function will have
> > > short ARG, and its complexity is still "just" linear.  We could optimize
> > > later if that was a real issue.
> > > 
> > > I would like to propose solution based on Eric's one, without using of
> > > '${VAR%.}' and '${VAR#.}' constructs -- sounds like this could be even
> > > more portable while it keeps almost the same speed (if we can use += its
> > > even faster).
> > > 
> > > I have yet a another patch trying to minimize option-parser overhead
> > > (that is focused on the POV of Richard, but that needs to be cleaned up a
> > > bit, I'll post hopefully tomorrow).
> > > 
> > > Any comment is welcome!
> > 
> > Re-attached (fixes for 'make syntax-check' and fixed one comment).
> 
> Hmm, one might-be-a-problem with this (catched by testsuite), when you
> have:
> 
>   $ cat build-aux/test-quoting
>   . `echo "$0" |${SED-sed} 's|[^/]*$||'`/funclib.sh
>   # source this for "GNU m4" detection methods
>   . `echo "$0" |${SED-sed} 's|[^/]*$||'`/extract-trace
> 
>   func_quote_for_eval "$@"
>   echo "$func_quote_for_eval_result"
> 
> Then:
> 
>   $ ./build-aux/test-quoting '"a b"' # fine
>   "\"a b\""
> 
>   $ ./build-aux/test-quoting '"*tool"' # broken
>   ./build-aux/test-quoting '"*tool"'
>   \"libtool\"
> 
> We would like to have an output \"*\".  I'm not aware of portable way
> how to disable wildcard expansion in shell, and autoconf 'Shellology'
> section haven't helped me.  In particular, the problem is here:
> 
>   x='a"[a-z]*"c'
>   IFS='"'
>   for i in $x; do # Here we wan't to disable wildcard expansion
>     echo $i
>   done
> 
> Any idea other than fallback to $sed_quote_subst in case of '*' or '['
> exists in ARG?

Attaching two (yet to be cleaned) patches doing the optimization.  Is
anybody able to test/comment on this particular solution?  That would be
really appreciated.

Pavel

_______________________________________________
https://lists.gnu.org/mailman/listinfo/libtool
0001-libtool-mitigate-the-sed_quote_subst-slowdown.patch (text/x-patch, 7.1 KB)
From 1b89ae66f88c3822b2afec128f288d409287780f Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <[email protected]>
Date: Sun, 4 Oct 2015 21:55:03 +0200
Subject: [PATCH 1/2] libtool: mitigate the $sed_quote_subst slowdown

When it is reasonably possible, use shell implementation for
quoting.

References:
http://lists.gnu.org/archive/html/libtool/2015-03/msg00005.html
http://lists.gnu.org/archive/html/libtool/2015-02/msg00000.html
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=20006

* gl/build-aux/funclib.sh (func_quote): New function that can be
used as substitution for '$SED $sed_quote_subst' call.
* build-aux/ltmain.in (func_emit_wrapper): Use func_quote instead
of '$SED $sed_quote_subst'.
(func_mode_link): Likewise.
* NEWS: Document.
* bootstrap: Sync with funclib.sh.
---
 NEWS                    |  3 +++
 bootstrap               | 56 +++++++++++++++++++++++++++++++++++++++++++++++--
 build-aux/ltmain.in     | 10 +++++----
 gl/build-aux/funclib.sh | 56 +++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 117 insertions(+), 8 deletions(-)

diff --git a/NEWS b/NEWS
index a3c5b12..7c23d03 100644
--- a/NEWS
+++ b/NEWS
@@ -17,6 +17,9 @@ NEWS - list of user-visible changes between releases of GNU Libtool
   - Fix significant slowdown of libtoolize for certain projects (regression
     introduced in 2.4.3 release) caused by infinite m4 macro recursion.
 
+  - Mitigate the slowdown of libtool script (introduced in v2.4.3) caused by
+    increased number of calls to '$SED $sed_quote_subst' (bug#20006).
+
 * Noteworthy changes in release 2.4.6 (2015-02-15) [stable]
 
 ** New features:
diff --git a/bootstrap b/bootstrap
index c179f51..2649478 100755
--- a/bootstrap
+++ b/bootstrap
@@ -230,7 +230,7 @@ vc_ignore=
 
 # Source required external libraries:
 # Set a version string for this script.
-scriptversion=2015-01-20.17; # UTC
+scriptversion=2015-10-04.22; # UTC
 
 # General shell script boiler plate, and helper functions.
 # Written by Gary V. Vaughan, 2004
@@ -1257,6 +1257,57 @@ func_relative_path ()
 }
 
 
+# func_quote ARG
+# --------------
+# Aesthetically quote one ARG, store the result into $func_quote_result.  Note
+# that we keep attention to performance here (so far O(N) complexity as long as
+# func_append is O(1)).
+func_quote ()
+{
+    $debug_cmd
+
+    func_quote_result=$1
+
+    case $func_quote_result in
+      *[\\\`\"\$]*)
+        case $func_quote_result in
+          *'*'*|*'['*)
+            func_quote_result=`$ECHO "$func_quote_result" | $SED "$sed_quote_subst"`
+            return 0
+            ;;
+        esac
+
+        func_quote_old_IFS=$IFS
+        for _G_char in '\' '`' '"' '$'
+        do
+          # STATE($1) PREV($2) SEPARATOR($3)
+          set start "" ""
+          func_quote_result=dummy"$_G_char$func_quote_result$_G_char"dummy
+          IFS=$_G_char
+          for _G_part in $func_quote_result
+          do
+            case $1 in
+            quote)
+              func_append func_quote_result "$3$2"
+              set quote "$_G_part" "\\$_G_char"
+              ;;
+            start)
+              set first "" ""
+              func_quote_result=
+              ;;
+            first)
+              set quote "$_G_part" ""
+              ;;
+            esac
+          done
+          IFS=$func_quote_old_IFS
+        done
+        ;;
+      *) ;;
+    esac
+}
+
+
 # func_quote_for_eval ARG...
 # --------------------------
 # Aesthetically quote ARGs to be evaled later.
@@ -1275,7 +1326,8 @@ func_quote_for_eval ()
     while test 0 -lt $#; do
       case $1 in
         *[\\\`\"\$]*)
-	  _G_unquoted_arg=`printf '%s\n' "$1" |$SED "$sed_quote_subst"` ;;
+          func_quote "$1"
+          _G_unquoted_arg=$func_quote_result ;;
         *)
           _G_unquoted_arg=$1 ;;
       esac
diff --git a/build-aux/ltmain.in b/build-aux/ltmain.in
index 0c40da0..24acefd 100644
--- a/build-aux/ltmain.in
+++ b/build-aux/ltmain.in
@@ -3346,7 +3346,8 @@ else
   if test \"\$libtool_execute_magic\" != \"$magic\"; then
     file=\"\$0\""
 
-    qECHO=`$ECHO "$ECHO" | $SED "$sed_quote_subst"`
+    func_quote "$ECHO"
+    qECHO=$func_quote_result
     $ECHO "\
 
 # A function that is used when there is no print builtin or printf.
@@ -8596,8 +8597,8 @@ EOF
 	    relink_command="$var=$func_quote_for_eval_result; export $var; $relink_command"
 	  fi
 	done
-	relink_command="(cd `pwd`; $relink_command)"
-	relink_command=`$ECHO "$relink_command" | $SED "$sed_quote_subst"`
+	func_quote "(cd `pwd`; $relink_command)"
+	relink_command=$func_quote_result
       fi
 
       # Only actually do things if not in dry run mode.
@@ -8843,7 +8844,8 @@ EOF
       done
       # Quote the link command for shipping.
       relink_command="(cd `pwd`; $SHELL \"$progpath\" $preserve_args --mode=relink $libtool_args @inst_prefix_dir@)"
-      relink_command=`$ECHO "$relink_command" | $SED "$sed_quote_subst"`
+      func_quote "$relink_command"
+      relink_command=$func_quote_result
       if test yes = "$hardcode_automatic"; then
 	relink_command=
       fi
diff --git a/gl/build-aux/funclib.sh b/gl/build-aux/funclib.sh
index 39d972e..8032e6b 100644
--- a/gl/build-aux/funclib.sh
+++ b/gl/build-aux/funclib.sh
@@ -1,5 +1,5 @@
 # Set a version string for this script.
-scriptversion=2015-01-20.17; # UTC
+scriptversion=2015-10-04.22; # UTC
 
 # General shell script boiler plate, and helper functions.
 # Written by Gary V. Vaughan, 2004
@@ -1026,6 +1026,57 @@ func_relative_path ()
 }
 
 
+# func_quote ARG
+# --------------
+# Aesthetically quote one ARG, store the result into $func_quote_result.  Note
+# that we keep attention to performance here (so far O(N) complexity as long as
+# func_append is O(1)).
+func_quote ()
+{
+    $debug_cmd
+
+    func_quote_result=$1
+
+    case $func_quote_result in
+      *[\\\`\"\$]*)
+        case $func_quote_result in
+          *'*'*|*'['*)
+            func_quote_result=`$ECHO "$func_quote_result" | $SED "$sed_quote_subst"`
+            return 0
+            ;;
+        esac
+
+        func_quote_old_IFS=$IFS
+        for _G_char in '\' '`' '"' '$'
+        do
+          # STATE($1) PREV($2) SEPARATOR($3)
+          set start "" ""
+          func_quote_result=dummy"$_G_char$func_quote_result$_G_char"dummy
+          IFS=$_G_char
+          for _G_part in $func_quote_result
+          do
+            case $1 in
+            quote)
+              func_append func_quote_result "$3$2"
+              set quote "$_G_part" "\\$_G_char"
+              ;;
+            start)
+              set first "" ""
+              func_quote_result=
+              ;;
+            first)
+              set quote "$_G_part" ""
+              ;;
+            esac
+          done
+          IFS=$func_quote_old_IFS
+        done
+        ;;
+      *) ;;
+    esac
+}
+
+
 # func_quote_for_eval ARG...
 # --------------------------
 # Aesthetically quote ARGs to be evaled later.
@@ -1044,7 +1095,8 @@ func_quote_for_eval ()
     while test 0 -lt $#; do
       case $1 in
         *[\\\`\"\$]*)
-	  _G_unquoted_arg=`printf '%s\n' "$1" |$SED "$sed_quote_subst"` ;;
+          func_quote "$1"
+          _G_unquoted_arg=$func_quote_result ;;
         *)
           _G_unquoted_arg=$1 ;;
       esac
-- 
2.5.0
0002-libbtool-optimize-parse-options-calls.patch (text/x-patch, 17.4 KB)
From c283b8a8e7540b65fd7a7d32fceec6ed42b5b2a7 Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <[email protected]>
Date: Mon, 5 Oct 2015 13:16:08 +0200
Subject: [PATCH 2/2] libbtool: optimize parse-options calls

Its not necessary to re-escape-for-eval in each function in the
hook-able function tree -- usually is enough if the leaf function
executes func_quote_for_eval() and its caller just re-uses the
CALLEE_return value.

* gl/build-aux/options-parser (func_run_hooks): Propagate 0 return
code down to caller if any $_G_hook succeeded.  Don't re-quote
the result as that has already been done by the succeeding hook
itself.
(func_parse_options): Quote only if we changed something and
use appropriate return code.
(func_validate_options): Likewise.
(func_options_prep): Likewise.
(func_options_finish): New hook-caller for 'func_options' hooks.
(func_options): Propagate return value down to top-level caller,
but pay attention we have always set $func_options_result.
* build-aux/ltmain.in (libtool_options_prep): Quote only if we
changed something and use appropriate return code.
(libtool_parse_options): Likewise.
* bootstrap: Sync gl/build-aux/with option-parser.
---
 bootstrap                   | 128 +++++++++++++++++++++++++++++++++-----------
 build-aux/ltmain.in         |  33 +++++++++---
 gl/build-aux/options-parser | 128 +++++++++++++++++++++++++++++++++-----------
 3 files changed, 220 insertions(+), 69 deletions(-)

diff --git a/bootstrap b/bootstrap
index 2649478..c4de711 100755
--- a/bootstrap
+++ b/bootstrap
@@ -1744,10 +1744,17 @@ func_remove_hook ()
 # than a whitespace-delimited list of legal shell function names, and
 # no effort is wasted trying to catch shell meta-characters or preserve
 # whitespace.
+# If at least one hook returns 0 (exit success) upon execution,
+# func_run_hooks () returns zero too.  Otherwise 1 is returned.
+# All hooks are expected to correctly return $HOOKNAME_result variable
+# upon success exection.  The output variable $func_run_hooks_result
+# is set to latest (successful) $HOOKNAME_result.
 func_run_hooks ()
 {
     $debug_cmd
 
+    _G_rc_run_hooks=false
+
     case " $hookable_fns " in
       *" $1 "*) ;;
       *) func_fatal_error "'$1' does not support hook funcions.n" ;;
@@ -1756,16 +1763,24 @@ func_run_hooks ()
     eval _G_hook_fns=\$$1_hooks; shift
 
     for _G_hook in $_G_hook_fns; do
-      eval $_G_hook '"$@"'
+      _G_hook_unchanged=${_G_hook}_unchanged
+      eval $_G_hook_unchanged=false
 
-      # store returned options list back into positional
-      # parameters for next 'cmd' execution.
-      eval _G_hook_result=\$${_G_hook}_result
-      eval set dummy "$_G_hook_result"; shift
+      if eval $_G_hook '"$@"'; then
+          # store returned options list back into positional
+          # parameters for next 'cmd' execution.
+          eval _G_hook_result=\$${_G_hook}_result
+          eval set dummy "$_G_hook_result"; shift
+          _G_rc_run_hooks=true
+      fi
     done
 
-    func_quote_for_eval ${1+"$@"}
-    func_run_hooks_result=$func_quote_for_eval_result
+    if $_G_rc_run_hooks; then
+        # No need to re-quote by func_quoteh
+        func_run_hooks_result=$_G_hook_result
+    fi
+
+    $_G_rc_run_hooks
 }
 
 
@@ -1839,6 +1854,24 @@ func_run_hooks ()
 # multiple option parsing hooks can be added safely.
 
 
+# func_options_finish [ARG]...
+# ----------------------------
+# All finishing suff required after the option parse loop.  This executes
+# all 'func_options' hooks.
+func_options_finish ()
+{
+    $debug_cmd
+
+    _G_func_options_finish_exit=false
+    if func_run_hooks func_options ${1+"$@"}; then
+      func_options_finish_result=$func_run_hooks_result
+      _G_func_options_finish_exit=:
+    fi
+
+    $_G_func_options_finish_exit
+}
+
+
 # func_options [ARG]...
 # ---------------------
 # All the functions called inside func_options are hookable. See the
@@ -1848,17 +1881,26 @@ func_options ()
 {
     $debug_cmd
 
-    func_options_prep ${1+"$@"}
-    eval func_parse_options \
-        ${func_options_prep_result+"$func_options_prep_result"}
-    eval func_validate_options \
-        ${func_parse_options_result+"$func_parse_options_result"}
+    _G_rc_options=false
 
-    eval func_run_hooks func_options \
-        ${func_validate_options_result+"$func_validate_options_result"}
+    for my_func in options_prep parse_options validate_options options_finish
+    do
+      if eval func_$my_func '${1+"$@"}'; then
+        eval _G_res_var='$'"func_${my_func}_result"
+        eval set dummy "$_G_res_var" ; shift
+        _G_rc_options=:
+      fi
+    done
 
-    # save modified positional parameters for caller
-    func_options_result=$func_run_hooks_result
+    if $_G_rc_options; then
+      func_options_result=$_G_res_var
+    else
+      # As a top-level function, we need *always* set the *_result
+      func_quote_for_eval ${1+"$@"}
+      func_options_result=$func_quote_for_eval_result
+    fi
+
+    $_G_rc_options
 }
 
 
@@ -1869,7 +1911,9 @@ func_options ()
 # positional parameters.  If a hook function modifies that list, and
 # needs to propogate that back to rest of this script, then the complete
 # modified list must be put in 'func_run_hooks_result' before
-# returning.
+# returning and 0 returned.  If no changes were done, script might return
+# 1 and caller of the hook will not read the 'func_run_hooks_result'
+# value.
 func_hookable func_options_prep
 func_options_prep ()
 {
@@ -1879,10 +1923,14 @@ func_options_prep ()
     opt_verbose=false
     opt_warning_types=
 
-    func_run_hooks func_options_prep ${1+"$@"}
+    _G_rc_options_prep=false
+    if func_run_hooks func_options_prep ${1+"$@"}; then
+        _G_rc_options_prep=:
+        # save modified positional parameters for caller
+        func_options_prep_result=$func_run_hooks_result
+    fi
 
-    # save modified positional parameters for caller
-    func_options_prep_result=$func_run_hooks_result
+    $_G_rc_options_prep
 }
 
 
@@ -1896,18 +1944,21 @@ func_parse_options ()
 
     func_parse_options_result=
 
+    _G_rc_parse_options=false
     # this just eases exit handling
     while test $# -gt 0; do
       # Defer to hook functions for initial option parsing, so they
       # get priority in the event of reusing an option name.
-      func_run_hooks func_parse_options ${1+"$@"}
-
-      # Adjust func_parse_options positional parameters to match
-      eval set dummy "$func_run_hooks_result"; shift
+      if func_run_hooks func_parse_options ${1+"$@"}; then
+          # Adjust func_parse_options positional parameters to match
+          eval set dummy "$func_run_hooks_result"; shift
+          _G_rc_parse_options=:
+      fi
 
       # Break out of the loop if we already parsed every option.
       test $# -gt 0 || break
 
+      _G_match_parse_options=:
       _G_opt=$1
       shift
       case $_G_opt in
@@ -1977,13 +2028,23 @@ func_parse_options ()
 
         --)           break ;;
         -*)           func_fatal_help "unrecognised option: '$_G_opt'" ;;
-        *)            set dummy "$_G_opt" ${1+"$@"}; shift; break ;;
+        *)            set dummy "$_G_opt" ${1+"$@"} ; shift
+                      _G_match_parse_options=false
+                      break
+                      ;;
       esac
+
+      $_G_match_parse_options && _G_rc_parse_options=:
     done
 
-    # save modified positional parameters for caller
-    func_quote_for_eval ${1+"$@"}
-    func_parse_options_result=$func_quote_for_eval_result
+
+    if $_G_rc_parse_options; then
+        # save modified positional parameters for caller
+        func_quote_for_eval ${1+"$@"}
+        func_parse_options_result=$func_quote_for_eval_result
+    fi
+
+    $_G_rc_parse_options
 }
 
 
@@ -1996,16 +2057,21 @@ func_validate_options ()
 {
     $debug_cmd
 
+    _G_rc_validate_options=false
+
     # Display all warnings if -W was not given.
     test -n "$opt_warning_types" || opt_warning_types=" $warning_categories"
 
-    func_run_hooks func_validate_options ${1+"$@"}
+    if func_run_hooks func_validate_options ${1+"$@"}; then
+      # save modified positional parameters for caller
+      func_validate_options_result=$func_run_hooks_result
+      _G_rc_validate_options=:
+    fi
 
     # Bail if the options were screwed!
     $exit_cmd $EXIT_FAILURE
 
-    # save modified positional parameters for caller
-    func_validate_options_result=$func_run_hooks_result
+    $_G_rc_validate_options
 }
 
 
diff --git a/build-aux/ltmain.in b/build-aux/ltmain.in
index 24acefd..e57042a 100644
--- a/build-aux/ltmain.in
+++ b/build-aux/ltmain.in
@@ -358,6 +358,8 @@ libtool_options_prep ()
     nonopt=
     preserve_args=
 
+    _G_rc_libtool_options_prep=:
+
     # Shorthand for --mode=foo, only valid as the first argument
     case $1 in
     clean|clea|cle|cl)
@@ -381,11 +383,18 @@ libtool_options_prep ()
     uninstall|uninstal|uninsta|uninst|unins|unin|uni|un|u)
       shift; set dummy --mode uninstall ${1+"$@"}; shift
       ;;
+    *)
+      _G_rc_libtool_options_prep=false
+      ;;
     esac
 
-    # Pass back the list of options.
-    func_quote_for_eval ${1+"$@"}
-    libtool_options_prep_result=$func_quote_for_eval_result
+    if $_G_rc_libtool_options_prep; then
+        # Pass back the list of options.
+        func_quote_for_eval ${1+"$@"}
+        libtool_options_prep_result=$func_quote_for_eval_result
+    fi
+
+    $_G_rc_libtool_options_prep
 }
 func_add_hook func_options_prep libtool_options_prep
 
@@ -397,9 +406,12 @@ libtool_parse_options ()
 {
     $debug_cmd
 
+    _G_rc_lt_parse_options=false
+
     # Perform our own loop to consume as many options as possible in
     # each iteration.
     while test $# -gt 0; do
+      _G_match_lt_parse_options=:
       _G_opt=$1
       shift
       case $_G_opt in
@@ -475,14 +487,21 @@ libtool_parse_options ()
                         ;;
 
 	# An option not handled by this hook function:
-        *)		set dummy "$_G_opt" ${1+"$@"};	shift; break  ;;
+        *)              set dummy "$_G_opt" ${1+"$@"} ; shift
+                        _G_match_lt_parse_options=false
+                        break
+                        ;;
       esac
+      $_G_match_lt_parse_options && _G_rc_lt_parse_options=:
     done
 
+    if $_G_rc_lt_parse_options; then
+        # save modified positional parameters for caller
+        func_quote_for_eval ${1+"$@"}
+        libtool_parse_options_result=$func_quote_for_eval_result
+    fi
 
-    # save modified positional parameters for caller
-    func_quote_for_eval ${1+"$@"}
-    libtool_parse_options_result=$func_quote_for_eval_result
+    $_G_rc_lt_parse_options
 }
 func_add_hook func_parse_options libtool_parse_options
 
diff --git a/gl/build-aux/options-parser b/gl/build-aux/options-parser
index d651f1d..73db1e3 100644
--- a/gl/build-aux/options-parser
+++ b/gl/build-aux/options-parser
@@ -157,10 +157,17 @@ func_remove_hook ()
 # than a whitespace-delimited list of legal shell function names, and
 # no effort is wasted trying to catch shell meta-characters or preserve
 # whitespace.
+# If at least one hook returns 0 (exit success) upon execution,
+# func_run_hooks () returns zero too.  Otherwise 1 is returned.
+# All hooks are expected to correctly return $HOOKNAME_result variable
+# upon success exection.  The output variable $func_run_hooks_result
+# is set to latest (successful) $HOOKNAME_result.
 func_run_hooks ()
 {
     $debug_cmd
 
+    _G_rc_run_hooks=false
+
     case " $hookable_fns " in
       *" $1 "*) ;;
       *) func_fatal_error "'$1' does not support hook funcions.n" ;;
@@ -169,16 +176,24 @@ func_run_hooks ()
     eval _G_hook_fns=\$$1_hooks; shift
 
     for _G_hook in $_G_hook_fns; do
-      eval $_G_hook '"$@"'
+      _G_hook_unchanged=${_G_hook}_unchanged
+      eval $_G_hook_unchanged=false
 
-      # store returned options list back into positional
-      # parameters for next 'cmd' execution.
-      eval _G_hook_result=\$${_G_hook}_result
-      eval set dummy "$_G_hook_result"; shift
+      if eval $_G_hook '"$@"'; then
+          # store returned options list back into positional
+          # parameters for next 'cmd' execution.
+          eval _G_hook_result=\$${_G_hook}_result
+          eval set dummy "$_G_hook_result"; shift
+          _G_rc_run_hooks=true
+      fi
     done
 
-    func_quote_for_eval ${1+"$@"}
-    func_run_hooks_result=$func_quote_for_eval_result
+    if $_G_rc_run_hooks; then
+        # No need to re-quote by func_quoteh
+        func_run_hooks_result=$_G_hook_result
+    fi
+
+    $_G_rc_run_hooks
 }
 
 
@@ -252,6 +267,24 @@ func_run_hooks ()
 # multiple option parsing hooks can be added safely.
 
 
+# func_options_finish [ARG]...
+# ----------------------------
+# All finishing suff required after the option parse loop.  This executes
+# all 'func_options' hooks.
+func_options_finish ()
+{
+    $debug_cmd
+
+    _G_func_options_finish_exit=false
+    if func_run_hooks func_options ${1+"$@"}; then
+      func_options_finish_result=$func_run_hooks_result
+      _G_func_options_finish_exit=:
+    fi
+
+    $_G_func_options_finish_exit
+}
+
+
 # func_options [ARG]...
 # ---------------------
 # All the functions called inside func_options are hookable. See the
@@ -261,17 +294,26 @@ func_options ()
 {
     $debug_cmd
 
-    func_options_prep ${1+"$@"}
-    eval func_parse_options \
-        ${func_options_prep_result+"$func_options_prep_result"}
-    eval func_validate_options \
-        ${func_parse_options_result+"$func_parse_options_result"}
+    _G_rc_options=false
 
-    eval func_run_hooks func_options \
-        ${func_validate_options_result+"$func_validate_options_result"}
+    for my_func in options_prep parse_options validate_options options_finish
+    do
+      if eval func_$my_func '${1+"$@"}'; then
+        eval _G_res_var='$'"func_${my_func}_result"
+        eval set dummy "$_G_res_var" ; shift
+        _G_rc_options=:
+      fi
+    done
 
-    # save modified positional parameters for caller
-    func_options_result=$func_run_hooks_result
+    if $_G_rc_options; then
+      func_options_result=$_G_res_var
+    else
+      # As a top-level function, we need *always* set the *_result
+      func_quote_for_eval ${1+"$@"}
+      func_options_result=$func_quote_for_eval_result
+    fi
+
+    $_G_rc_options
 }
 
 
@@ -282,7 +324,9 @@ func_options ()
 # positional parameters.  If a hook function modifies that list, and
 # needs to propogate that back to rest of this script, then the complete
 # modified list must be put in 'func_run_hooks_result' before
-# returning.
+# returning and 0 returned.  If no changes were done, script might return
+# 1 and caller of the hook will not read the 'func_run_hooks_result'
+# value.
 func_hookable func_options_prep
 func_options_prep ()
 {
@@ -292,10 +336,14 @@ func_options_prep ()
     opt_verbose=false
     opt_warning_types=
 
-    func_run_hooks func_options_prep ${1+"$@"}
+    _G_rc_options_prep=false
+    if func_run_hooks func_options_prep ${1+"$@"}; then
+        _G_rc_options_prep=:
+        # save modified positional parameters for caller
+        func_options_prep_result=$func_run_hooks_result
+    fi
 
-    # save modified positional parameters for caller
-    func_options_prep_result=$func_run_hooks_result
+    $_G_rc_options_prep
 }
 
 
@@ -309,18 +357,21 @@ func_parse_options ()
 
     func_parse_options_result=
 
+    _G_rc_parse_options=false
     # this just eases exit handling
     while test $# -gt 0; do
       # Defer to hook functions for initial option parsing, so they
       # get priority in the event of reusing an option name.
-      func_run_hooks func_parse_options ${1+"$@"}
-
-      # Adjust func_parse_options positional parameters to match
-      eval set dummy "$func_run_hooks_result"; shift
+      if func_run_hooks func_parse_options ${1+"$@"}; then
+          # Adjust func_parse_options positional parameters to match
+          eval set dummy "$func_run_hooks_result"; shift
+          _G_rc_parse_options=:
+      fi
 
       # Break out of the loop if we already parsed every option.
       test $# -gt 0 || break
 
+      _G_match_parse_options=:
       _G_opt=$1
       shift
       case $_G_opt in
@@ -390,13 +441,23 @@ func_parse_options ()
 
         --)           break ;;
         -*)           func_fatal_help "unrecognised option: '$_G_opt'" ;;
-        *)            set dummy "$_G_opt" ${1+"$@"}; shift; break ;;
+        *)            set dummy "$_G_opt" ${1+"$@"} ; shift
+                      _G_match_parse_options=false
+                      break
+                      ;;
       esac
+
+      $_G_match_parse_options && _G_rc_parse_options=:
     done
 
-    # save modified positional parameters for caller
-    func_quote_for_eval ${1+"$@"}
-    func_parse_options_result=$func_quote_for_eval_result
+
+    if $_G_rc_parse_options; then
+        # save modified positional parameters for caller
+        func_quote_for_eval ${1+"$@"}
+        func_parse_options_result=$func_quote_for_eval_result
+    fi
+
+    $_G_rc_parse_options
 }
 
 
@@ -409,16 +470,21 @@ func_validate_options ()
 {
     $debug_cmd
 
+    _G_rc_validate_options=false
+
     # Display all warnings if -W was not given.
     test -n "$opt_warning_types" || opt_warning_types=" $warning_categories"
 
-    func_run_hooks func_validate_options ${1+"$@"}
+    if func_run_hooks func_validate_options ${1+"$@"}; then
+      # save modified positional parameters for caller
+      func_validate_options_result=$func_run_hooks_result
+      _G_rc_validate_options=:
+    fi
 
     # Bail if the options were screwed!
     $exit_cmd $EXIT_FAILURE
 
-    # save modified positional parameters for caller
-    func_validate_options_result=$func_run_hooks_result
+    $_G_rc_validate_options
 }
 
 
-- 
2.5.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.