>>>>> On Tue, 31 May 2022, Ionen Wolkens wrote:

> +# @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.

This sounds like a simple enough task ...

> +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}" )
> +
> +                     # eval 2>/dev/null to silence \0 warnings if sed binary 
> files
> +                     eval '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}"
> +
> +             eval '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
> +
> +             [[ ${contents} != "${newcontents}" ]] && changed=1
> +
> +             [[ -v verbose ]] &&
> +                     diff -u --color --label="${files[*]}" 
> --label="${_esed_output}" \
> +                             <(echo "${contents}") <(echo "${newcontents}")
> +     else
> +             [[ -v verbose ]] && einfo "${FUNCNAME[0]}: sed -i ${*} ..."
> +
> +             sed -i "${@}" || die "failed to run: sed -i ${*}"
> +
> +             for ((i=0; i<${#files[@]}; i++)); do
> +                     eval 'newcontents=$(<"${files[i]}")' 2>/dev/null \
> +                             || die "failed to read: ${files[i]}"
> +
> +                     if [[ ${contents[i]} != "${newcontents}" ]]; then
> +                             changed=1
> +                             [[ -v verbose ]] || break
> +                     fi
> +
> +                     [[ -v verbose ]] &&
> +                             diff -u --color --label="${files[i]}"{,} \
> +                                     <(echo "${contents[i]}") <(echo 
> "${newcontents}")
> +             done
> +     fi
> +
> +     [[ -v changed ]] \
> +             || die "no-op: ${FUNCNAME[0]} ${*}${_esed_command:+ (from: 
> ${_esed_command})}"
> +}

... but then it's almost 100 lines of shell code, including very
convoluted parsing of parameters. The code for detection whether a
parameter is actually a file is a heuristic at best and looks rather
brittle. Also, don't use eval because it is evil.

So IMHO this isn't the right approach to the problem. If anything, make
it a simple function with well defined arguments, e.g. exactly one
expression and exactly one file, and call "sed -i" on it.

Then again, we had something similar in the past ("dosed" as a package
manager command) but later banned it for good reason.

Detection whether the file contents changed also seems complicated.
Why not compute a hash of the file before and after sed operated on it?
md5sum or even cksum should be good enough, since security isn't an
issue here.

Ulrich

Attachment: signature.asc
Description: PGP signature

Reply via email to