feat: add tab autocomplete to wordlist menu file path prompts

Replace input() with ctx.select_file_with_autocomplete() for all file
and directory path prompts in the 7 wordlist tools submenu functions.
Non-path prompts (lengths, masks, offsets, mode selection) remain as
plain input() calls.

Update tests to set ctx.select_file_with_autocomplete.side_effect for
file path values and leave builtins.input patches only for non-path
inputs.
This commit is contained in:
Justin Bollinger
2026-03-20 10:30:08 -04:00
parent 600e21c034
commit 59f0052c0e
2 changed files with 86 additions and 54 deletions

View File

@@ -951,11 +951,13 @@ def rule_tools_submenu(ctx: Any) -> None:
def wordlist_filter_length(ctx: Any) -> None:
"""Prompt for paths and lengths, then filter wordlist by word length."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -969,11 +971,13 @@ def wordlist_filter_length(ctx: Any) -> None:
def wordlist_filter_charclass_include(ctx: Any) -> None:
"""Prompt for paths and mask, then keep only words matching required char classes."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -987,11 +991,13 @@ def wordlist_filter_charclass_include(ctx: Any) -> None:
def wordlist_filter_charclass_exclude(ctx: Any) -> None:
"""Prompt for paths and mask, then remove words containing excluded char classes."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1005,11 +1011,13 @@ def wordlist_filter_charclass_exclude(ctx: Any) -> None:
def wordlist_cut_substring(ctx: Any) -> None:
"""Prompt for paths, offset, and optional length, then extract substring from each word."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1024,11 +1032,13 @@ def wordlist_cut_substring(ctx: Any) -> None:
def wordlist_split_by_length(ctx: Any) -> None:
"""Prompt for input wordlist and output directory, then split by word length."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outdir = input("[*] Enter output directory path: ").strip()
outdir = ctx.select_file_with_autocomplete("[*] Enter output directory path").strip()
if not outdir:
print("[!] Output directory cannot be empty.")
return
@@ -1047,15 +1057,19 @@ def wordlist_subtract_words(ctx: Any) -> None:
mode = input("[*] Choose mode (1/2): ").strip()
if mode == "1":
infile = input("[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
remove_file = input("[*] Enter path to wordlist to subtract: ").strip()
remove_file = ctx.select_file_with_autocomplete(
"[*] Enter path to wordlist to subtract", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(remove_file):
print(f"[!] File not found: {remove_file}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
@@ -1064,15 +1078,19 @@ def wordlist_subtract_words(ctx: Any) -> None:
else:
print("[!] Subtraction failed.")
elif mode == "2":
infile = input("[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return
raw = input("[*] Enter remove file paths (comma-separated): ").strip()
raw = ctx.select_file_with_autocomplete(
"[*] Enter remove file paths", allow_multiple=True, base_dir=ctx.hcatWordlists
).strip()
remove_files = [r.strip() for r in raw.split(",") if r.strip()]
if not remove_files:
print("[!] No remove files provided.")
@@ -1087,11 +1105,13 @@ def wordlist_subtract_words(ctx: Any) -> None:
def wordlist_shard(ctx: Any) -> None:
"""Prompt for input/output paths, modulus, and offset, then shard the wordlist."""
infile = input("\n[*] Enter path to input wordlist: ").strip()
infile = ctx.select_file_with_autocomplete(
"\n[*] Enter path to input wordlist", base_dir=ctx.hcatWordlists
).strip()
if not os.path.isfile(infile):
print(f"[!] File not found: {infile}")
return
outfile = input("[*] Enter path to output wordlist: ").strip()
outfile = ctx.select_file_with_autocomplete("[*] Enter path to output wordlist").strip()
if not outfile:
print("[!] Output path cannot be empty.")
return

View File

@@ -35,22 +35,23 @@ class TestWordlistFilterLength:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_called_once_with(str(infile), str(outfile), 4, 8)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_filter_length(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_not_called()
def test_rejects_empty_outfile(self, tmp_path):
ctx = _make_ctx()
infile = tmp_path / "in.txt"
infile.write_text("test\n")
with patch("builtins.input", side_effect=[str(infile), ""]):
wordlist_filter_length(ctx)
ctx.select_file_with_autocomplete.side_effect = [str(infile), ""]
wordlist_filter_length(ctx)
ctx.wordlist_filter_len.assert_not_called()
def test_prints_success_message(self, tmp_path, capsys):
@@ -58,7 +59,8 @@ class TestWordlistFilterLength:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
out = capsys.readouterr().out
assert "success" in out.lower() or "wrote" in out.lower() or str(outfile) in out
@@ -69,7 +71,8 @@ class TestWordlistFilterLength:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "4", "8"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["4", "8"]):
wordlist_filter_length(ctx)
out = capsys.readouterr().out
assert "fail" in out.lower() or "error" in out.lower() or "!" in out
@@ -81,14 +84,15 @@ class TestWordlistFilterCharclassInclude:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "7"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["7"]):
wordlist_filter_charclass_include(ctx)
ctx.wordlist_filter_req_include.assert_called_once_with(str(infile), str(outfile), 7)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_filter_charclass_include(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_charclass_include(ctx)
ctx.wordlist_filter_req_include.assert_not_called()
@@ -98,14 +102,15 @@ class TestWordlistFilterCharclassExclude:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "8"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["8"]):
wordlist_filter_charclass_exclude(ctx)
ctx.wordlist_filter_req_exclude.assert_called_once_with(str(infile), str(outfile), 8)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_filter_charclass_exclude(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_filter_charclass_exclude(ctx)
ctx.wordlist_filter_req_exclude.assert_not_called()
@@ -115,7 +120,8 @@ class TestWordlistCutSubstring:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", "4"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["2", "4"]):
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, 4)
@@ -124,14 +130,15 @@ class TestWordlistCutSubstring:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outfile = tmp_path / "out.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "2", ""]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["2", ""]):
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_called_once_with(str(infile), str(outfile), 2, None)
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_cut_substring(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_cut_substring(ctx)
ctx.wordlist_cutb.assert_not_called()
@@ -141,8 +148,8 @@ class TestWordlistSplitByLength:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outdir = tmp_path / "split"
with patch("builtins.input", side_effect=[str(infile), str(outdir)]):
wordlist_split_by_length(ctx)
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)]
wordlist_split_by_length(ctx)
ctx.wordlist_splitlen.assert_called_once_with(str(infile), str(outdir))
def test_creates_outdir_if_missing(self, tmp_path):
@@ -150,14 +157,14 @@ class TestWordlistSplitByLength:
infile = tmp_path / "in.txt"
infile.write_text("test\n")
outdir = tmp_path / "split" / "nested"
with patch("builtins.input", side_effect=[str(infile), str(outdir)]):
wordlist_split_by_length(ctx)
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outdir)]
wordlist_split_by_length(ctx)
assert outdir.exists()
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_split_by_length(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_split_by_length(ctx)
ctx.wordlist_splitlen.assert_not_called()
@@ -169,8 +176,8 @@ class TestWordlistSubtractWords:
removefile = tmp_path / "remove.txt"
removefile.write_text("word1\n")
outfile = tmp_path / "out.txt"
# "1" = single remove
with patch("builtins.input", side_effect=["1", str(infile), str(removefile), str(outfile)]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(removefile), str(outfile)]
with patch("builtins.input", side_effect=["1"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract_single.assert_called_once_with(str(infile), str(removefile), str(outfile))
@@ -183,11 +190,12 @@ class TestWordlistSubtractWords:
removefile2 = tmp_path / "remove2.txt"
removefile2.write_text("word2\n")
outfile = tmp_path / "out.txt"
# "2" = multiple removes, then two remove files separated by comma or newline
with patch(
"builtins.input",
side_effect=["2", str(infile), str(outfile), f"{removefile1},{removefile2}"],
):
ctx.select_file_with_autocomplete.side_effect = [
str(infile),
str(outfile),
f"{removefile1},{removefile2}",
]
with patch("builtins.input", side_effect=["2"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract.assert_called_once_with(
str(infile), str(outfile), str(removefile1), str(removefile2)
@@ -195,7 +203,8 @@ class TestWordlistSubtractWords:
def test_single_remove_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", side_effect=["1", "/nonexistent.txt"]):
ctx.select_file_with_autocomplete.return_value = "/nonexistent.txt"
with patch("builtins.input", side_effect=["1"]):
wordlist_subtract_words(ctx)
ctx.wordlist_subtract_single.assert_not_called()
@@ -206,7 +215,8 @@ class TestWordlistShard:
infile = tmp_path / "in.txt"
infile.write_text("word1\nword2\nword3\n")
outfile = tmp_path / "shard.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "0"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "0"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_called_once_with(str(infile), str(outfile), 3, 0)
@@ -215,14 +225,15 @@ class TestWordlistShard:
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "3", "3"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["3", "3"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
def test_rejects_nonexistent_infile(self, tmp_path):
ctx = _make_ctx()
with patch("builtins.input", return_value="/nonexistent/file.txt"):
wordlist_shard(ctx)
ctx.select_file_with_autocomplete.return_value = "/nonexistent/file.txt"
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()
def test_rejects_mod_less_than_2(self, tmp_path):
@@ -230,7 +241,8 @@ class TestWordlistShard:
infile = tmp_path / "in.txt"
infile.write_text("word1\n")
outfile = tmp_path / "shard.txt"
with patch("builtins.input", side_effect=[str(infile), str(outfile), "1", "0"]):
ctx.select_file_with_autocomplete.side_effect = [str(infile), str(outfile)]
with patch("builtins.input", side_effect=["1", "0"]):
wordlist_shard(ctx)
ctx.wordlist_gate.assert_not_called()