POSIX compliance...

Thanks to all you guys about posix compliance I have learnt an enormous amount over the last few days.
I have written a program that is an Egg Timer with simple animation.
I now realise how sophisticated 'bash' is compared to full posix compliance.
The code below has passed all of the tests from ShellCheck and boy have I found out a lot from it.
So the only way for me to learn was to create this program...
Enjoy...
(No doubt the big guns will find a better way... ;o) )

#!/bin/sh
# Egg_Timer.sh
# Fully POSIX compliant.
# Tested on OSX 10.7.5, OSX 10.11.3, Ubuntu64 and CygWin64 on Windows 8.1.
# It has fully passed CheckShell's tests. Tuesday July 5th, 2016.
# Public Domain, 2016, B.Walker, G0LCU.

mins=3
secs=0
fsecs=1.00000
keyboard="?"
width=12
horiz=36
vert=3
spaces='              '
stars='**************'
char=0
platform=$( uname )

# Generate the sinewave beep.
if [ ! -e "/tmp/beep.wav" ]
then
	> /tmp/beep.wav
	printf "\122\111\106\106\144\037\000\000\127\101\126\105\146\155\164\040\020\000\000\000\001\000\001\000\100\037\000\000\100\037\000\000\001\000\010\000\144\141\164\141\100\037\000\000" >> /tmp/beep.wav
	while [ $char -le 999 ]
	do
		printf "\200\046\000\046\177\331\377\331" >> /tmp/beep.wav
		char=$(( char + 1 ))
	done
fi

clrscn()
{
	# A default CygWin and CygWin64 install does not have 'clear' nor 'tput'!
	printf "\033c\033[0m\033[2J\033[H"
}

display()
{
	clrscn
	echo '	  At a glance Egg Timer, Public Domain, 2016, B.Walker, G0LCU.
				   ____________  
				 / ************ \ 
				||**************|| 
				 \**************/ 
				  \************/ 
				   \**********/ 
				    \********/ 
				     \******/ 
				      \****/ 
				       \**/ 
				        || 
				       /..\ 
				      / .. \ 
				     /  ..  \ 
				    /   ..   \ 
				   /    ..    \ 
				  /     ..     \ 
				 /      ..      \ 
				||      ..      || 
				 \      **      / 
__________________________________==============________________________________'
	echo "			      Time set to $secs seconds."
}

set_time()
{
	clrscn
	echo ''
	echo '		Enter the timer time in minutes first and seconds next.'
	echo ''
	printf 'Enter the number of minutes:- '
	read -r mins
	echo ''
	printf 'Enter the number of seconds:- '
	read -r secs
	echo ''
	printf "Timer set to %s? minutes and %s? seconds, are these correct? (Y/N):- " "$mins" "$secs"
	read -r keyboard
	if [ "$keyboard" = "N" ] || [ "$keyboard" = "n" ]
	then
		set_time
	fi

	# Check for typos and other errors.
	case $mins in
		''|*[!0-9]*) mins=3 ;;
	esac
	case $secs in
		''|*[!0-9]*) secs=0 ;;
	esac
	if [ $mins -gt 59 ] || [ $mins -lt 0 ]
	then
		mins=59
	fi
	if [ $secs -gt 59 ] || [ $secs -lt 0 ]
	then
		secs=59
	fi
	secs=$(( ( mins * 60 ) + secs ))
	if [ $secs -lt 15 ]
	then
		secs=15
	fi
	# A default CygWin or CygWin64 install does not have 'bc' nor 'dc'.
	fsecs=$( awk -v fp=$secs 'BEGIN { print ( fp / 17 ); }' )
}

