2

I've made a bash script to make a random password and (if the "flag" is correct) store it in a txt file. I've added an alias to the path of the script so I can run it as a single command. I call it "rand".

What I already have works, but it's not in any way elegant. First of all, I want the flags to be included in any order, not fixed as they are now. At the moment I only have two. -p for "secure password" and -s for "save". I want to add at least one more, -u for "user name", but since I'm not happy with the current state of the code, I've hold off on that one for now.

I want to run the command like this:

rand -FLAGS_IN_ANY_ORDER -NUMBER -FILE_NAME

Here is the current version of the code:

#!/bin/bash

num=$#
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

if [[ "$1" = "-p" ]]; then
    cat /dev/urandom \
    | tr -dc A-Za-z0-9.-@ \
    | head -c $2 ; echo

elif [[ "$1" = "-ps" ]] && [[ "$3" = * ]]; then
    cat /dev/urandom \
    | tr -dc A-Za-z0-9.-@ \
    | head -c $2 > $DIR/$3.txt && echo >> $DIR/$3.txt

    cat $DIR/$3.txt

elif [[ "$1" = "-s" ]] && [[ "$3" = * ]]; then
    cat /dev/urandom \
    | tr -dc A-Za-z0-9 \
    | head -c $2 > $DIR/$3.txt && echo >> $DIR/$3.txt

    cat $DIR/$3.txt

else
    cat /dev/urandom \
    | tr -dc A-Za-z0-9 \
    | head -c $1 ; echo
fi

Ideally, I want as few lines of code as possible, which won't be the case with all these if statements.

I have tried getopts and cases, but I couldn't get that to work at all. This is the only version of the code I have so far that doesn't break.

In my defense though, I'm new to UNIX like systems and I only started bash scripting a few weeks ago.

How would you go about solving this?

3

The following script is longer than yours, but all of the extra length is because I added a usage function to print a usage help message (it also serves to print an error message when an error condition is detected) and added lots of comments to explain what the code does and why.

The code that parses the options and does the work is a lot shorter and simpler, and easier to expand (e.g. it currently takes a -u username option but doesn't do anything with it because you didn't mention how it should be used). All the special-case handling is eliminated, the "decisions" about what to do and how to do it are made in the case statement and in a few if/then tests immediately after it.

I've also added a -d option so that you can specify an output directory...but that's redundant as anything except an example of how to add another option, because I've also added code that checks if the output file contains a / character, and only prepend $outdir (renamed from your $DIR - it's a good habit to use lowercase variables names in your script, reserving uppercase for standard utilities) if it doesn't. i.e. you can use either -f /path/to/file or -d /path/to -f file.

BTW, rather than make an alias with the full path to this script, I recommend just putting it in /usr/local/bin or creating a bin/ sub-directory in your home dir and adding ~/bin to your $PATH. Then you can just write and save as many custom scripts as you want in there.

#!/bin/bash

usage() {
  # this function prints a usage summary, optionally printing an error message
  local ec=0

  if [ $# -ge 2 ] ; then
    # if printing an error message, the first arg is the exit code, and
    # all remaining args are the message.
    ec="$1" ; shift
    printf "%s\n\n" "$*" >&2
  fi

  cat <<EOF
Usage:
       $(basename $0) [-p | -s file | -u <user> | -d <dir> ] <[-l] length>

Generates a random password of specified length, containing only
alpha-numeric characters.

Required arguments:

  -l <length>     Length of password.  Just the length without '-l' also works.

Options:

  -p              Allow some punctuation characters in password.
  -s <file>       Save output to filename.
  -d <dir>        Output directory for the save file
  -u <user>       Username.  At present this does nothing.

  -h              This help message.
EOF

  exit $ec
}

# Initialise variables
length=''
file=''
username=''
pattern='A-Za-z0-9'
outdir=$(dirname "$0")

# -s, -l, -u and -d require an arg. -h and -p don't. The leading ':' in
# the getopts string enables 'silent' error mode (i.e. we handle
# any errors ourselves)
while getopts ':hps:l:u:d:' opt; do
  case "$opt" in 
    p) pattern='A-Za-z0-9.-@' ;;
    s) file="$OPTARG" ;;
    l) length="$OPTARG" ;;
    u) username="$OPTARG" ;;
    d) outdir="$OPTARG" ;;

    h) usage ;;
    :) usage 1 "-$OPTARG requires an argument" ;;
    ?) usage 1 "Unknown option '$opt'" ;;
  esac
