I am learning bash scripting and while I was looking on creating myself a spinner while processing something in my script I came across this code in this question:

echo -n ' '
while true
printf "\b${sp:i++%${#sp}:1}"

This is what I was looking for: a spinner type that doesn't concatenate, just erases its entry and echos the next character in $sp

I do not understand however how this is working in the

printf "\b${sp:i++%${#sp}:1}"

Could someone elaborate it for me, so I can learn what is going on? From a programming stand point it look like a ternary for loop but after sp:i++ I am lost. I know 1 means to only show one character in the $sp var.

I do also know this while is ever ending.

Also worth noting that there are several differences between echo and printf as I tried echo with echo "\b${sp:i++%${#sp}:1}" that did not provide the same results ... reference

  • So is it echo or printf that you are asking about? Feb 18 '20 at 19:07

The echo is not doing anything smart at all. It is just echoing two characters:

  • \b is BackSpace. That makes it go back one column on the screen.

  • A visible character, which is one of the 4 chars in the variable sp. That make it go one space forward on the screen, too. So it keeps putting the characters on top of each other. So you see / - \ | in order.

The thing inside ${ .... } is a shell expansion that loops round the 4 characters using an index i.

  • It picks the character using a substring expansion ${sp:i:1}, which picks one character at position i from the content of $sp.

  • The i++ part increments i every time it is used, to move on one character in sp.

  • The % is a modulus operation, necessary to "return to the beginning" when i runs over the end of sp.

  • The ${#sp} is a shell expansion that returns the length of sp.

This trick needs a delay (like sleep 0.25) inside the loop to slow down the spin.

It needs to detect that whatever you are waiting for has finished. If this shell started an asynchronous process, it could run "jobs" and see if it now has no child processes. If it is waiting for a file to be created, it could test for that.

  • I understood your explanation a little better thank you. Probably because I had no knowledge of shell expansion in which I clearly need to learn about. I do agree about the delay when I had this going it was way too fast. How would you implement the delay/sleep since this is inside a shell expansion?
    – gstlouis
    Feb 20 '20 at 13:52
  • sorry easy Q, I just stuck sleep 0.5 under printf "\b${sp:i++%${#sp}:1}" and I've got a delay.
    – gstlouis
    Feb 20 '20 at 14:13

There are two variables at play here. You can see better what's going on with this:

sp="abcd" i=0                                         # Initialisation
printf "%d %s %d\n" "$i" "${sp:i++%${#sp}:1}" "$i"    # Repeat this line a few times

What you see is that $i increments by one ($i++) each time the expression is used. This is taken, modulo the length of $sp, as a starting index into the string $sp. So when $i is 6, then 6 % 4 is 2 so the third character of $sp is printed (string offsets start from zero)

You could unpack the expression like this,

sp="abcd" i=0
len_sp=${#sp}              # length of $sp

i=$(( i + 1 ))             # increment $i
mod_i=$(( i % len_sp ))    # wrap around length of $sp
sp_sub=${sp:mod_i:1}       # get substring of one character
printf "%s" "$sp_sub"      # print it

The other interesting parts are echo -n ' ', which prints a space without a trailing newline, and the printf "\b", which prints a backspace.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.