Gentoo Archives: gentoo-dev

From: Ionen Wolkens <ionen@g.o>
To: gentoo-dev@l.g.o
Subject: [gentoo-dev] [PATCH v3 1/2] esed.eclass: new eclass
Date: Sat, 04 Jun 2022 20:46:49
Message-Id: 20220604204634.27567-2-ionen@gentoo.org
In Reply to: [gentoo-dev] [PATCH v3 0/2] Add esed.eclass for modifying with checks for no-op by Ionen Wolkens
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

Replies

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