# Main loop.
while true
do
	set_time
	horiz=36
	keyboard="?"
	width=12
	spaces='              '
	stars='**************'
	display
	sleep "$fsecs"
	printf "\033[3;36f      \033[24;1f"
	for vert in 3 4 5
	do
		printf "\033[%u;35f%s\033[24;1f" "$(( 24 - vert ))" "$stars"
		printf "\033[%u;35f%s\033[24;1f" "$vert" "$spaces"
		sleep "$fsecs"
		printf "\033[%u;41f**\033[24;1f" "$(( 23 - vert ))"
		sleep "$fsecs"
	done
	for vert in 6 7 8 9 10
	do
		char=$( awk -v star="$stars" -v wide="$width" 'BEGIN { print substr(star,1,wide); }' )
		printf "\033[%u;%uf%s\033[24;1f" "$(( 24 - vert ))" "$horiz" "$char"
		char=$( awk -v space="$spaces" -v wide="$width" 'BEGIN { print substr(space,1,wide); }' )
		printf "\033[%u;%uf%s\033[24;1f" "$vert" "$horiz" "$char"
		sleep "$fsecs"
		printf "\033[%u;41f**\033[24;1f" "$(( 23 - vert ))"
		sleep "$fsecs"
		horiz=$(( horiz + 1 ))
		width=$(( width - 2 ))
	done
	printf "\033[11;%uf  \033[24;1f" "$horiz"
	for char in 1 2 3
	do
		if [ "$platform" = "Darwin" ]
		then
			afplay /tmp/beep.wav > /dev/null 2>&1
		else
			aplay /tmp/beep.wav > /dev/null 2>&1
		fi
		if [ "$( awk -v name="$platform" 'BEGIN { print substr(name,1,6); } ')" = "CYGWIN" ]
		then
			cat /tmp/beep.wav > /dev/dsp
		fi
	done
	printf 'Run again? (Y/N):- '
	read -r keyboard
	if [ "$keyboard" = "N" ] || [ "$keyboard" = "n" ]
	then
		break
	fi
done
clrscn
exit 0
# Egg_Timer.sh end.

Hi Barry,

That looks quite neat and Posixy!

Some comments and ideas:

  • The sleep command is not used in a POSIX compliant ways, since it gets fed a float here, and in POSIX it can only handle integers. An alternative is maybe to fill the hourglass more or less from the start, depending on the number of minutes / seconds, and compensate the rest with the first sleep command.
  • Instead of using awk to cut the first 6 characters you could use "${platform%"${platform#??????}"}" or better yet, replace the if statements with one case statement and use it pattern matching capability CYGWIN*) , so you do not need to cut the platform string.
  • The awk command substitutions in the 2nd for loop take time and will skew the time slightly. An alternative is to use parameter expansions or predefine the strings so that it does not add as much to the time used by the sleep commands..
  • Perhaps you could have a seconds countdown instead of the static number.
  • If more time is entered than what the hourglass can handle, graphically turn the hourglass (nice scripting challenge?)
1 Like

Apologies for any typos...

Thank you for these comments. I was half expecting to be slated... ;oD

I did check the auxiliary programs sleep and awk but could find nothing definite to tell me what was the current POSIX position for these.

I had lines similar to these:-

#!/bin/sh
text="1234567890"
char=$( cut -c2-6 <<< "$text" )
printf "%s" "${text:1:5}"
echo "$char"
$ shellcheck myscript
 
Line 3:
char=$( cut -c2-6 <<< "$text" )
                      ^-- SC2039: In POSIX sh, here-strings are undefined.
 
Line 4:
printf "%s" "${text:1:5}"
             ^-- SC2039: In POSIX sh, string indexing is undefined.

$ _

These really threw me and constant searching gave me the results inside the main code.
I could not find your "${platform%"${platform#??????}"}" anywhere, perhaps it was my search phrases but I spent hours trying to find non-time consuming methods without much success. Many thanks for those snippets.
I never even thought about using pettern matching, maybe because being an amateur I think differently to pros.

I was giong to use the "afplay", "aplay" and "/dev/dsp" as timers, (I will let you think about that one ;o) ), but decided against it as these are platform specific and would require some serious experimentation.

However I was aware of timings but that was not too important to me as learning POSIX requirements was; and guys like you supplying info helps others, (and judging by the posts on here, possibly tens of thousands of them), to steer through this quagmire too.

Again thanks, makes me feel good... ;o)

