Skip to content

Instantly share code, notes, and snippets.

@john-tornblom
Created April 25, 2021 13:22
Show Gist options
  • Save john-tornblom/5a9db5a3ea7b34890858517b3be86b5e to your computer and use it in GitHub Desktop.
Save john-tornblom/5a9db5a3ea7b34890858517b3be86b5e to your computer and use it in GitHub Desktop.
Replace literal strings in source code with gettext macro calls
#!/usr/bin/env python3
# encoding: utf-8
# Copyright (C) 2021 John Törnblom
"""
Replace literal strings in source code with gettext macro calls.
Requires python3 with clang bindings. In ubuntu:
sudo apt-get install python3-clang
"""
import ctypes.util
import fileinput
import glob
import os
from dataclasses import dataclass
from clang import cindex
from clang.cindex import Index, Config, CursorKind
for libname in ['clang', 'clang-9', 'clang-10']:
filename = ctypes.util.find_library(libname)
if filename:
Config.set_library_file(filename)
break
@dataclass(frozen=True)
class StringDescription:
offset : int
length : int
local : bool
def is_scope(kind):
return kind in (CursorKind.FUNCTION_DECL, CursorKind.CXX_METHOD,
CursorKind.CONSTRUCTOR, CursorKind.DESTRUCTOR,
CursorKind.CONVERSION_FUNCTION)
class StringTracking:
current_scope = None
filename = None
strings = None
def __init__(self, filename):
self.filename = os.path.abspath(filename)
self.current_scope = 0
self.strings = set()
def analyze(self, node):
if not node.location.file:
return
filename = os.path.abspath(node.location.file.name)
if filename != self.filename:
return
if is_scope(node.kind):
self.current_scope += 1
for child in node.get_children():
self.analyze(child)
if is_scope(node.kind):
self.current_scope -= 1
if node.kind != CursorKind.STRING_LITERAL:
return
sd = StringDescription(node.location.offset,
len(node.spelling),
self.current_scope > 0)
self.strings.add(sd)
def patch_strings(filename, strings):
with open(filename, 'rb') as f:
code = list(f.read())
for s in sorted(strings, key=lambda s: -s.offset):
pos = s.offset
# skip macros that expands to string literals
if code[pos] != ord('"'):
continue
if not s.local:
code.insert(pos, ord('N'))
pos += 1
code.insert(pos, ord('_'))
else:
code.insert(pos, ord('_'))
code.insert(pos+1, ord('('))
code.insert(pos+1+s.length+1, ord(')'))
with open(filename, 'wb') as f:
f.write(bytearray(code))
def main():
cdb = cindex.CompilationDatabase.fromDirectory('build')
for cmd in cdb.getAllCompileCommands():
if cmd.filename.find('Source') < 0: continue
index = Index.create()
tu = index.parse(None, list(cmd.arguments)).cursor
st = StringTracking(cmd.filename)
for child in tu.get_children():
st.analyze(child)
if st.strings:
print(cmd.filename)
patch_strings(cmd.filename, st.strings)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment