1 |
Signed-off-by: Ionen Wolkens <ionen@g.o> |
2 |
--- |
3 |
eclass/esed.eclass | 265 +++++++++++++++++++++++++++++++++++++++++++++ |
4 |
1 file changed, 265 insertions(+) |
5 |
create mode 100644 eclass/esed.eclass |
6 |
|
7 |
diff --git a/eclass/esed.eclass b/eclass/esed.eclass |
8 |
new file mode 100644 |
9 |
index 00000000000..414daceaf8b |
10 |
--- /dev/null |
11 |
+++ b/eclass/esed.eclass |
12 |
@@ -0,0 +1,265 @@ |
13 |
+# Copyright 2022 Gentoo Authors |
14 |
+# Distributed under the terms of the GNU General Public License v2 |
15 |
+ |
16 |
+# @ECLASS: esed.eclass |
17 |
+# @MAINTAINER: |
18 |
+# Ionen Wolkens <ionen@g.o> |
19 |
+# @AUTHOR: |
20 |
+# Ionen Wolkens <ionen@g.o> |
21 |
+# @SUPPORTED_EAPIS: 8 |
22 |
+# @BLURB: sed(1) and alike wrappers that die if did not modify any files |
23 |
+# @EXAMPLE: |
24 |
+# |
25 |
+# @CODE |
26 |
+# # sed(1) wrappers, die if no changes |
27 |
+# esed s/a/b/ file.c # -i is default |
28 |
+# enewsed s/a/b/ project.pc.in "${T}"/project.pc |
29 |
+# |
30 |
+# # bash-only simple fixed string alternatives, also die if no changes |
31 |
+# erepl string replace file.c |
32 |
+# ereplp ^match string replace file.c # like /^match/s:string:replace:g |
33 |
+# erepld ^match file.c # deletes matching lines, like /^match/d |
34 |
+# use prefix && enewreplp ^prefix= /usr "${EPREFIX}"/usr pn.pc.in pn.pc |
35 |
+# |
36 |
+# # find(1) wrapper that sees shell functions, dies if no files found |
37 |
+# efind . -name '*.c' -erun esed s/a/b/ # dies if no files changed |
38 |
+# efind . -name '*.c' -erun sed s/a/b/ # only dies if no files found |
39 |
+# @CODE |
40 |
+# |
41 |
+# Migration notes: be wary of non-deterministic cases involving variables, |
42 |
+# e.g. s|lib|$(get_libdir)|, s|-O3|${CFLAGS}|, or s|/usr|${EPREFIX}/usr|. |
43 |
+# erepl/esed() die if these do nothing, like libdir being 'lib' on x86. |
44 |
+# Either verify, keep sed(1), or ensure a change (extra space, @libdir@). |
45 |
+# |
46 |
+# Where possible, it is also good to consider if using patches is more |
47 |
+# suitable to ensure adequate changes. These functions are also unsafe |
48 |
+# for binary files containing null bytes (erepl() will remove them). |
49 |
+ |
50 |
+case ${EAPI} in |
51 |
+ 8) ;; |
52 |
+ *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; |
53 |
+esac |
54 |
+ |
55 |
+if [[ ! ${_ESED_ECLASS} ]]; then |
56 |
+_ESED_ECLASS=1 |
57 |
+ |
58 |
+# @ECLASS_VARIABLE: ESED_VERBOSE |
59 |
+# @DEFAULT_UNSET |
60 |
+# @USER_VARIABLE |
61 |
+# @DESCRIPTION: |
62 |
+# If set to a non-empty value, erepl/esed() and wrappers will use diff(1) |
63 |
+# to display file differences. Recommended for maintainers to easily |
64 |
+# confirm the changes being made. |
65 |
+ |
66 |
+# @FUNCTION: esed |
67 |
+# @USAGE: [-E|-r|-n] [-e <expression>]... [--] <file>... |
68 |
+# @DESCRIPTION: |
69 |
+# sed(1) wrapper that dies if any of the expressions did not modify any files. |
70 |
+# sed's -i/--in-place is forced, -e can be omitted if only one expression, and |
71 |
+# arguments must be passed in the listed order with files last. Each -e will |
72 |
+# be a separate sed(1) call to evaluate changes of each. |
73 |
+esed() { |
74 |
+ (( ${#} >= 2 )) || die "too few arguments for ${_esed_cmd[0]:-${FUNCNAME[0]}}" |
75 |
+ |
76 |
+ local endopts=false args=() contents=() exps=() files=() |
77 |
+ local -i i |
78 |
+ for ((i=1; i<=${#}; i++)); do |
79 |
+ if [[ ${!i} =~ ^- ]] && ! ${endopts}; then |
80 |
+ case ${!i} in |
81 |
+ --) endopts=true ;; |
82 |
+ -E|-n|-r) args+=( ${!i} ) ;; |
83 |
+ -e) |
84 |
+ i+=1 |
85 |
+ [[ ${!i} ]] || die "missing argument to -e" |
86 |
+ exps+=( "${!i}" ) |
87 |
+ ;; |
88 |
+ *) die "unrecognized option for ${FUNCNAME[0]}" ;; |
89 |
+ esac |
90 |
+ elif (( ! ${#exps[@]} )); then |
91 |
+ exps+=( "${!i}" ) # like sed, if no -e, first non-option is exp |
92 |
+ else |
93 |
+ [[ -f ${!i} ]] || die "not a file: ${!i}" |
94 |
+ files+=( "${!i}" ) |
95 |
+ contents+=( "$(<"${!i}")" ) || die "failed reading: ${!i}" |
96 |
+ fi |
97 |
+ done |
98 |
+ (( ${#files[@]} )) || die "no files in ${FUNCNAME[0]} arguments" |
99 |
+ |
100 |
+ if [[ ${_esed_output} ]]; then |
101 |
+ (( ${#files[@]} == 1 )) || die "${_esed_cmd[0]} needs exactly one input file" |
102 |
+ |
103 |
+ # swap file for output to simplify sequential sed'ing |
104 |
+ cp -- "${files[0]}" "${_esed_output}" || die |
105 |
+ files[0]=${_esed_output} |
106 |
+ fi |
107 |
+ |
108 |
+ local changed exp newcontents sed |
109 |
+ for exp in "${exps[@]}"; do |
110 |
+ sed=( sed -i "${args[@]}" -e "${exp}" -- "${files[@]}" ) |
111 |
+ [[ ${ESED_VERBOSE} ]] && einfo "${sed[*]}" |
112 |
+ |
113 |
+ "${sed[@]}" </dev/null || die "failed: ${sed[*]}" |
114 |
+ |
115 |
+ changed=false |
116 |
+ for ((i=0; i<${#files[@]}; i++)); do |
117 |
+ newcontents=$(<"${files[i]}") || die "failed reading: ${files[i]}" |
118 |
+ |
119 |
+ if [[ ${contents[i]} != "${newcontents}" ]]; then |
120 |
+ changed=true |
121 |
+ |
122 |
+ [[ ${ESED_VERBOSE} ]] || break |
123 |
+ |
124 |
+ diff -u --color --label="${files[i]}"{,} \ |
125 |
+ <(echo "${contents[i]}") <(echo "${newcontents}") |
126 |
+ fi |
127 |
+ done |
128 |
+ |
129 |
+ ${changed} \ |
130 |
+ || die "no-op: ${FUNCNAME[0]} ${*}${_esed_cmd[0]:+ (from: ${_esed_cmd[*]})}" |
131 |
+ done |
132 |
+} |
133 |
+ |
134 |
+# @FUNCTION: enewsed |
135 |
+# @USAGE: <esed-argument>... <output-file> |
136 |
+# @DESCRIPTION: |
137 |
+# esed() wrapper to save the result to <output-file>. Intended to replace |
138 |
+# ``sed ... input > output`` given esed() does not support stdin/out. |
139 |
+enewsed() { |
140 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
141 |
+ local _esed_output=${*: -1:1} |
142 |
+ esed "${@:1:${#}-1}" |
143 |
+} |
144 |
+ |
145 |
+# @FUNCTION: erepl |
146 |
+# @USAGE: <string> <replacement> <file>... |
147 |
+# @DESCRIPTION: |
148 |
+# Do basic bash-only ``${<file>//"<string>"/<replacement>}`` per-line |
149 |
+# replacement in files(s). Dies if no changes were made. Suggested over |
150 |
+# sed(1) where possible for simplicity and avoiding issues with delimiters. |
151 |
+# Warning: erepl-based functions strip null bytes, use for text only. |
152 |
+erepl() { |
153 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
154 |
+ ereplp '.*' "${@}" |
155 |
+} |
156 |
+ |
157 |
+# @FUNCTION: enewrepl |
158 |
+# @USAGE: <erepl-argument>... <output-file> |
159 |
+# @DESCRIPTION: |
160 |
+# erepl() wrapper to save the result to <output-file>. |
161 |
+enewrepl() { |
162 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
163 |
+ local _esed_output=${*: -1:1} |
164 |
+ ereplp '.*' "${@:1:${#}-1}" |
165 |
+} |
166 |
+ |
167 |
+# @FUNCTION: erepld |
168 |
+# @USAGE: <line-pattern-match> <file>... |
169 |
+# @DESCRIPTION: |
170 |
+# Deletes lines in file(s) matching ``[[ ${line} =~ <pattern> ]]``. |
171 |
+erepld() { |
172 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
173 |
+ local _esed_repld=1 |
174 |
+ ereplp "${@}" |
175 |
+} |
176 |
+ |
177 |
+# @FUNCTION: enewrepld |
178 |
+# @USAGE: <erepld-argument>... <output-file> |
179 |
+# @DESCRIPTION: |
180 |
+# erepl() wrapper to save the result to <output-file>. |
181 |
+enewrepld() { |
182 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
183 |
+ local _esed_output=${*: -1:1} |
184 |
+ erepld "${@:1:${#}-1}" |
185 |
+} |
186 |
+ |
187 |
+# @FUNCTION: ereplp |
188 |
+# @USAGE: <line-match-pattern> <string> <replacement> <file>... |
189 |
+# @DESCRIPTION: |
190 |
+# Like erepl() but replaces only on ``[[ ${line} =~ <pattern> ]]``. |
191 |
+ereplp() { |
192 |
+ local -i argsmin=$(( ${_esed_repld:-0}==1?2:4 )) |
193 |
+ (( ${#} >= argsmin )) \ |
194 |
+ || die "too few arguments for ${_esed_cmd[0]:-${FUNCNAME[0]}}" |
195 |
+ |
196 |
+ [[ ! ${_esed_output} || ${#} -le ${argsmin} ]] \ |
197 |
+ || die "${_esed_cmd[0]} needs exactly one input file" |
198 |
+ |
199 |
+ local contents changed=false file line newcontents |
200 |
+ for file in "${@:argsmin}"; do |
201 |
+ mapfile contents < "${file}" || die |
202 |
+ newcontents=() |
203 |
+ |
204 |
+ for line in "${contents[@]}"; do |
205 |
+ if [[ ${line} =~ ${1} ]]; then |
206 |
+ if [[ ${_esed_repld} == 1 ]]; then |
207 |
+ changed=true |
208 |
+ else |
209 |
+ newcontents+=( "${line//"${2}"/${3}}" ) |
210 |
+ [[ ${line} != "${newcontents[-1]}" ]] && changed=true |
211 |
+ fi |
212 |
+ else |
213 |
+ newcontents+=( "${line}" ) |
214 |
+ fi |
215 |
+ done |
216 |
+ printf %s "${newcontents[@]}" > "${_esed_output:-${file}}" || die |
217 |
+ |
218 |
+ if [[ ${ESED_VERBOSE} ]]; then |
219 |
+ einfo "${FUNCNAME[0]} ${*:1:argsmin-1} ${file} ${_esed_output:+(to ${_esed_output})}" |
220 |
+ diff -u --color --label="${file}" --label="${_esed_output:-${file}}" \ |
221 |
+ <(printf %s "${contents[@]}") <(printf %s "${newcontents[@]}") |
222 |
+ fi |
223 |
+ done |
224 |
+ |
225 |
+ ${changed} || die "no-op: ${_esed_cmd[*]:-${FUNCNAME[0]} ${*}}" |
226 |
+} |
227 |
+ |
228 |
+# @FUNCTION: enewreplp |
229 |
+# @USAGE: <ereplp-argument>... <output-file> |
230 |
+# @DESCRIPTION: |
231 |
+# ereplp() wrapper to save the result to <output-file>. |
232 |
+enewreplp() { |
233 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
234 |
+ local _esed_output=${*: -1:1} |
235 |
+ ereplp "${@:1:${#}-1}" |
236 |
+} |
237 |
+ |
238 |
+# @FUNCTION: efind |
239 |
+# @USAGE: <find-argument>... -erun <command> <argument>... |
240 |
+# @DESCRIPTION: |
241 |
+# find(1) wrapper that dies if no files were found. <command> can be a shell |
242 |
+# function, e.g. ``efind ... -erun erepl /usr /opt``. -print0 is added to |
243 |
+# find arguments, and found files to end of arguments (``{} +`` is unused). |
244 |
+# Found files must not exceed args limits. Use is discouraged if files add |
245 |
+# up to a large total size (50+MB), notably with slower erepl/esed(). Shell |
246 |
+# functions called this way are expected to ``|| die`` themselves on error. |
247 |
+efind() { |
248 |
+ (( ${#} >= 3 )) || die "too few arguments for ${FUNCNAME[0]}" |
249 |
+ |
250 |
+ local _esed_cmd=( ${FUNCNAME[0]} "${@}" ) |
251 |
+ |
252 |
+ local find=( find ) |
253 |
+ while (( ${#} )); do |
254 |
+ if [[ ${1} =~ -erun ]]; then |
255 |
+ shift |
256 |
+ break |
257 |
+ fi |
258 |
+ find+=( "${1}" ) |
259 |
+ shift |
260 |
+ done |
261 |
+ find+=( -print0 ) |
262 |
+ |
263 |
+ local files |
264 |
+ mapfile -d '' -t files < <("${find[@]}" || die "failed: ${find[*]}") |
265 |
+ |
266 |
+ (( ${#files[@]} )) || die "no files from: ${find[*]}" |
267 |
+ (( ${#} )) || die "missing -erun arguments for ${FUNCNAME[0]}" |
268 |
+ |
269 |
+ # skip `|| die` for shell functions (should be handled internally) |
270 |
+ if declare -f "${1}" >/dev/null; then |
271 |
+ "${@}" "${files[@]}" |
272 |
+ else |
273 |
+ "${@}" "${files[@]}" || die "failed: ${*} ${files[*]}" |
274 |
+ fi |
275 |
+} |
276 |
+ |
277 |
+fi |
278 |
-- |
279 |
2.35.1 |