EDIT:
I forgot to add:- The rotating hourglass idea using ASCII - OUCH, but as you say a real challenge to draw at 45 degrees per movement... ;oO

Hi Barry, you are welcome of course.

One addition:
The awk command substitutions could be replaced by printf statements and integrated in the next printf statement, for example the first one could be:

printf "\033[%u;%.${width}s\033[24;1f" "$(( 24 - vert ))" "$horiz" "$stars"

The dot "." in the %s format specifier marks the precision of the string.

--
With regards to the sleep command:

The POSIX specification says:

sleep: Operands

And the OSX man sleep(1)

and GNU coreutils sleep:

1 Like

Hi Scrutinizer...
Many thanks for your post I will try your latest method this evening.

In the meantime......

I have an idea for a floating point sleep replacement using sleep.
Apart from the pseudo-code initialise something; while test something; do something_else; increment something; done ,
is there a POSIX equivalent to the BASIC form of 'FOR' loop without using seq :-

FOR n = number1 TO number2
[do something......]
NEXT

Again I have not found anything on the Interwebs.
Putting 'number1 TO number2' as a list works for small numbers say '1 2 3 4 5 6 7 8 9 10' but just imagine if that was '1 to 1000'.

It amazes me that this absolutely minmal use of posix 'for' loop is not catered for.
As I have written above I could use a 'while' loop which could inculde a pseud-STEP subcommand but this will need an initialisation line per call, easy yes, but a PITA.
It must be the way I search... ;o/

TIA.

Some shells (such as bash and 1993 or later versions of ksh ) provide:

for((n=1; n<=100; n++))
do	echo "process n=$n"
done

but this is an extension that is not yet specified by the standards. As you said, standard ways to do the same thing require an initialization step before the loop, such as in:

n=1
while [ $n -le 100 ]
do	echo "process n=$n"
	n=$((n + 1))
done

and in:

n=0
while [ $((++n <= 100)) -eq 1 ]
do	echo "process n=$n"
done

Note: As noted by Scrutinizer in post #8, the POSIX standards do not require $((++n)) to work. (It works with a recent bash and with a 1993 or later version of ksh , but is an extension to the requirements specified by the standards.)

n=0
while [ $(((n += 1) <= 100)) -eq 1 ]
do	echo "process n=$n"
done

should work in an POSIX-conforming shell.

1 Like

Well I have been experimenting with sleep <secs> to give me a smaller time accurate to about + or - 5mS on this MacBook Pro, OSX 10.7.5.

It would be good enough for the Egg_Timer.sh and passes the ShellCheck test.

It is fully posix compliant with SLEEPs unity seconds timer only.

Boy am I gonna get some flak over this one, (yes I bend the rules a little)... ;o)

#!/bin/sh
# delay.sh <secs> <millisecs[0-999]>
secs="$1"
millisecs=$( awk -v mS="$2" -v correction=0.94 ' BEGIN { print (( mS / 2 ) * correction ); } ' )
millisecs="${millisecs%.*}"

# Accuracy on my MBP OSX 10.7.5 + or - 5mS of FSD.
mS()
{
	loop=0
	while [ "$loop" -le "$millisecs" ]
	do
		sleep 0
		wait
		loop=$(( loop + 1 ))
	done
}
# Test the mS function only.
time mS
sleep "$secs"

As I quoted in a previous post I could create a resonably accurate timer using 'afplay', 'aplay' or '/dev/dsp'. I might use afplay and make it Apple dedicated...

Thanks Don for the info...

I await the flak... ;o)

Support for pre-/postfix operators like in ++n is not a requirement in the POSIX specification.

Shell Command Language:Arithmetic Expansion

I suggest using something like this instead:

n=0
while [ $(( n+=1 )) -le 100 ]
do
  echo "process n=$n"
done
2 Likes

I'm not sure if this is flak or not, but I'm not sure that I understand what this script is trying to do.

