lint-shell.py raw

   1  #!/usr/bin/env python3
   2  #
   3  # Copyright (c) 2018-present The Bitcoin Core developers
   4  # Distributed under the MIT software license, see the accompanying
   5  # file COPYING or http://www.opensource.org/licenses/mit-license.php.
   6  
   7  """
   8  Check for shellcheck warnings in shell scripts.
   9  """
  10  
  11  import subprocess
  12  import re
  13  import sys
  14  
  15  # Disabled warnings:
  16  DISABLED = [
  17      'SC2162', # read without -r will mangle backslashes.
  18  ]
  19  
  20  def check_shellcheck_install():
  21      try:
  22          subprocess.run(['shellcheck', '--version'], stdout=subprocess.DEVNULL, check=True)
  23      except FileNotFoundError:
  24          print('Skipping shell linting since shellcheck is not installed.')
  25          sys.exit(0)
  26  
  27  def get_files(command):
  28      output = subprocess.run(command, stdout=subprocess.PIPE, text=True)
  29      files = output.stdout.split('\n')
  30  
  31      # remove whitespace element
  32      files = list(filter(None, files))
  33      return files
  34  
  35  def main():
  36      check_shellcheck_install()
  37  
  38      # build the `exclude` flag
  39      exclude = '--exclude=' + ','.join(DISABLED)
  40  
  41      # build the `sourced files` list
  42      sourced_files_cmd = [
  43          'git',
  44          'grep',
  45          '-El',
  46          r'^# shellcheck shell=',
  47      ]
  48      sourced_files = get_files(sourced_files_cmd)
  49  
  50      # build the `guix files` list
  51      guix_files_cmd = [
  52          'git',
  53          'grep',
  54          '-El',
  55          r'^#!\/usr\/bin\/env bash',
  56          '--',
  57          'contrib/guix',
  58          'contrib/shell',
  59      ]
  60      guix_files = get_files(guix_files_cmd)
  61  
  62      # build the other script files list
  63      files_cmd = [
  64          'git',
  65          'ls-files',
  66          '--',
  67          '*.sh',
  68      ]
  69      files = get_files(files_cmd)
  70      reg = re.compile(r'src/[leveldb,secp256k1,minisketch]')
  71  
  72      def should_exclude(fname: str) -> bool:
  73          return bool(reg.match(fname))
  74  
  75      # remove everything that doesn't match this regex
  76      files[:] = [file for file in files if not should_exclude(file)]
  77  
  78      # build the `shellcheck` command
  79      shellcheck_cmd = [
  80          'shellcheck',
  81          '--external-sources',
  82          '--check-sourced',
  83          '--source-path=SCRIPTDIR',
  84      ]
  85      shellcheck_cmd.append(exclude)
  86      shellcheck_cmd.extend(sourced_files)
  87      shellcheck_cmd.extend(guix_files)
  88      shellcheck_cmd.extend(files)
  89  
  90      # run the `shellcheck` command
  91      try:
  92          subprocess.check_call(shellcheck_cmd)
  93      except subprocess.CalledProcessError:
  94          sys.exit(1)
  95  
  96  if __name__ == '__main__':
  97      main()
  98