#!/bin/bash
# mv-redir  -- redirected file copying
# Copyright (C) 2009 David A. Wheeler and Institute for Defense Analyses
# 
# "MIT" license
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# 
# This program redirects the mv destination to inside another directory
# REDIR_DESTDIR, if writing to the original destination's directory is
# privileged.  Source files are already in REDIR_DESTDIR will take precedence
# when used as sources.
# This automatically creates redirected target directories if needed.
# Note: this is implemented by parameter-twiddling and calling the "real" cp,
# and depends on the helper program redir_name.
# Example:
#  REDIR_DESTDIR=/tmp/mydir mv myprogram /usr/bin
# will move "myprogram" to "/tmp/mydir/usr/bin/myprogram".
#
# Creating this "mv" program began with this command:
#  cp cp mv
# That's not a command that would make sense very often :-).

# LIMITATIONS/BUGS:
# Options with optional arguments cannot accept arguments that begin with "-",
#    due to a limitation of getopt(1).
# Shell I/O redirection (e.g., < and >) aren't redirected

IFS=`printf '\n\t'`

progname="mv-redir"
err="${progname}: error: "

if `getopt -T >/dev/null 2>&1` ; [ $? -ne 4 ] ; then
  echo "$err Need an enhanced getopt(1) that can handle quoted text." >&2
  exit 1
fi

declare -a newargs
declare -a files
declare -i minus1=-1

# These are the options of GNU "mv", from its code as packaged in
# Fedora 11; code below uses this order
#  {"backup", optional_argument, NULL, 'b'},
#  {"force", no_argument, NULL, 'f'},
#  {"interactive", no_argument, NULL, 'i'},
#  {"no-target-directory", no_argument, NULL, 'T'},
#  {"reply", required_argument, NULL, REPLY_OPTION}, /* Deprecated 2005-07-03,
#                                                       remove in 2008. */
#  {"strip-trailing-slashes", no_argument, NULL, STRIP_TRAILING_SLASHES_OPTION},
#  {"suffix", required_argument, NULL, 'S'},
#  {"target-directory", required_argument, NULL, 't'},
#  {"update", no_argument, NULL, 'u'},
#  {"verbose", no_argument, NULL, 'v'},
#  {GETOPT_HELP_OPTION_DECL},
#  {GETOPT_VERSION_OPTION_DECL},
#  {NULL, 0, NULL, 0}
# NOTE: --reply not supported.

TEMP=`getopt -o "b::fiTS:t:uv" \
      --long backup:: --long force \
      --long interactive --long no-target-directory \
      --long strip-trailing-slashes --long suffix: \
      --long target-directory: \
      --long update --long verbose \
      -n "$progname"  -- "$@"`

# Do this to handle whitespace; see getopt(1).
eval set -- "$TEMP"

no_target_directory=""
target_directory=""

while true ; do
  case "$1" in
    -f|--force|-i|--interactive|--strip-trailing-slashes|-u|--update|-v|--verbose|--help|--version)
      newargs[${#newargs[*]}]="$1"
      shift ;;
    --backup|-b) # Optional argument
      case "$2" in
        -*) # No argument.
          newargs[${#newargs[*]}]="$1"
          shift ;;
        *) # Option has an argument.
          newargs[${#newargs[*]}]="$1=$2"
          shift ; shift ;;
      esac ;;
    -S|--suffix)
      # Pass through the option's required argument.
      newargs[${#newargs[*]}]="$1=$2"
      shift ; shift ;;

    -T|--no-target-directory) # FIXME: Should we do anything about this?
      no_target_directory=true
      newargs[${#newargs[*]}]="$1"
      shift ;;

    -t|--target-directory)
      if [ -n "$target_directory" ] ; then
        echo "$err Multiple target directoriess: '$target_directory' and '$2'" >&2
        exit 1
      fi
      target_directory="$2"
      shift ; shift ;;

    --) shift; break ;;
    -*) # Shouldn't normally get here (getopt should screen these),
        # but if we get here, handle it gracefully.
      echo "$progname: Warning - unrecognized option: $1" >&2
      newargs[${#newargs[*]}]="$1"
      shift ;;
    *) echo "$err Internal error with argument $1!" >&2 ; exit 1 ;;
  esac
done

# Determine list of files and how many, but don't consume last one yet.
while [ $# -gt 1 ] ; do
  files[${#files[*]}]="$1"
  shift
done
file2name=""
number_of_files=${#files[*]}
if [ $# -gt 0 ] ; then
  number_of_files=$(( $number_of_files + 1 ))
fi

# Determine what to do, based on the number of files passed.
if [ $number_of_files -eq 0 ] ; then
  echo "$err No files to process." >&2
  exit 1
elif [ $number_of_files -eq 1 ] ; then
  if [ ! -n "$target_directory" ] ; then
    echo "$err No destination provided." >&2
    exit 1
  fi
  files[${#files[*]}]="$1"
  shift
elif [ $number_of_files -eq 2 ] ; then
  if [ -n "$target_directory" ] ; then
    files[${#files[*]}]="$1"
    shift
  else
    # We must determine if the 2nd argument (now $1) is a directory or file -
    # it's a dir if it ends in "/", is a dir, or is a dir when redirected.
    redirected_name=`redir_name "$1"`
    if [ ${1:$minus1:1} = "/" -o -d "$1" -o -d "$redirected_name" ] ; then
      target_directory="$1"
      shift
    else
      # Special case: "mv file1 file2"; determine if we should redirect file2.
      file2name=`redir_name --write "$1"`
      shift
    fi
  fi
elif [ $number_of_files -gt 2 ] ; then
  if [ -n "$target_directory" ] ; then
    echo "$err Target directory set, but too many files" >&2
    exit 1
  fi
  target_directory="$1"
  shift
fi
if [ $# -gt 0 ] ; then  # This should never happen.
  echo "$err Unprocessed end file." >&2
  exit 1
fi

# Now we have the complete list of files to read from, and the
# target_directory or file2name to write to.

# Redirect where to read files FROM, if there are updated files/dirs there.
# FIXME: If "from" dir exists in redirected dir, do something special?
for i in "${!files[@]}" ; do
  files[$i]=`redir_name --read "${files[$i]}"`
done

if [ -n "$target_directory" ] ; then
  target_directory=`redir_name "$target_directory"`
  /bin/mkdir -p "$target_directory" || exit
  if [ ! -d "$target_directory" ] ; then
    echo "$err Failed to create $target_directory" >&2
    exit 1
  fi
  newargs[${#newargs[*]}]="-t"
  newargs[${#newargs[*]}]="$target_directory"
elif [ -n "$file2name" ] ; then
  # We're copying to a specific named destination file; add it to file list:
  # Slip the new name into the "files" list; cp will interpret this correctly
  files[${#files[*]}]="$file2name"
fi

for file in "${files[@]}" ; do
  newargs[${#newargs[*]}]="$file"
done

exec /bin/mv "${newargs[@]}" || exit

