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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  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. "habvSc:f:o:p:s:",
  103. (
  104. "help",
  105. "abs-path",
  106. "basename",
  107. "version",
  108. "silent",
  109. "comment-char="
  110. "file-prefix="
  111. "output-file=",
  112. "prefix=",
  113. "spaces=",
  114. )
  115. )
  116. except getopt.GetoptError as err:
  117. eprint(err.msg)
  118. print("Here's how you use %s:"%(name))
  119. give_help(name,1)
  120. for opt, arg in opts:
  121. if opt in ("-h", "--help"):
  122. give_help(name)
  123. elif opt in ("-a", "--abs-path"):
  124. parameters.abspath = True
  125. elif opt in ("-b", "--basename"):
  126. parameters.basename = True
  127. elif opt in ("-c", "--comment-char"):
  128. parameters.comment_char = arg
  129. elif opt in ("-f", "--file-prefix"):
  130. parameters.file_prefix = arg
  131. elif opt in ("-o", "--output-file"):
  132. parameters.output = arg
  133. elif opt in ("-p", "--prefix"):
  134. parameters.prefix = arg
  135. elif opt in ("-S", "--silent"):
  136. parameters.silent = True
  137. elif opt in ("-s", "--spaces"):
  138. if arg == "none":
  139. parameters.no_spaces = 2
  140. elif arg == "can":
  141. parameters.no_spaces = 1
  142. elif arg == "must":
  143. parameters.no_spaces = 0
  144. else:
  145. eprint(
  146. "Argument to '--spaces' must be one of 'none', "
  147. + "'can', 'must'"
  148. )
  149. quit(3)
  150. elif opt in ("-v", "--version"):
  151. print_version()
  152. parameters.files = other_args# }}}
  153. def print_wrapped(text, indent="\t", moreindent="\t"):# {{{
  154. for i in textwrap.wrap(
  155. text, initial_indent=indent, subsequent_indent=indent+moreindent,
  156. width = maxwd
  157. ):
  158. print(i)# }}}
  159. def print_opt(opt, text, indent="\t", moreindent="\t"):# {{{
  160. print_wrapped(
  161. opt + ":"
  162. + ("\t" if (len(opt)+1 < len("\t".expandtabs())) else " ")
  163. + text,
  164. indent=indent, moreindent=moreindent
  165. )# }}}
  166. def give_help(name,err=0):# {{{
  167. print_wrapped(
  168. name + " -- Some programme to get named entries for listings-ext\n",
  169. indent='', moreindent=' '
  170. )
  171. print_wrapped("Usage: %s [options] <filename> ...\n"%(name), indent='')
  172. print("Options:")
  173. print_opt("-a, --abs-path", "use the absolute filepaths in the output")
  174. print_opt("-b, --basename",
  175. "prefix every identifier with the processed file's basename "
  176. + "followed by a period."
  177. )
  178. print_opt("-c, --comment-char=<regex>",
  179. "use <regex> as the comment starting character. For "
  180. + "instance to match C-style comments this would be '//|/\*'."
  181. )
  182. print_opt("-f, --file-prefix=<string>",
  183. "prefixes every file in the output with this <string>, this is "
  184. + "useful if the script is run from within another directory than "
  185. + "LaTeX."
  186. )
  187. print_opt("-h", "print this help and exit")
  188. print_opt("-o, --output-file=<output filename>",
  189. "if this argument is present, the output "
  190. + "will be written into a file <output filename>; "
  191. + "if <output filename> is an empty string, the output is "
  192. + "redirected into a file with a basename corresponding to the "
  193. + "name of the current directory with the extension '.lst'"
  194. )
  195. print_opt("-p, --prefix=<string>",
  196. "prefix every identifier with this <string>, if both this and the "
  197. + "'--basename' option is used, the prefix is added before the "
  198. + "basename."
  199. )
  200. print_opt("-S, --silent",
  201. "If used the program doesn't report which files to process and "
  202. + "where to write the output to."
  203. )
  204. print_opt("-s, --spaces=<choice>",
  205. "Defines how spaces at the begin of the line are handled. "
  206. + "<choice> must be one of:"
  207. )
  208. print_opt("'must'",
  209. "(default) there must be spaces before the start of the "
  210. + "comment",
  211. indent="\t\t", moreindent=" "
  212. )
  213. print_opt("'can'",
  214. "there can be spaces before the start of the comment",
  215. indent="\t\t", moreindent=" "
  216. )
  217. print_opt("'none'",
  218. "no spaces are allowed before the start of the comment",
  219. indent="\t\t", moreindent=" "
  220. )
  221. print_opt("-v, --version", "Print some version information and exit")
  222. quit(err)# }}}
  223. def compile_regex(comment_char, no_spaces):# {{{
  224. if no_spaces == 0:
  225. spaces = "(?:\s+)"
  226. elif no_spaces == 1:
  227. spaces = "(?:\s*)"
  228. elif no_spaces == 2:
  229. spaces = ""
  230. return re.compile(
  231. "\A" + spaces + "(?:" + comment_char + ") ([abej])e: "
  232. + "(["+tags_allowed+"]+)"
  233. )# }}}
  234. def handle_join(argstr):# {{{
  235. match = join_argsregex.match(argstr)
  236. if match == None: return []
  237. else:
  238. return join_argregex.findall(match.group(0))# }}}
  239. def process_file(fname, parameters):# {{{
  240. fPath = Path(fname)
  241. if not fPath.is_file():
  242. eprint("%% Couldn't find file %s"%(fname))
  243. return
  244. try:
  245. fHandle = fPath.open()
  246. except:
  247. eprint("%% Couldn't open file %s"%(fname))
  248. return
  249. if parameters.comment_char == False:
  250. try:
  251. comment_char = extensiondict[fPath.suffix]
  252. except KeyError:
  253. eprint("%% Unknown file extension of file %s"%(fname))
  254. eprint("% Guessing comments to start with one of:\n%\t",end='')
  255. for i in ('#', '%', '!', ';', '//', '/*', '--'):
  256. eprint("'%s' "%(i),end='')
  257. eprint()
  258. comment_char = "#|%|!|;|//|/\*|--"
  259. else:
  260. comment_char = parameters.comment_char
  261. if parameters.abspath:
  262. fname = PurePosixPath(fPath.absolute()).as_posix()
  263. if os_name == "nt": fname = fname.replace("\\", "", 1)
  264. elif os_name == "nt":
  265. fname = PurePosixPath(fPath).as_posix()
  266. regprog = compile_regex(comment_char, parameters.no_spaces)
  267. cp = Object()
  268. cp.a = set()
  269. cp.j = {}
  270. cp.be = []
  271. be = Object()
  272. be.name = None
  273. be.start = None
  274. line = Object()
  275. line.str = ""
  276. line.nr = 0
  277. while True: # read the file {{{
  278. line.nr += 1
  279. line.str = fHandle.readline()
  280. if line.str == "": break
  281. match = regprog.match(line.str)
  282. if match == None: continue
  283. if match.group(1) == 'a':
  284. cp.a.add(match.group(2))
  285. elif match.group(1) == 'b':
  286. if be.name == None:
  287. be.name = match.group(2)
  288. be.start = line.nr
  289. else:
  290. eprint("Nested code blocks are not supported!")
  291. quit(4)
  292. elif match.group(1) == 'e':
  293. if match.group(2) == be.name:
  294. cp.be += ((be.start, line.nr, be.name),)
  295. be.name = None
  296. else:
  297. eprint("Code block '%s' ended by '%s'"%(be.name,
  298. match.group(2)))
  299. quit(5)
  300. elif match.group(1) == 'j':
  301. argstr = line.str[len(match.group(0)):]
  302. joining = handle_join(argstr)
  303. for j in joining:
  304. if j in cp.j: cp.j[j].add(match.group(2))
  305. else: cp.j[j] = set((match.group(2),))
  306. #}}}
  307. fHandle.close()
  308. be = {}
  309. ba = []
  310. bj = {}
  311. for start, end, name in cp.be:
  312. if start + 1 < end:
  313. lines = "%d-%d"%(start + 1, end - 1)
  314. if name in be: be[name] += ",%s"%(lines)
  315. else: be[name] = lines
  316. if name in cp.j:
  317. for n in cp.j[name]:
  318. if n in bj: bj[n] += ",%s"%(lines)
  319. else: bj[n] = lines
  320. if any(cp.a): ba += (lines,)
  321. if any(cp.a):
  322. ba = [ (a, ','.join(ba)) for a in cp.a ]
  323. bj = [ (name, lines) for name, lines in bj.items() ]
  324. be = [ (name, lines) for name, lines in be.items() ]
  325. return (fname, ba + bj + be)# }}}
  326. def write_code_point(f, code_point, out, p, basename, fp):# {{{
  327. b = Path(f).stem + "." if basename else ""
  328. out.write(
  329. "\\lstdef{" + p + b + code_point[0] + "}"
  330. + "{" + fp + f + "}"
  331. + "{" + code_point[1] + "}\n"
  332. )# }}}
  333. def main(name,args):# {{{
  334. parameters = Object()
  335. parameters.files = []
  336. parameters.output = False
  337. parameters.abspath = False
  338. parameters.silent = False
  339. parameters.comment_char = False
  340. parameters.basename = False
  341. parameters.no_spaces = 0
  342. parameters.prefix = ""
  343. parameters.file_prefix = ""
  344. arg_parse(name,args,parameters)
  345. if len(parameters.files) == 0:
  346. eprint("No input files given")
  347. print("Here's how you use %s:"%(name))
  348. give_help(name,2)
  349. needs_close = True
  350. if parameters.output == False:
  351. out = stdout
  352. needs_close = False
  353. written_to = "stdout"
  354. elif parameters.output == "":
  355. written_to = getcwd().split('/')[-1] + ".lst"
  356. try:
  357. out = open(written_to, "w")
  358. except:
  359. eprint("Couldn't open %s for writing"%(written_to))
  360. quit(1)
  361. else:
  362. written_to = parameters.output
  363. try:
  364. out = open(written_to, "w")
  365. except:
  366. eprint("Couldn't open %s for writing"%(written_to))
  367. quit(1)
  368. if not parameters.silent:
  369. print("% Files you want me to process:")
  370. for i in parameters.files:
  371. print("%%\t%s"%(i))
  372. print("%% Output is written to: %s"%(written_to))
  373. code_points = [
  374. process_file(f, parameters) or ("",[])
  375. for f in parameters.files
  376. ]
  377. for f, cc in code_points:
  378. for c in cc:
  379. write_code_point(f, c, out, parameters.prefix, parameters.basename,
  380. parameters.file_prefix)
  381. if needs_close: out.close()# }}}
  382. if __name__ == "__main__":
  383. main(argv[0],argv[1:])