done

shift $((OPTIND -1))

# Length can be specified as either `-l nnn` or just `nnn` on the cmd line
# this code exits with an error message if no length is specified.
#
# It would probably be better to give length a default value instead.
# Just change `length=''` under the Initialise variables comment above
# to whatever you want as the default, and then delete the '[ -z "$1" ]'
# line below. Remember to update the usage message - documentation should
# always match what the code does.
if [ -z "$length" ] ; then
  [ -z "$1" ] && usage 1 "Length option is required"
  length="$1"
  shift
fi

# if there are any remaining args on the line, we don't know what to do
# with them, so exit with an error message.
[ -n "$*" ] && usage 1 "Unknown arguments: '$*'"

password=$(cat /dev/urandom | tr -dc "$pattern" | head -c "$length")

# I don't know why you want an extra newline in the output, but do it anyway.
printf "$password\n\n"

if [ -n "$file" ] ; then
   # if $file doesn't have a /, pre-pend "$outdir"
   [[ $file =~ '/' ]] || file="$outdir/$file"
   printf "$password\n\n" > "$file" 
fi

BTW, numerous password-generating programs already exist. e.g. pwgen and makepasswd.

I used to use pwgen to generate 16+ digit passwords until I wrote my own passphrase generator that randomly selects a number of words from /usr/share/dict/words and joins them together with randomly-generated 1-3 digit numbers and/or punctuation. /usr/share/dict/words is all-lowercase, so my script randomly capitalises some of the letters. This generates very long but easy to remember passwords. The random digits and punctuation and capitalisation increases the search space for brute-force cracking and helps to ensure that the password's strength isn't compromised by the predictability of the word dictionary.

e.g.

$ random-password.sh 
31 surVeying % deRAngement 6 ratiocinations 51 sunDowns
$ printf '%s' '31 surVeying % deRAngement 6 ratiocinations 51 sunDowns' | wc -c
55

Password length is far more important than complexity - any password less than 10 characters long can be cracked in a very short time with modern-ish hardware (a second or less for <= 7 characters, hours for 8 characters, a few months for 10 characters). A 55 character password is effectively uncrackable within the lifetime of the universe by current technology (so should be safe for at least 10 years).

The problem is that the longer a password is, the simpler it needs to be for a human to be able to remember it. I find that I only need to remember the start of the password and by typing it a few times, I automatically associate (and thus remember) the next digits and words in the nonsense phrase.

  • Thanks! Although, after reading about your password script, using /usr/share/dict/words I think I'll have to start over. Seems like a much better way to go at it. Thanks for the advice! – discipulus Mar 4 '18 at 12:59
  • I'd suggest playing with the example code here and figuring out how it works, before you start working on an improved password generator. Easier to learn first. getopts handling is a basic tool that can be used to improve all your scripts once you know how it works, so it's worth putting in a bit of effort up front....many of my sh scripts start with that basic structure: 1. usage() function; 2. variable initialisation; 3. getopts; 4. other arg handling and validation 5. main code. Or swap the order of 1 & 2 - it's often better to put the configurable defaults up top for visibility. – cas Mar 5 '18 at 5:11
0

This is how I normally do my options but I think it will end up making your script longer...it does take options in any order though.

while (( $# )); do
    case $1 in
        -n)
            shift
            number=$1
        ;;
        -f)
            shift
            file_name=$1
        ;;
        -p)
            shift
            password=$1
            run=pass
        ;;
        -s)
            run=save
        ;;
        -u)
            shift
            username=$1
        ;;
        *)
            thing=$1
            save=what
        ;;
        shift
    esac
done
case $save in
    save)
        cat /dev/urandom \
            | tr -dc A-Za-z0-9.-@ \
            | head -c "$number" > "${DIR}/${file_name}.txt" && echo >> "${DIR}/${file_name}.txt"
        cat "${DIR}/${file_name}.txt"
    ;;
    pass)
        cat /dev/urandom \
        | tr -dc A-Za-z0-9.-@ \
        | head -c "$number" ; echo
    ;;
    *)
        cat /dev/urandom \
        | tr -dc A-Za-z0-9 \
        | head -c "$thing" ; echo
    ;;
