#!/bin/bash
# cp-redir  -- redirected file copying
# Copyright (C) 2009 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 cp 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 ./cp myprogram /usr/bin
# will copy "myprogram" to "/tmp/mydir/usr/bin/myprogram".

# 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
# --parents supported, but won't work if the original files are being
#    copied from a redirected location.

IFS=`printf '\n\t'`

progname="cp-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 "cp", from its code; code below uses this order
#   {"archive", no_argument, NULL, 'a'},
#   {"backup", optional_argument, NULL, 'b'},
#   {"copy-contents", no_argument, NULL, COPY_CONTENTS_OPTION},
#   {"dereference", no_argument, NULL, 'L'},
#   {"force", no_argument, NULL, 'f'},
#   {"interactive", no_argument, NULL, 'i'},
#   {"link", no_argument, NULL, 'l'},
#   {"no-clobber", no_argument, NULL, 'n'},
#   {"no-dereference", no_argument, NULL, 'P'},
#   {"no-preserve", required_argument, NULL, NO_PRESERVE_ATTRIBUTES_OPTION},
#   {"no-target-directory", no_argument, NULL, 'T'},
#   {"one-file-system", no_argument, NULL, 'x'},
#   {"parents", no_argument, NULL, PARENTS_OPTION},
#   {"path", no_argument, NULL, PARENTS_OPTION},   /* Deprecated.  */
#   {"preserve", optional_argument, NULL, PRESERVE_ATTRIBUTES_OPTION},
#   {"recursive", no_argument, NULL, 'R'},
#   {"remove-destination", no_argument, NULL, UNLINK_DEST_BEFORE_OPENING},
#   {"sparse", required_argument, NULL, SPARSE_OPTION},
#   {"strip-trailing-slashes", no_argument, NULL, STRIP_TRAILING_SLASHES_OPTION},
#   {"suffix", required_argument, NULL, 'S'},
#   {"symbolic-link", no_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}

TEMP=`getopt -o "abdfHilLnprst:uvxPRS:T" \
      --long archive --long backup:: --long copy-contents \
      --long dereference --long force --long interactive \
      --long link --long no-clobber --long no-dereference \
      --long no-preserve: --long no-target-directory \
      --long one-file-system --long parents --long path \
      --long preserve:: --long recursive --long remove-destination \
      --long sparse: --long strip-trailing-slashes --long suffix: \
      --long symbolic-link --long target-directory: --long update \
      --long verbose --long help --long version \
      -n "$progname"  -- "$@"`

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

no_target_directory=""
target_directory=""

while true ; do
  case "$1" in
    -a|--archive|-d|--copy-contents|-L|--dereference|-f|--force|-i|--interactive|-l|--link|-n|--no-clobber|-P|--no-dereference|-x|--one-file-system|--parents|--path|-R|-r|--recursive|--remove-destination|--strip-trailing-slashes|-s|--symbolic-link|-u|--update|-v|--verbose|-p|-b|--help|--version)
      newargs[${#newargs[*]}]="$1"
      shift ;;
    --backup|--preserve) # Optional argument
      case "$2" in
        -*) # No argument.
          newargs[${#newargs[*]}]="$1"
          shift ;;
        *) # Option has an argument.
          newargs[${#newargs[*]}]="$1=$2"
          shift ; shift ;;
      esac ;;
    --copy-contents|--no-preserve|--parents|--remove-destination|--sparse|--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 directories: '$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
      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
  # FIXME: This rename won't work with --parents:
  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/cp "${newargs[@]}" || exit

