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.
-
2\$\begingroup\$ Not sure if portable enough : gist.github.com/jtriley/1108174 \$\endgroup\$SylvainD– SylvainD2015年02月01日 21:21:42 +00:00Commented Feb 1, 2015 at 21:21
3 Answers 3
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))))
-
\$\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\$janos– janos2015年02月02日 10:37:04 +00:00Commented Feb 2, 2015 at 10:37 -
\$\begingroup\$ Glad you like it :-) \$\endgroup\$SylvainD– SylvainD2015年02月02日 10:38:14 +00:00Commented Feb 2, 2015 at 10:38
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.
-
2\$\begingroup\$ You're right about the naming, and well spotted the bug in that length check. Thanks! \$\endgroup\$janos– janos2015年02月02日 07:42:16 +00:00Commented Feb 2, 2015 at 7:42
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
Explore related questions
See similar questions with these tags.