On 24/03/10 12:15, Pádraig Brady wrote:
> Also I noticed that `mv -b ./file.tmp ./file` is not atomic
> because the renames are done like: rename(file,bak); rename(tmp,file);

I summarized the issues involved with replacing files and it was surprisingly 
popular:
http://www.reddit.com/r/programming/comments/bhnab/replacing_a_file_on_unix_is_hard/

It was indirectly suggested there to use hardlinks for atomic backups,
which I've amended the attached script to do.  Other changes include
falling back to cp -a if --attributes-only is not supported, and
correctly removing all temp files.

I wonder should `mv -b` (and sed -i) use the ln method to
try to be atomic and then fall back to the double rename?
I'll look into it at some stage.

cheers,
Pádraig.
#!/bin/sh

me=$(basename $0)

usage() {
  err=$1; out=2
  [ $err -eq 0 ] && out=1
  printf "\
$me [OPTION] 'FILTER' FILE...
Replace the contents of each FILE after processing with FILTER

      --atomic            at no point leave a FILE unavailable or inconsistent
      --backup[=CONTROL]  make a backup of each existing destination file
  -b                      like --backup but does not accept an argument
  -C, --compare           compare each pair of source and destination files, and
                            in some cases, do not modify the destination at all.
  -p, --preserve-timestamps  apply access/modification times of SOURCE files
                             to corresponding destination files.

  -S, --suffix=SUFFIX     override the usual backup suffix

The backup suffix is \`~', unless set with --suffix or SIMPLE_BACKUP_SUFFIX.
The version control method may be selected via the --backup option or through
the VERSION_CONTROL environment variable.  Here are the values:

  none            never make backups (even if --backup is given)
  numbered        make numbered backups
  existing        numbered if numbered backups exist, simple otherwise
  simple          always make simple backups

Report "$me" bugs to [email protected]
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report "$me" translation bugs to <http://translationproject.org/team/>
" >&$out
  exit $err
}

version() {
  Cmd=$1; Date=2010; Version=8.5 #TODO: auto update
  #TODO: translation
  printf "\
$Cmd (GNU coreutils) $Version
Copyright (C) $Date Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Pádraig Brady.
"
  exit 0
}

if getopt -T >/dev/null 2>&1; test $? -ne 4; then
  # Enhanced `getopt` not available and POSIX `getopts`
  # only supports short options, so revert to simple manual parsing
  if test $# -lt 2; then
    test "$1" = "--help" && usage 0
    test "$1" = "--version" && version "$me"
    usage 1
  fi
else
  OPT=$(
    getopt -o bCpS: \
           --long atomic,backup::,compare,help,\
preserve-timestamps,suffix:,version \
           -n"$me" -- "$@" ||
    usage 1
  )
  eval set -- "$OPT"

  while true; do
    case "$1" in
      --atomic)
        atomic=1; shift 1;;
      -b)
        test "$VERSION_CONTROL" || VERSION_CONTROL="existing"
        backup="$VERSION_CONTROL"; shift;;
      --backup)
        case "$2" in
          "") backup=existing; shift 2;;
          *) backup="$2"; shift 2;;
        esac;;
      -C|--compare) compare=1; shift;;
      -p|--preserve-timestamps) preserve_times=1; shift;;
      -S|--suffix) suffix="$2"; shift 2;;
      --help) usage 0; shift;;
      --version) version "$me"; shift;;
      --) shift; break ;;
      *) printf "%s\n" "Option processing error" >&2; exit 1;;
    esac
  done

  if test "$backup"; then
    backup="--backup=$backup"
    test "$suffix" && backup="$backup --suffix=$suffix"
  fi

  if test $# -lt 1; then
    printf "\
$me: missing file operand
Try \`$me --help' for more information.
"
  fi
fi

filter="$1"; shift

cleanup() { rm -f "$tf"; }
trap "cleanup" EXIT

filter_file() {
  ret=0

  if $filter < "$file" > "$tf"; then
    if test "$preserve_times"; then
      touch "$tf" --reference="$file" || { ret=1; fail=1; }
    fi
  else
    ret=1; fail=1
  fi

  if test $ret = 0; then
    if test "$compare"; then
      cmp -s -- "$tf" "$file"
      status=$?
      test $status -eq 0 && { ret=1; } # Don't process further
      test $status -gt 1 && { ret=1; fail=1; }
    fi
  fi

  return $ret
}

# Revert to a slower way of copying attributes if the fast way is unavailable
attr="--attributes-only"
cp --attributes-only --version >/dev/null 2>&1 || attr="-a"

fail=0
for file in "$@"; do
  dir=$(dirname -- "$file")
  cleanup
  tf=$(mktemp -q --tmpdir="$dir")
  #XXX: Need to cleanup always?
  backup_err=0
  if test -e "$tf" && cp $attr -- "$file" "$tf" 2>/dev/null; then
    # Modify file atomically.

    # Note if we passed $backup to `mv` then the data will be
    # atomically consistent but the file may be missing for a short period.
    # Therefore we make an explicit backup first.
    # Note `sed -i.bak` uses the quick rename() method, thus having the issue.
    # Note sed/mv could use this hardlink method to implement backups?
    # XXX: should try the first hardlink in the non atomic enforcing case also?
    if test "$backup" && test "$atomic"; then
      mv_backup=""
      bak=$(cp $backup -vf -l "$file" "$file" | sed "s/.* -> \`\(.*\)'/\1/") ||
      bak=$(cp $backup -vf -a "$file" "$file" | sed "s/.* -> \`\(.*\)'/\1/") ||
      { backup_err=1; fail=1; rm -f "$bak"; }
    else
      mv_backup="$backup"
    fi

    # We could (prompt to) `chmod u+rw` here to allow updating non rw files?
    if ! test -w "$file"; then
      # This clause is only so we present a better error message when
      # the file is readonly, as then the error is reported against "$tf"
      rm -f "$bak"
      printf "%s\n" "$me: $file: Permission denied" >&2
      fail=1
    else
      test $backup_err=0 && filter_file &&
      { mv $mv_backup -- "$tf" "$file" || fail=1; } || # rename
      rm -f "$bak"
    fi
  elif test "$atomic"; then # repeat to output errors
    cleanup
    tf=$(mktemp --tmpdir="$dir") &&
    cp $attr -- "$file" "$tf"
  else
    # $dir may not be writeable.  In this case we
    # use $TMPDIR, but don't use mv to unlink/copy
    # as $TMPDIR might not support all attrs of $dir.
    # Also we can't unlink in unwriteable dir.
    # We could (prompt to) `chmod u+rw` here to allow updating non rw files?
    cleanup # dir/file.tmp
    tf=$(mktemp)
    filter_file &&
    { cp $backup -- "$tf" "$file" || backup_err=1; fail=1; } # truncate and copy
    test $backup_err = 0 && test "$preserve_times" &&
      { touch "$file" --reference="$tf" || fail=1; }
  fi
done

exit $fail

Reply via email to