/
manly.py
194 lines (156 loc) · 5.38 KB
/
manly.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
"""
manly
~~~~~
This script is used (through its' cli) to extract information from
manual pages. More specifically, it tells the user, how the given
flags modify a command's behaviour.
In the code "options" refer to options for manly and "flags" refer
to options for the given command.
"""
from __future__ import print_function
__author__ = "Carl Bordum Hansen"
__version__ = "0.4.1"
import argparse
import functools
import os
import re
import subprocess
import sys
print_err = functools.partial(print, file=sys.stderr)
# A backport from subprocess to cover differences between 2/3.4 and 3.5
# This allows the same args to be passed into CPE regardless of version.
# This can be replaced with an import at 2.7 EOL
# See: https://github.com/carlbordum/manly/issues/27
class CalledProcessError(subprocess.CalledProcessError):
def __init__(self, returncode, cmd, output=None, stderr=None):
self.returncode = returncode
self.cmd = cmd
self.output = output
self.stderr = stderr
_ANSI_BOLD = "%s"
if sys.stdout.isatty():
_ANSI_BOLD = "\033[1m%s\033[0m"
USAGE_EXAMPLE = """example:
$ manly rm --preserve-root -rf
rm - remove files or directories
================================
-f, --force
ignore nonexistent files and arguments, never prompt
--preserve-root
do not remove '/' (default)
-r, -R, --recursive
remove directories and their contents recursively"""
VERSION = (
"manly %s\nCopyright (c) 2017 %s.\nMIT License: see LICENSE.\n\n"
"Written by %s and Mark Jameson."
) % (__version__, __author__, __author__)
def parse_flags(raw_flags):
"""Return a list of flags.
Concatenated flags will be split into individual flags
(eg. '-la' -> '-l', '-a'), but the concatenated flag will also be
returned, as some command use single dash names (e.g `clang` has
flags like "-nostdinc") and some even mix both.
"""
flags = set()
for flag in raw_flags:
# Filter out non-flags
if not flag.startswith("-"):
continue
flags.add(flag)
# Split and sperately add potential single-letter flags
if not flag.startswith("--"):
flags.update("-" + char for char in flag[1:])
return list(flags)
def parse_manpage(page, flags):
"""Return a list of blocks that match *flags* in *page*."""
current_section = []
output = []
for line in page.splitlines():
if line:
current_section.append(line)
continue
section = "\n".join(current_section)
section_top = section.strip().split("\n")[:2]
first_line = section_top[0].split(",")
segments = [seg.strip() for seg in first_line]
try:
segments.append(section_top[1].strip())
except IndexError:
pass
for flag in flags:
for segment in segments:
if segment.startswith(flag):
output.append(
re.sub(r"(^|\s)%s" % flag, _ANSI_BOLD % flag, section).rstrip()
)
break
current_section = []
return output
def manly(command):
if isinstance(command, str):
command = command.split(" ")
program = command[0]
flags = command[1:]
# we set MANWIDTH, so we don't rely on the users terminal width
# try `export MANWIDTH=80` -- makes manuals more readable imo :)
man_env = {}
man_env.update(os.environ)
man_env["MANWIDTH"] = "80"
try:
process = subprocess.Popen(
["man", "--", program],
env=man_env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
out, err = (s.decode("utf-8") for s in process.communicate())
# emulate subprocess.run of py3.5, for easier changing in the future
if process.returncode:
raise CalledProcessError(
process.returncode, ["man", "--", program], out, err
)
except OSError as e:
print_err("manly: Could not execute 'man'")
print_err(e)
sys.exit(127)
except CalledProcessError as e:
print_err(e.stderr.strip())
sys.exit(e.returncode)
manpage = out
flags = parse_flags(flags)
output = parse_manpage(manpage, flags)
title = _ANSI_BOLD % (
re.search(r"(?<=^NAME\n\s{5}).+", manpage, re.MULTILINE).group(0).strip()
)
return title, output
def main():
parser = argparse.ArgumentParser(
prog="manly",
description="Explain how FLAGS modify a COMMAND's behaviour.",
epilog=USAGE_EXAMPLE,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument("command", nargs=argparse.REMAINDER, help="")
parser.add_argument(
"-v",
"--version",
action="version",
version=VERSION,
help="display version information and exit.",
)
args = parser.parse_args()
if not len(args.command):
print_err(
"manly: missing COMMAND\nTry 'manly --help' for more information.",
)
sys.exit(1)
title, output = manly(args.command)
if output:
print("\n%s" % title)
print("=" * (len(title) - 8), end="\n\n")
for flag in output:
print(flag, end="\n\n")
else:
print_err("manly: No matching flags found.")
if __name__ == "__main__":
main()