Signed-off-by: Ionen Wolkens <io...@gentoo.org>
---
 eclass/esed.eclass | 201 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 201 insertions(+)
 create mode 100644 eclass/esed.eclass

diff --git a/eclass/esed.eclass b/eclass/esed.eclass
new file mode 100644
index 00000000000..f327c3bbdf4
--- /dev/null
+++ b/eclass/esed.eclass
@@ -0,0 +1,201 @@
+# Copyright 2022 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+# @ECLASS: esed.eclass
+# @MAINTAINER:
+# Ionen Wolkens <io...@gentoo.org>
+# @AUTHOR:
+# Ionen Wolkens <io...@gentoo.org>
+# @SUPPORTED_EAPIS: 8
+# @BLURB: sed(1) wrappers that die if expressions did not modify any files
+# @EXAMPLE:
+#
+# @CODE
+# esed 's/a/b/' src/file.c # -i is default, dies if 'a' does not become 'b'
+#
+# enewsed 's/a/b/' project.pc.in "${T}"/project.pc # stdin/out not supported
+#
+# esedfind . -type f -name '*.c' -esed 's/a/b/' # dies if zero files changed
+#
+# local esedexps=(
+#     # dies if /any/ of these did nothing, -e 's/a/b/' -e 's/c/d/' would not
+#     's/a/b/'
+#     's/c/d/' # bug 000000
+#     # use quotes around "$(use..)" to avoid word splitting/globs, won't run
+#     # sed(1) for empty elements (i.e. if USE is disabled)
+#     "$(usev fnord "s/foo bar/${baz}/")"
+# )
+# esed Makefile lib/Makefile # unsets esedexps so it's not re-used
+#
+# use prefix && esed "s|^prefix=|&${EPREFIX}|" project.pc # deterministic
+# @CODE
+#
+# Migration note: be wary of non-deterministic esed() involving variables,
+# e.g. s|lib|$(get_libdir)|, s|-O3|${CFLAGS}|, and the above ${EPREFIX} one.
+# esed() dies if these do nothing, like libdir being 'lib' on x86.  Either
+# verify, keep sed(1), or ensure a change (extra space, @placeholders@).
+
+case ${EAPI} in
+       8) ;;
+       *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
+esac
+
+if [[ ! -v _ESED_ECLASS ]]; then
+_ESED_ECLASS=1
+
+# @ECLASS_VARIABLE: ESED_VERBOSE
+# @DEFAULT_UNSET
+# @USER_VARIABLE
+# @DESCRIPTION:
+# If set to a non-empty value, esed() and its wrappers will use diff(1)
+# if available to display file differences.
+
+# @VARIABLE: esedexps
+# @DEFAULT_UNSET
+# @DESCRIPTION:
+# Bash array that can optionally contain sed expressions to use sequencially
+# on separate sed calls when using esed() and its wrappers.  Allows inspection
+# of modifications per-expressions.  Unset after use so it's not used in
+# subsequent calls.  Will not run sed(1) for empty array elements.
+
+# @FUNCTION: esed
+# @USAGE: <sed-argument>...
+# @DESCRIPTION:
+# sed(1) wrapper that dies if the expression(s) did not modify any files.
+# sed's -i/--in-place is forced, and so stdin/out cannot be used.
+esed() {
+       local -i i
+
+       if [[ ${esedexps@a} =~ a ]]; then
+               # expression must be before -- but after the rest for e.g. -E 
to work
+               local -i pos
+               for ((pos=1; pos<=${#}; pos++)); do
+                       [[ ${!pos} == -- ]] && break
+               done
+
+               for ((i=0; i<${#esedexps[@]}; i++)); do
+                       [[ ${esedexps[i]} ]] &&
+                               esedexps= esed "${@:1:pos-1}" -e 
"${esedexps[i]}" "${@:pos}"
+               done
+
+               unset esedexps
+               return 0
+       fi
+
+       # Roughly attempt to find files in arguments by checking if it's a
+       # readable file (aka s/// is not a file) and does not start with -
+       # (unless after --), then store contents for comparing after sed.
+       local contents=() endopts files=()
+       for ((i=1; i<=${#}; i++)); do
+               if [[ ${!i} == -- && ! -v endopts ]]; then
+                       endopts=1
+               elif [[ ${!i} =~ ^(-i|--in-place)$ && ! -v endopts ]]; then
+                       # detect rushed sed -i -> esed -i, -i also silently 
breaks enewsed
+                       die "passing ${!i} to ${FUNCNAME[0]} is invalid"
+               elif [[ ${!i} =~ ^(-f|--file)$ && ! -v endopts ]]; then
+                       i+=1 # ignore script files
+               elif [[ ( ${!i} != -* || -v endopts ) && -f ${!i} && -r ${!i} 
]]; then
+                       files+=( "${!i}" )
+
+                       # 2>/dev/null to silence null byte warnings if sed 
binary files
+                       { contents+=( "$(<"${!i}")" ); } 2>/dev/null \
+                               || die "failed to read: ${!i}"
+               fi
+       done
+       (( ${#files[@]} )) || die "no readable files found from '${*}' 
arguments"
+
+       local verbose
+       [[ ${ESED_VERBOSE} ]] && type diff &>/dev/null && verbose=1
+
+       local changed newcontents
+       if [[ -v _esed_output ]]; then
+               [[ -v verbose ]] &&
+                       einfo "${FUNCNAME[0]}: sed ${*} > ${_esed_output} ..."
+
+               sed "${@}" > "${_esed_output}" \
+                       || die "failed to run: sed ${*} > ${_esed_output}"
+
+               { newcontents=$(<"${_esed_output}"); } 2>/dev/null \
+                       || die "failed to read: ${_esed_output}"
+
+               local IFS=$'\n' # sed concats with newline even if none at EOF
+               contents=${contents[*]}
+               unset IFS
+
+               if [[ ${contents} != "${newcontents}" ]]; then
+                        changed=1
+
+                       [[ -v verbose ]] &&
+                               diff -u --color --label="${files[*]}" 
--label="${_esed_output}" \
+                                       <(echo "${contents}") <(echo 
"${newcontents}")
+               fi
+       else
+               [[ -v verbose ]] && einfo "${FUNCNAME[0]}: sed -i ${*} ..."
+
+               sed -i "${@}" || die "failed to run: sed -i ${*}"
+
+               for ((i=0; i<${#files[@]}; i++)); do
+                       { newcontents=$(<"${files[i]}"); } 2>/dev/null \
+                               || die "failed to read: ${files[i]}"
+
+                       if [[ ${contents[i]} != "${newcontents}" ]]; then
+                               changed=1
+
+                               [[ -v verbose ]] || break
+
+                               diff -u --color --label="${files[i]}"{,} \
+                                       <(echo "${contents[i]}") <(echo 
"${newcontents}")
+                       fi
+               done
+       fi
+
+       [[ -v changed ]] \
+               || die "no-op: ${FUNCNAME[0]} ${*}${_esed_command:+ (from: 
${_esed_command})}"
+}
+
+# @FUNCTION: enewsed
+# @USAGE: <esed-argument>... <output-file>
+# @DESCRIPTION:
+# esed() wrapper to save the result to <output-file>.  Same as using
+# `sed ... input > output` given esed() does not support stdin/out.
+enewsed() {
+       local _esed_command="${FUNCNAME[0]} ${*}"
+       local _esed_output=${*: -1:1}
+       esed "${@:1:${#}-1}"
+}
+
+# @FUNCTION: esedfind
+# @USAGE: <find-argument>... [-esed [<esed-argument>...]]
+# @DESCRIPTION:
+# esed() wrapper to ease use with find(1) given -exec wouldn't see a shell
+# function.  Will die if find(1) found no files, or if not a single file
+# was changed.  -esed is optional with the esedexps=( .. ) array.  -print0
+# will be appended to <find-arguments>.
+#
+# Requires that the found list not exceed args limit for file changes to be
+# evaluated together in a single esed() call.  Use is discouraged if modifying
+# files with a large total size (50+MB), as they will be loaded in memory
+# and compared ineffectively by the shell.
+esedfind() {
+       local _esed_command="${FUNCNAME[0]} ${*}"
+
+       local find=( find )
+       while (( ${#} )); do
+               if [[ ${1} == -esed ]]; then
+                       shift
+                       break
+               fi
+               find+=( "${1}" )
+               shift
+       done
+       find+=( -print0 )
+
+       local files
+       mapfile -d '' -t files < <("${find[@]}" || die "failed to run: 
${find[*]}")
+
+       (( ${#files[@]} )) || die "no files found from: ${find[*]}"
+
+       esed "${@}" -- "${files[@]}"
+}
+
+fi
-- 
2.35.1


Reply via email to