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