-
Notifications
You must be signed in to change notification settings - Fork 0
/
replace_vars.py
159 lines (142 loc) · 5.67 KB
/
replace_vars.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# SPDX-FileCopyrightText: 2021 Robin Vobruba <[email protected]>
#
# SPDX-License-Identifier: GPL-3.0-or-later
import abc
import re
import sys
from string import Template
import click
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
class TextFilter(metaclass=abc.ABCMeta):
''' Abstract class; to be overridden. '''
@abc.abstractmethod
@abc.abstractmethod
def describe_intent(self) -> None:
''' Mock function stub; to be overridden. '''
raise NotImplementedError('users must define "describe_intent" to use this base class')
@abc.abstractmethod
def filter(self, text) -> str:
''' Mock function stub; to be overridden. '''
raise NotImplementedError('users must define "filter" to use this base class')
class RegexTextFilter(TextFilter):
''' Allows to filter a text by a regex search and replace. '''
def __init__(self, search_p, repl_p):
self.search = re.compile(search_p)
self.repl = repl_p
def describe_intent(self):
return "INFO: replacing (regex): '%s' -> '%s'" % (self.search.pattern, self.repl)
def filter(self, text):
return self.search.sub(self.repl, text)
class TemplateFilter(TextFilter):
'''
Allows to filter a text with a template;
see the string.Template class.
'''
def __init__(self, template_class, replacements):
self.template_class = template_class
self.replacements = replacements
def describe_intent(self):
lines = []
for key, value in self.replacements.items():
lines.append("INFO: replacing (static): '${%s}' -> '%s'" % (key, value)) # FIXME This is not generic yet, but fixed to our own filter: TemplatePedanticBash
return '\n'.join(lines)
def filter(self, text):
template = self.template_class(text)
return template.safe_substitute(self.replacements)
class TemplatePedanticBash(Template):
'''
Replaces only braced identifiers like "${key}" (not "$key").
Use "$" in front to escpape, like "$${key}".
'''
delimiter = '$'
pattern = r'''
\$(?:
(?P<escaped>\$) | # Escape sequence of two delimiters
\b\B(?P<named>[_a-z][_a-z0-9]*) | # Disable un-braced name matching
{(?P<braced>[_a-z][_a-z0-9]*)} | # delimiter and a braced identifier
(?P<invalid>) # Other ill-formed delimiter exprs
)
'''
class KeyValuePairType(click.ParamType):
'''
Allows to parse strings into key+value pairs,
separated by either ':' or '='.
This is for the click command-line parsing lib.
'''
name = 'key-value-pair'
sep = None
def __init__(self, sep=None):
self.sep = sep
if not self.sep:
self.sep = '[:=]'
def convert(self, value, param, ctx) -> [str, str]:
try:
key_value = re.split(self.sep, value, 1)
if len(key_value) < 2:
raise ValueError("No key-value separator (regex: '%s') found!" % self.sep)
return key_value
except ValueError as err:
self.fail(('"%s" is not a valid %s. ' +
'It needs to be of the form "key<separator>value".;\n' +
'Error: %s')
% (value, self.name, err), param, ctx)
KEY_VALLUE_PAIR = KeyValuePairType()
@click.group(context_settings=CONTEXT_SETTINGS)
@click.version_option()
def replace_vars() -> None:
pass
def replace_vars_by_lines(in_file, out_file, replacements, dry=False,
verbose=False, pre_filter=None, post_filter=None) -> None:
with open(in_file, "r") as src:
with open(out_file, "w") as dst:
replace_vars_by_lines_in_stream(src, dst, replacements, dry,
verbose, pre_filter, post_filter)
def replace_vars_by_lines_in_stream(fp_in, fp_out, replacements, dry=False,
verbose=False, pre_filter=None, post_filter=None) -> None:
if not replacements:
print('WARNING: No replacements supplied!', file=sys.stderr)
filters = []
if pre_filter:
filters.append(pre_filter)
filters.append(TemplateFilter(TemplatePedanticBash, replacements))
if post_filter:
filters.append(post_filter)
filter_stream(fp_in, fp_out, filters, dry, verbose)
def filter_stream(fp_in, fp_out, filters=[], dry=False, verbose=False) -> None:
if not filters:
print('WARNING: No filters supplied!', file=sys.stderr)
if verbose:
for fltr in filters:
print(fltr.describe_intent(), file=sys.stderr)
if not dry:
for line in fp_in:
for fltr in filters:
line = fltr.filter(line)
fp_out.write(line)
@click.command(context_settings=CONTEXT_SETTINGS)
@click.argument("src", type=click.File("r"))
@click.argument("dst", type=click.File("w"))
@click.argument("replacements", type=KEY_VALLUE_PAIR, nargs=-1)
@click.option('--dry', is_flag=True, help='Whether to skip the actual replacing')
@click.option('--verbose', is_flag=True, help='Whether to output additional info to stderr')
def cli(src, dst, replacements, dry=False, verbose=False) -> None:
'''
This script works similar to the Unix `cat` command but it writes
into a specific file (which could be the standard output as denoted by
the ``-`` sign).
\b
Copy stdin to stdout:
replace_vars - -
\b
Copy foo.txt and bar.txt to stdout:
replace_vars foo.txt bar.txt -
\b
Write stdin into the file foo.txt
replace_vars - foo.txt
'''
replacements_dict = {}
for key, value in replacements:
replacements_dict[key] = value
replace_vars_by_lines_in_stream(src, dst, replacements_dict, dry, verbose)
if __name__ == '__main__':
cli()