Just using POSIX-defined features in your shell script does not mean that your shell script will give you the same results on all POSIX-conforming systems.

If the idea is to sleep for a particular number of seconds and milliseconds on OS X version 10.7.5 with the particular amount and type of memory present on your system with the same CPU you're using and with the same disk drive you're using under similar load conditions, the code above might come close to doing what you want. But, for small sub-second sleeps, the amount of time spent forking and execing awk and sleep may overwhelm your other calculations. And, if you invoke this script several times, the first time it runs is likely to be slower than subsequent runs since awk , sleep , and this script will need to be loaded from disk the first time it runs, while subsequent runs in the not too distant future can be loaded from the cache.

So, although the way you are performing the calculations in this script is portable, the calculations you are performing are not portable to any other hardware and software configuration because what you are really calculating is how long it will take to fork() , exec sleep , run sleep 0 , and waitpid() for the exit status from the sleep 0 . All of this is very dependent on your hardware and is not portable to any different configuration. Note also that the wait in your loop is a relatively quick no-op. The wait would only take any significant amount of time if your script had started background jobs that had not previously been waited for. (Your script never starts any background jobs, so all the wait built-in does is loop through the zero background jobs running in the current shell waiting for each one to complete before it returns a zero exit status.)

Although:

sleep sss.mmm

where sss is a given number of seconds and mmm is a given number of milliseconds to sleep is not portable between various systems, it is portable and will give you much more consistent sleep times between various versions of hardware running various versions of OS X (at least for releases of OS X greater than or equal to 10.0). It will also work on other BSD or Linux based systems and, if you use a 1993 or later ksh instead of bash , on any system using that shell. ( ksh93 has a built-in sleep that accepts fractional seconds; bash version 3.2.57 does not have a built-in sleep on OS X.)

If you want a portable way to sleep for a given number of seconds and nano-seconds in a shell script that will work on any POSIX-conforming system, you'll need to write your own nanosleep utility in C that calls the nanosleep() function with the seconds and nanoseconds specified by operands given when it is invoked and then call that utility from your script. Or you can configure your script to invoke the system's sleep utility if it supports sub-second sleeps, or invoke your nanosleep utility otherwise.

1 Like

Hi.

Also:

            For delays of finer granularity than one second, the Time::HiRes
            module (from CPAN, and starting from Perl 5.8 part of the standard
            distribution) provides usleep(). You may also use Perl's
            four-argument version of select() leaving the first three
            arguments undefined, or you might be able to use the "syscall"
            interface to access setitimer(2) if your system supports it. See
            perlfaq8 for details.

Excerpt from perldoc -f sleep

For example:

#!/usr/bin/env perl

# @(#) p1       Demonstrate usleep;

use Time::HiRes qw( usleep nanosleep );

$microseconds = 2500000;
usleep($microseconds);

# nanosleep ($nanoseconds);

exit(0);

which would produce something like:

time ./p1

real    0m2.514s
user    0m0.008s
sys     0m0.000s

On an older OS/X:

OS, ker|rel, machine: Apple/BSD, Darwin 9.8.0, Power Macintosh
Distribution        : Mac OS X 10.5.8 (leopard, workstation)
perl 5.8.8

real    0m2.578s
user    0m0.045s
sys     0m0.021s

Best wishes ... cheers, drl

1 Like

Hi Don...
You are of course correct in what you have quoted. I was well aware of my sleep alternative's limitations so I decided to embed a delay into a shell script. You suggested a C program to do the job, so here goes...

#!/bin/sh
# delay <milliseconds>
# MacBook Pro, 13 Inch, Circa August 2012, OSX 10.7.5, using sh and limited /bin/echo command.
clear

