1#!/usr/bin/env python3
2#
3# Change prefixes in files/filenames. Useful for creating different versions
4# of a codebase that don't conflict at compile time.
5#
6# Example:
7# $ ./scripts/changeprefix.py lfs lfs3
8#
9# Copyright (c) 2022, The littlefs authors.
10# Copyright (c) 2019, Arm Limited. All rights reserved.
11# SPDX-License-Identifier: BSD-3-Clause
12#
13
14import glob
15import itertools
16import os
17import os.path
18import re
19import shlex
20import shutil
21import subprocess
22import tempfile
23
24GIT_PATH = ['git']
25
26
27def openio(path, mode='r', buffering=-1):
28    # allow '-' for stdin/stdout
29    if path == '-':
30        if mode == 'r':
31            return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering)
32        else:
33            return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering)
34    else:
35        return open(path, mode, buffering)
36
37def changeprefix(from_prefix, to_prefix, line):
38    line, count1 = re.subn(
39        '\\b'+from_prefix,
40        to_prefix,
41        line)
42    line, count2 = re.subn(
43        '\\b'+from_prefix.upper(),
44        to_prefix.upper(),
45        line)
46    line, count3 = re.subn(
47        '\\B-D'+from_prefix.upper(),
48        '-D'+to_prefix.upper(),
49        line)
50    return line, count1+count2+count3
51
52def changefile(from_prefix, to_prefix, from_path, to_path, *,
53        no_replacements=False):
54    # rename any prefixes in file
55    count = 0
56
57    # create a temporary file to avoid overwriting ourself
58    if from_path == to_path and to_path != '-':
59        to_path_temp = tempfile.NamedTemporaryFile('w', delete=False)
60        to_path = to_path_temp.name
61    else:
62        to_path_temp = None
63
64    with openio(from_path) as from_f:
65        with openio(to_path, 'w') as to_f:
66            for line in from_f:
67                if not no_replacements:
68                    line, n = changeprefix(from_prefix, to_prefix, line)
69                    count += n
70                to_f.write(line)
71
72    if from_path != '-' and to_path != '-':
73        shutil.copystat(from_path, to_path)
74
75    if to_path_temp:
76        os.rename(to_path, from_path)
77    elif from_path != '-':
78        os.remove(from_path)
79
80    # Summary
81    print('%s: %d replacements' % (
82        '%s -> %s' % (from_path, to_path) if not to_path_temp else from_path,
83        count))
84
85def main(from_prefix, to_prefix, paths=[], *,
86        verbose=False,
87        output=None,
88        no_replacements=False,
89        no_renames=False,
90        git=False,
91        no_stage=False,
92        git_path=GIT_PATH):
93    if not paths:
94        if git:
95            cmd = git_path + ['ls-tree', '-r', '--name-only', 'HEAD']
96            if verbose:
97                print(' '.join(shlex.quote(c) for c in cmd))
98            paths = subprocess.check_output(cmd, encoding='utf8').split()
99        else:
100            print('no paths?', file=sys.stderr)
101            sys.exit(1)
102
103    for from_path in paths:
104        # rename filename?
105        if output:
106            to_path = output
107        elif no_renames:
108            to_path = from_path
109        else:
110            to_path = os.path.join(
111                os.path.dirname(from_path),
112                changeprefix(from_prefix, to_prefix,
113                    os.path.basename(from_path))[0])
114
115        # rename contents
116        changefile(from_prefix, to_prefix, from_path, to_path,
117            no_replacements=no_replacements)
118
119        # stage?
120        if git and not no_stage:
121            if from_path != to_path:
122                cmd = git_path + ['rm', '-q', from_path]
123                if verbose:
124                    print(' '.join(shlex.quote(c) for c in cmd))
125                subprocess.check_call(cmd)
126            cmd = git_path + ['add', to_path]
127            if verbose:
128                print(' '.join(shlex.quote(c) for c in cmd))
129            subprocess.check_call(cmd)
130
131
132if __name__ == "__main__":
133    import argparse
134    import sys
135    parser = argparse.ArgumentParser(
136        description="Change prefixes in files/filenames. Useful for creating "
137            "different versions of a codebase that don't conflict at compile "
138            "time.",
139        allow_abbrev=False)
140    parser.add_argument(
141        'from_prefix',
142        help="Prefix to replace.")
143    parser.add_argument(
144        'to_prefix',
145        help="Prefix to replace with.")
146    parser.add_argument(
147        'paths',
148        nargs='*',
149        help="Files to operate on.")
150    parser.add_argument(
151        '-v', '--verbose',
152        action='store_true',
153        help="Output commands that run behind the scenes.")
154    parser.add_argument(
155        '-o', '--output',
156        help="Output file.")
157    parser.add_argument(
158        '-N', '--no-replacements',
159        action='store_true',
160        help="Don't change prefixes in files")
161    parser.add_argument(
162        '-R', '--no-renames',
163        action='store_true',
164        help="Don't rename files")
165    parser.add_argument(
166        '--git',
167        action='store_true',
168        help="Use git to find/update files.")
169    parser.add_argument(
170        '--no-stage',
171        action='store_true',
172        help="Don't stage changes with git.")
173    parser.add_argument(
174        '--git-path',
175        type=lambda x: x.split(),
176        default=GIT_PATH,
177        help="Path to git executable, may include flags. "
178            "Defaults to %r." % GIT_PATH)
179    sys.exit(main(**{k: v
180        for k, v in vars(parser.parse_intermixed_args()).items()
181        if v is not None}))
182