@@ -321,5 +321,119 @@ def roles(session: nox.Session):
321321 session .run ("molecule" , "-vv" , "test" , "--scenario-name" , scenario , external = True , env = env )
322322
323323
324+ @nox .session (reuse_venv = True , default = False )
325+ def regen_shellcheck_ignores (session : nox .Session ):
326+ """Regenerate shellcheck ignore entries in tests/sanity/ignore-*.txt."""
327+ import html
328+ import re
329+ import subprocess
330+ import urllib .request
331+ from html .parser import HTMLParser
332+
333+ sanity_dir = Path ("tests" ) / "sanity"
334+ if not sanity_dir .is_dir ():
335+ session .error ("Run this from the top directory of the project" )
336+
337+ class TitleParser (HTMLParser ):
338+ def __init__ (self , body : str ) -> None :
339+ super ().__init__ ()
340+ self .in_title = False
341+ self .title_parts = []
342+ self .feed (body )
343+
344+ def handle_starttag (self , tag , attrs ):
345+ if tag .lower () == "title" :
346+ self .in_title = True
347+
348+ def handle_endtag (self , tag ):
349+ if tag .lower () == "title" :
350+ self .in_title = False
351+
352+ def handle_data (self , data ):
353+ if self .in_title :
354+ self .title_parts .append (data )
355+
356+ @property
357+ def title (self ) -> str :
358+ return html .unescape ("" .join (self .title_parts ).strip ())
359+
360+ sc_desc_cache = {}
361+
362+ def retrieve_sc_description (sc_code : str ) -> str :
363+ if sc_code in sc_desc_cache :
364+ return sc_desc_cache [sc_code ]
365+ url = f"https://www.shellcheck.net/wiki/{ sc_code } "
366+ try :
367+ with urllib .request .urlopen (url , timeout = 10 ) as resp :
368+ charset = resp .headers .get_content_charset () or "utf-8"
369+ body = resp .read ().decode (charset , errors = "replace" )
370+ except Exception as e :
371+ sc_desc_cache [sc_code ] = f"(failed to fetch: { e } )"
372+ return sc_desc_cache [sc_code ]
373+ parser = TitleParser (body )
374+ title = parser .title .replace (f"ShellCheck: { sc_code } – " , "" ).rstrip ("." )
375+ sc_desc_cache [sc_code ] = f"{ title } - { url } " if title else f"(no title found at { url } )"
376+ return sc_desc_cache [sc_code ]
377+
378+ ig_version_re = re .compile (r".*/ignore-(?P<version>\d\.\d+)\.txt" )
379+
380+ def get_version (filename ):
381+ if not (match := ig_version_re .search (str (filename ))):
382+ raise ValueError (f"ignore filename not recognized: { filename } " )
383+ version = match .group ("version" )
384+ return f"ac{ version .replace ('.' , '' )} " , version
385+
386+ ignore_files = sorted (sanity_dir .glob ("ignore-*.txt" ))
387+ tox_targets = [get_version (f )[0 ] for f in ignore_files [:- 1 ]] + ["dev" ]
388+ ignore_versions = [get_version (f )[1 ] for f in ignore_files ]
389+
390+ for tox_target , ignore_version in zip (tox_targets , ignore_versions , strict = True ):
391+ ignore_file = sanity_dir / f"ignore-{ ignore_version } .txt"
392+ session .log (f"Processing { ignore_file } ({ tox_target } /{ ignore_version } )" )
393+
394+ with ignore_file .open ("r" , encoding = "utf-8" ) as f :
395+ kept = [ll for ll in f if "shellcheck" not in ll ]
396+ with ignore_file .open ("w" , encoding = "utf-8" ) as f :
397+ f .writelines (kept )
398+
399+ proc = subprocess .run (
400+ [
401+ "andebox" ,
402+ "tox-test" ,
403+ "-e" ,
404+ tox_target ,
405+ "--" ,
406+ "sanity" ,
407+ "--python" ,
408+ "default" ,
409+ "--docker" ,
410+ "default" ,
411+ "--test" ,
412+ "shellcheck" ,
413+ ],
414+ stdout = subprocess .PIPE ,
415+ stderr = subprocess .STDOUT ,
416+ text = True ,
417+ check = False ,
418+ )
419+
420+ found = set ()
421+ for line in proc .stdout .splitlines ():
422+ if not line .startswith ("ERROR" ):
423+ continue
424+ parts = line .split (":" , 5 )
425+ if "/" not in parts [1 ]:
426+ continue
427+ path , sc_code = parts [1 ].strip (), parts [4 ].strip ()
428+ desc = retrieve_sc_description (sc_code )
429+ found .add (f"{ path } shellcheck:{ sc_code } # { desc } " )
430+
431+ if found :
432+ with ignore_file .open ("a" , encoding = "utf-8" ) as f :
433+ f .writelines (f"{ ll } \n " for ll in sorted (found ))
434+
435+ session .log (f"Added { len (found )} shellcheck ignore entries to { ignore_file } " )
436+
437+
324438if __name__ == "__main__" :
325439 nox .main ()
0 commit comments