Gentoo Archives: gentoo-dev

From: Oskari Pirhonen <xxc3ncoredxx@×××××.com>
To: gentoo-dev@l.g.o
Subject: Re: [gentoo-dev] [PATCH v3 1/2] esed.eclass: new eclass
Date: Sun, 05 Jun 2022 19:43:07
Message-Id: Yp0HP7RPC2cY6J5k@dj3ntoo
In Reply to: [gentoo-dev] [PATCH v3 1/2] esed.eclass: new eclass by Ionen Wolkens
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

Attachments

File name MIME type
signature.asc application/pgp-signature

Replies

Subject Author
Re: [gentoo-dev] [PATCH v3 1/2] esed.eclass: new eclass Oskari Pirhonen <xxc3ncoredxx@×××××.com>