19
\$\begingroup\$

pwgen is a nice password generator utility. When you run it, it fills the terminal with a bunch of random passwords, giving you many options to choose from and pick something you like, for example:

lvk3U7cKJYkl pLBJ007977Qx b9xhj8NWPfWQ
pMgUJBUuXwpG OAAqf6Y9TXqc fJOyxoGYCRSQ
bpbwp6f2MxEH fUYTJUqg0ZMB GjVVEQxuer0k
oqTEvV1LmdJu si47MkHNRpAw 3GKV8NdGMvwf

Although there are ports of pwgen in multiple systems, it's not so easy to find in Windows. So I put together a simple Python script that's more portable, as it can run in any system with Python.

I added some extra features I often want:

  • Skip characters that may be ambiguous, such as l1ioO0Z2I
  • Avoid doubled characters (slow down typing)

Here it goes:

#!/usr/bin/env python
from __future__ import print_function
import random
import string
import re
from argparse import ArgumentParser
terminal_width = 80
terminal_height = 25
default_length = 12
alphabet_default = string.ascii_letters + string.digits
alphabet_complex = alphabet_default + '`~!@#$%^&*()_+-={}[];:<>?,./'
alphabet_easy = re.sub(r'[l1ioO0Z2I]', '', alphabet_default)
double_letter = re.compile(r'(.)1円')
def randomstring(alphabet, length=16):
 return ''.join(random.choice(alphabet) for _ in range(length))
def has_double_letter(word):
 return double_letter.search(word) is not None
def easy_to_type_randomstring(alphabet, length=16):
 while True:
 word = randomstring(alphabet, length)
 if not has_double_letter(word):
 return word
