PageRenderTime 55ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/share/tools/create_manpage_completions.py

https://github.com/psychocandy/fish-shell
Python | 955 lines | 896 code | 23 blank | 36 comment | 28 complexity | a09a2fc91cb23a1885d6917380e9a601 MD5 | raw file
Possible License(s): LGPL-2.0
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # Run me like this: ./create_manpage_completions.py /usr/share/man/man1/* > man_completions.fish
  4. """
  5. <OWNER> = Siteshwar Vashisht
  6. <YEAR> = 2012
  7. Copyright (c) 2012, Siteshwar Vashisht
  8. All rights reserved.
  9. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  10. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  11. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  12. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  13. """
  14. import string, sys, re, os.path, gzip, traceback, getopt, errno
  15. from deroff import Deroffer
  16. # Whether we're Python 3
  17. IS_PY3 = sys.version_info[0] >= 3
  18. # This gets set to the name of the command that we are currently executing
  19. CMDNAME = ""
  20. # Information used to track which of our parsers were successful
  21. PARSER_INFO = {}
  22. # builtcommand writes into this global variable, yuck
  23. built_command_output = []
  24. # Diagnostic output
  25. diagnostic_output = []
  26. diagnostic_indent = 0
  27. # Three diagnostic verbosity levels
  28. VERY_VERBOSE, BRIEF_VERBOSE, NOT_VERBOSE = 2, 1, 0
  29. # Pick some reasonable default values for settings
  30. global VERBOSITY, WRITE_TO_STDOUT, DEROFF_ONLY
  31. VERBOSITY, WRITE_TO_STDOUT, DEROFF_ONLY = NOT_VERBOSE, False, False
  32. def add_diagnostic(dgn, msg_verbosity = VERY_VERBOSE):
  33. # Add a diagnostic message, if msg_verbosity <= VERBOSITY
  34. if msg_verbosity <= VERBOSITY:
  35. diagnostic_output.append(' '*diagnostic_indent + dgn)
  36. def flush_diagnostics(where):
  37. if diagnostic_output:
  38. output_str = '\n'.join(diagnostic_output) + '\n'
  39. where.write(output_str)
  40. diagnostic_output[:] = []
  41. # Make sure we don't output the same completion multiple times, which can happen
  42. # For example, xsubpp.1.gz and xsubpp5.10.1.gz
  43. # This maps commands to lists of completions
  44. already_output_completions = {}
  45. def compileAndSearch(regex, input):
  46. options_section_regex = re.compile(regex , re.DOTALL)
  47. options_section_matched = re.search( options_section_regex, input)
  48. return options_section_matched
  49. def unquoteDoubleQuotes(data):
  50. if (len(data) < 2):
  51. return data
  52. if data[0] == '"' and data[len(data)-1] == '"':
  53. data = data[1:len(data)-1]
  54. return data
  55. def unquoteSingleQuotes(data):
  56. if (len(data) < 2):
  57. return data
  58. if data[0] == '`' and data[len(data)-1] == '\'':
  59. data = data[1:len(data)-1]
  60. return data
  61. # Make a string of characters that are deemed safe in fish without needing to be escaped
  62. # Note that space is not included
  63. g_fish_safe_chars = frozenset(string.ascii_letters + string.digits + '_+-|/:=@~')
  64. def fish_escape_single_quote(str):
  65. # Escape a string if necessary so that it can be put in single quotes
  66. # If it has no non-safe chars, there's nothing to do
  67. if g_fish_safe_chars.issuperset(str):
  68. return str
  69. str = str.replace('\\', '\\\\') # Replace one backslash with two
  70. str = str.replace("'", "\\'") # Replace one single quote with a backslash-single-quote
  71. return "'" + str + "'"
  72. def output_complete_command(cmdname, args, description, output_list):
  73. comps = ['complete -c', cmdname]
  74. comps.extend(args)
  75. if description:
  76. comps.append('--description')
  77. comps.append(description)
  78. output_list.append(' '.join(comps))
  79. def builtcommand(options, description):
  80. # print "Options are: ", options
  81. man_optionlist = re.split(" |,|\"|=|[|]", options)
  82. fish_options = []
  83. for option in man_optionlist:
  84. option = option.strip()
  85. # Skip some problematic cases
  86. if option in ['-', '--']: continue
  87. if option.startswith('--'):
  88. # New style long option (--recursive)
  89. fish_options.append('-l ' + fish_escape_single_quote(option[2:]))
  90. elif option.startswith('-') and len(option) == 2:
  91. # New style short option (-r)
  92. fish_options.append('-s ' + fish_escape_single_quote(option[1:]))
  93. elif option.startswith('-') and len(option) > 2:
  94. # Old style long option (-recursive)
  95. fish_options.append('-o ' + fish_escape_single_quote(option[1:]))
  96. # Determine which options are new (not already in existing_options)
  97. # Then add those to the existing options
  98. existing_options = already_output_completions.setdefault(CMDNAME, set())
  99. fish_options = [opt for opt in fish_options if opt not in existing_options]
  100. existing_options.update(fish_options)
  101. # Maybe it's all for naught
  102. if not fish_options: return
  103. first_period = description.find(".")
  104. if first_period >= 45 or (first_period == -1 and len(description) > 45):
  105. description = description[:45] + '... [See Man Page]'
  106. elif first_period >= 0:
  107. description = description[:first_period]
  108. # Escape some more things
  109. description = fish_escape_single_quote(description)
  110. escaped_cmd = fish_escape_single_quote(CMDNAME)
  111. output_complete_command(escaped_cmd, fish_options, description, built_command_output)
  112. def removeGroffFormatting(data):
  113. # data = data.replace("\fI","")
  114. # data = data.replace("\fP","")
  115. data = data.replace("\\fI","")
  116. data = data.replace("\\fP","")
  117. data = data.replace("\\f1","")
  118. data = data.replace("\\fB","")
  119. data = data.replace("\\fR","")
  120. data = data.replace("\\e","")
  121. data = re.sub(".PD( \d+)","",data)
  122. data = data.replace(".BI","")
  123. data = data.replace(".BR","")
  124. data = data.replace("0.5i","")
  125. data = data.replace(".rb","")
  126. data = data.replace("\\^","")
  127. data = data.replace("{ ","")
  128. data = data.replace(" }","")
  129. data = data.replace("\ ","")
  130. data = data.replace("\-","-")
  131. data = data.replace("\&","")
  132. data = data.replace(".B","")
  133. data = data.replace("\-","-")
  134. data = data.replace(".I","")
  135. data = data.replace("\f","")
  136. return data
  137. class ManParser:
  138. def isMyType(self, manpage):
  139. return False
  140. def parseManPage(self, manpage):
  141. return False
  142. def name(self):
  143. return "no-name"
  144. class Type1ManParser(ManParser):
  145. def isMyType(self, manpage):
  146. # print manpage
  147. options_section_matched = compileAndSearch("\.SH \"OPTIONS\"(.*?)", manpage)
  148. if options_section_matched == None:
  149. return False
  150. else:
  151. return True
  152. def parseManPage(self, manpage):
  153. options_section_regex = re.compile( "\.SH \"OPTIONS\"(.*?)(\.SH|\Z)", re.DOTALL)
  154. options_section_matched = re.search( options_section_regex, manpage)
  155. options_section = options_section_matched.group(0)
  156. # print options_section
  157. options_parts_regex = re.compile("\.PP(.*?)\.RE", re.DOTALL)
  158. options_matched = re.search(options_parts_regex, options_section)
  159. # print options_matched
  160. add_diagnostic('Command is ' + CMDNAME)
  161. if options_matched == None:
  162. add_diagnostic('Unable to find options')
  163. if( self.fallback(options_section) ):
  164. return True
  165. elif (self.fallback2(options_section) ):
  166. return True
  167. return False
  168. while (options_matched != None):
  169. # print len(options_matched.groups())
  170. # print options_matched.group()
  171. data = options_matched.group(1)
  172. last_dotpp_index = data.rfind(".PP")
  173. if (last_dotpp_index != -1):
  174. data = data[last_dotpp_index+3:]
  175. data = removeGroffFormatting(data)
  176. data = data.split(".RS 4")
  177. # print data
  178. if (len (data) > 1): #and len(data[1]) <= 300):
  179. optionName = data[0].strip()
  180. if ( optionName.find("-") == -1):
  181. add_diagnostic(optionName + " doesn't contain - ")
  182. # return False
  183. else:
  184. optionName = unquoteDoubleQuotes(optionName)
  185. optionName = unquoteSingleQuotes(optionName)
  186. optionDescription = data[1].strip().replace("\n"," ")
  187. # print >> sys.stderr, "Option: ", optionName," Description: ", optionDescription , '\n'
  188. builtcommand(optionName, optionDescription)
  189. else:
  190. add_diagnostic('Unable to split option from description')
  191. return False
  192. options_section = options_section[options_matched.end()-3:]
  193. options_matched = re.search(options_parts_regex, options_section)
  194. def fallback(self, options_section):
  195. add_diagnostic('Falling Back')
  196. options_parts_regex = re.compile("\.TP( \d+)?(.*?)\.TP", re.DOTALL)
  197. options_matched = re.search(options_parts_regex, options_section)
  198. if options_matched == None:
  199. add_diagnostic('Still not found')
  200. return False
  201. while options_matched != None:
  202. data = options_matched.group(2)
  203. data = removeGroffFormatting(data)
  204. data = data.strip()
  205. data = data.split("\n",1)
  206. if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400):
  207. optionName = data[0].strip()
  208. if ( optionName.find("-") == -1):
  209. add_diagnostic(optionName + "doesn't contains -")
  210. else:
  211. optionName = unquoteDoubleQuotes(optionName)
  212. optionName = unquoteSingleQuotes(optionName)
  213. optionDescription = data[1].strip().replace("\n"," ")
  214. # print "Option: ", optionName," Description: ", optionDescription , '\n'
  215. builtcommand(optionName, optionDescription)
  216. else:
  217. add_diagnostic('Unable to split option from description')
  218. return False
  219. options_section = options_section[options_matched.end()-3:]
  220. options_matched = re.search(options_parts_regex, options_section)
  221. return True
  222. def fallback2(self, options_section):
  223. add_diagnostic('Falling Back2')
  224. ix_remover_regex = re.compile("\.IX.*")
  225. trailing_num_regex = re.compile('\\d+$')
  226. options_parts_regex = re.compile("\.IP (.*?)\.IP", re.DOTALL)
  227. options_section = re.sub(ix_remover_regex, "", options_section)
  228. options_matched = re.search(options_parts_regex, options_section)
  229. if options_matched == None:
  230. add_diagnostic('Still not found2')
  231. return False
  232. while options_matched != None:
  233. data = options_matched.group(1)
  234. # print "Data is : ", data
  235. data = removeGroffFormatting(data)
  236. data = data.strip()
  237. data = data.split("\n",1)
  238. if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400):
  239. # print "Data[0] is: ", data[0]
  240. # data = re.sub(trailing_num_regex, "", data)
  241. optionName = re.sub(trailing_num_regex, "", data[0].strip())
  242. if ('-' not in optionName):
  243. add_diagnostic(optionName + " doesn't contain -")
  244. else:
  245. optionName = optionName.strip()
  246. optionName = unquoteDoubleQuotes(optionName)
  247. optionName = unquoteSingleQuotes(optionName)
  248. optionDescription = data[1].strip().replace("\n"," ")
  249. # print "Option: ", optionName," Description: ", optionDescription , '\n'
  250. builtcommand(optionName, optionDescription)
  251. else:
  252. # print data
  253. add_diagnostic('Unable to split option from description')
  254. return False
  255. options_section = options_section[options_matched.end()-3:]
  256. options_matched = re.search(options_parts_regex, options_section)
  257. return True
  258. def name(self):
  259. return "Type1"
  260. class Type2ManParser(ManParser):
  261. def isMyType(self, manpage):
  262. options_section_matched = compileAndSearch("\.SH OPTIONS(.*?)", manpage)
  263. if options_section_matched == None:
  264. return False
  265. else:
  266. return True
  267. def parseManPage(self, manpage):
  268. options_section_regex = re.compile( "\.SH OPTIONS(.*?)(\.SH|\Z)", re.DOTALL)
  269. options_section_matched = re.search( options_section_regex, manpage)
  270. # if (options_section_matched == None):
  271. # print "Falling Back"
  272. # options_section_regex = re.compile( "\.SH OPTIONS(.*?)$", re.DOTALL)
  273. # options_section_matched = re.search( options_section_regex, manpage)
  274. # print manpage
  275. options_section = options_section_matched.group(1)
  276. # print options_section
  277. # print options_section
  278. # sys.exit(1)
  279. # options_parts_regex = re.compile("\.TP(.*?)\.TP", re.DOTALL)
  280. options_parts_regex = re.compile("\.[I|T]P( \d+(\.\d)?i?)?(.*?)\.[I|T]P", re.DOTALL)
  281. # options_parts_regex = re.compile("\.TP(.*?)[(\.TP)|(\.SH)]", re.DOTALL)
  282. options_matched = re.search(options_parts_regex, options_section)
  283. add_diagnostic('Command is ' + CMDNAME)
  284. if options_matched == None:
  285. add_diagnostic(self.name() + ': Unable to find options')
  286. return False
  287. while (options_matched != None):
  288. # print len(options_matched.groups())
  289. data = options_matched.group(3)
  290. data = removeGroffFormatting(data)
  291. data = data.strip()
  292. data = data.split("\n",1)
  293. # print >> sys.stderr, data
  294. if (len(data)>1 and len(data[1].strip())>0): # and len(data[1])<400):
  295. optionName = data[0].strip()
  296. if '-' not in optionName:
  297. add_diagnostic(optionName + " doesn't contain -")
  298. else:
  299. optionName = unquoteDoubleQuotes(optionName)
  300. optionName = unquoteSingleQuotes(optionName)
  301. optionDescription = data[1].strip().replace("\n"," ")
  302. # print "Option: ", optionName," Description: ", optionDescription , '\n'
  303. builtcommand(optionName, optionDescription)
  304. else:
  305. # print >> sys.stderr, data
  306. add_diagnostic('Unable to split option from description')
  307. # return False
  308. options_section = options_section[options_matched.end()-3:]
  309. options_matched = re.search(options_parts_regex, options_section)
  310. def name(self):
  311. return "Type2"
  312. class Type3ManParser(ManParser):
  313. def isMyType(self, manpage):
  314. options_section_matched = compileAndSearch("\.SH DESCRIPTION(.*?)", manpage)
  315. if options_section_matched == None:
  316. return False
  317. else:
  318. return True
  319. def parseManPage(self, manpage):
  320. options_section_regex = re.compile( "\.SH DESCRIPTION(.*?)(\.SH|\Z)", re.DOTALL)
  321. options_section_matched = re.search( options_section_regex, manpage)
  322. options_section = options_section_matched.group(1)
  323. # print options_section
  324. # sys.exit(1)
  325. options_parts_regex = re.compile("\.TP(.*?)\.TP", re.DOTALL)
  326. options_matched = re.search(options_parts_regex, options_section)
  327. add_diagnostic('Command is ' + CMDNAME)
  328. if options_matched == None:
  329. add_diagnostic('Unable to find options section')
  330. return False
  331. while (options_matched != None):
  332. # print len(options_matched.groups())
  333. data = options_matched.group(1)
  334. data = removeGroffFormatting(data)
  335. data = data.strip()
  336. data = data.split("\n",1)
  337. if (len(data)>1): # and len(data[1])<400):
  338. optionName = data[0].strip()
  339. if ( optionName.find("-") == -1):
  340. add_diagnostic(optionName + "doesn't contain -")
  341. else:
  342. optionName = unquoteDoubleQuotes(optionName)
  343. optionName = unquoteSingleQuotes(optionName)
  344. optionDescription = data[1].strip().replace("\n"," ")
  345. # print >> sys.stderr, "Option: ", optionName," Description: ", optionDescription , '\n'
  346. builtcommand(optionName, optionDescription)
  347. else:
  348. add_diagnostic('Unable to split option from description')
  349. return False
  350. options_section = options_section[options_matched.end()-3:]
  351. options_matched = re.search(options_parts_regex, options_section)
  352. def name(self):
  353. return "Type3"
  354. class Type4ManParser(ManParser):
  355. def isMyType(self, manpage):
  356. options_section_matched = compileAndSearch("\.SH FUNCTION LETTERS(.*?)", manpage)
  357. if options_section_matched == None:
  358. return False
  359. else:
  360. return True
  361. def parseManPage(self, manpage):
  362. options_section_regex = re.compile( "\.SH FUNCTION LETTERS(.*?)(\.SH|\Z)", re.DOTALL)
  363. options_section_matched = re.search( options_section_regex, manpage)
  364. options_section = options_section_matched.group(1)
  365. # print options_section
  366. # sys.exit(1)
  367. options_parts_regex = re.compile("\.TP(.*?)\.TP", re.DOTALL)
  368. options_matched = re.search(options_parts_regex, options_section)
  369. add_diagnostic('Command is ' + CMDNAME)
  370. if options_matched == None:
  371. print >> sys.stderr, "Unable to find options section"
  372. return False
  373. while (options_matched != None):
  374. # print len(options_matched.groups())
  375. data = options_matched.group(1)
  376. data = removeGroffFormatting(data)
  377. data = data.strip()
  378. data = data.split("\n",1)
  379. if (len(data)>1): # and len(data[1])<400):
  380. optionName = data[0].strip()
  381. if ( optionName.find("-") == -1):
  382. add_diagnostic(optionName + " doesn't contain - ")
  383. else:
  384. optionName = unquoteDoubleQuotes(optionName)
  385. optionName = unquoteSingleQuotes(optionName)
  386. optionDescription = data[1].strip().replace("\n"," ")
  387. # print "Option: ", optionName," Description: ", optionDescription , '\n'
  388. builtcommand(optionName, optionDescription)
  389. else:
  390. add_diagnostic('Unable to split option from description')
  391. return False
  392. options_section = options_section[options_matched.end()-3:]
  393. options_matched = re.search(options_parts_regex, options_section)
  394. return True
  395. def name(self):
  396. return "Type4"
  397. class TypeDarwinManParser(ManParser):
  398. def isMyType(self, manpage):
  399. options_section_matched = compileAndSearch("\.S[hH] DESCRIPTION", manpage)
  400. return options_section_matched != None
  401. def trim_groff(self, line):
  402. # Remove initial period
  403. if line.startswith('.'):
  404. line = line[1:]
  405. # Skip leading groff crud
  406. while re.match('[A-Z][a-z]\s', line):
  407. line = line[3:]
  408. return line
  409. def is_option(self, line):
  410. return line.startswith('.It Fl')
  411. def parseManPage(self, manpage):
  412. got_something = False
  413. lines = manpage.splitlines()
  414. # Discard lines until we get to ".sh Description"
  415. while lines and not (lines[0].startswith('.Sh DESCRIPTION') or lines[0].startswith('.SH DESCRIPTION')):
  416. lines.pop(0)
  417. while lines:
  418. # Pop until we get to the next option
  419. while lines and not self.is_option(lines[0]):
  420. lines.pop(0)
  421. if not lines:
  422. continue
  423. # Extract the name
  424. name = self.trim_groff(lines.pop(0)).strip()
  425. if not name: continue
  426. name = name.split(None, 2)[0]
  427. # Extract the description
  428. desc = ''
  429. while lines and not self.is_option(lines[0]):
  430. # print "*", lines[0]
  431. desc = desc + lines.pop(0)
  432. # print "name: ", name
  433. if name == '-':
  434. # Skip double -- arguments
  435. continue
  436. elif len(name) > 1:
  437. # Output the command
  438. builtcommand('--' + name, desc)
  439. got_something = True
  440. elif len(name) == 1:
  441. builtcommand('-' + name, desc)
  442. got_something = True
  443. return got_something
  444. def name(self):
  445. return "Darwin man parser"
  446. class TypeDeroffManParser(ManParser):
  447. def isMyType(self, manpage):
  448. return True # We're optimists
  449. def is_option(self, line):
  450. return line.startswith('-')
  451. def could_be_description(self, line):
  452. return len(line) > 0 and not line.startswith('-')
  453. def parseManPage(self, manpage):
  454. d = Deroffer()
  455. d.deroff(manpage)
  456. output = d.get_output()
  457. lines = output.split('\n')
  458. got_something = False
  459. # Discard lines until we get to DESCRIPTION or OPTIONS
  460. while lines and not (lines[0].startswith('DESCRIPTION') or lines[0].startswith('OPTIONS') or lines[0].startswith('COMMAND OPTIONS')):
  461. lines.pop(0)
  462. # Look for BUGS and stop there
  463. for idx in range(len(lines)):
  464. line = lines[idx]
  465. if line.startswith('BUGS'):
  466. # Drop remaining elements
  467. lines[idx:] = []
  468. break
  469. while lines:
  470. # Pop until we get to the next option
  471. while lines and not self.is_option(lines[0]):
  472. line = lines.pop(0)
  473. if not lines:
  474. continue
  475. options = lines.pop(0)
  476. # Pop until we get to either an empty line or a line starting with -
  477. description = ''
  478. while lines and self.could_be_description(lines[0]):
  479. if description: description += ' '
  480. description += lines.pop(0)
  481. builtcommand(options, description)
  482. got_something = True
  483. return got_something
  484. def name(self):
  485. return "Deroffing man parser"
  486. # Return whether the file at the given path is overwritable
  487. # Raises IOError if it cannot be opened
  488. def file_is_overwritable(path):
  489. result = False
  490. file = open(path, 'r')
  491. for line in file:
  492. # Skip leading empty lines
  493. line = line.strip()
  494. if not line:
  495. continue
  496. # We look in the initial run of lines that start with #
  497. if not line.startswith('#'):
  498. break
  499. # See if this contains the magic word
  500. if 'Autogenerated' in line:
  501. result = True
  502. break
  503. file.close()
  504. return result
  505. # Return whether the file at the given path either does not exist, or exists but appears to be a file we output (and hence can overwrite)
  506. def file_missing_or_overwritable(path):
  507. try:
  508. return file_is_overwritable(path)
  509. except IOError as err:
  510. if err.errno == 2:
  511. # File does not exist, full steam ahead
  512. return True
  513. else:
  514. # Something else happened
  515. return False
  516. # Delete the file if it is autogenerated
  517. def cleanup_autogenerated_file(path):
  518. try:
  519. if file_is_overwritable(path):
  520. os.remove(path)
  521. except (OSError, IOError):
  522. pass
  523. def parse_manpage_at_path(manpage_path, yield_to_dirs, output_directory):
  524. filename = os.path.basename(manpage_path)
  525. # Clear diagnostics
  526. global diagnostic_indent
  527. diagnostic_output[:] = []
  528. diagnostic_indent = 0
  529. # Set up some diagnostics
  530. add_diagnostic('Considering ' + manpage_path)
  531. diagnostic_indent += 1
  532. if manpage_path.endswith('.gz'):
  533. fd = gzip.open(manpage_path, 'r')
  534. manpage = fd.read()
  535. if IS_PY3: manpage = manpage.decode('latin-1')
  536. else:
  537. if IS_PY3:
  538. fd = open(manpage_path, 'r', encoding='latin-1')
  539. else:
  540. fd = open(manpage_path, 'r')
  541. manpage = fd.read()
  542. fd.close()
  543. manpage = str(manpage)
  544. # Get the "base" command, e.g. gcc.1.gz -> gcc
  545. cmd_base = CMDNAME.split('.', 1)[0]
  546. ignoredcommands = ["cc", "g++", "gcc", "c++", "cpp", "emacs", "gprof", "wget", "ld", "awk"]
  547. if cmd_base in ignoredcommands:
  548. return
  549. # Ignore perl's gazillion man pages
  550. ignored_prefixes = ['perl', 'zsh']
  551. for prefix in ignored_prefixes:
  552. if cmd_base.startswith(prefix):
  553. return
  554. # Ignore the millions of links to BUILTIN(1)
  555. if manpage.find('BUILTIN 1') != -1:
  556. return
  557. # Clear the output list
  558. built_command_output[:] = []
  559. if DEROFF_ONLY:
  560. parsers = [TypeDeroffManParser()]
  561. else:
  562. parsers = [Type1ManParser(), Type2ManParser(), Type4ManParser(), Type3ManParser(), TypeDarwinManParser(), TypeDeroffManParser()]
  563. parsersToTry = [p for p in parsers if p.isMyType(manpage)]
  564. success = False
  565. if not parsersToTry:
  566. add_diagnostic(manpage_path + ": Not supported")
  567. else:
  568. for parser in parsersToTry:
  569. parser_name = parser.name()
  570. add_diagnostic('Trying parser ' + parser_name)
  571. diagnostic_indent += 1
  572. success = parser.parseManPage(manpage)
  573. diagnostic_indent -= 1
  574. if success:
  575. PARSER_INFO.setdefault(parser_name, []).append(CMDNAME)
  576. break
  577. if success:
  578. if WRITE_TO_STDOUT:
  579. output_file = sys.stdout
  580. else:
  581. fullpath = os.path.join(output_directory, CMDNAME + '.fish')
  582. try:
  583. if file_missing_or_overwritable(fullpath):
  584. output_file = open(fullpath, 'w')
  585. else:
  586. add_diagnostic("Not overwriting the file at '%s'" % fullpath)
  587. except IOError as err:
  588. add_diagnostic("Unable to open file '%s': error(%d): %s" % (fullpath, err.errno, err.strerror))
  589. return False
  590. built_command_output.insert(0, "# %s: %s" % (CMDNAME, parser.name()))
  591. # Output the magic word Autogenerated so we can tell if we can overwrite this
  592. built_command_output.insert(1, "# Autogenerated from man pages")
  593. for line in built_command_output:
  594. output_file.write(line)
  595. output_file.write('\n')
  596. output_file.write('\n')
  597. add_diagnostic(manpage_path + ' parsed successfully')
  598. if output_file != sys.stdout:
  599. output_file.close()
  600. else:
  601. parser_names = ', '.join(p.name() for p in parsersToTry)
  602. #add_diagnostic('%s contains no options or is unparsable' % manpage_path, BRIEF_VERBOSE)
  603. add_diagnostic('%s contains no options or is unparsable (tried parser %s)' % (manpage_path, parser_names), BRIEF_VERBOSE)
  604. return success
  605. # Indicates whether the given filename has a presence in one of the yield-to directories
  606. # If so, there's a bespoke completion and we should not generate one
  607. def file_in_yield_directory(filename, yield_to_dirs):
  608. for yield_dir in yield_to_dirs:
  609. test_path = os.path.join(yield_dir, filename)
  610. if os.path.isfile(test_path):
  611. # Yield to the existing file
  612. return True
  613. return False
  614. # Indicates whether we want to skip this command because it already had a non-autogenerated completion
  615. def should_skip_man_page(output_path, filename, yield_to_dirs):
  616. # No reason to skip if we're writing to stdout
  617. if WRITE_TO_STDOUT:
  618. return false
  619. # Check all the yield directories
  620. for yield_dir in yield_to_dirs:
  621. test_path = os.path.join(yield_dir, filename)
  622. if os.path.isfile(test_path):
  623. # Yield to the existing file
  624. return true
  625. # See if there's a hand-written file already
  626. if not file_missing_or_overwritable(output_path):
  627. return true
  628. # We made it through, so don't skip
  629. return false
  630. def parse_and_output_man_pages(paths, output_directory, yield_to_dirs, show_progress):
  631. global diagnostic_indent, CMDNAME
  632. paths.sort()
  633. total_count = len(paths)
  634. successful_count, index = 0, 0
  635. padding_len = len(str(total_count))
  636. last_progress_string_length = 0
  637. if show_progress and not WRITE_TO_STDOUT:
  638. print("Parsing man pages and writing completions to {0}".format(output_directory))
  639. for manpage_path in paths:
  640. index += 1
  641. # Get the "base" command, e.g. gcc.1.gz -> gcc
  642. man_file_name = os.path.basename(manpage_path)
  643. CMDNAME = man_file_name.split('.', 1)[0]
  644. output_file_name = CMDNAME + '.fish'
  645. # Show progress if we're doing that
  646. if show_progress:
  647. progress_str = ' {0} / {1} : {2}'.format((str(index).rjust(padding_len)), total_count, man_file_name)
  648. # Pad on the right with spaces so we overwrite whatever we wrote last time
  649. padded_progress_str = progress_str.ljust(last_progress_string_length)
  650. last_progress_string_length = len(progress_str)
  651. sys.stdout.write("\r{0}\r".format(padded_progress_str))
  652. sys.stdout.flush()
  653. # Maybe we want to skip this item
  654. skip = False
  655. if not WRITE_TO_STDOUT:
  656. # Compute the path that we would write to
  657. output_path = os.path.join(output_directory, output_file_name)
  658. if file_in_yield_directory(output_file_name, yield_to_dirs):
  659. # We're duplicating a bespoke completion - delete any existing completion
  660. skip = True
  661. cleanup_autogenerated_file(output_path)
  662. elif not file_missing_or_overwritable(output_path):
  663. # Don't overwrite a user-created completion
  664. skip = True
  665. # Now skip if requested
  666. if skip:
  667. continue
  668. try:
  669. if parse_manpage_at_path(manpage_path, yield_to_dirs, output_directory):
  670. successful_count += 1
  671. except IOError:
  672. diagnostic_indent = 0
  673. add_diagnostic('Cannot open ' + manpage_path)
  674. except (KeyboardInterrupt, SystemExit):
  675. raise
  676. except:
  677. add_diagnostic('Error parsing %s: %s' % (manpage_path, sys.exc_info()[0]), BRIEF_VERBOSE)
  678. flush_diagnostics(sys.stderr)
  679. traceback.print_exc(file=sys.stderr)
  680. flush_diagnostics(sys.stderr)
  681. print("") #Newline after loop
  682. add_diagnostic("Successfully parsed %d / %d pages" % (successful_count, total_count), BRIEF_VERBOSE)
  683. flush_diagnostics(sys.stderr)
  684. def get_paths_from_manpath():
  685. # Return all the paths to man(1) files in the manpath
  686. import subprocess, os
  687. proc = subprocess.Popen(['manpath'], stdout=subprocess.PIPE)
  688. manpath, err_data = proc.communicate()
  689. parent_paths = manpath.decode().strip().split(':')
  690. if not parent_paths:
  691. sys.stderr.write("Unable to get the manpath (tried manpath)\n")
  692. sys.exit(-1)
  693. result = []
  694. for parent_path in parent_paths:
  695. directory_path = os.path.join(parent_path, 'man1')
  696. try:
  697. names = os.listdir(directory_path)
  698. except OSError as e:
  699. names = []
  700. names.sort()
  701. for name in names:
  702. result.append(os.path.join(directory_path, name))
  703. return result
  704. def usage(script_name):
  705. print("Usage: {0} [-v, --verbose] [-s, --stdout] [-d, --directory] [-p, --progress] files...".format(script_name))
  706. print("""Command options are:
  707. -h, --help\t\tShow this help message
  708. -v, --verbose [0, 1, 2]\tShow debugging output to stderr. Larger is more verbose.
  709. -s, --stdout\tWrite all completions to stdout (trumps the --directory option)
  710. -d, --directory [dir]\tWrite all completions to the given directory, instead of to ~/.config/fish/completions
  711. -y, --yield-to [dir]\tSkip completions that are already present in the given directory
  712. -m, --manpath\tProcess all man1 files available in the manpath (as determined by manpath)
  713. -p, --progress\tShow progress
  714. """)
  715. if __name__ == "__main__":
  716. script_name = sys.argv[0]
  717. try:
  718. opts, file_paths = getopt.gnu_getopt(sys.argv[1:], 'v:sd:hmpy:z', ['verbose=', 'stdout', 'directory=', 'help', 'manpath', 'progress', 'yield-to='])
  719. except getopt.GetoptError as err:
  720. print(err.msg) # will print something like "option -a not recognized"
  721. usage(script_name)
  722. sys.exit(2)
  723. # If a completion already exists in one of the yield-to directories, then don't overwrite it
  724. # And even delete an existing autogenerated one
  725. yield_to_dirs = []
  726. use_manpath, show_progress, custom_dir = False, False, False
  727. output_directory = ''
  728. for opt, value in opts:
  729. if opt in ('-v', '--verbose'):
  730. VERBOSITY = int(value)
  731. elif opt in ('-s', '--stdout'):
  732. WRITE_TO_STDOUT = True
  733. elif opt in ('-d', '--directory'):
  734. output_directory = value
  735. elif opt in ('-h', '--help'):
  736. usage(script_name)
  737. sys.exit(0)
  738. elif opt in ('-m', '--manpath'):
  739. use_manpath = True
  740. elif opt in ('-p', '--progress'):
  741. show_progress = True
  742. elif opt in ('-y', '--yield-to'):
  743. yield_to_dirs.append(value)
  744. if not os.path.isdir(value):
  745. sys.stderr.write("Warning: yield-to directory does not exist: '{0}'\n".format(value))
  746. elif opt in ('-z'):
  747. DEROFF_ONLY = True
  748. else:
  749. assert False, "unhandled option"
  750. if use_manpath:
  751. # Fetch all man1 files from the manpath
  752. file_paths.extend(get_paths_from_manpath())
  753. if not file_paths:
  754. print("No paths specified")
  755. sys.exit(0)
  756. if not WRITE_TO_STDOUT and not output_directory:
  757. # Default to ~/.config/fish/completions/
  758. # Create it if it doesn't exist
  759. output_directory = os.path.expanduser('~/.config/fish/completions/')
  760. try:
  761. os.makedirs(output_directory)
  762. except OSError as e:
  763. if e.errno != errno.EEXIST:
  764. raise
  765. if True:
  766. parse_and_output_man_pages(file_paths, output_directory, yield_to_dirs, show_progress)
  767. else:
  768. # Profiling code
  769. import cProfile, pstats
  770. cProfile.run('parse_and_output_man_pages(file_paths, output_directory, yield_to_dirs, show_progress)', 'fooprof')
  771. p = pstats.Stats('fooprof')
  772. p.sort_stats('cumulative').print_stats(100)
  773. # Here we can write out all the parser infos
  774. if False:
  775. for name in PARSER_INFO:
  776. print('Parser ' + name + ':')
  777. print('\t' + ', '.join(PARSER_INFO[name]))
  778. print('')