Summer's Words

"Can you rewrite it to be POSIX-compliant?"

Premise

On a wonderful horrible warm cold summer winter, someone asked me something benign:

Can you rewrite [the non-POSIX.1-2017 shell code] to be POSIX[.1-2017]-compliant?

The answer? No.

Not easily without pain: Besides the substring syntax and the array usage (both not in POSIX.1-2017), the biggest painpoint of POSIX.1-2017 shell (and also Bash in an otherwise POSIX.1-2017 environment), there is no easy way to deal with bytes outside of pipelines, so while POSIX.1-2017 date can output integers which can be converted with POSIX.1-2017 printf, we start having issues with the random number side of things.

Byte Wrangling

Probably the easiest way would be to first make a POSIX.1-2017 shell LUT in another language (Bash I suppose):

#!/usr/bin/env bash
echo 'case $1 in ' 
for INTEGER in {0..255}; do
    eval 'BYTE=$'"'"'\x'"$(printf '%02x' "$INTEGER")'"
    BYTESTR="${BYTE@Q}"
    if [[ $BYTESTR = '$'"'"'\'* ]]; then
        BYTESTR="'$BYTE'"
    fi
    printf '%s)O=%s;;' "$BYTESTR" "$INTEGER"
done
echo 'esac;printf "%s\n" "$O"'

And immediately put that into a file we never have to look at again:

bash ./lutgen > ./lut
chmod +x ./lut

(If I was one of those cool bloggers this is where I'd have an image of someone showing up beside some text saying "Don't look at the result with your naked eyes! always use cat -v for such files!")

(Instead, I'm uncool enough to find out that escapes are going to be finally added to POSIX shell soon, hence why I'm versioning my POSIX references)

Timestamp Wrangling

Now we can continue to refactor the pre-existing solution:

#!/usr/bin/env sh
OUTPUT=$(printf "%012x" "$(date +%s%3N)")

Wait! Do you see it? Look closer: %s%3N, guess which part of this isn't in POSIX.1-2017? if you answered %3N, you are wrong! All of it isn't in POSIX.1-2017!

... Without having the correct development utilities, so we can use any other language (After all, a C compiler is only an optional part of POSIX compliance), we might be in a bit of a tough spot.

Might be.

But what if we do it ourselves?

POSIX guide to datetime math.

#!/usr/bin/env sh
notoctal(){ sed 's/^0*\(.\)/\1/'; }
tm_year=$(($(date -u +%Y | notoctal)-1900))
SECONDS_SINCE_EPOCH=$((
    $(date -u +%S | notoctal) +
    $(date -u +%M | notoctal)*60 +
    $(date -u +%H | notoctal)*3600 +
    ($(date -u +%j | notoctal)-1)*86400 +
    (tm_year-70)*31536000 +
    ((tm_year-69)/4)*86400 -
    ((tm_year-1)/100)*86400 +
    ((tm_year+299)/400)*86400
))
echo "$SECONDS_SINCE_EPOCH"

retrospective addition: realizing I had to handle things that would be potentially misparsed as octals nearly made me cry.

retrospective addition: I'm also not the only one who came to this solution

Lets just save that as ./seconds, and then my editor crashed for unknown reasons.

The Script

So anyways, drum roll please:

#!/usr/bin/env sh
randbyte() { dd bs=1 count=1 < /dev/urandom 2>/dev/null; }
randbyteint() { ./lut "$(randbyte)" 2>/dev/null; }
tobytehex() { printf "%02x" "$1";}
randbytehex() { tobytehex "$(randbyteint)";}
OUTPUT=$(printf "%012x" "$(./seconds)000")
# still stealing from original code though
OUTPUT=$OUTPUT$(tobytehex "$((0x70 | ( $(randbyteint) & 0x0F ) ))")
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(tobytehex "$((0x80 | ( $(randbyteint) & 0x3F ) ))")
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
OUTPUT=$OUTPUT$(randbytehex)
echo "$OUTPUT"

And the final icing on the cake? There is no modern POSIX.1-2017 only shell so I can't test it for compliance anyways!

retrospective addition: There is also no /dev/urandom in POSIX, or /dev/random, Maybe something for a second part?

Conclusion

Lets all collectively run away from POSIX compliance, and from shells, they are both scary.

Discussion

#bash #posix #sh