Search code examples
stringshellposix

How can I generate an alphanumeric string using only POSIX-compliant shell tools?


I'm trying to generate an eight character alphanumeric string using POSIX compliant shell tools. I'm trying to use /dev/urandom to produce this string (incidentally I did not know that /dev/random and /dev/urandom are not specified by POSIX, but I'm going with it because they are present on Linux, FreeBSD, Mac OS, AIX etc.)

There are a plethora of guides that show how this can be done, but none that I've found are close to being posixly correct. In particular I keep seeing the use of head -c (the -c argument is not defined in POSIX) like below:

head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13 ; echo ''

Another concern I have is that almost none of the solutions I have seen respect the difference between a byte stream and a character stream, which makes me worried that I'm producing strings that are unsafe.

This is the best I can seem to come up with, but I don't fully understand it:

strings -n 1 < /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1

It's POSIX compliant (minus the /dev/urandom) but have I achieved my end correctly? If so, is there a better way to achive this end? It would also be cool if there was a way to produce a random string without /dev/urandom, but I think I'm dreaming on that one.


Solution

  • Just use awk, which POSIX mandates provide a rand function.

    $ cat password.awk
    BEGIN {
        srand();
        chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        s = "";
        for(i=0;i<8;i++) {
            s = s "" substr(chars, int(rand()*62), 1);
        }
        print s
    }
    $ awk -f password.awk
    Cl7A4KVx
    

    (This can almost certainly be code-golfed into something shorter; my awk skills are somewhat limited.)

    If you need multiple passwords, pass the number needed as an argument so that you don't need to run awk more than once per second:

    BEGIN {
      srand();
      chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
      for(i=0;i<n;i++) {
        s = "";
        for(j=0;j<8;j++) {
          s = s "" substr(chars, int(rand()*62), 1);
        }
        print s
      }
    }
    

    To pass a value of n, run as awk -v n=5 -f password.awk to generate 5 passwords.

    Alternatively, you can pass a different seed directly to awk:

    BEGIN {
      srand(seed);
      chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
      s = "";
      for(i=0;i<8;i++) {
          s = s "" substr(chars, int(rand()*62), 1);
      }
      print s
    }
    

    with awk -v seed=$newseed -f password.akw. Note that newseed itself needs to have a different value each time, but using a simple sequential sequence of seeds should be sufficient.

    seed=$(date +%s)
    awk -v seed=$seed -f password.awk; seed=$((seed + 1))
    awk -v seed=$seed -f password.awk; seed=$((seed + 1))
    awk -v seed=$seed -f password.awk; seed=$((seed + 1))
    # etc