feat(wordlist-optimize): accept directories as input, inline tab-completion display, add Wordlist/Rule Tools to main menu

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-05-05 15:42:56 -04:00
co-authored by Claude Sonnet 4.6
parent 60ae6656ad
commit 9ec56d4ead
3 changed files with 80 additions and 16 deletions
+19 -7
View File
@@ -1195,17 +1195,29 @@ def wordlist_shard(ctx: Any) -> None:
def wordlist_optimize(ctx: Any) -> None:
"""Prompt for input wordlists and output directory, then optimize."""
raw = ctx.select_file_with_autocomplete(
"\n[*] Enter input wordlist paths (comma-separated)",
"\n[*] Enter input wordlist paths (comma-separated files or directories)",
base_dir=ctx.hcatWordlists,
).strip()
inputs = [p.strip() for p in raw.split(",") if p.strip()]
if not inputs:
raw_entries = [p.strip() for p in raw.split(",") if p.strip()]
if not raw_entries:
print("[!] No input wordlists provided.")
return
missing = [p for p in inputs if not os.path.isfile(p)]
if missing:
print("[!] Files not found:")
for p in missing:
inputs: list[str] = []
not_found: list[str] = []
for entry in raw_entries:
if os.path.isfile(entry):
inputs.append(entry)
elif os.path.isdir(entry):
files = [os.path.join(entry, f) for f in ctx.list_wordlist_files(entry)]
if not files:
print(f"[!] No wordlist files found in: {entry}")
return
inputs.extend(files)
else:
not_found.append(entry)
if not_found:
print("[!] Not found (not a file or directory):")
for p in not_found:
print(f" {p}")
return
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
+15 -4
View File
@@ -1079,12 +1079,17 @@ def select_file_with_autocomplete(
except IndexError:
return None
def display_matches(substitution, matches, longest_match_length):
print()
for match in matches:
print(f" {match}")
readline.redisplay()
# Configure readline for tab completion
readline.set_completer_delims(" \t\n;")
# Disable the "Display all X possibilities?" prompt
try:
readline.parse_and_bind("set completion-query-items -1")
except Exception:
readline.set_completion_display_matches_hook(display_matches)
except AttributeError:
pass
try:
readline.parse_and_bind("tab: complete")
@@ -4892,7 +4897,9 @@ def main():
("2", "Download wordlists from Weakpass"),
("3", "Download wordlists from Hashmob.net"),
("4", "Download rules from Hashmob.net"),
("5", "Exit"),
("5", "Wordlist Tools"),
("6", "Rule File Tools"),
("7", "Exit"),
]
menu_loop = True
while menu_loop:
@@ -4930,6 +4937,10 @@ def main():
sys.exit(0)
# Otherwise continue the menu loop
elif choice == "5":
wordlist_tools_submenu()
elif choice == "6":
rule_tools_submenu()
elif choice == "7":
sys.exit(0)
else:
if (
+46 -5
View File
@@ -330,7 +330,10 @@ class TestWordlistOptimize:
f"{wl_a},{wl_b}",
outdir,
]
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
with (
patch("hate_crack.attacks.os.path.isfile", return_value=True),
patch("hate_crack.attacks.os.path.isdir", return_value=False),
):
wordlist_optimize(ctx)
ctx.wordlist_optimize.assert_called_once_with(
[str(wl_a), str(wl_b)], outdir
@@ -338,6 +341,35 @@ class TestWordlistOptimize:
out = capsys.readouterr().out
assert outdir in out
def test_directory_expansion(self, tmp_path, capsys):
ctx = _make_ctx()
wl_dir = str(tmp_path / "wls")
outdir = str(tmp_path / "out")
ctx.select_file_with_autocomplete.side_effect = [wl_dir, outdir]
ctx.list_wordlist_files.return_value = ["a.txt", "b.txt"]
with (
patch("hate_crack.attacks.os.path.isfile", return_value=False),
patch("hate_crack.attacks.os.path.isdir", return_value=True),
):
wordlist_optimize(ctx)
ctx.wordlist_optimize.assert_called_once_with(
[os.path.join(wl_dir, "a.txt"), os.path.join(wl_dir, "b.txt")], outdir
)
def test_empty_directory_rejection(self, tmp_path, capsys):
ctx = _make_ctx()
wl_dir = str(tmp_path / "wls")
ctx.select_file_with_autocomplete.return_value = wl_dir
ctx.list_wordlist_files.return_value = []
with (
patch("hate_crack.attacks.os.path.isfile", return_value=False),
patch("hate_crack.attacks.os.path.isdir", return_value=True),
):
wordlist_optimize(ctx)
out = capsys.readouterr().out
assert "No wordlist files found" in out
ctx.wordlist_optimize.assert_not_called()
def test_empty_input_rejection(self, capsys):
ctx = _make_ctx()
ctx.select_file_with_autocomplete.return_value = ","
@@ -359,10 +391,13 @@ class TestWordlistOptimize:
existing = tmp_path / "a.txt"
existing.write_text("word\n")
ctx.select_file_with_autocomplete.return_value = f"{existing},/nonexistent/missing.txt"
with patch("hate_crack.attacks.os.path.isfile", side_effect=lambda p: p == str(existing)):
with (
patch("hate_crack.attacks.os.path.isfile", side_effect=lambda p: p == str(existing)),
patch("hate_crack.attacks.os.path.isdir", return_value=False),
):
wordlist_optimize(ctx)
out = capsys.readouterr().out
assert "Files not found" in out
assert "Not found" in out
ctx.wordlist_optimize.assert_not_called()
def test_empty_outdir_rejection(self, tmp_path, capsys):
@@ -370,7 +405,10 @@ class TestWordlistOptimize:
wl = tmp_path / "a.txt"
wl.write_text("word\n")
ctx.select_file_with_autocomplete.side_effect = [str(wl), ""]
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
with (
patch("hate_crack.attacks.os.path.isfile", return_value=True),
patch("hate_crack.attacks.os.path.isdir", return_value=False),
):
wordlist_optimize(ctx)
out = capsys.readouterr().out
assert "Output directory cannot be empty" in out
@@ -383,7 +421,10 @@ class TestWordlistOptimize:
wl.write_text("word\n")
outdir = str(tmp_path / "out")
ctx.select_file_with_autocomplete.side_effect = [str(wl), outdir]
with patch("hate_crack.attacks.os.path.isfile", return_value=True):
with (
patch("hate_crack.attacks.os.path.isfile", return_value=True),
patch("hate_crack.attacks.os.path.isdir", return_value=False),
):
wordlist_optimize(ctx)
out = capsys.readouterr().out
assert "Optimization failed" in out