[PATCH 11/11] pragma once: conversion script (in Python 2)

From: Alexey Dobriyan
Date: Tue Feb 09 2021 - 06:47:34 EST


Script accepts list of files to be converted from the command line,
strips include guard if any and inserts "#pragma once" directive in
the beginning.

The following patterns are recognised:

#ifndef FOO_H #ifndef FOO_H
#define FOO_H #ifndef FOO_H 1

#endif
#endif // comment
#endif /* one line comment */

This is how almost all include guards look like.

Scripts doesn't pretend to be a compiler. For example, comments inside
preprocessor directive aren't recognised because people don't write code
like this:

# /*
* legal C
*/ def\
ine FOO /*
* no, we don't care
*/

Trailing multiline comments aren't recognised as well.

Script can cut through SPDX comments:

/* SPDX-License-Identifier: xxx
* <=== pragma once will be inserted here
* Copyright ...
*/

In other words, the script is simple but not too simple.

It cowardly exits and doesn't do anything as a safety measure in case of
an existing pragma once directive, missing/broken include guard or a bug.
Running it second time shouldn't do anything.

Signed-off-by: Alexey Dobriyan <adobriyan@xxxxxxxxx>
---
scripts/pragma-once.py | 159 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 159 insertions(+)
create mode 100755 scripts/pragma-once.py

diff --git a/scripts/pragma-once.py b/scripts/pragma-once.py
new file mode 100755
index 000000000000..7c8a274aad28
--- /dev/null
+++ b/scripts/pragma-once.py
@@ -0,0 +1,159 @@
+#!/usr/bin/python2
+# Copyright (c) 2021 Alexey Dobriyan <adobriyan@xxxxxxxxx>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+# Change include guard to "#pragma once" directive in place.
+import os
+import re
+import sys
+
+def read_file(filename):
+ with open(filename) as f:
+ return f.read()
+
+def write_file(filename, buf):
+ tmp = '%s.pragma-once' % filename
+ with open(tmp, 'w') as f:
+ f.write(buf)
+ os.rename(tmp, filename)
+
+def ws(c):
+ return c == ' ' or c == '\t' or c == '\n'
+
+re_ifndef = re.compile('#[ \t]*ifndef[ \t]+([A-Za-z_][A-Za-z0-9_]*)\n')
+re_define = re.compile('#[ \t]*define[ \t]+([A-Za-z_][A-Za-z0-9_]*)([ \t]+1)?\n')
+re_endif1 = re.compile('#[ \t]*endif[ \t]*//')
+re_endif2 = re.compile('#[ \t]*endif[ \t]*/\*')
+
+def pragma_once(c):
+ i = 0
+
+ # skip leading whitespace and comments
+ while i < len(c):
+ if ws(c[i]):
+ i += 1
+ elif c[i] == '/' and c[i + 1] == '*':
+ i = c.index('*/', i + 2) + 2
+ elif c[i] == '/' and c[i + 1] == '/':
+ i = c.index('\n', i + 2) + 1
+ else:
+ break;
+
+ # find #ifndef
+ ifndef_start = i
+ match = re_ifndef.match(c, i)
+ guard = match.group(1)
+ i = match.end()
+
+ # find #define
+ match = re_define.match(c, i)
+ if match.group(1) != guard:
+ raise
+ i = match.end()
+
+ while ws(c[i]):
+ i += 1
+
+ define_end = i
+
+ # trim whitespace before #ifndef
+ i = ifndef_start
+ while i > 0 and ws(c[i - 1]):
+ i -= 1
+ if c[i] == '\n':
+ i += 1
+ ifndef_start = i
+
+ #print repr(c[ifndef_start:define_end])
+
+ # find #endif
+ i = c.rindex('\n#endif', i) + 1
+ endif_start = i
+
+ match = None
+ if match is None:
+ match = re_endif1.match(c, i)
+ if match:
+ try:
+ i = c.index('\n', match.end()) + 1
+ except ValueError:
+ i = len(c)
+
+ if match is None:
+ match = re_endif2.match(c, i)
+ if match:
+ try:
+ i = c.index('*/', match.end()) + 2
+ except ValueError:
+ i = len(c)
+
+ if match is None:
+ i = endif_start + len('#endif')
+
+ while i < len(c) and ws(c[i]):
+ i += 1
+ if i != len(c):
+ raise
+
+ endif_end = i
+
+ # trim whitespace before #endif
+ i = endif_start
+ while i > 0 and ws(c[i - 1]):
+ i -= 1
+ if c[i] == '\n':
+ i += 1
+ endif_start = i
+
+ #print repr(c[endif_start:endif_end])
+
+ if define_end > endif_start:
+ raise
+
+ spdx_end = None
+ pragma_once = '#pragma once\n'
+ cut_comment = False
+ if c.startswith('/* SPDX'):
+ spdx_end = c.index('\n') + 1
+ if not (c[spdx_end - 3] == '*' and c[spdx_end - 2] == '/'):
+ cut_comment = True
+ elif c.startswith('// SPDX') or c.startswith('//SPDX'):
+ spdx_end = c.index('\n') + 1
+
+ if spdx_end is None:
+ l = [pragma_once, c[0:ifndef_start]]
+ elif cut_comment:
+ l = [c[0:spdx_end - 1], ' */\n', pragma_once, '/*\n', c[spdx_end:ifndef_start]]
+ else:
+ l = [c[0:spdx_end], pragma_once, c[spdx_end:ifndef_start]]
+
+ l.append(c[define_end:endif_start])
+ l.append(c[endif_end:])
+ return ''.join(l)
+
+def main():
+ for filename in sys.argv[1:]:
+ s = ''
+ try:
+ buf = read_file(filename)
+ if buf.find('#pragma once') == -1:
+ write_file(filename, pragma_once(buf))
+ else:
+ s = 'skip '
+ except:
+ pass
+ print '#pragma once: %s%s' % (s, filename)
+
+if __name__ == '__main__':
+ main()
--
2.29.2