From c3abe9e996d48118db91888ebab2ce901cdbdcac Mon Sep 17 00:00:00 2001 From: Ole Tange Date: Wed, 9 Sep 2020 21:09:09 +0200 Subject: [PATCH] find-first-fail: renamed from binsearch. --- find-first-fail/find-first-fail | 214 ++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100755 find-first-fail/find-first-fail diff --git a/find-first-fail/find-first-fail b/find-first-fail/find-first-fail new file mode 100755 index 0000000..fcd8019 --- /dev/null +++ b/find-first-fail/find-first-fail @@ -0,0 +1,214 @@ +#!/bin/bash + +: <<'=cut' +=encoding utf8 + +=head1 NAME + +find-first-fail - find the lowest argument that makes a command fail + + +=head1 SYNOPSIS + +B [-2] [-q] I + + +=head1 DESCRIPTION + +B runs I with a single number. It returns highest +value that I succeeds for. + +It finds the value by first testing the value 1. As long as the value +succeeds, the value is doubled. When the value fails, B +does a binary search between this value and the previous value. + +If the value 1 fails, B instead searches for the highest +value that I fails for. + + +=head1 OPTIONS + +=over 4 + +=item B<-2> + +Instead of passing the command a single argument, give the command 2 +arguments: I I. + +=item B<-q> + +Quiet. Ignore output from I. + + +=back + + +=head1 EXAMPLES + +=head2 Find the last file + +This is a silly way to find the last non-existing file (namely 244): + + touch {245..800} + find-first-fail ls + +This is a silly way to find the last file (namely 800): + + touch {1..800} + find-first-fail ls + +=head2 Test a bash function + +Test how long an argument /bin/echo can take + + . $(which find-first-fail) + singleecho() { + /bin/echo $(perl -e 'print "x"x'$1) >/dev/null + } + find-first-fail singleecho + +=head2 Test a bash function that takes from and to as arguments + +Use a function that takes two arguments. It finds the line number +after HOME=. + + . $(which find-first-fail) + greplines() { + env | perl -ne "$1..$2 and print" | grep HOME= + } + find-first-fail -2 -q greplines + +=head2 Test complex command and show what is run + +Complex commands can also be run: + + find-first-fail -v perl -e 'exit(shift > 129)' + + +=head1 AUTHOR + +Copyright (C) 2020 Ole Tange, +http://ole.tange.dk and Free Software Foundation, Inc. + + +=head1 LICENSE + +Copyright (C) 2012 Free Software Foundation, Inc. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +at your option any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + + +=head1 SEE ALSO + +B(1) + +=cut + +find-first-fail() { + _find-first-fail() { + low=$1 + high=$2 + # echo $low-$high + if [ $low -gt $(($high - 2)) ]; then + return + fi + shift + shift + middle=$(( ( $low + $high ) / 2 )) + if _run $low $middle "$@" ; then + low=$middle + else + high=$middle + fi + _find-first-fail $low $high "$@" + } + + _run() { + # run: + # cmd $low $high + # or: + # cmd $value + # Output is ignored if $quiet + # Exit value is negated if $not + _inner_run() { + # _inner_run is needed if cmd is complex like: + # perl -e 'exit( (shift) + (shift) > 10)' + if $opt2 ; then + $verbose && echo "${cmd[@]}" "$a" "$b" + "${cmd[@]}" "$a" "$b" + else + $verbose && echo "${cmd[@]}" "$b" + "${cmd[@]}" "$b" + fi + } + local a="$1" + local b="$2" + shift + shift + # echo "a=$a b=$b $@" + local cmd=( "$@" ) + eval "$not" _inner_run "$quiet" + } + + quiet="" + opt2=false + verbose=false + + # Parse and remove options + while getopts "2vq" options; do + case "${options}" in + (2) opt2=true;; + (q) quiet=">/dev/null 2>/dev/null";; + (v) verbose=true;; + (-) break;; + esac + done + shift $(( OPTIND - 1)) + + # If function(1) = false: run 'not function()' instead + if _run 1 1 "$@" ; then + not='' + else + not='!' + fi + + # exponential search for the first value that is false + # low = previous value (function($low) == true) + # high = low * 2 (function($high) == false) + high=1 + while _run 1 $high "$@" ; do + low=$high + high=$(( $high*2 )) + if [ $high -gt 4611686018427387900 ] ; then + echo "$0: Error: exit value does not change of '$@'" >&2 + return + fi + done + + # low = tested good + # high = tested fail + # Search low..high + # echo "low: $low high: $high not: $not" + _find-first-fail $low $high "$@" 2>/dev/null + echo $low +} + +if [ -z "$*" ] ; then + # source the bash function + # . $(which find-first-fail) + true +else + # find-first-fail command + find-first-fail "$@" +fi