Use of coproc in bash - an example, not a problem.
ProgrammingThis forum is for all programming questions.
The question does not have to be directly related to Linux and any language is fair game.
Notices
Welcome to LinuxQuestions.org, a friendly and active Linux Community.
You are currently viewing LQ as a guest. By joining our community you will have the ability to post topics, receive our newsletter, use the advanced search, subscribe to threads and access many other special features. Registration is quick, simple and absolutely free. Join our community today!
Note that registered members see fewer ads, and ContentLink is completely disabled once you log in.
If you have any problems with the registration process or your account login, please contact us. If you need to reset your password, click here.
Having a problem logging in? Please visit this page to clear all LQ-related cookies.
Get a virtual cloud desktop with the Linux distro that you want in less than five minutes with Shells! With over 10 pre-installed distros to choose from, the worry-free installation life is here! Whether you are a digital nomad or just looking for flexibility, Shells can put your Linux machine on the device that you want to use.
Exclusive for LQ members, get up to 45% off per month. Click here for more info.
Use of coproc in bash - an example, not a problem.
I wanted to set up a general "spinner" system that I could use when I was running something that took a long while to complete, but which produced little output while running.
So, here's a code package I wrote that uses the new (in bash 4) coproc command to start a spinner (on stdout) running as a background process while the foreground process can run unchanged.
I post it here for comment and/or improvement, and because I could find few examples of coproc usage.
Code:
#!/bin/bash
##########################################################
#
# Display a "spinner"
#
# Argument: One of "start," "stop," or "toggle."
# If no argument, "toggle" is assumed.
#
# Note: This function uses a globle variables, __Spinner...
# to control its action.
#
##########################################################
#
# Function to get the state of the spinner.
#
# Arguments: none
#
# Return: 1 if the spinner is running, otherwise 0
#
#########################################################
function spinning()
{
local running
if [ -z "${__Spinner_State_PID}" ] || [ $((__Spinner_State_PID + 0)) -eq 0 ]
then
running=0
else
running=1
fi
return ${running}
}
##########################################################
#
# Create or kill a coprocess running a spinner on stdout
#
##########################################################
function toggle_spin()
{
local running
spinning
running=$?
# Is the spinner running?
if [ ${running} -eq 0 ]
then
# No. Create it.
exec 5<&0 # We want to use the parrent process' stdout
# The coprocess is written as an inline function so it can be named
coproc __Spinner_State \
(sym="|/-\\-"
i=-1
while true
do
i=$(( (++i)%5 ))
echo -n ${sym:$((i)):1} >&5
sleep 0.1s
echo -en $'\b' >&5
done)
else
# Yes. Terminate it.
eval "exec ${__Spinner_State[0]}>&-"
eval "exec ${__Spinner_State[1]}>&-"
eval 5>&-
kill ${__Spinner_State_PID} &>/dev/null
__Spinner_State_PID=0
fi
}
#############################################################
#
# Master control function
#
#############################################################
function spinner()
{
local running
spinning
running=$?
if [ $# -gt 1 ] || [ "${1:0:1}" == "-" ]
then
cat <<EOF >&2
spinner: Start, stop, or toggle a "spinner" on /dev/stdout
Usage: spinner {toggle | start | stop}
Default: toggle
Return: 0 if the spinner is running, otherwise 1.
Note: Generally one woud call spinner with no argument
to toggle the spinner on or off. See the test function
(commented out) at the end of this file.
EOF
return ${running}
fi
case "${1:2:1}" in
("a"|"A") if [ ${running} == 1 ]
then
toggle_spin
fi;;
("o"|"O") if [ ${running} == 0 ]
then
toggle_spin
fi;;
(*) toggle_spin;;
esac
spinning
return $?
}
#
# Test program (Uncomment for testing)
#
#spinner
#if [ $? -ne 0 ]
#then
# for ((i=1;i<6;++i))
# do
# sleep 5s
# echo -n $'\b'". "
# done
# echo
# spinner
#else
# echo "The spinner failed to start."
#fi
Probably only niggly personal things, but just in case:
1. Why not keep to a theme when testing and using arithmetic? I find the jumping from [] or [[]] to (()) and back distracting and generally have to re-read to see what type of test is being done
2. Additional to above, the use of (()) inside [] or [[]] would seem to be a waste:
Code:
[ $((__Spinner_State_PID + 0)) -eq 0 ]
More simply this could just be:
Code:
(( 0 == __Spinner_State_PID + 0 ))
3. I also use the above format for assignment as well:
Code:
i=$(( (++i)%5 ))
Becomes:
Code:
(( i = (++i)%5 ))
4. Variables used in splices do not require additional expanding and can be used raw:
Code:
echo -n ${sym:$((i)):1} >&5
Becomes:
Code:
echo -n ${sym:i:1} >&5
5. I understand the need for eval when using a variable as the descriptor, but see no need for it in the below (unless I misunderstand the process here):
Code:
eval 5>&-
Shouldn't this be:
Code:
exec 5>&-
6. Instead of hoping the people have spelt 'start' and 'stop' correctly I prefer to use the whole string and to avoid user use of the shift or caps lock keys, force either upper or lower case:
Code:
case "${1,,}" in
start) ...;;
stop) ...;;
*) ...;;
esac
Well, gail, if I didn't need a little salt, would I have posted here?
Rather than addressing all your points, let me just say that I learned my bash many years ago, and old habits hard can be hard to break. I did make most of the changes you suggested, and the test code worked as well after those changes.
You point (6) is well taken, but I thought that the most common usage would be the "toggle" default. I was working on an awk implementation of the Double Metaphone algorithm when I distracted myself with this exercise. Since I was looking at some C++ code that was jumping through hoops to find single characters, I thought an illustration of how to pull single characters from a string in bash might not be remiss.
As to your point (5), yes, the eval is unnecessary. In fact, since the coproc in killed, I'm not sure if closing the pipes and the stdout copy is even necessary. I typically close files after I finish with them since not doing so can sometimes cause strange things to happen, but killing a process at one end of the pipe usually closes the pipe (and any other resources used by the process).
Oh, the fifth "spin" element was an error (it made the wheel jerk), and sleeping for 0.2s made it look somewhat smoother.
All that being said, here, with appreciation, is a revised version:
Code:
#!/bin/bash
##########################################################
#
# Display a "spinner" until called a second time.
#
# Argument: none
#
# Note: This function uses global variables, __Spinner...
# to control its action.
#
##########################################################
#
# Function to get the state of the spinner.
#
# Arguments: none
#
# Return: 1 if the spinner is running, otherwise 0
#
#########################################################
function spinning()
{
local running
if [ -z "${__Spinner_State_PID}" ] || ((0 == __Spinner_State_PID + 0))
then
running=0
else
running=1
fi
return ${running}
}
##########################################################
#
# Create or kill a coprocess running a spinner on stdout
#
##########################################################
function toggle_spin()
{
local running
spinning
running=$?
# Is the spinner running?
if ((running==0))
then
# No. Create it.
exec 5<&0 # We want to use the parent process' stdout
# The coprocess is written as an inline function so it can be named
coproc __Spinner_State \
(sym="|/-\\"
i=-1
while true
do
(( i = (++i)%4 ))
echo -n ${sym:i:1} >&5
sleep 0.2s
echo -en $'\b' >&5
done)
else
# Yes. Terminate it.
kill ${__Spinner_State_PID} &>/dev/null
__Spinner_State_PID=0
fi
}
#############################################################
#
# Master control function
#
#############################################################
function spinner()
{
local running
spinning
running=$?
if [ $# -gt 0 ] || [ "${1:0:1}" == "-" ]
then
cat <<EOF >&2
Usage: spinner [--help]
Return: 0 if the spinner is running, otherwise 1.
EOF
return ${running}
fi
toggle_spin
spinning
return $?
}
#
# Test program (Uncomment for testing)
#
#spinner
#if [ $? -ne 0 ]
#then
# for ((i=1;i<6;++i))
# do
# sleep 5s
# echo -n $'\b'". "
# done
# echo
# spinner
#else
# echo "The spinner failed to start."
#fi
If anyone's interested, here's the timing of the last run I did of the test program:
Code:
$ time bash Spinner
.....
real 0m25.077s
user 0m0.001s
sys 0m0.062s
It is funny but you are quite right that people just don't like to sit and wait for something to finish and the act of a spinner or progress bar just seems to put everyone at ease (myself included).
I'll be interested to see if coproc can help in some of my work too.
Well, yes, dot was similar. I was looking for examples of coproc usage, and this one seemed easy.
A more practical program would use the input and output pipes to send messages to the coprocess and receive replies from it, much like a DBUS system in user space. In fact, since bash has a dbus interface, there may be no real need for coproc, but it might be useful in some cases. For example, if you wanted to simulate a windowing system on a terminal display, you might use coproc and curses to create different "windows" in a more modular way then existing systems (like, e.g., emacs, mc, etc.,) currently use to solve that problem.
Similarly in “spinner” you do not have to “return $?” at the end.
On the topic of “spinner”, why is this function calling “spinning” twice? it could be easily reworked to use if-then-else-fi without the need to have two call places of “spinning”.
And to test if spinning, just do:
Code:
if spinning; then
: …
else
: …
fi
Code:
exec 5<&0 # We want to use the parent process' stdout
Except 0 is stdin. Besides you should be able to manage without exec by simply adding “5>&1” at the end of “coproc” invocation.
Code:
(( i = (++i)%4 ))
This is just confusing. Use addition like normal people:
Code:
(( i = (i + 1) % 4 ))
Lastly, drop the “function” keyword, it's cleaner.
grail, your third point is a matter of preference. I myself find “i=$(( (i + 1) % 4 ))” nicer. Plus it's POSIX, whereas “((…))” isn't.
LinuxQuestions.org is looking for people interested in writing
Editorials, Articles, Reviews, and more. If you'd like to contribute
content, let us know.