TTY OV  1
A cross platform python terminal
Loading...
Searching...
No Matches
cli.py
Go to the documentation of this file.
1"""
2# +==== BEGIN polyguard =================+
3# LOGO:
4# input
5#
6# @#$%! hello
7# | |
8# +--+--+
9# |
10# v
11# +------------+
12# | POLY GUARD |
13# +------------+
14# | |
15# v v
16# BLOCKED PASSED
17# KO OK
18# /STOP
19# PROJECT: polyguard
20# FILE: cli.py
21# CREATION DATE: 21-03-2026
22# LAST Modified: 19:47:9 21-03-2026
23# DESCRIPTION:
24# A module that provides a set of swearwords to listen to when filtering while allowing to toggle on and off different languages.
25# /STOP
26# COPYRIGHT: (c) Henry Letellier
27# PURPOSE: This is the code in charge of simulating a pseudo tty for the ones that start the module without piping content into it.
28# TTY-friendly CLI wrapper for PolyGuard.
29#
30# Provides a simple entrypoint that accepts a path to the DB and a test
31# word to check. The CLI is intentionally minimal to be easy to extend.
32# // AR
33# +==== END polyguard =================+
34"""
35
36from typing import Iterable, Optional, List
37import argparse
38import sys
39
40from . import constants as POLY_CONST
41from .polyguard import PolyGuard
42
43
44def _iter_input_lines(stream: Iterable[str]):
45 """Iterate over lines from a stream, yielding non-empty lines.
46
47 Strips trailing newlines from each line before yielding.
48
49 Args:
50 stream: Iterable of strings (e.g., from sys.stdin).
51
52 Yields:
53 str: Non-empty lines with trailing newlines removed.
54 """
55 for line in stream:
56 text = line.rstrip("\n")
57 if text:
58 yield text
59
60
61def _resolve_lang(token: str):
62 """Resolve a user-provided token to a `POLY_CONST.Langs` member.
63
64 Accepts either the enum value ('en_uk') or the enum name ('EN_UK').
65 Returns None if no match is found.
66 """
67 if not token:
68 return None
69
70 norm = token.lower().replace("-", "_")
71 try:
72 return POLY_CONST.Langs(norm)
73 except ValueError:
74 # Try matching by enum name
75 name = token.upper().replace("-", "_")
76 if hasattr(POLY_CONST.Langs, name):
77 return getattr(POLY_CONST.Langs, name)
78 return None
79
80
81class CLI:
82 """Simple CLI class for PolyGuard interactive and batch usage.
83
84 Kept at module level to allow import and unit testing.
85 """
86
87 def __init__(self, guard: PolyGuard):
88 self.guard = guard
89
90 def run_single(self, word: str) -> int:
91 """Check a single word and print the result.
92
93 Args:
94 word: The word to check.
95
96 Returns:
97 int: Always returns 0.
98 """
99 is_swear = self.guard.is_a_swearword(word)
100 status = POLY_CONST.STATUS_OK
101 if is_swear:
102 status = POLY_CONST.STATUS_BLOCKED
103 print(f"{word}: {status}")
104 return 0
105
106 def run_stdin(self) -> int:
107 """Read lines from stdin and check each for profanity.
108
109 Outputs 'BLOCKED' or 'OK' for each line.
110
111 Returns:
112 int: Always returns 0.
113 """
114 for line in _iter_input_lines(sys.stdin):
115 result = self.guard.is_a_swearword(line)
116 if result:
117 print(POLY_CONST.STATUS_BLOCKED)
118 else:
119 print(POLY_CONST.STATUS_OK)
120 return 0
121
122 def cmd_log(self, tokens: list[str]) -> bool:
123 """Toggle logging output on or off.
124
125 Parses 'on' or 'off' from the second token and updates the guard and
126 sqlite handler's logging flags.
127
128 Args:
129 tokens: Command tokens where tokens[1] should be 'on' or 'off'.
130
131 Returns:
132 bool: Always returns True (command processed).
133 """
134 if len(tokens) != 2:
135 print(f"Usage: {POLY_CONST.COMMAND_TOKEN}log <on|off>")
136 return True
137 val = tokens[1].lower() in ("on", "1", "true", "yes")
138 self.guard.log = val
139 if self.guard.sqlite is not None:
140 self.guard.sqlite.log = val
141 if val:
142 print("Logging enabled")
143 else:
144 print("Logging disabled")
145 return True
146
147 def cmd_langopt(self, tokens: list[str]) -> bool:
148 """Enable or disable a specific language in the PolyGuard configuration.
149
150 Parses language code from tokens[1] and 'on'/'off' status from tokens[2],
151 then updates the guard's language configuration.
152
153 Args:
154 tokens: Command tokens where tokens[1] is language and tokens[2]
155 is 'on' or 'off'.
156
157 Returns:
158 bool: Always returns True (command processed).
159 """
160 if len(tokens) != 3:
161 print(f"Usage: {POLY_CONST.COMMAND_TOKEN}langopt <lang> <on|off>")
162 return True
163 lang_token = tokens[1]
164 lang_enum = _resolve_lang(lang_token)
165
166 if lang_enum is None:
167 print(f"Unknown language: {lang_token}")
168 return True
169
170 val = tokens[2].lower() in ("on", "1", "true", "yes")
171 try:
172 setattr(self.guard.default_choice, lang_enum.value, val)
173 status = "disabled"
174 if val:
175 status = "enabled"
176 print(f"Set {lang_enum.value} {status}")
177 except Exception as exc:
178 print(f"Failed to set language option: {exc}")
179 return True
180
181 def cmd_langs(self, tokens: list[str]) -> bool:
182 """List all available languages in the database with word counts.
183
184 Displays languages compactly on lines of ~80 characters, marking
185 enabled languages with [enabled] tag.
186
187 Args:
188 tokens: Command tokens (not used).
189
190 Returns:
191 bool: Always returns True (command processed).
192 """
193 if not self.guard.ensure_connection():
194 print("No DB connection available")
195 return True
196
197 try:
198 mapping = self.guard.sqlite.list_languages()
199 except Exception as exc:
200 print(f"Failed to query DB: {exc}")
201 return True
202
203 if not mapping:
204 print("No languages found in DB")
205 return True
206
207 # Fold entries to lines of ~80 chars for compact display
208 entries = []
209 for lang_code, count in sorted(mapping.items()):
210 enabled = False
211 try:
212 enabled = bool(getattr(self.guard.default_choice, lang_code))
213 except Exception:
214 enabled = False
215 mark = ""
216 if enabled:
217 mark = "[enabled]"
218 entries.append(f"{lang_code}({count}){mark}")
219
220 max_width = 80
221 line = []
222 cur_len = 0
223 for e in entries:
224 add_len = len(e)
225 if line:
226 add_len += 2
227 if cur_len + add_len > max_width and line:
228 print(", ".join(line))
229 line = [e]
230 cur_len = len(e)
231 else:
232 if line:
233 line.append(e)
234 cur_len += add_len
235 else:
236 line = [e]
237 cur_len = len(e)
238
239 if line:
240 print(", ".join(line))
241
242 return True
243
244 def cmd_langstatus(self, tokens: list[str]) -> bool:
245 """Display the current enabled/disabled status of all languages.
246
247 Lists each language code with its current 'on' or 'off' status as
248 determined by the guard's default language configuration.
249
250 Args:
251 tokens: Command tokens (not used).
252
253 Returns:
254 bool: Always returns True (command processed).
255 """
256 for lang in POLY_CONST.Langs:
257 try:
258 enabled = bool(getattr(self.guard.default_choice, lang.value))
259 except Exception:
260 enabled = False
261 status = "off"
262 if enabled:
263 status = "on"
264 print(f"{lang.value}: {status}")
265 return True
266
267 def cmd_word(self, tokens: list[str]) -> bool:
268 """Check a word, optionally for a specific language.
269
270 Supports multi-word phrases. If the final token matches a known language,
271 it is treated as the language filter. Returns the match status and any
272 matched words from the database (if a specific language is given).
273
274 Args:
275 tokens: Command tokens where tokens[1:] is the word/phrase and
276 tokens[-1] may be a language code.
277
278 Returns:
279 bool: Always returns True (command processed).
280 """
281 if len(tokens) < 2:
282 print(f"Usage: {POLY_CONST.COMMAND_TOKEN}word <word> [<lang>]")
283 return True
284
285 # Support multi-word phrases. If the final token resolves to a language,
286 # treat it as the optional language param, otherwise the whole remainder
287 # is the phrase to check.
288 if len(tokens) >= 3:
289 possible_lang = tokens[-1]
290 lang_enum = _resolve_lang(possible_lang)
291 if lang_enum is not None:
292 word = " ".join(tokens[1:-1])
293 else:
294 word = " ".join(tokens[1:])
295 lang_enum = None
296 else:
297 word = tokens[1]
298 lang_enum = None
299
300 if lang_enum is not None:
301 if not self.guard.ensure_connection():
302 print("No DB connection available")
303 return True
304 try:
305 found = self.guard.sqlite.has_word(lang_enum, word)
306 if found:
307 print(POLY_CONST.STATUS_BLOCKED)
308 else:
309 print(POLY_CONST.STATUS_OK)
310 except Exception as exc:
311 print(f"DB query failed: {exc}")
312 return True
313
314 # No explicit language requested; use current config (supports phrases)
315 result = self.guard.is_a_swearword(word)
316 if result:
317 print(POLY_CONST.STATUS_BLOCKED)
318 else:
319 print(POLY_CONST.STATUS_OK)
320 return True
321
322 def repl(self) -> int:
323 """Run the interactive CLI REPL loop.
324
325 Accepts user input for words to check or colon-prefixed commands.
326 Supports batch processing from stdin and interactive commands like
327 ':log', ':langopt', ':langs', ':langstatus', ':word', ':help', ':man'.
328
329 Returns:
330 int: Always returns 0 on normal exit or interrupt.
331 """
332 print(POLY_CONST.POLY_BOOT_MSG)
333
334 try:
335 while True:
336 try:
337 text = input(POLY_CONST.POLY_PROMPT)
338 except EOFError:
339 break
340
341 if not text:
342 continue
343
344 text = text.strip()
345
346 # Commands are prefixed with ':' to avoid clashing with words to check
347 if not text.startswith(POLY_CONST.COMMAND_TOKEN):
348 # Treat entire input as a word to check
349 result = self.guard.is_a_swearword(text)
350 if result:
351 print(POLY_CONST.STATUS_BLOCKED)
352 else:
353 print(POLY_CONST.STATUS_OK)
354 continue
355
356 # Remove prefix and split into tokens for command dispatch
357 cmd_body = text[POLY_CONST.COMMAND_TOKEN_LENGTH:]
358 tokens = cmd_body.split()
359 if not tokens:
360 continue
361 base = tokens[0].lower()
362
363 if base in ("quit", "exit"):
364 break
365
366 if base == "help":
367 print(POLY_CONST.POLY_HELP_TEXT)
368 continue
369
370 if base == "man":
371 print(POLY_CONST.POLY_MAN_TEXT)
372 continue
373
374 if base == "db":
375 print(
376 POLY_CONST.DB_PATH_FMT.format(
377 path=self.guard.db_path
378 )
379 )
380 continue
381
382 # Dispatch other commands to dedicated handlers
383 if base == "log":
384 self.cmd_log(tokens)
385 continue
386
387 if base == "langopt":
388 self.cmd_langopt(tokens)
389 continue
390
391 if base == "langs":
392 self.cmd_langs(tokens)
393 continue
394
395 if base in ("langstatus", "langsstatus"):
396 self.cmd_langstatus(tokens)
397 continue
398
399 if base == "word":
400 self.cmd_word(tokens)
401 continue
402
403 print(f"Unknown command: {base}")
404
405 except KeyboardInterrupt:
406 print()
407
408 return 0
409
410
411def main(argv: Optional[List[str]] = None) -> int:
412 """CLI entrypoint for PolyGuard.
413
414 Dispatches to single-word check, stdin batch processing, or interactive
415 REPL based on arguments and whether stdin is a TTY.
416
417 Args:
418 argv: Optional list of command-line arguments. If None, uses sys.argv.
419
420 Returns:
421 int: Exit code (0 for success, non-zero for error).
422 """
423 parser = argparse.ArgumentParser(
424 prog="polyguard", description="Run PolyGuard checks from the TTY or stdin")
425
426 parser.add_argument(
427 "--db-path",
428 default=None,
429 help="Path to SQLite DB (overrides package default)"
430 )
431 parser.add_argument(
432 "--word",
433 default=None,
434 help="A single word to test for being a swearword"
435 )
436
437 args = parser.parse_args(argv)
438
439 conf = POLY_CONST.LangConfig()
440
441 guard = PolyGuard(conf, db_path=args.db_path)
442 cli = CLI(guard)
443
444 # If a single word was passed as an argument, check and print result
445 if args.word:
446 return cli.run_single(args.word)
447
448 # If stdin is not a TTY, read lines from stdin and process them
449 if not sys.stdin.isatty():
450 return cli.run_stdin()
451
452 # Otherwise open a simple REPL
453 return cli.repl()
454
455
456if __name__ == "__main__":
457 raise SystemExit(main())
bool cmd_langs(self, list[str] tokens)
Definition cli.py:181
int repl(self)
Definition cli.py:322
bool cmd_langopt(self, list[str] tokens)
Definition cli.py:147
bool cmd_word(self, list[str] tokens)
Definition cli.py:267
int run_stdin(self)
Definition cli.py:106
__init__(self, PolyGuard guard)
Definition cli.py:87
bool cmd_log(self, list[str] tokens)
Definition cli.py:122
int run_single(self, str word)
Definition cli.py:90
bool cmd_langstatus(self, list[str] tokens)
Definition cli.py:244
_iter_input_lines(Iterable[str] stream)
Definition cli.py:44
_resolve_lang(str token)
Definition cli.py:61
int main(Optional[List[str]] argv=None)
Definition cli.py:411