The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl -w

use strict;
use warnings;

use vars qw($VERSION);

$VERSION = 1.02;

sub emit;
sub msg;
sub warning;
sub error;
sub done;

my $num_warnings = 0;

my $count = 1;
my $phrase_len = 0;
my $size = 5;
my ($min_word_len, $max_word_len);
my $source = '/usr/share/dict/words';
my %charset = (
    ':std' => [ 'A'..'H', 'J'..'N', 'P'..'Z', ('a'..'n', 'p'..'z') x 2, '2'..'9' ],
    ':alpha' => [ 'A'..'Z', 'a'..'z' ],
    ':ALPHA' => [ 'A'..'Z' ],
    ':alphanum' => [ 'A'..'Z', 'a'..'z', '0'..'9' ],
    ':ALPHANUM' => [ 'A'..'Z', '0'..'9' ],
    ':num' => [ '0'..'9' ],
    ':hex' => [ '0'..'9', 'a'..'f' ],
    ':HEX' => [ '0'..'9', 'A'..'F' ],
    ':bin' => [ "\x00".."\xFF" ],
    ':bin7' => [ "\x00".."\x7F" ],
);
my $chars = ':std';
my $join = ' ';
my $help = 0;

while (@ARGV) {
    my $arg = shift;
    if      ( $arg =~ /^-w|--word$/   ) {
        $phrase_len = 0;
    } elsif ( $arg =~ /^-p|--phrase$/ ) {
        $phrase_len = shift || error "Missing phrase length value";
    } elsif ( $arg =~ /^-s|--source$/ ) {
        $source = shift || error "Missing source value";
    } elsif ( $arg =~ /^-l|--word-length$/ ) {
        my $len = shift || error "Missing length value(s)";
        $len =~ /^(\d+)(-(\d+))?$/
            or error "Bad length spec: $arg";
        ($min_word_len, $max_word_len) = ($1, $3);
        $min_word_len ||= 3;
        $max_word_len ||= $min_word_len;
    } elsif ( $arg =~ /^-c|--chars$/  ) {
        $chars = shift || error "Invalid chars";
    } elsif ( $arg =~ /^-n|--count$/  ) {
        $count = shift;
        error "Invalid count"
            unless defined $count
            and $count =~ /^\d+$/
            and $count > 0;
    } elsif ( $arg =~ /^-j|--join$/  ) {
        $join = shift;
        error "Invalid join" unless defined $join;
    } elsif ( $arg =~ /^-h|--help$/  ) {
        $help = 1;
    } else {
        error "Unknown option: $arg";
        exit 1;
    }
}

($min_word_len, $max_word_len) = $phrase_len ? (4,7) : (7,14)
    unless defined $min_word_len;

my @chars = exists $charset{$chars} ? @{$charset{$chars}} : split //, $chars;

if ($help) {
    
    # XXX Not very helpful
    msg "Sorry, you'll have to read my source code for help";
    done;
    
}

if ($phrase_len) {

    # --- Read in all lines of length $size
    open SOURCE, $source
        or error "Couldn't open source file '$source'";
    my @words;
    while (<SOURCE>) {
        next unless /^[a-z]/;
        chomp;
        next unless length() >= $min_word_len && length() <= $max_word_len;
        push @words, $_;
    }
    close SOURCE;
    
    # --- Pick words randomly
    while ($count--) {
        my @phrase;
        for (1..$phrase_len) {
            my $word;
            my $tries = scalar @words;
            until (defined $word or $tries-- == 0) {
                my $r = rand @words;
                $word = $words[$r];
                undef $words[$r];
            }
            error "Source doesn't have enough suitable words to finish the passphrase"
                unless defined $word;
            push @phrase, $word;
        }
        print join($join, @phrase), "\n";
    }
    
} else {
    
    while ($count--) {
        my $password = join '', @chars[
            map { rand @chars }
            ( 1..rand_in_range($min_word_len, $max_word_len) )
        ];
        print "$password\n";
    }
    
}

sub rand_in_range {
    my ($min, $max) = @_;
    return $min + int rand($max - $min + 1);
}

sub emit { print STDERR @_ }

sub msg { emit map { "$_\n" } @_ }

sub warning {
    $num_warnings++;
    emit "WARNING ($num_warnings): ", map { "$_\n" } @_;
}

sub error {
    emit 'ERROR: ', map { "$_\n" } @_;
    exit 1
}

sub done { exit 0 }


=head1 NAME

randpass - generate random passwords and passphrases

=head1 SYNOPSIS

randpass [ options ]

=head1 DESCRIPTION

Generate random passwords and passphrases in a particular `style'.

=head1 OPTIONS

=over 4

=item -w, --word

Generate passwords (the default).

=item -p, --phrase num

Generate passphrases with the specified number of words.  The passphrase
that is generated will not contain duplicate words (e.g., C<urial hayseed
dumpish urial>).  This may not be a range.

=item -n, --count num

Generate the specified number of passwords or passphrases.  This may not
be a range.

=item -l, --word-length num_or_range

The length of the password, or of each word in the passphrase.

If a range is specified (e.g., C<--word-length 8-14>) then the length
of the password (or of the words in the passphrase) will fall randomly
within that range (including both endpoints).  Half-open ranges (e.g.,
C<--word-length 3->) are not allowed.

The default is 7-14 for passwords and 4-7 for passphrases.

=item -c, --chars string_or_special

The set of characters (specified as a sequence of characters) used in
generating a password.  This is currently ignored if passphrases are being
generated.

You may specify a named set instead.  Choose among these...

=over 4

=item :std

  ('A'..'H', 'J'..'N', 'P'..'Z', ('a'..'n', 'p'..'z') x 2, '2'..'9')

This is the default.

=item :alpha

  ('A'..'Z', 'a'..'z' )

=item :alphanum

  ('A'..'Z', 'a'..'z', '0'..'9' )

=item :num

  ('0'..'9' )

=item :hex

Hexadecimal digits (lowercase).

  ('0'..'9', 'a'..'f' )

=item :HEX

Hexadecimal digits (uppercase).

  ('0'..'9', 'A'..'F' )

=item :bin

Binary data (bytes 0 through 255).

  ( "\x00".."\xFF" )

=item :bin7

Binary data (bytes 0 through 127).

  ( "\x00".."\x7F" )

=item -s, --source file_name

Specify the source file from which words will be drawn in generating
a passphrase.  This file will typically consist of a single word
per line (but creative uses of C<randpass> may do otherwise for interesting
results).

The default is C</usr/share/dict/words>.  The special file name C<->
may be used to specify standard input.

Note: If the source file doesn't have enough lines (of sufficient length)
to generate the full passphrase, the program exits with code 1 and prints
a suitable error message to standard error.

=item -j, --join string

When generating a passphrase, connect the words with the specified
string rather than a space.

=item -h, --help

Display help.

=back

=back

=head1 VERSION

1.02

=head1 AUTHOR

Paul Hoffman < nkuitse AT cpan DOT org >

=head1 COPYRIGHT

Copyright 2003 Paul M. Hoffman.  All rights reserved.

This script is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.