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

diff --git a/eclass/esed.eclass b/eclass/esed.eclass
new file mode 100644
index 00000000000..414daceaf8b
--- /dev/null
+++ b/eclass/esed.eclass
@@ -0,0 +1,265 @@
+# 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) and alike wrappers that die if did not modify any files
+# @EXAMPLE:
+#
+# @CODE
+# # sed(1) wrappers, die if no changes
+# esed s/a/b/ file.c # -i is default
+# enewsed s/a/b/ project.pc.in "${T}"/project.pc
+#
+# # bash-only simple fixed string alternatives, also die if no changes
+# erepl string replace file.c
+# ereplp ^match string replace file.c # like /^match/s:string:replace:g
+# erepld ^match file.c # deletes matching lines, like /^match/d
+# use prefix && enewreplp ^prefix= /usr "${EPREFIX}"/usr pn.pc.in pn.pc
+#
+# # find(1) wrapper that sees shell functions, dies if no files found
+# efind . -name '*.c' -erun esed s/a/b/ # dies if no files changed
+# efind . -name '*.c' -erun sed s/a/b/ # only dies if no files found
+# @CODE
+#
+# Migration notes: be wary of non-deterministic cases involving variables,
+# e.g. s|lib|$(get_libdir)|, s|-O3|${CFLAGS}|, or s|/usr|${EPREFIX}/usr|.
+# erepl/esed() die if these do nothing, like libdir being 'lib' on x86.
+# Either verify, keep sed(1), or ensure a change (extra space, @libdir@).
+#
+# Where possible, it is also good to consider if using patches is more
+# suitable to ensure adequate changes.  These functions are also unsafe
+# for binary files containing null bytes (erepl() will remove them).
+
+case ${EAPI} in
+       8) ;;
+       *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
+esac
+
+if [[ ! ${_ESED_ECLASS} ]]; then
+_ESED_ECLASS=1
+
+# @ECLASS_VARIABLE: ESED_VERBOSE
+# @DEFAULT_UNSET
+# @USER_VARIABLE
+# @DESCRIPTION:
+# If set to a non-empty value, erepl/esed() and wrappers will use diff(1)
+# to display file differences.  Recommended for maintainers to easily
+# confirm the changes being made.
+
+# @FUNCTION: esed
+# @USAGE: [-E|-r|-n] [-e <expression>]... [--] <file>...
+# @DESCRIPTION:
+# sed(1) wrapper that dies if any of the expressions did not modify any files.
+# sed's -i/--in-place is forced, -e can be omitted if only one expression, and
+# arguments must be passed in the listed order with files last.  Each -e will
+# be a separate sed(1) call to evaluate changes of each.
+esed() {
+       (( ${#} >= 2 )) || die "too few arguments for 
${_esed_cmd[0]:-${FUNCNAME[0]}}"
+
+       local endopts=false args=() contents=() exps=() files=()
+       local -i i
+       for ((i=1; i<=${#}; i++)); do
+               if [[ ${!i} =~ ^- ]] && ! ${endopts}; then
+                       case ${!i} in
+                               --) endopts=true ;;
+                               -E|-n|-r) args+=( ${!i} ) ;;
+                               -e)
+                                       i+=1
+                                       [[ ${!i} ]] || die "missing argument to 
-e"
+                                       exps+=( "${!i}" )
+                                       ;;
+                               *) die "unrecognized option for ${FUNCNAME[0]}" 
;;
+                       esac
+               elif (( ! ${#exps[@]} )); then
+                       exps+=( "${!i}" ) # like sed, if no -e, first 
non-option is exp
+               else
+                       [[ -f ${!i} ]] || die "not a file: ${!i}"
+                       files+=( "${!i}" )
+                       contents+=( "$(<"${!i}")" ) || die "failed reading: 
${!i}"
+               fi
+       done
+       (( ${#files[@]} )) || die "no files in ${FUNCNAME[0]} arguments"
+
+       if [[ ${_esed_output} ]]; then
+               (( ${#files[@]} == 1 )) || die "${_esed_cmd[0]} needs exactly 
one input file"
+
+               # swap file for output to simplify sequential sed'ing
+               cp -- "${files[0]}" "${_esed_output}" || die
+               files[0]=${_esed_output}
+       fi
+
+       local changed exp newcontents sed
+       for exp in "${exps[@]}"; do
+               sed=( sed -i "${args[@]}" -e "${exp}" -- "${files[@]}" )
+               [[ ${ESED_VERBOSE} ]] && einfo "${sed[*]}"
+
+               "${sed[@]}" </dev/null || die "failed: ${sed[*]}"
+
+               changed=false
+               for ((i=0; i<${#files[@]}; i++)); do
+                       newcontents=$(<"${files[i]}") || die "failed reading: 
${files[i]}"
+
+                       if [[ ${contents[i]} != "${newcontents}" ]]; then
+                               changed=true
+
+                               [[ ${ESED_VERBOSE} ]] || break
+
+                               diff -u --color --label="${files[i]}"{,} \
+                                       <(echo "${contents[i]}") <(echo 
"${newcontents}")
+                       fi
+               done
+
+               ${changed} \
+                       || die "no-op: ${FUNCNAME[0]} ${*}${_esed_cmd[0]:+ 
(from: ${_esed_cmd[*]})}"
+       done
+}
+
+# @FUNCTION: enewsed
+# @USAGE: <esed-argument>... <output-file>
+# @DESCRIPTION:
+# esed() wrapper to save the result to <output-file>.  Intended to replace
+# ``sed ... input > output`` given esed() does not support stdin/out.
+enewsed() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       local _esed_output=${*: -1:1}
+       esed "${@:1:${#}-1}"
+}
+
+# @FUNCTION: erepl
+# @USAGE: <string> <replacement> <file>...
+# @DESCRIPTION:
+# Do basic bash-only ``${<file>//"<string>"/<replacement>}`` per-line
+# replacement in files(s).  Dies if no changes were made.  Suggested over
+# sed(1) where possible for simplicity and avoiding issues with delimiters.
+# Warning: erepl-based functions strip null bytes, use for text only.
+erepl() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       ereplp '.*' "${@}"
+}
+
+# @FUNCTION: enewrepl
+# @USAGE: <erepl-argument>... <output-file>
+# @DESCRIPTION:
+# erepl() wrapper to save the result to <output-file>.
+enewrepl() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       local _esed_output=${*: -1:1}
+       ereplp '.*' "${@:1:${#}-1}"
+}
+
+# @FUNCTION: erepld
+# @USAGE: <line-pattern-match> <file>...
+# @DESCRIPTION:
+# Deletes lines in file(s) matching ``[[ ${line} =~ <pattern> ]]``.
+erepld() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       local _esed_repld=1
+       ereplp "${@}"
+}
+
+# @FUNCTION: enewrepld
+# @USAGE: <erepld-argument>... <output-file>
+# @DESCRIPTION:
+# erepl() wrapper to save the result to <output-file>.
+enewrepld() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       local _esed_output=${*: -1:1}
+       erepld "${@:1:${#}-1}"
+}
+
+# @FUNCTION: ereplp
+# @USAGE: <line-match-pattern> <string> <replacement> <file>...
+# @DESCRIPTION:
+# Like erepl() but replaces only on ``[[ ${line} =~ <pattern> ]]``.
+ereplp() {
+       local -i argsmin=$(( ${_esed_repld:-0}==1?2:4 ))
+       (( ${#} >= argsmin )) \
+               || die "too few arguments for ${_esed_cmd[0]:-${FUNCNAME[0]}}"
+
+       [[ ! ${_esed_output} || ${#} -le ${argsmin} ]] \
+               || die "${_esed_cmd[0]} needs exactly one input file"
+
+       local contents changed=false file line newcontents
+       for file in "${@:argsmin}"; do
+               mapfile contents < "${file}" || die
+               newcontents=()
+
+               for line in "${contents[@]}"; do
+                       if [[ ${line} =~ ${1} ]]; then
+                               if [[ ${_esed_repld} == 1 ]]; then
+                                       changed=true
+                               else
+                                       newcontents+=( "${line//"${2}"/${3}}" )
+                                       [[ ${line} != "${newcontents[-1]}" ]] 
&& changed=true
+                               fi
+                       else
+                               newcontents+=( "${line}" )
+                       fi
+               done
+               printf %s "${newcontents[@]}" > "${_esed_output:-${file}}" || 
die
+
+               if [[ ${ESED_VERBOSE} ]]; then
+                       einfo "${FUNCNAME[0]} ${*:1:argsmin-1} ${file} 
${_esed_output:+(to ${_esed_output})}"
+                       diff -u --color --label="${file}" 
--label="${_esed_output:-${file}}" \
+                               <(printf %s "${contents[@]}") <(printf %s 
"${newcontents[@]}")
+               fi
+       done
+
+       ${changed} || die "no-op: ${_esed_cmd[*]:-${FUNCNAME[0]} ${*}}"
+}
+
+# @FUNCTION: enewreplp
+# @USAGE: <ereplp-argument>... <output-file>
+# @DESCRIPTION:
+# ereplp() wrapper to save the result to <output-file>.
+enewreplp() {
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+       local _esed_output=${*: -1:1}
+       ereplp "${@:1:${#}-1}"
+}
+
+# @FUNCTION: efind
+# @USAGE: <find-argument>... -erun <command> <argument>...
+# @DESCRIPTION:
+# find(1) wrapper that dies if no files were found.  <command> can be a shell
+# function, e.g. ``efind ... -erun erepl /usr /opt``.  -print0 is added to
+# find arguments, and found files to end of arguments (``{} +`` is unused).
+# Found files must not exceed args limits.  Use is discouraged if files add
+# up to a large total size (50+MB), notably with slower erepl/esed().  Shell
+# functions called this way are expected to ``|| die`` themselves on error.
+efind() {
+       (( ${#} >= 3 )) || die "too few arguments for ${FUNCNAME[0]}"
+
+       local _esed_cmd=( ${FUNCNAME[0]} "${@}" )
+
+       local find=( find )
+       while (( ${#} )); do
+               if [[ ${1} =~ -erun ]]; then
+                       shift
+                       break
+               fi
+               find+=( "${1}" )
+               shift
+       done
+       find+=( -print0 )
+
+       local files
+       mapfile -d '' -t files < <("${find[@]}" || die "failed: ${find[*]}")
+
+       (( ${#files[@]} )) || die "no files from: ${find[*]}"
+       (( ${#} )) || die "missing -erun arguments for ${FUNCNAME[0]}"
+
+       # skip `|| die` for shell functions (should be handled internally)
+       if declare -f "${1}" >/dev/null; then
+               "${@}" "${files[@]}"
+       else
+               "${@}" "${files[@]}" || die "failed: ${*} ${files[*]}"
+       fi
+}
+
+fi
-- 
2.35.1


Reply via email to