Idempotent redirect to a file

July 6, 2017, 11:33 p.m.

Let's say you want to redirect the STDOUT of some command to a file (a config file for example) using the (bash) shell. In ideal case destination file should not be touched if it already has exactly the same content you are going to write into it. How to do that in a single pipe?

Here's how:

echo "New contents" | diff -duaN "$target_path" - | patch --binary -s -p0 "$target_path"

Full example

#!/usr/bin/env bash

set -eu -o pipefail

to_stderr () {
  >&2 cat
}

printable_only () {
  tr -cd '\11\12\15\40-\176'
}

pipe_debug () {
  tee >(printable_only | to_stderr)
}

to_file () {
  local target_path="$1"
  local restore_pipefail

  # diff will return non-zero exit code if file differs, therefore
  # pipefail shell attribute should be disabled for this
  # special case
  restore_pipefail=$(shopt -p -o pipefail)
  set +o pipefail

  diff -duaN "$target_path" - | pipe_debug | patch --binary -s -p0 "$target_path"

  eval "$restore_pipefail"
}

md5 () {
  md5sum -b | cut -f 1 -d ' '
}

sample_binary_data () {
  local i

  for (( i=0; i<=255; i++ )); do
    printf "\x$(printf %x "$i")"
  done
}

sample_text_data () {
  cat <<EOF
Here be dragons
EOF
}


testfile='./testfile'

echo "Binary data MD5 $(sample_binary_data | md5)"
echo "Text data MD5: $(sample_text_data | md5)"

sample_text_data >"$testfile"

echo "$testfile is going to be modified"
sample_binary_data | to_file "$testfile"

echo "$testfile is NOT going to be modified"
sample_binary_data | to_file "$testfile"

echo "$testfile MD5: $(md5 <"$testfile")"

echo "$testfile is going to be modified"
sample_text_data | to_file "$testfile"

echo "$testfile is NOT going to be modified"
sample_text_data | to_file "$testfile"

echo "$testfile MD5: $(md5 <"$testfile")"

Sample output

Binary data MD5 e2c865db4162bed963bfaa9ef6ac18f0
Text data MD5: 890923a3ff411987e645531cc33548f6
./testfile is going to be modified
--- ./testfile  2017-07-07 07:21:39.853604436 +0100
+++ -   2017-07-07 07:21:39.856596473 +0100
@@ -1 +1,2 @@
-Here be dragons
+   
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
\ No newline at end of file
./testfile is NOT going to be modified
./testfile MD5: e2c865db4162bed963bfaa9ef6ac18f0
./testfile is going to be modified
--- ./testfile  2017-07-07 07:21:39.994605593 +0100
+++ -   2017-07-07 07:21:40.146792083 +0100
@@ -1,2 +1 @@
-   
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
\ No newline at end of file
+Here be dragons
./testfile is NOT going to be modified
./testfile MD5: 890923a3ff411987e645531cc33548f6