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