Gentoo Archives: gentoo-dev

From: Ionen Wolkens <ionen@g.o>
To: gentoo-dev@l.g.o
Subject: [gentoo-dev] [PATCH 1/2] esed.eclass: new eclass
Date: Tue, 31 May 2022 11:24:31
Message-Id: 20220531112319.29168-2-ionen@gentoo.org
In Reply to: [gentoo-dev] [PATCH 0/2] Add esed.eclass for sed that dies if caused no changes by Ionen Wolkens
1 Signed-off-by: Ionen Wolkens <ionen@g.o>
2 ---
3 eclass/esed.eclass | 199 +++++++++++++++++++++++++++++++++++++++++++++
4 1 file changed, 199 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..69f546804c4
10 --- /dev/null
11 +++ b/eclass/esed.eclass
12 @@ -0,0 +1,199 @@
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) wrappers that die if expressions did not modify any files
23 +# @EXAMPLE:
24 +#
25 +# @CODE
26 +# esed 's/a/b/' src/file.c # -i is default, dies if 'a' does not become 'b'
27 +#
28 +# enewsed 's/a/b/' project.pc.in "${T}"/project.pc # stdin/out not supported
29 +#
30 +# esedfind . -type f -name '*.c' -esed 's/a/b/' # dies if zero files changed
31 +#
32 +# local esedexps=(
33 +# # dies if /any/ of these did nothing, -e 's/a/b/' -e 's/c/d/' would not
34 +# 's/a/b/'
35 +# 's/c/d/' # bug 000000
36 +# # use quotes around "$(use..)" to avoid word splitting/globs, won't run
37 +# # sed(1) for empty elements (i.e. if USE is disabled)
38 +# "$(usev fnord "s/foo bar/${baz}/")"
39 +# )
40 +# esed Makefile lib/Makefile # unsets esedexps so it's not re-used
41 +#
42 +# use prefix && esed "s|^prefix=|&${EPREFIX}|" project.pc # deterministic
43 +# @CODE
44 +#
45 +# Migration note: be wary of non-deterministic esed() involving variables,
46 +# e.g. s|lib|$(get_libdir)|, s|-O3|${CFLAGS}|, and the above ${EPREFIX} one.
47 +# esed() dies if these do nothing, like libdir being 'lib' on x86. Either
48 +# verify, keep sed(1), or ensure a change (extra space, @placeholders@).
49 +
50 +case ${EAPI} in
51 + 8) ;;
52 + *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
53 +esac
54 +
55 +if [[ ! -v _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, esed() and its wrappers will use diff(1)
63 +# if available to display file differences.
64 +
65 +# @VARIABLE: esedexps
66 +# @DEFAULT_UNSET
67 +# @DESCRIPTION:
68 +# Bash array that can optionally contain sed expressions to use sequencially
69 +# on separate sed calls when using esed() and its wrappers. Allows inspection
70 +# of modifications per-expressions. Unset after use so it's not used in
71 +# subsequent calls. Will not run sed(1) for empty array elements.
72 +
73 +# @FUNCTION: esed
74 +# @USAGE: <sed-argument>...
75 +# @DESCRIPTION:
76 +# sed(1) wrapper that dies if the expression(s) did not modify any files.
77 +# sed's -i/--in-place is forced, and so stdin/out cannot be used.
78 +esed() {
79 + local -i i
80 +
81 + if [[ ${esedexps@a} =~ a ]]; then
82 + # expression must be before -- but after the rest for e.g. -E to work
83 + local -i pos
84 + for ((pos=1; pos<=${#}; pos++)); do
85 + [[ ${!pos} == -- ]] && break
86 + done
87 +
88 + for ((i=0; i<${#esedexps[@]}; i++)); do
89 + [[ ${esedexps[i]} ]] &&
90 + esedexps= esed "${@:1:pos-1}" -e "${esedexps[i]}" "${@:pos}"
91 + done
92 +
93 + unset esedexps
94 + return 0
95 + fi
96 +
97 + # Roughly attempt to find files in arguments by checking if it's a
98 + # readable file (aka s/// is not a file) and does not start with -
99 + # (unless after --), then store contents for comparing after sed.
100 + local contents=() endopts files=()
101 + for ((i=1; i<=${#}; i++)); do
102 + if [[ ${!i} == -- && ! -v endopts ]]; then
103 + endopts=1
104 + elif [[ ${!i} =~ ^(-i|--in-place)$ && ! -v endopts ]]; then
105 + # detect rushed sed -i -> esed -i, -i also silently breaks enewsed
106 + die "passing ${!i} to ${FUNCNAME[0]} is invalid"
107 + elif [[ ${!i} =~ ^(-f|--file)$ && ! -v endopts ]]; then
108 + i+=1 # ignore script files
109 + elif [[ ( ${!i} != -* || -v endopts ) && -f ${!i} && -r ${!i} ]]; then
110 + files+=( "${!i}" )
111 +
112 + # eval 2>/dev/null to silence \0 warnings if sed binary files
113 + eval 'contents+=( "$(<"${!i}")" )' 2>/dev/null \
114 + || die "failed to read: ${!i}"
115 + fi
116 + done
117 + (( ${#files[@]} )) || die "no readable files found from '${*}' arguments"
118 +
119 + local verbose
120 + [[ ${ESED_VERBOSE} ]] && type diff &>/dev/null && verbose=1
121 +
122 + local changed newcontents
123 + if [[ -v _esed_output ]]; then
124 + [[ -v verbose ]] &&
125 + einfo "${FUNCNAME[0]}: sed ${*} > ${_esed_output} ..."
126 +
127 + sed "${@}" > "${_esed_output}" \
128 + || die "failed to run: sed ${*} > ${_esed_output}"
129 +
130 + eval 'newcontents=$(<${_esed_output})' 2>/dev/null \
131 + || die "failed to read: ${_esed_output}"
132 +
133 + local IFS=$'\n' # sed concats with newline even if none at EOF
134 + contents=${contents[*]}
135 + unset IFS
136 +
137 + [[ ${contents} != "${newcontents}" ]] && changed=1
138 +
139 + [[ -v verbose ]] &&
140 + diff -u --color --label="${files[*]}" --label="${_esed_output}" \
141 + <(echo "${contents}") <(echo "${newcontents}")
142 + else
143 + [[ -v verbose ]] && einfo "${FUNCNAME[0]}: sed -i ${*} ..."
144 +
145 + sed -i "${@}" || die "failed to run: sed -i ${*}"
146 +
147 + for ((i=0; i<${#files[@]}; i++)); do
148 + eval 'newcontents=$(<"${files[i]}")' 2>/dev/null \
149 + || die "failed to read: ${files[i]}"
150 +
151 + if [[ ${contents[i]} != "${newcontents}" ]]; then
152 + changed=1
153 + [[ -v verbose ]] || break
154 + fi
155 +
156 + [[ -v verbose ]] &&
157 + diff -u --color --label="${files[i]}"{,} \
158 + <(echo "${contents[i]}") <(echo "${newcontents}")
159 + done
160 + fi
161 +
162 + [[ -v changed ]] \
163 + || die "no-op: ${FUNCNAME[0]} ${*}${_esed_command:+ (from: ${_esed_command})}"
164 +}
165 +
166 +# @FUNCTION: enewsed
167 +# @USAGE: <esed-argument>... <output-file>
168 +# @DESCRIPTION:
169 +# esed() wrapper to save the result to <output-file>. Same as using
170 +# `sed ... input > output` given esed() does not support stdin/out.
171 +enewsed() {
172 + local _esed_command="${FUNCNAME[0]} ${*}"
173 + local _esed_output=${*: -1:1}
174 + esed "${@:1:${#}-1}"
175 +}
176 +
177 +# @FUNCTION: esedfind
178 +# @USAGE: <find-argument>... [-esed [<esed-argument>...]]
179 +# @DESCRIPTION:
180 +# esed() wrapper to ease use with find(1) given -exec wouldn't see a shell
181 +# function. Will die if find(1) found no files, or if not a single file
182 +# was changed. -esed is optional with the esedexps=( .. ) array. -print0
183 +# will be appended to <find-arguments>.
184 +#
185 +# Requires that the found list not exceed args limit for file changes to be
186 +# evaluated together in a single esed() call. Use is discouraged if modifying
187 +# files with a large total size (50+MB), as they will be loaded in memory
188 +# and compared ineffectively by the shell.
189 +esedfind() {
190 + local _esed_command="${FUNCNAME[0]} ${*}"
191 +
192 + local find=( find )
193 + while (( ${#} )); do
194 + if [[ ${1} == -esed ]]; then
195 + shift
196 + break
197 + fi
198 + find+=( "${1}" )
199 + shift
200 + done
201 + find+=( -print0 )
202 +
203 + local files
204 + mapfile -d '' -t files < <("${find[@]}" || die "failed to run: ${find[*]}")
205 +
206 + (( ${#files[@]} )) || die "no files found from: ${find[*]}"
207 +
208 + esed "${@}" -- "${files[@]}"
209 +}
210 +
211 +fi
212 --
213 2.35.1

Replies