Skip site navigation (1)Skip section navigation (2)
Date:      Thu, 11 Nov 2010 18:28:11 -0800
From:      Devin Teske <dteske@vicor.com>
To:        freebsd-hackers@freebsd.org
Subject:   Re: Spinner Function for Shell Scripts
Message-ID:  <1289528891.30235.130.camel@localhost.localdomain>
In-Reply-To: <1289506363.30235.113.camel@localhost.localdomain>
References:  <1289506363.30235.113.camel@localhost.localdomain>

next in thread | previous in thread | raw e-mail | index | archive | help
On Thu, 2010-11-11 at 12:12 -0800, Devin Teske wrote:
> Hi fellow hackers... I come with baring gifts!
> 
> So, just as the subject-line says, ... here's an efficient and robust
> spinner function compatible with many shells.
> 
> But, before I get into the code, let me first explain that going to
> Google and searching for "spinner shell script" results in 100+
> different types of "FAIL" either because:
> 
> a. the proposed solution doesn't properly handle signals (like Ctrl+C)
> b. doesn't escape the commands being executed, which leads to syntax
> errors in the eval statement
> c. the proposed solution is not efficient enough (for example, executing
> a simple command like "true" -- which returns immediately -- should
> result in no spinner being displayed and return very fast)
> d. stdout/stderr is not properly masked from output while displaying the
> spinner
> e. the command exit-status is not preserved
> f. or the proposed solution attempts to use background job-control (e.g.
> w/ &/pidwait/wait/bg/fg/kill/etc.) which can have nasty side-effects.
> 
> A special note about that last one: Solutions involving background job-
> control are especially annoying because if signals are not properly
> trapped, the spinner could potentially be left running (that is, if the
> spinner is the thing in the background versus vice-versa). Doubly
> annoying is that in stress-testing these implementations, it appears
> that using kill(1) to kill the process can occasionally (~1-in-50)
> produce an  errant unmaskable "Terminated" or "Killed" or "Hangup"
> message (depending on which signal is used to do your killing of the
> spinner). For example (from testing), sending any one of these signals
> can cause premature termination if not trapped: SIGINT SIGTERM SIGPIPE
> SIGXCPU SIGXFSZ SIGFPE SIGTRAP SIGABRT SIGSEGV SIGALRM SIGPROF SIGUSR1
> SIGUSR2 SIGHUP SIGVTALRM (NOTE: we're talking about shell scripts here).
> 
> I feel that I've perhaps finally developed the end-all be-all spinner
> function (BSD Licensing applies):
> 
> #!/bin/sh
> # -*- tab-width:  4 -*- ;; Emacs
> # vi: set tabstop=4     :: Vi/ViM
> #
> ############################################################ COPYRIGHT
> #
> # Devin Teske (c)2006-2010. All Rights Reserved.
> #
> # Redistribution and use in source and binary forms, with or without
> # modification, are permitted provided that the following conditions
> # are met:
> # 1. Redistributions of source code must retain the above copyright
> #    notice, this list of conditions and the following disclaimer.
> # 2. Redistributions in binary form must reproduce the above copyright
> #    notice, this list of conditions and the following disclaimer in the
> #    documentation and/or other materials provided with the distribution.
> #
> # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
> # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
> # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
> # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
> # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> # DAMAGES (INLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
> # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
> # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
> # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
> # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
> # SUCH DAMAGE.
> #
> ############################################################ GLOBALS
> 
> # Global exit status variables
> export SUCCESS=0
> export FAILURE=1
> 
> ############################################################ FUNCTIONS
> 
> # eval_spin $command [ $args ... ]
> #
> # Execute a command while displaying a rotating spinner.
> #


I've been refining this...

UPDATE: Fix `eval_spin ...' to work with compound statements, such as
`eval_spin "sleep 1; sleep 2; sleep 3"'

UPDATE: Fix `eval_spin <<EOF' and `... | eval_spin' to preserve
leading/trailing whitespace.

UPDATE: Further simplified and cleaned-up.


eval_spin()
{
	#
	# Execute a command w/ spinner
	#
	(
		if [ $# -gt 0 ]; then
			# Take commands from positional arguments
			( eval "$@" ) > /dev/null 2>&1
			echo $?
		else
			# Take commands from standard input
			( eval "$( cat )" ) > /dev/null 2>&1
			echo $?
		fi
	) | (
		n=1
		spin="/-\\|"
		DONE=

		printf " "
		while [ ! "$DONE" ]; do
			DONE=$( /bin/sh -c 'read -t 0 DONE; echo $DONE' )
			printf "\b%s" $( echo "$spin" | sed -e \
				"s/.\{0,$(( $n % ${#spin} ))\}\(.\).*/\1/" )
			n=$(( $n + 1 ))
		done
		printf "\b \b"
		exit $DONE
	)
}




> 
> ############################################################ MAIN SOURCE
> 
> eval_spin "$@"
> 
> ################################################################################
> # END
> ################################################################################
> 
> And now... for some quick examples of usage...
> 
> ... show-off that we can handle both arguments and stdin ...
> 
> eval_spin sleep 3
> # spins for 3 seconds
> 
> eval_spin << EOF
> sleep 3
> EOF
> # spins for 3 seconds
> 
> echo sleep 3 | eval_spin
> # spins for 3 seconds
> 
> ... show-off that -- since we don't fork -- we can do functions ...
> 
> myfunc(){ sleep 3; }
> eval_spin myfunc
> # spins for 3 seconds
> 
> ... show-off that we preserve the exit status ...
> 
> eval_spin true
> # immediately returns, exit status is zero
> 
> eval_spin false
> # immediately returns, exit status is one
> 
> eval_spin 1
> # immediately returns, exit status is 127 (syntax error)
> 
> ... show-off that we support user-generated interrupt signal ...
> 
> eval_spin sleep 100
> # press Ctrl-C... exit status is 130 (interrupted)
> 
> ... show-off our efficiency ...
> 
> time sleep 5
> # Takes 5.003s
> time eval_spin sleep 5
> # Takes 5.059s
> 
> ... show that efficiency is retained in ramping-up ...
> 
> time sleep 10
> # Takes 10.004s
> time eval_spin sleep 10
> # Takes 10.041s
> 
> ... show that efficiency is key ...
> 
> time eval_spin true
> # Takes 0.029s
> 
> ... and you can already see from the code, I don't use kill(1), I don't
> use `&', and I don't use background job-control features of the shell.
> 
> The only odd-ball thing you'll find in the code is that I invoke /bin/sh
> to use bourne-shell's `read' built-in so that in the event that we are
> sourced into another shell (such as bash), we don't end up using that
> shells `read' built-in (testing shows that bash's `read' doesn't
> function the same with respect to our `-t 0' syntax). Removing the
> direct-invocation of /bin/sh does not buy you any significant efficiency
> gains (so the portability that the statement gives us was favored).
> 
> We've generalized this function into a central include that we include
> into our shell scripts using the `.' built-in. However, if you want to
> adapt this for boot-scripts, I can rewrite it to:
> 
> a. Not-use sed(1) (which lives in /usr/bin so isn't available at boot-
> time).
> b. Not redirect output to /dev/null
> c. Replace printf with echo
> 
> Both of which are trivial,...
> 
> To get rid of sed(1), we just need to implement a substr function (BSD
> Licensing applies -- same copyright as above)...
> 
> # substr $string $start [ $length ]
> #
> # Obtain a substring. The starting position may be negative (relative to end)
> # or positive (relative to beginning). The length is in bytes to the right of
> # the starting position. Returns with failure status on error.
> #
> substr()
> {
>     local string="$1" start="${2:-0}" len="${3:-0}"
> 
>     # Check arguments
>     [ "$string" ] || return $FAILURE
>     [ $start -gt ${#string} ] && return $SUCCESS
> 
>     # Advance to the starting position
>     [ ${start} -lt 0 ] && start=$((${#string} + $start))
>     [ ${start} -lt 0 ] && start=0
>     while [ $start -gt 0 ]; do
>         string="${string#?}"
>         start=$(($start - 1))
>     done
> 
>     # Truncate to the proper length
>     [ $len -le 0 ] && len=${#string}
>     while [ ${#string} -gt $len ]; do
>         string="${string%?}"
>     done
> 
>     echo -n "$string"
> }
> 
> In which case, the following sed(1) usage (from above):
> 
> 	printf "\b%s" $( echo "$spin" | sed -e \
> 		"s/.\{0,$(( $n % ${#spin} ))\}\(.\).*/\1/" )
> 
> Becomes instead (also taking care to get rid of printf):
> 
> 	echo "^H$( substr "$spin" $(($n % ${#spin})) 1 )"
> 
> ...
> 
> Writing a version of eval_spin that is entirely free of all external
> dependencies (safe for one exception: /bin/sh) for the purpose of
> inclusion into /etc/rc.subr is something that intrigues me. I could
> imagine rewriting all of the rc.d scripts to use it... with other
> fundamentals to beautify the boot-process.
-- 
Cheers,
Devin Teske

-> CONTACT INFORMATION <-
Business Solutions Consultant II
FIS - fisglobal.com
510-735-5650 Mobile
510-621-2038 Office
510-621-2020 Office Fax
909-477-4578 Home/Fax
devin.teske@fisglobal.com

-> LEGAL DISCLAIMER <-
This message  contains confidential  and proprietary  information
of the sender,  and is intended only for the person(s) to whom it
is addressed. Any use, distribution, copying or disclosure by any
other person  is strictly prohibited.  If you have  received this
message in error,  please notify  the e-mail sender  immediately,
and delete the original message without making a copy.

-> END TRANSMISSION <-




Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?1289528891.30235.130.camel>