Skip to content

Commit 3c07f06

Browse files
authored
feat(cli): add search subcommand to filter commands (#381)
1 parent 17f5c19 commit 3c07f06

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

cli/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
import typer
66

7-
from commands import hello, list, show, version
7+
from commands import hello, list, search, show, version
88

99
app = typer.Typer(help="101 Linux Commands CLI 🚀")
1010
app.add_typer(hello.app, name="hello")
1111
app.add_typer(list.app, name="list")
1212
app.add_typer(version.app, name="version")
1313
app.command()(show.show)
14+
app.add_typer(search.app, name="search")
1415

1516

1617
def main() -> None:

cli/commands/search.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from typing import Dict, List
2+
3+
import typer
4+
5+
app = typer.Typer(
6+
help=("Search available commands by keyword " "(name or description).")
7+
)
8+
9+
10+
def _get_available_commands() -> List[Dict[str, str]]:
11+
"""
12+
Mocked list of available commands.
13+
Replace with real registry/source when available.
14+
"""
15+
return [
16+
{"name": "ls", "description": "List directory contents"},
17+
{"name": "grep", "description": "Search for PATTERN in files"},
18+
{
19+
"name": "find",
20+
"description": "Search for files in a directory hierarchy",
21+
},
22+
{
23+
"name": "awk",
24+
"description": "Pattern scanning and processing language",
25+
},
26+
{
27+
"name": "cat",
28+
"description": ("Concatenate files and print on the standard " "output"),
29+
},
30+
{"name": "head", "description": "Output the first part of files"},
31+
{"name": "tail", "description": "Output the last part of files"},
32+
{
33+
"name": "sed",
34+
"description": ("Stream editor for filtering and transforming " "text"),
35+
},
36+
]
37+
38+
39+
def _search_commands(
40+
keyword: str,
41+
commands: List[Dict[str, str]],
42+
) -> List[Dict[str, str]]:
43+
k = (keyword or "").strip().lower()
44+
if not k:
45+
return []
46+
return [
47+
cmd
48+
for cmd in commands
49+
if (k in cmd.get("name", "").lower())
50+
or (k in cmd.get("description", "").lower())
51+
]
52+
53+
54+
@app.callback(invoke_without_command=True)
55+
def search(
56+
keyword: str = typer.Argument(
57+
...,
58+
help="Keyword to search for, e.g. 'grep'",
59+
),
60+
) -> None:
61+
"""
62+
Search available commands by keyword (matches command name or description).
63+
Example: python cli.py search grep
64+
"""
65+
results = _search_commands(keyword, _get_available_commands())
66+
if not results:
67+
typer.echo("No commands found.")
68+
raise typer.Exit(code=1)
69+
70+
for cmd in results:
71+
name = cmd.get("name", "").strip()
72+
desc = cmd.get("description", "").strip()
73+
typer.echo(f"{name}: {desc}")

cli/test_cli.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,32 @@ def test_show_invalid():
9999
assert "Unknown" in combined_output or "Error" in combined_output
100100

101101

102+
def test_search_match():
103+
"""It should find matches and exit 0."""
104+
result = subprocess.run(
105+
[sys.executable, "cli.py", "search", "grep"],
106+
capture_output=True,
107+
text=True,
108+
cwd=os.path.dirname(__file__),
109+
check=False,
110+
)
111+
assert result.returncode == 0, result.stderr
112+
assert any(line.startswith("grep:") for line in result.stdout.splitlines())
113+
114+
115+
def test_search_no_match():
116+
"""It should print 'No commands found.' and exit non-zero."""
117+
result = subprocess.run(
118+
[sys.executable, "cli.py", "search", "this-should-not-exist-xyz"],
119+
capture_output=True,
120+
text=True,
121+
cwd=os.path.dirname(__file__),
122+
check=False,
123+
)
124+
assert result.returncode != 0
125+
assert "No commands found." in result.stdout
126+
127+
102128
if __name__ == "__main__":
103129
test_cli_help()
104130
test_hello_command()
@@ -110,4 +136,6 @@ def test_show_invalid():
110136
test_show_ls()
111137
test_show_grep()
112138
test_show_invalid()
139+
test_search_match()
140+
test_search_no_match()
113141
print("✅ All tests passed!")

0 commit comments

Comments
 (0)