Gentoo Archives: gentoo-dev

From: Ionen Wolkens <ionen@g.o>
To: gentoo-dev@l.g.o
Subject: [gentoo-dev] [PATCH v2 1/1] esed.eclass: new eclass
Date: Fri, 03 Jun 2022 11:37:48
Message-Id: 20220603113646.25973-2-ionen@gentoo.org
In Reply to: [gentoo-dev] [PATCH v2 0/1] esed by Ionen Wolkens
1 Signed-off-by: Ionen Wolkens <ionen@g.o>
2 ---
3 eclass/esed.eclass | 201 +++++++++++++++++++++++++++++++++++++++++++++
4 1 file changed, 201 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..f327c3bbdf4
10 --- /dev/null
11 +++ b/eclass/esed.eclass
12 @@ -0,0 +1,201 @@
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 + # 2>/dev/null to silence null byte warnings if sed binary files
113 + { 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 + { 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 + if [[ ${contents} != "${newcontents}" ]]; then
138 + changed=1
139 +
140 + [[ -v verbose ]] &&
141 + diff -u --color --label="${files[*]}" --label="${_esed_output}" \
142 + <(echo "${contents}") <(echo "${newcontents}")
143 + fi
144 + else
145 + [[ -v verbose ]] && einfo "${FUNCNAME[0]}: sed -i ${*} ..."
146 +
147 + sed -i "${@}" || die "failed to run: sed -i ${*}"
148 +
149 + for ((i=0; i<${#files[@]}; i++)); do
150 + { newcontents=$(<"${files[i]}"); } 2>/dev/null \
151 + || die "failed to read: ${files[i]}"
152 +
153 + if [[ ${contents[i]} != "${newcontents}" ]]; then
154 + changed=1
155 +
156 + [[ -v verbose ]] || break
157 +
158 + diff -u --color --label="${files[i]}"{,} \
159 + <(echo "${contents[i]}") <(echo "${newcontents}")
160 + fi
161 + done
162 + fi
163 +
164 + [[ -v changed ]] \
165 + || die "no-op: ${FUNCNAME[0]} ${*}${_esed_command:+ (from: ${_esed_command})}"
166 +}
167 +
168 +# @FUNCTION: enewsed
169 +# @USAGE: <esed-argument>... <output-file>
170 +# @DESCRIPTION:
171 +# esed() wrapper to save the result to <output-file>. Same as using
172 +# `sed ... input > output` given esed() does not support stdin/out.
173 +enewsed() {
174 + local _esed_command="${FUNCNAME[0]} ${*}"
175 + local _esed_output=${*: -1:1}
176 + esed "${@:1:${#}-1}"
177 +}
178 +
179 +# @FUNCTION: esedfind
180 +# @USAGE: <find-argument>... [-esed [<esed-argument>...]]
181 +# @DESCRIPTION:
182 +# esed() wrapper to ease use with find(1) given -exec wouldn't see a shell
183 +# function. Will die if find(1) found no files, or if not a single file
184 +# was changed. -esed is optional with the esedexps=( .. ) array. -print0
185 +# will be appended to <find-arguments>.
186 +#
187 +# Requires that the found list not exceed args limit for file changes to be
188 +# evaluated together in a single esed() call. Use is discouraged if modifying
189 +# files with a large total size (50+MB), as they will be loaded in memory
190 +# and compared ineffectively by the shell.
191 +esedfind() {
192 + local _esed_command="${FUNCNAME[0]} ${*}"
193 +
194 + local find=( find )
195 + while (( ${#} )); do
196 + if [[ ${1} == -esed ]]; then
197 + shift
198 + break
199 + fi
200 + find+=( "${1}" )
201 + shift
202 + done
203 + find+=( -print0 )
204 +
205 + local files
206 + mapfile -d '' -t files < <("${find[@]}" || die "failed to run: ${find[*]}")
207 +
208 + (( ${#files[@]} )) || die "no files found from: ${find[*]}"
209 +
210 + esed "${@}" -- "${files[@]}"
211 +}
212 +
213 +fi
214 --
215 2.35.1

Replies

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