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>