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

next in thread | raw e-mail | index | archive | help
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.
#
eval_spin()
{
	local commands=
	if [ $# -gt 0 ]; then
		# Take commands from positional arguments
		while [ $# -gt 0 ]; do
			commands="$commands${commands:+ }'$1'"
			shift 1
		done
	else
		# Take commands from standard input
		while read -r LINE; do
			commands="$commands
$LINE"
		done
	fi
	[ "$commands" ] || return $SUCCESS

	#
	# Execute the command w/ spinner
	#
	(
		eval "$commands" > /dev/null 2>&1
		echo $?
	) | (
		n=1
		spin="/-\\|"
		DONE=

		echo -n " "
		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.

-> FUN STUFF <-
-----BEGIN GEEK CODE BLOCK-----
Version 3.1
GAT/CS d(+) s: a- C++(++++) UB++++$ P++(++++) L++(++++) !E--- W++ N? o? K- w O
M+ V- PS+ PE Y+ PGP- t(+) 5? X+(++) R>++ tv(+) b+(++) DI+(++) D(+) G+>++ e>+ h
r>++ y+ 
------END GEEK CODE BLOCK------
http://www.geekcode.com/

-> END TRANSMISSION <-




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