# Use /bin/echo here for minimalist echo command.
/bin/echo '/* delay.c */
/* Usage:- delay <milliseconds[0 to 1000000]> */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	unsigned int milliseconds;
	milliseconds = 0;

	/* This allows for NULL millisecond argument and more arguments too. */
	if (argc <= 1 || argc >= 3)
	{
		printf ("\nUsage:- delay <milliseconds[0 to 1000000]>\n\n");
		return 1;
	}

	/* Check for number of characters, no less than 1 and no more than 7. */
	if (strlen(argv[1]) <= 0 || strlen(argv[1]) >= 8) (argv[1]) = "0";

	/* Characters in function atoi() returns integer 0. */
	milliseconds = atoi(argv[1]);

	/* Finally, ONLY allow 0 to 1000000 range. */
	if (milliseconds <= 0 || milliseconds >= 1000001) milliseconds = 0;

	usleep (milliseconds * 1000);
	return 0;
}' > /tmp/delay.c
echo "Show C source..."
cat /tmp/delay.c
echo "Compile the C source..."
gcc /tmp/delay.c -o /tmp/delay
ls -l /tmp/delay*
echo "Delay for 3 seconds - /tmp/delay 3000..."
/tmp/delay 3000
echo "Return code is $?..."
echo "Now show error report - /tmp/delay..."
/tmp/delay
echo "Return code is $?..."

Results:-

Last login: Mon Jul 11 22:09:24 on ttys000
AMIGA:barrywalker~> cd Desktop/Code/Shell
AMIGA:barrywalker~/Desktop/Code/Shell> ./mS_delay.sh


Show C source...
/* delay.c */
/* Usage:- delay <milliseconds[0 to 1000000]> */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
	unsigned int milliseconds;
	milliseconds = 0;

	/* This allows for NULL millisecond argument and more arguments too. */
	if (argc <= 1 || argc >= 3)
	{
		printf ("\nUsage:- delay <milliseconds[0 to 1000000]>\n\n");
		return 1;
	}

	/* Check for number of characters, no less than 1 and no more than 7. */
	if (strlen(argv[1]) <= 0 || strlen(argv[1]) >= 8) (argv[1]) = "0";

	/* Characters in function atoi() returns integer 0. */
	milliseconds = atoi(argv[1]);

	/* Finally, ONLY allow 0 to 1000000 range. */
	if (milliseconds <= 0 || milliseconds >= 1000001) milliseconds = 0;

	usleep (milliseconds * 1000);
	return 0;
}
Compile the C source...
-rwxr-xr-x  1 barrywalker  wheel  8824 11 Jul 22:14 /tmp/delay
-rw-r--r--  1 barrywalker  wheel   802 11 Jul 22:14 /tmp/delay.c
Delay for 3 seconds - /tmp/delay 3000...
Return code is 0...
Now show error report - /tmp/delay...

Usage:- delay <milliseconds[0 to 1000000]>

Return code is 1...
AMIGA:barrywalker~/Desktop/Code/Shell> _

ShellCheck results for the script:-

$ shellcheck myscript
 
Line 7:
/bin/echo '/* delay.c */
          ^-- SC2028: echo won't expand escape sequences. Consider printf.

$ _

I don't want to expand any escape sequences...

Hi drl...
I could do it in python too.

#!/usr/bin/python
# fpt.py <floating point time>
import time
import sys
fpt=float(sys.argv[1])
time.sleep(fpt)
exit(0)

Thanks both for the feedback...

I thought you guys migh be interested in this...

The code in my post #11 works without a hitch on a stock AMIGA A1200(HD), AMIGA-OS 3.0x, with and wihout a 4MB Fastram upgrade, running the 'ADE' unix install for the AMIGA...

Is it slow to run the shell script? Yes.
Are there any errors? No.
Even /usr/bin/echo exists - cool.

It uses 'ksh[88?]' inside the default AMIGA shell window which has a subset of terminal escape codes.

KSH version is:-
@(#)PD KSH v5.2.12 Geek Gadgets Amiga port 97/08/13 .

GCC version is:-

Reading specs from /gg/lib/gcc-lib/m68k-amigaos/2.95.3/specs
gcc version 2.95.3 20010315 (release)

AFAIAC this is solved.