1 | #!/usr/bin/python3 |
---|
2 | import os |
---|
3 | import sys |
---|
4 | import time |
---|
5 | import glob |
---|
6 | import random |
---|
7 | import math |
---|
8 | import argparse |
---|
9 | import subprocess |
---|
10 | |
---|
11 | |
---|
12 | TARGET_DIR = os.path.join(os.path.dirname(sys.argv[0]), 'wordlists') |
---|
13 | |
---|
14 | |
---|
15 | def clone_wordlists(): |
---|
16 | """Clone a copy of the wordlists if needed. Update the wordlist if it |
---|
17 | hasn't been in a while. |
---|
18 | """ |
---|
19 | if os.path.exists(TARGET_DIR): |
---|
20 | try: |
---|
21 | last_modified = os.stat(os.path.join(TARGET_DIR, '.git/FETCH_HEAD')).st_mtime |
---|
22 | except FileNotFoundError: |
---|
23 | last_modified = 0 |
---|
24 | if last_modified < time.time() - 7 * 24 * 60 * 60: # Check for updates weekly |
---|
25 | subprocess.check_call(['git', 'pull'], cwd=TARGET_DIR) |
---|
26 | else: |
---|
27 | subprocess.check_call(['git', 'clone', 'https://github.com/imsky/wordlists.git'], |
---|
28 | cwd=os.path.dirname(TARGET_DIR)) |
---|
29 | |
---|
30 | |
---|
31 | def load(part_of_speech): |
---|
32 | """Load all unique words of a given part of speech from the wordlists. |
---|
33 | """ |
---|
34 | words = set() |
---|
35 | for filename in glob.glob(os.path.join(TARGET_DIR, part_of_speech, '*.txt')): |
---|
36 | words.update(w.strip() for w in open(filename, encoding='utf-8') if w.strip()) |
---|
37 | return words |
---|
38 | |
---|
39 | |
---|
40 | def main(argv=None): |
---|
41 | """Generate a random-but-memorable username. |
---|
42 | """ |
---|
43 | if argv is None: |
---|
44 | argv = sys.argv |
---|
45 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
---|
46 | description=main.__doc__) |
---|
47 | parser.add_argument("--adjectives", "-a", default=1, type=int, |
---|
48 | help="number of adjectives") |
---|
49 | parser.add_argument("--divider", default='', |
---|
50 | help="character or string between words") |
---|
51 | parser.add_argument("--bits", default=0, type=int, |
---|
52 | help="minimum bits of entropy") |
---|
53 | parser.add_argument("--verbose", action='store_true', |
---|
54 | help="be chatty") |
---|
55 | args = parser.parse_args(argv[1:]) |
---|
56 | |
---|
57 | clone_wordlists() |
---|
58 | # load() returns a set, but we need a subscriptable type, so convert to |
---|
59 | # lists. But there's no reason to sort them, since we're only taking |
---|
60 | # random picks from the lists. |
---|
61 | nouns = list(load('nouns')) |
---|
62 | adjectives = list(load('adjectives')) |
---|
63 | |
---|
64 | adjective_count = args.adjectives |
---|
65 | # Determine the number of bits of randomness given the adjectives count |
---|
66 | bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives)) |
---|
67 | # Increase the number of adjectives as needed to achieve the minimum bits |
---|
68 | # of randomness |
---|
69 | while bits < args.bits: |
---|
70 | adjective_count += 1 |
---|
71 | bits = math.log2(len(nouns)) + adjective_count * math.log2(len(adjectives)) |
---|
72 | |
---|
73 | if args.verbose: |
---|
74 | sys.stderr.write(f"Found {len(nouns)} nouns and {len(adjectives)} adjectives;" |
---|
75 | f" using {adjective_count} adjective{adjective_count != 1 and 's' or ''} " |
---|
76 | f"and one noun giving {bits :0.2f} bits of entropy.\n") |
---|
77 | |
---|
78 | # Use the best source of randomness available. |
---|
79 | prng = random.SystemRandom() |
---|
80 | words = [] |
---|
81 | for i in range(adjective_count): |
---|
82 | words.append(prng.choice(adjectives)) |
---|
83 | # We don't remove used adjectives from the list of choices, so that |
---|
84 | # means it's possible to have repeats, such as "wise-small-wise-ant" |
---|
85 | words.append(prng.choice(nouns)) |
---|
86 | result = args.divider.join(words) |
---|
87 | sys.stdout.write(f"{result}\n") |
---|
88 | return 0 |
---|
89 | |
---|
90 | |
---|
91 | if __name__ == '__main__': |
---|
92 | sys.exit(main()) |
---|