Subject: bug#20006: Bash-specific performance by avoiding sed

From: Pavel Raiskup <[email-masked]>
Date: Mon, 05 Oct 2015 15:28:56 +0200

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
>From 1b89ae66f88c3822b2afec128f288d409287780f Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <[email-masked]>
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

>From c283b8a8e7540b65fd7a7d32fceec6ed42b5b2a7 Mon Sep 17 00:00:00 2001
From: Pavel Raiskup <[email-masked]>
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