Skip to content

Commit 48b9777

Browse files
committed
Added show command to CLI tool
1 parent 10da55a commit 48b9777

4 files changed

Lines changed: 86 additions & 0 deletions

File tree

‎docs/pages/cli.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ usage examples:
5151
- echo -en "secret" | codext strength base64 rot-13
5252
- codext compose
5353
- codext compose -a alphanumeric -l 4 -k mysecret -m mychain -n
54+
- codext show rle
55+
- codext show base64-url rot-13 example-macro
5456
```
5557

5658
!!! note "Input/output"
@@ -63,6 +65,10 @@ usage examples:
6365

6466
Encodings can be chained as shown in the last examples of the help message. This can be practical for quickly manipulating data.
6567

68+
!!! note "Codecs documentation"
69+
70+
`codext show` displays the documentation of the given codecs, encodings or macros: the codec's name, category and module, then the docstring of this module (for a native codec, the one of its module from the `encodings` package), or the chain of encodings of a macro. An unknown name is reported with similar names (e.g. `codext show vigener` suggests `vigenere`).
71+
6672
### Execution examples
6773

6874
**Scenario 1**: 2-stages encoded flag

‎src/codext/__main__.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44
"""
55
import argparse
66
import sys
7+
from difflib import get_close_matches
8+
from encodings import normalize_encoding
9+
from encodings.aliases import aliases
710
from getpass import getpass
11+
from importlib import import_module
812

13+
from . import __common__
914
from .__common__ import *
1015
from .__common__ import PERS_MACROS_FILE
1116
from .compose import QUESTIONS, report as compose_report
@@ -90,6 +95,30 @@ def __ask(question, given=None):
9095
print(f" please answer with a number from 1 to {len(choices)}", file=sys.stderr)
9196

9297

98+
def __show(name):
99+
""" Describe a codec, an encoding or a macro: its name, its category, its module and the docstring of this module,
100+
which documents the codec ; a macro is described by its chain of encodings. """
101+
ci = lookup(name)
102+
if isinstance(ci, CodecMacro):
103+
pers = name in __common__.PERS_MACROS
104+
chain = (__common__.PERS_MACROS if pers else __common__.MACROS)[name]
105+
return f"macro : {name} ({f'personal, in {PERS_MACROS_FILE}' if pers else 'embedded'})\n" \
106+
f"chain : {' · '.join(chain)}"
107+
p = ci.parameters
108+
codec, cat, module = p.get('name', ci.name), p.get('category'), p.get('module')
109+
# the native codecs are documented by their module from the encodings package (e.g. encodings.cp500)
110+
if cat == "native":
111+
n = normalize_encoding(ci.name).lower()
112+
module = f"encodings.{aliases.get(n, n)}"
113+
try:
114+
doc = (import_module(module).__doc__ or "").strip()
115+
except (ImportError, TypeError, ValueError):
116+
doc = ""
117+
lines = [f"codec : {codec} ({cat})"] + ([f"encoding : {name}"] if name != codec else []) + \
118+
[f"module : {module}", "", doc or "No description available."]
119+
return "\n".join(lines)
120+
121+
93122
def main():
94123
class _CustomFormatter(argparse.RawTextHelpFormatter):
95124
def __init__(self, prog, **kwargs):
@@ -121,6 +150,8 @@ def _format_action_invocation(self, action):
121150
"echo -en \"test\" | codext encode base64 gzip | codext guess gzip -c base",
122151
"codext compose",
123152
"codext compose -a alphanumeric -l 4 -k mysecret -m mychain -n",
153+
"codext show rle",
154+
"codext show base64-url rot-13 example-macro",
124155
])
125156
kw = {'formatter_class': _CustomFormatter}
126157
parser = argparse.ArgumentParser(prog="codext", description=descr, epilog=examples, **kw)
@@ -218,6 +249,8 @@ def _format_action_invocation(self, action):
218249
compose.add_argument("-s", "--seed", type=int, help="seed for drawing the chain (default: random)")
219250
compose.add_argument("-x", "--expansion", choices=list(QUESTIONS[2]['choices']),
220251
help="how many times larger than the input the output may be (default: 5)")
252+
show = sparsers.add_parser("show", help="show the documentation of codecs, encodings or macros")
253+
show.add_argument("encoding", nargs="+", help="names of codecs, encodings or macros")
221254
search = sparsers.add_parser("search", help="search for codecs")
222255
search.add_argument("pattern", nargs="+", help="encoding pattern to search")
223256
listi = sparsers.add_parser("list", help="list items")
@@ -241,6 +274,20 @@ def _format_action_invocation(self, action):
241274
results.extend(codecs.search(enc))
242275
print(", ".join(results) or "No encoding found")
243276
return 0
277+
# show the documentation of codecs, encodings or macros
278+
elif args.command == "show":
279+
status, out = 0, []
280+
for name in args.encoding:
281+
try:
282+
out.append(__show(name))
283+
except LookupError as e:
284+
status = 1
285+
similar = get_close_matches(name, list_encodings() + list_macros(), 5, .7)
286+
print(f"codext: {str(e)[0].lower()}{str(e)[1:]}" +
287+
(f" (similar: {', '.join(similar)})" if similar else ""), file=sys.stderr)
288+
if out:
289+
print(("\n\n" + "-" * 80 + "\n\n").join(out))
290+
return status
244291
# add/remove macros (not requiring to input a file or text)
245292
elif args.command == "add-macro":
246293
add_macro(args.name, *args.encoding)

‎src/codext/base/_base.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"""
1313
from argparse import ArgumentParser, RawTextHelpFormatter
1414
from functools import lru_cache
15+
from inspect import currentframe
1516
from math import lcm, log
1617
from string import ascii_lowercase as lower, ascii_uppercase as upper, digits, printable
1718
from sys import stderr, stdout
@@ -266,6 +267,11 @@ def _decode(input, errors="strict"):
266267
except AttributeError:
267268
g = [n]
268269
kwargs['guess'] = kwargs.get('guess', g)
270+
# the codec is documented by the module that declares it (e.g. baseN for base64), not by this factory
271+
f = currentframe().f_back
272+
while f.f_globals.get('__name__') in (__name__, __name__ + "2n"):
273+
f = f.f_back
274+
kwargs.setdefault('module', f.f_globals.get('__name__'))
269275
add(n, encode, decode, pattern, entropy=nb, **kwargs)
270276

271277

‎tests/test_common.py‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,33 @@ def test_handle_macros(self):
465465
self.assertIsNone(codext.add_macro("VALID-MACRO", first, "base64"))
466466
self.assertIsNone(codext.remove("VALID-MACRO"))
467467

468+
def test_main_show(self):
469+
import os, subprocess
470+
src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src")
471+
env = {**os.environ, 'PYTHONPATH': src + os.pathsep + os.environ.get('PYTHONPATH', "")}
472+
cli = lambda *args: subprocess.run([sys.executable, "-m", "codext", "show"] + list(args), input=b"", env=env,
473+
capture_output=True)
474+
# a codec is described by the docstring of its module, an encoding by the one of its codec
475+
r = cli("rle")
476+
self.assertEqual(r.returncode, 0)
477+
self.assertIn(b"codec : rle (compression)", r.stdout)
478+
self.assertIn(b"Run-Length Encoding Codec", r.stdout)
479+
r = cli("base64-url")
480+
self.assertIn(b"encoding : base64-url", r.stdout)
481+
self.assertIn(b"module : codext.base.baseN", r.stdout)
482+
# a native codec is described by its module from the encodings package, a macro by its chain
483+
self.assertIn(b"module : encodings.cp500", cli("cp500").stdout)
484+
r = cli("rot-13", "example-macro")
485+
self.assertIn(b"-" * 80, r.stdout)
486+
self.assertIn("chain : gzip · base62".encode(), r.stdout)
487+
# unknown names are reported with similar ones, while the known ones are still shown
488+
r = cli("vigener", "rle")
489+
self.assertEqual(r.returncode, 1)
490+
self.assertIn(b"unknown encoding: vigener (similar: vigenere)", r.stderr)
491+
self.assertIn(b"Run-Length Encoding Codec", r.stdout)
492+
# the codecs made by the base factory are documented by the module that declares them
493+
self.assertEqual(codext.lookup("base32").parameters['module'], "codext.base.baseN")
494+
468495
def test_main_binary_output(self):
469496
import os, subprocess
470497
src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src")

0 commit comments

Comments
 (0)