You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

listings-extx.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. #!/bin/python3
  2. ## listings-extx.py -- Some program to get named entries for listings-ext
  3. ## Author: Jonathan P. Spratte <j.spratte(at)yahoo.de>
  4. ## Version: 1.0.1
  5. ## Keywords: LaTeX, listings
  6. ## Copyright (C) 2019 Jonathan P. Spratte <j.spratte(at)yahoo.de>
  7. ##-------------------------------------------------------------------
  8. ##
  9. ## This program is free software: you can redistribute it and/or modify
  10. ## it under the terms of the GNU General Public License as published by
  11. ## the Free Software Foundation, either version 3 of the License, or
  12. ## (at your option) any later version.
  13. ##
  14. ## This program is distributed in the hope that it will be useful,
  15. ## but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ## GNU General Public License for more details.
  18. ##
  19. ## You should have received a copy of the GNU General Public License
  20. ## along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. import getopt, textwrap, re
  22. from os import getcwd
  23. from os import name as os_name
  24. from sys import argv, stdout, stderr
  25. from pathlib import Path, PurePosixPath
  26. from shutil import get_terminal_size
  27. extensiondict = {# {{{
  28. ".c" : "/\*|//",
  29. ".cc" : "/\*|//",
  30. ".cpp" : "/\*|//",
  31. ".cxx" : "/\*|//",
  32. ".py" : "#",
  33. ".tex" : "%",
  34. ".sh" : "#",
  35. ".cs" : "//",
  36. ".m" : "/\*|%",
  37. ".mm" : "/\*",
  38. ".java" : "//|/\*",
  39. ".jav" : "//|/\*",
  40. ".j" : "//|/\*",
  41. ".php" : "//",
  42. ".css" : "/\*",
  43. ".html" : "<!--",
  44. ".htm" : "<!--",
  45. ".xhtml" : "<!--",
  46. ".jhtml" : "<!--",
  47. ".xml" : "<!--",
  48. ".rb" : "#",
  49. ".rbw" : "#",
  50. ".go" : "//",
  51. ".js" : "//",
  52. ".lua" : "--",
  53. ".pl" : "#",
  54. ".r" : "#",
  55. ".rs" : "//|/\*",
  56. ".scpt" : "--",
  57. ".sql" : "--",
  58. ".for" : "!",
  59. ".ftn" : "!",
  60. ".f" : "!",
  61. ".f77" : "!",
  62. ".f90" : "!",
  63. ".erl" : "%",
  64. ".coffee" : "#",
  65. ".clj" : ";",
  66. ".swift" : "//",
  67. ".scala" : "//|/\*",
  68. ".pas" : "{\*|{",
  69. ".pp" : "{\*|{",
  70. ".p" : "{\*|{",
  71. }# }}}
  72. tags_allowed = "\w:"
  73. join_argsregex = re.compile("\A(?:\s*=)?["+tags_allowed+" ]+")
  74. join_argregex = re.compile("["+tags_allowed+"]+")
  75. maxwd = min(80, get_terminal_size(fallback=(70,24))[0])
  76. def eprint(*args, **kwargs):# {{{
  77. print(*args, file=stderr, **kwargs)# }}}
  78. class Object(object): # hacky way to have a nice parameters structure {{{
  79. pass# }}}
  80. def print_version():# {{{
  81. print("listings-extx.py v1.0.1")
  82. print("Copyright (C) 2019 Jonathan P. Spratte")
  83. print_wrapped(
  84. "License GPLv3+: GNU GPL version 3 or later "
  85. + "<https://gnu.org/licenses/gpl.html>.",
  86. indent='', moreindent=''
  87. )
  88. print_wrapped(
  89. "This is free software: you are free to change and redistribute "
  90. + "it.",
  91. indent='', moreindent=''
  92. )
  93. print_wrapped(
  94. "There is NO WARRANTY, to the extent permitted by law.",
  95. indent='', moreindent=''
  96. )
  97. quit()# }}}
  98. def arg_parse(name,argv,parameters):# {{{
  99. try:
  100. opts, other_args = getopt.gnu_getopt(
  101. argv,
  102. "habvSs:o:c:",
  103. (
  104. "help",
  105. "abs-path",
  106. "basename",
  107. "version",
  108. "silent",
  109. "spaces=",
  110. "output-file=",
  111. "comment-char="
  112. )
  113. )
  114. except getopt.GetoptError as err:
  115. eprint(err.msg)
  116. print("Here's how you use %s:"%(name))
  117. give_help(name,1)
  118. for opt, arg in opts:
  119. if opt in ("-h", "--help"):
  120. give_help(name)
  121. elif opt in ("-a", "--abs-path"):
  122. parameters.abspath = True
  123. elif opt in ("-b", "--basename"):
  124. parameters.basename = True
  125. elif opt in ("-c", "--comment-char"):
  126. parameters.comment_char = arg
  127. elif opt in ("-o", "--output-file"):
  128. parameters.output = arg
  129. elif opt in ("-p", "--prefix"):
  130. parameters.prefix = arg
  131. elif opt in ("-S", "--silent"):
  132. parameters.silent = True
  133. elif opt in ("-s", "--spaces"):
  134. if arg == "none":
  135. parameters.no_spaces = 2
  136. elif arg == "can":
  137. parameters.no_spaces = 1
  138. elif arg == "must":
  139. parameters.no_spaces = 0
  140. else:
  141. eprint(
  142. "Argument to '--spaces' must be one of 'none', "
  143. + "'can', 'must'"
  144. )
  145. quit(3)
  146. elif opt in ("-v", "--version"):
  147. print_version()
  148. parameters.files = other_args# }}}
  149. def print_wrapped(text, indent="\t", moreindent="\t"):# {{{
  150. for i in textwrap.wrap(
  151. text, initial_indent=indent, subsequent_indent=indent+moreindent,
  152. width = maxwd
  153. ):
  154. print(i)# }}}
  155. def print_opt(opt, text, indent="\t", moreindent="\t"):# {{{
  156. print_wrapped(
  157. opt + ":"
  158. + ("\t" if (len(opt)+1 < len("\t".expandtabs())) else " ")
  159. + text,
  160. indent=indent, moreindent=moreindent
  161. )# }}}
  162. def give_help(name,err=0):# {{{
  163. print_wrapped(
  164. name + " -- Some programme to get named entries for listings-ext\n",
  165. indent='', moreindent=' '
  166. )
  167. print_wrapped("Usage: %s [options] <filename> ...\n"%(name), indent='')
  168. print("Options:")
  169. print_opt("-a, --abs-path", "use the absolute filepaths in the output")
  170. print_opt("-b, --basename",
  171. "prefix every identifier with the processed file's basename "
  172. + "followed by a period."
  173. )
  174. print_opt("-c, --comment-char=<regex>",
  175. "use <regex> as the comment starting character. For "
  176. + "instance to match C-style comments this would be '//|/\*'."
  177. )
  178. print_opt("-h", "print this help and exit")
  179. print_opt("-o, --output-file=<output filename>",
  180. "if this argument is present, the output "
  181. + "will be written into a file <output filename>; "
  182. + "if <output filename> is an empty string, the output is "
  183. + "redirected into a file with a basename corresponding to the "
  184. + "name of the current directory with the extension '.lst'"
  185. )
  186. print_opt("-p, --prefix=<string>",
  187. "prefix every identifier with this <string>, if both this and the "
  188. + "'--basename' option is used, the prefix is added before the "
  189. + "basename."
  190. )
  191. print_opt("-S, --silent",
  192. "If used the program doesn't report which files to process and "
  193. + "where to write the output to."
  194. )
  195. print_opt("-s, --spaces=<choice>",
  196. "Defines how spaces at the begin of the line are handled. "
  197. + "<choice> must be one of:"
  198. )
  199. print_opt("'must'",
  200. "(default) there must be spaces before the start of the "
  201. + "comment",
  202. indent="\t\t", moreindent=" "
  203. )
  204. print_opt("'can'",
  205. "there can be spaces before the start of the comment",
  206. indent="\t\t", moreindent=" "
  207. )
  208. print_opt("'none'",
  209. "no spaces are allowed before the start of the comment",
  210. indent="\t\t", moreindent=" "
  211. )
  212. print_opt("-v, --version", "Print some version information and exit")
  213. quit(err)# }}}
  214. def compile_regex(comment_char, no_spaces):# {{{
  215. if no_spaces == 0:
  216. spaces = "(?:\s+)"
  217. elif no_spaces == 1:
  218. spaces = "(?:\s*)"
  219. elif no_spaces == 2:
  220. spaces = ""
  221. return re.compile(
  222. "\A" + spaces + "(?:" + comment_char + ") ([abej])e: "
  223. + "(["+tags_allowed+"]+)"
  224. )# }}}
  225. def handle_join(argstr):# {{{
  226. match = join_argsregex.match(argstr)
  227. if match == None: return []
  228. else:
  229. return join_argregex.findall(match.group(0))# }}}
  230. def process_file(fname, parameters):# {{{
  231. fPath = Path(fname)
  232. if not fPath.is_file():
  233. eprint("%% Couldn't find file %s"%(fname))
  234. return
  235. try:
  236. fHandle = fPath.open()
  237. except:
  238. eprint("%% Couldn't open file %s"%(fname))
  239. return
  240. if parameters.comment_char == False:
  241. try:
  242. comment_char = extensiondict[fPath.suffix]
  243. except KeyError:
  244. eprint("%% Unknown file extension of file %s"%(fname))
  245. eprint("% Guessing comments to start with one of:\n%\t",end='')
  246. for i in ('#', '%', '!', ';', '//', '/*', '--'):
  247. eprint("'%s' "%(i),end='')
  248. eprint()
  249. comment_char = "#|%|!|;|//|/\*|--"
  250. else:
  251. comment_char = parameters.comment_char
  252. if parameters.abspath:
  253. fname = PurePosixPath(fPath.absolute()).as_posix()
  254. if os_name == "nt": fname = fname.replace("\\", "", 1)
  255. elif os_name == "nt":
  256. fname = PurePosixPath(fPath).as_posix()
  257. regprog = compile_regex(comment_char, parameters.no_spaces)
  258. cp = Object()
  259. cp.a = set()
  260. cp.j = {}
  261. cp.be = []
  262. be = Object()
  263. be.name = None
  264. be.start = None
  265. line = Object()
  266. line.str = ""
  267. line.nr = 0
  268. while True: # read the file {{{
  269. line.nr += 1
  270. line.str = fHandle.readline()
  271. if line.str == "": break
  272. match = regprog.match(line.str)
  273. if match == None: continue
  274. if match.group(1) == 'a':
  275. cp.a.add(match.group(2))
  276. elif match.group(1) == 'b':
  277. if be.name == None:
  278. be.name = match.group(2)
  279. be.start = line.nr
  280. else:
  281. eprint("Nested code blocks are not supported!")
  282. quit(4)
  283. elif match.group(1) == 'e':
  284. if match.group(2) == be.name:
  285. cp.be += ((be.start, line.nr, be.name),)
  286. be.name = None
  287. else:
  288. eprint("Code block '%s' ended by '%s'"%(be.name,
  289. match.group(2)))
  290. quit(5)
  291. elif match.group(1) == 'j':
  292. argstr = line.str[len(match.group(0)):]
  293. joining = handle_join(argstr)
  294. for j in joining:
  295. if j in cp.j: cp.j[j].add(match.group(2))
  296. else: cp.j[j] = set((match.group(2),))
  297. #}}}
  298. fHandle.close()
  299. be = {}
  300. ba = []
  301. bj = {}
  302. for start, end, name in cp.be:
  303. if start + 1 < end:
  304. lines = "%d-%d"%(start + 1, end - 1)
  305. if name in be: be[name] += ",%s"%(lines)
  306. else: be[name] = lines
  307. if name in cp.j:
  308. for n in cp.j[name]:
  309. if n in bj: bj[n] += ",%s"%(lines)
  310. else: bj[n] = lines
  311. if any(cp.a): ba += (lines,)
  312. if any(cp.a):
  313. ba = [ (a, ','.join(ba)) for a in cp.a ]
  314. bj = [ (name, lines) for name, lines in bj.items() ]
  315. be = [ (name, lines) for name, lines in be.items() ]
  316. return (fname, ba + bj + be)# }}}
  317. def write_code_point(f, code_point, out, prefix, basename):# {{{
  318. b = Path(f).stem + "." if basename else ""
  319. out.write("\\lstdef{"+p+b+code_point[0]+"}{"+f+"}{"+code_point[1]+"}\n")# }}}
  320. def main(name,args):# {{{
  321. parameters = Object()
  322. parameters.files = []
  323. parameters.output = False
  324. parameters.abspath = False
  325. parameters.silent = False
  326. parameters.comment_char = False
  327. parameters.basename = False
  328. parameters.no_spaces = 0
  329. parameters.prefix = ""
  330. arg_parse(name,args,parameters)
  331. if len(parameters.files) == 0:
  332. eprint("No input files given")
  333. print("Here's how you use %s:"%(name))
  334. give_help(name,2)
  335. needs_close = True
  336. if parameters.output == False:
  337. out = stdout
  338. needs_close = False
  339. written_to = "stdout"
  340. elif parameters.output == "":
  341. written_to = getcwd().split('/')[-1] + ".lst"
  342. try:
  343. out = open(written_to, "w")
  344. except:
  345. eprint("Couldn't open %s for writing"%(written_to))
  346. quit(1)
  347. else:
  348. written_to = parameters.output
  349. try:
  350. out = open(written_to, "w")
  351. except:
  352. eprint("Couldn't open %s for writing"%(written_to))
  353. quit(1)
  354. if not parameters.silent:
  355. print("% Files you want me to process:")
  356. for i in parameters.files:
  357. print("%%\t%s"%(i))
  358. print("%% Output is written to: %s"%(written_to))
  359. code_points = [
  360. process_file(f, parameters) or ("",[])
  361. for f in parameters.files
  362. ]
  363. if not needs_close: print()
  364. for f, cc in code_points:
  365. for c in cc:
  366. write_code_point(f, c, out, parameters.prefix, parameters.basename)
  367. if needs_close: out.close()# }}}
  368. if __name__ == "__main__":
  369. main(argv[0],argv[1:])