def pwgen(alphabet, easy, length=16):
 for _ in range(terminal_height - 3):
 for _ in range(terminal_width // (length + 1)):
 if easy:
 print(easy_to_type_randomstring(alphabet, length), end=' ')
 else:
 print(randomstring(alphabet, length), end=' ')
 print()
def main():
 parser = ArgumentParser(description='Generate random passwords')
 parser.add_argument('-a', '--alphabet',
 help='override the default alphabet')
 parser.add_argument('--complex', action='store_true', default=False,
 help='use a very complex default alphabet', dest='complex_')
 parser.add_argument('--easy', action='store_true', default=False,
 help='use a simple default alphabet, without ambiguous or doubled characters')
 parser.add_argument('-l', '--length', type=int, default=default_length)
 args = parser.parse_args()
 alphabet = args.alphabet
 complex_ = args.complex_
 easy = args.easy
 length = args.length
 if alphabet is None:
 if complex_:
 alphabet = alphabet_complex
 elif easy:
 alphabet = alphabet_easy
 else:
 alphabet = alphabet_default
 elif len(alphabet) < length:
 length = len(alphabet)
 pwgen(alphabet, easy, length)
if __name__ == '__main__':
 main()

How would you improve this? I'm looking for comments about all aspects of this code.

I know that the terminal_width = 80 and terminal_height = 25 variables don't really reflect what their names imply. It's not terribly important, and good enough for my purposes, but if there's a way to make the script detect the real terminal width and height without importing dependencies that reduce portability, that would be pretty awesome.

Reinderien
71k5 gold badges76 silver badges256 bronze badges
asked Feb 1, 2015 at 19:43
\$\endgroup\$
1

3 Answers 3

8
\$\begingroup\$

Mostly a matter of personal preference but I'd define a variable in pwgen like :

get_string = easy_to_type_randomstring if easy else randomstring

to avoid duplicated logic.

Then, you can simplify your code by using join instead of having multiple print.

def pwgen(alphabet, easy, length=16):
 get_string = easy_to_type_randomstring if easy else randomstring
 for _ in range(terminal_height - 3):
 print(' '.join(get_string(alphabet, length)
 for _ in range(terminal_width // (length + 1))))
answered Feb 2, 2015 at 10:07
\$\endgroup\$
2
  • \$\begingroup\$ That's more than just your taste, it's an excellent point! Duplicated logic is clearly not cool, and your solution is also more efficient, since it evaluates the if-else only once before the loop. Very well spotted, and thanks a lot! \$\endgroup\$ Commented Feb 2, 2015 at 10:37
  • \$\begingroup\$ Glad you like it :-) \$\endgroup\$ Commented Feb 2, 2015 at 10:38
9
\$\begingroup\$

Looking at the following:

terminal_width = 80
terminal_height = 25
default_length = 12
alphabet_default = string.ascii_letters + string.digits
alphabet_complex = alphabet_default + '`~!@#$%^&*()_+-={}[];:<>?,./'
alphabet_easy = re.sub(r'[l1ioO0Z2I]', '', alphabet_default)
double_letter = re.compile(r'(.)1円')

you never change them, so they are constants. Constants are written ALL CAPS in Python as a convention.


elif len(alphabet) < length:
 length = len(alphabet)

Are you sure the user really wants what you are doing here? Maybe he wants a 20 character password using only the 10 digits, with this you are silently not doing what he expects.

answered Feb 1, 2015 at 21:31
\$\endgroup\$
1
  • 2
    \$\begingroup\$ You're right about the naming, and well spotted the bug in that length check. Thanks! \$\endgroup\$ Commented Feb 2, 2015 at 7:42
3
\$\begingroup\$

Aside generic advice from Typical password generator in Python that I recommend you review,

if there's a way to make the script detect the real terminal width and height without importing dependencies that reduce portability

Use shutil.

Rather than re.sub, I consider a simpler solution to be set subtraction.

Rather than the double_letter regular expression and a (theoretically infinite) number of retries, why not deliberately construct a string whose letters are guaranteed to not repeat?

I don't think it's very helpful to distinguish between --easy and --complex. "Complex" can just be the default (and the absence of "easy").

This is puzzling:

if len(alphabet) < length:
 length = len(alphabet)

Why truncate the password length to the alphabet length?

All together,

import argparse
import secrets
import shutil
import string
import typing
AMBIGUOUS = frozenset('l1ioO0Z2I')
# Don't use string.printable: most of the whitespace characters are impractical
ALPHA_DEFAULT = string.digits + string.ascii_letters + string.punctuation + ' '
ALPHA_EASY = ''.join(frozenset(string.digits + string.ascii_letters) - AMBIGUOUS)
def parse_args() -> argparse.Namespace:
 parser = argparse.ArgumentParser(description='Generate random passwords')
 parser.add_argument(
 '-a', '--alphabet',
 help='alphabet (default: ASCII printable characters)',
 )
 parser.add_argument(
 '-e', '--easy', action='store_true',
 help='use a simple default alphabet, without ambiguous or doubled characters',
 )
 parser.add_argument(
 '-l', '--length', type=int, default=16,
 )
 return parser.parse_args()
def select_alphabet(easy: bool, alphabet: str | None) -> str:
 if alphabet is None:
 if easy:
 return ALPHA_EASY
 return ALPHA_DEFAULT
 return alphabet
class PasswordGenerator(typing.NamedTuple):
 rand: secrets.SystemRandom
 easy: bool
 alphabet: str
 length: int
 @classmethod
 def from_args(cls, args: argparse.Namespace) -> typing.Self:
 return cls(
 rand=secrets.SystemRandom(),
 easy=args.easy,
 alphabet=select_alphabet(args.easy, args.alphabet),
 length=args.length,
 )
 def _nonrepeating_chars(self) -> typing.Iterator[str]:
 """Assumes length >= 1"""
 c = self.rand.choice(self.alphabet)
 yield c
 as_set = frozenset(self.alphabet)
 for _ in range(self.length - 1):
 c = self.rand.choice(tuple(as_set - {c}))
 yield c
 def get_password(self) -> str:
 if self.easy:
 return ''.join(self._nonrepeating_chars())
 return ''.join(self.rand.choices(self.alphabet, k=self.length))
 def dump(self, line_gap: int = 3) -> None:
 term = shutil.get_terminal_size()
 xidx = range(term.columns//(self.length + 1))
 for y in range(term.lines - line_gap):
 print(' '.join(self.get_password() for x in xidx))
def main() -> None:
 args = parse_args()
 generator = PasswordGenerator.from_args(args)
 generator.dump()
if __name__ == '__main__':
 main()
> python .79276円.py -e
jTMY5BGmBq4znEmP PHgLMWLPLNanygQj YwVDqNdDnsnJv8eB 5bMmFgr3TrLGhVba 4dgN6xAsfqmhjntB 6zSXmVKWqVfKFkcx
DXnVbfPxCM3xeXJu zCKeHuwc6N5HgNaD 7ACgay3vqtVC5z9J YSqwDHjm5XVDySdP hnUHU4tFNv6bjQDS 9UkXjsGLUuMPRYtR 
7C7jpqxkYkbeRhy9 Mj7wF5uYMkeGLX8K HgwF7KyV7BVpabEG sHGhAWrcveJKWNaT rc5D4cur3JVSM4jd Cwk9pVTx5dfXhgv5 
f3B5rzqtVnbuTf6F sUhMhCRnjVMqhdE4 FYhBgDSaACAXGutb vbc5FgAMXkFz8tMc qnrzVLFvYmkWbq8L vwuexJaUMNx5cskb 
zE8Uy3pLsQng3z5Q EGtPsDX4WbqeUvgN DVXATyFjcXYSengq AyNg7JsUhp9NjUyE CYW3KCdXAYxF7UjT 6HYR4S7pBTaXC9SU 
ahHNJWBrJdYSFPEd FGtgXAs4mgX8HU5x YjtKUB5pkmGrKae5 bE5cK6Xe7DFSX8Bv NbxKkKsCDbaKqunH Esk4bFDqEpMGSaJ5 
7BGr5b6Gpvr8Pem4 3sdEqpd4zLMVBhuJ 5XbxryhGJKdGEhBK nKrt4xvxmVzhLvVE 5EuN9vXA4sruzyYs 9uFd7twmayf5mKX9 
HhvUzsCgaMCsJ5tq 5q6DjpqsH3MEUpkh HUkDb8mErkm8UYLv XQS5VSdCTHmNsw6h yRngFD8jDNvByXuk dBhEBJBwzek7sdTz 
eGjQyeR8sDpYxszL U9WrH56VUF3EvDbg kPBLJmzEGsm6cNLP E3QxBvHDAK3L5zDg utQvAc9SyLzCs7tc wLQAcmd637NS4wGh 
k3FDbxbm3FyeLbDA zRQaz7PxrhwvY5r3 tUK5SFLrUvkELsws 9VUCXSx4F4vr4pFW 5B3NFtfkbFcfuyn6 kswRnGqAhC5xXMYt 
JR9kxX38sr8TwCbm Cds5BAtfjXbQLhHh zrt5GFzx3TGkuNf7 eCsbpUN4ARpKDycQ NgyJAwG9nRJ3Gn5y hCswsAqh7LySnDwa 
KfWdftWczb8hVFVg EtNReku8TjpVHcvn pbxWnfK8TAPpBPVt D8JGsmAJTnRBRAkC hvAFtbFaVrupq7uK sLxFMYDapxQ9sBmM 
ywGE5Pru6qM5M6hL UPUu8j8ANxGdkF4C 4HYqrt5Rfz7Ybh7M NScNbMh7WQP5R7jd DnRVpzCnDSewPJjT s3eGn49gWVTMeUrk 
kcvEhumzw3NxMzJq MdM8tsnP3zfXTmJP EuekhfFxPxMTFMDa GEAwtFeHvcN67bBt Jt5RtTnrHTNyjWez YGaJkcVuBqPpUfav 
> python .79276円.py -e -a '123456' -l 5
54363 41252 42454 61521 14615 14263 64625 26346 14243 32651 35316 45634 41323 65624 14245 41653 12345
34165 56154 45341 54143 62512 25324 64612 54324 31325 12613 23645 41236 23531 32656 25124 65651 45351 
26326 31214 36214 54236 36213 15132 53653 35346 13234 24316 43425 42616 63531 36216 14163 36425 15356 
23562 36152 16343 14513 36363 14642 62132 26532 15413 64342 56121 25453 52652 45453 46451 53412 61346 
43145 35121 31454 13653 64541 35652 61363 53254 26542 43414 31426 21352 42435 65143 24215 32356 24343 
65636 51626 53612 53565 53216 62342 42525 61632 36261 63426 51342 31513 35343 21413 56343 65636 35246 
51453 43634 56545 25346 23545 53525 56435 62643 61434 41314 35232 51463 53231 45635 54252 31326 15264 
13161 35313 53451 46215 42134 32641 31615 61461 64356 63534 25232 62346 25263 65252 13563 54161 52565 
35646 46353 31364 41254 43414 54614 64656 13142 52142 51364 64521 61214 41423 25613 41616 41561 62162 
34245 14141 53542 34252 54543 34612 43215 43531 62415 16163 34252 21413 61634 64162 23651 21541 35461 
65231 43523 46214 42612 15314 34326 64615 12413 52464 15341 13464 52626 23624 54523 23251 61314 54515 
12134 45246 32136 21623 43613 54326 12124 41424 23526 41565 62353 25125 24351 13425 56263 23456 61436 
42523 13563 53563 64254 16132 25352 26321 43651 56423 16212 65623 54213 21425 25153 62365 61341 13515 
21325 15323 51312 65261 43156 25265 21614 41435 56315 16132 56151 24313 43136 56123 16424 64145 14652 
answered Sep 29, 2024 at 18:35
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.