+ if [[ ${contents[i]} != "${newcontents}" ]]; then
+ changed=1
+ [[ -v verbose ]] || break
+ fi
+
+ [[ -v verbose ]] &&
+ diff -u --color --label="${files[i]}"{,} \
+ <(echo "${contents[i]}") <(echo "${newcontents}")
+ [[ ${contents} != "${newcontents}" ]] && changed=1
+
+ [[ -v verbose ]] &&
+ diff -u --color --label="${files[*]}" --label="${_esed_output}" \
+ <(echo "${contents}") <(echo "${newcontents}")
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.
+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})}"
+}
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.
On Fri, Jun 03, 2022 at 09:14:05AM +0200, Ulrich Mueller wrote:[...]
brittle. Also, don't use eval because it is evil.
This is static evals, they're just evaluating a flat string and it's
no different than.. well just running it as-is except it works around
a noise issue (the 2>/dev/null doesn't register without it).
eval is mostly evil when it's:
eval "${expanded_variable}=${what_is_this_even}"
Seeing eval "var=\${not_expanded}" as "evil" makes no sense to me,
it's a harmless warning silencer. I feel this is just overreaction
to the eval keyword (also in other dev ML post).
To reproduce the warning:
$ var=$(printf "\0") # var=$(<file-with-null-bytes)
bash: warning: command substitution: ignored null byte in input
doesn't work: var=$(printf "\0") 2>/dev/null
works: eval 'var=$(printf "\0")' 2>/dev/null
Sysop: | Keyop |
---|---|
Location: | Huddersfield, West Yorkshire, UK |
Users: | 475 |
Nodes: | 16 (2 / 14) |
Uptime: | 18:47:03 |
Calls: | 9,487 |
Calls today: | 6 |
Files: | 13,617 |
Messages: | 6,121,093 |