vip (Source)

#!/bin/bash

# VIP - "VI/Preserve" Edit one or more files and preserve their modification 
# timestamps and inode number
# The modification time is not perfectly preserved; it is incremented by one
# second so rsync and similar utilities that examine timestamps will know the
# file has actually changed.

# BUUS: This script is part of Brian's Useful Utilities Set

function die {
    echo "$1" >&2
    exit 1
}

[ -z "$1" ] && die "usage: vip filename [filename ...]"

DEBUG=false
if [ "$1" = -d ]; then DEBUG=true; shift; fi

[ -d /dev/shm ] && RAMDISK_SW=true || RAMDISK_SW=false

typeset -i FILE_NUM=0
declare -a FILE_LIST            # List of files we're editing
declare -a FILE_MTIME_SEC       # Timestamps on those files (one second granularity)
declare -a FILE_MTIME_FINE      # Timestamps on those files (fine granularity)
declare -a FILE_EDIT_LIST       # Files as they were copied for editing
WARN_SW=false
EDIT_SW=false               # true = we found at least one file to edit

export TZ=Z

# Figure out which files from the command line list we can edit
for FILE in "$@"
do
    $DEBUG && echo "debug: Considering \"$FILE\""
    while [ -L "$FILE" ]; do FILE="$(readlink "$FILE")"; done
    if [ ! -f $FILE ]
    then
        echo "vip: $FILE: not found -- skipping"
        WARN_SW=true
    elif [ ! -w "$FILE" ]
    then
        echo "vip: $FILE: no write permission -- skipping" >&2
        WARN_SW=true
    else
        FILE_LIST[$FILE_NUM]="$FILE"
        $DEBUG && echo "debug: OK to edit as number $FILE_NUM"
        FILE_NUM=$((FILE_NUM+1))
        EDIT_SW=true
    fi
done

# Pause if there were warnings but we also have files we can edit
if $WARN_SW && $EDIT_SW
then
    echo -n "Press Enter to continue: "; read JUNK
fi
# Prepare each file for editing
N=0
for FILE in "${FILE_LIST[@]}"
do
    eval $(stat --format 'F_SIZE=%s F_MTIME_SEC=%Y F_MTIME_FINE="%y"' "$FILE")
    # 2016-02-18 18:32:02.141592600 -0600       <-- F_MTIME_FINE
    # 01...+....1....+....2....+....3....+
    MTIME_FRACTIONAL_PART=""
    FILE_MTIME_SEC[$N]=${F_MTIME_SEC}
    FILE_MTIME_FINE[$N]=${F_MTIME_FINE}
    [ "${#F_MTIME_FINE}" -gt 19 ] && MTIME_FRACTIONAL_PART=".${F_MTIME_FINE:20:9}"
    FILE_MTIME_FRACTIONAL_PART[$N]=${MTIME_FRACTIONAL_PART}
    FILE_EDIT_FN="$(basename "$FILE")"
    if $RAMDISK_SW && [ $F_SIZE -lt 1048576 ]
    then
        FILE_EDIT_FN="/dev/shm/__$N.$FILE_EDIT_FN"
    else
        FILE_EDIT_FN="/tmp/__$N.$FILE_EDIT_FN"
    fi
    $DEBUG && echo "debug: [$N] $FILE, copy to \"$FILE_EDIT_FN\""
    cp -a "$FILE" "$FILE_EDIT_FN"
    FILE_EDIT_LIST[$N]="$FILE_EDIT_FN"
    N=$((N+1))
done

# Edit the copied files
$DEBUG && $EDIT_SW && echo "debug: starting vim"
$EDIT_SW && vim "${FILE_EDIT_LIST[@]}"

# Cat the edited files back to the original files (which keeps the original
# file on the same inode) and update its timestamp
N=0
for FILE in "${FILE_LIST[@]}"
do
    EDIT_FN="${FILE_EDIT_LIST[$N]}"
    EDIT_MTIME_SEC="$(stat --format '%Y' "$EDIT_FN")"
    if $DEBUG
    then
        echo "debug: considering \"$FILE\""
        echo "debug: * Original mtime: ${FILE_MTIME_SEC[$N]} | ${FILE_MTIME_FINE[$N]}"
        echo "debug: *   Edited mtime: $EDIT_MTIME_SEC"
    fi
    if [ "${FILE_MTIME_SEC[$N]}" = "$EDIT_MTIME_SEC" ]
    then
        echo "$FILE: not changed"
    else
        $DEBUG && echo "debug: * cat from \"$EDIT_FN\""
        cat "$EDIT_FN" >"$FILE"
        X_MTIME="${FILE_MTIME_SEC[$N]}${FILE_MTIME_FRACTIONAL_PART[$N]}"
        $DEBUG && echo "debug: * touch --date @(${X_MTIME} + 1) \"$FILE\""
        touch --date @$((FILE_MTIME_SEC[$N] + 1))${FILE_MTIME_FRACTIONAL_PART[$N]} "$FILE"
        $DEBUG && echo "debug: ${FILE}: $(stat -c '%y' $FILE)"
    fi
    rm -f "$EDIT_FN"
    N=$((N+1))
done

# vim: tabstop=4