esac

The while loop will run as long as there are still options. shift moves to the next option so it will loop through each of the arguments you pass and run them through the case statement to properly handle them.

0

This is a variation on your script:

#!/bin/bash

opt_l=8                 # password length is 8 by default
opt_o=/dev/stdout       # write to standard output by default
opt_s=0                 # insecure password by default
opt_u=                  # no default username

while getopts 'l:o:su:' opt; do
    case "$opt" in
        l) opt_l=$OPTARG ;;
        o) opt_o=$OPTARG ;;
        s) opt_s=1       ;;
        u) opt_u=$OPTARG ;;
        *) echo 'Error parsing options' >&2
           exit 1
   esac
done

args=( "$opt_l" 1 )

if (( opt_s )); then
    args=( -s "${args[@]}" )
fi

if [ -n "$opt_u" ]; then
    printf '%s: %s\n' "$opt_u" "$( pwgen "${args[@]}" )" >"$opt_o"
else
    pwgen "${args[@]}" >"$opt_o"
fi

I've changed some of the names of the flags so that they are more in line with what standard tools use. Instead of printing the error message when finding an unsupported flag, you could output some usage info to the user.

The main thing is the use of getopts here. It takes a specification as a string of the options that you're using. The : in the string means that the preceding option character takes an argument (other options are booleans).

In the while loop, $opt will be the option and $OPTARG will be the option argument, if the flag takes an argument.

One usually also does shift "$(( OPTIND - 1 ))" after the loop, but as this script does not accept additional operands on the command line apart from the options and their arguments, this is not needed. The shift would make sure that additional operands would be available as $1, $2 etc.

You would use the script as

$ ./script.sh -l 4         # generates password of length 4 in the terminal
$ ./script.sh -l 4 -o file # generates password of length 4 and saves to "file"
$ ./script.sh -u bob       # generates password of length 8, prepends with username "bob"
$ ./script.sh -s -u bob    # generates more random password of length 8, prepends with username "bob"

-s -u bob is the same as -ubob -s and -subob, and as -u alice -s -u bob (later options override earlier ones).

It would be fairly easy to remove pwgen from this and just use another password generator. To simplify this, one could put the password generator in its own function which means that the main script would never have to be modified when replacing the generator:

#!/bin/bash

passgen () {
    local args=( "$opt_l" 1 )

    if (( opt_s )); then
        args=( -s "${args[@]}" )
    fi

    pwgen "${args[@]}"
}

opt_l=8                 # password length is 8 by default
opt_o=/dev/stdout       # write to standard output by default
opt_s=0                 # insecure password by default
opt_u=                  # no default username

while getopts 'l:o:su:' opt; do
    case "$opt" in
        l) opt_l=$OPTARG ;;
        o) opt_o=$OPTARG ;;
        s) opt_s=1       ;;
        u) opt_u=$OPTARG ;;
        *) echo 'Error parsing options' >&2
           exit 1
   esac
done

if [ -n "$opt_u" ]; then
    printf '%s: %s\n' "$opt_u" "$( passgen )" >"$opt_o"
else
    passgen >"$opt_o"
fi

For example, this is more or less your generator:

passgen () {
    local source=/dev/urandom
    local chars='A-Za-z0-9'

    if (( opt_s )); then
        chars="$chars"'.-@'
    fi

    tr -dc "$chars" <"$source" | head -c "$opt_l"
}

... and the following generates a passphrase using words from /usr/share/dict/words (the number of words is taken from the -l option). If the -s option is used, it adds a random number to the end instead of the last word.

passgen () {
    local dict=/usr/share/dict/words
    local numwords=$opt_l

    if (( opt_s )); then
        numwords=$(( numwords - 1 ))
    fi

    {
        shuf -n "$numwords" "$dict"

        if (( opt_s )); then
            printf '%d\n' "$RANDOM"
        fi
    } | tr '\n' ' ' | sed 's/ $//'
}

Running with this last passgen function, the command

./script.sh -s -u bob -l 3

may generate something like

bob: cassis befluster 22625

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.