#!/bin/bash
# ln-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.

IFS=`printf '\n\t'`

progname="ln-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 "ln", from its code; code below uses this order
#  {"backup", optional_argument, NULL, 'b'},
#  {"directory", no_argument, NULL, 'F'},
#  {"no-dereference", no_argument, NULL, 'n'},
#  {"no-target-directory", no_argument, NULL, 'T'},
#  {"force", no_argument, NULL, 'f'},
#  {"interactive", no_argument, NULL, 'i'},
#  {"suffix", required_argument, NULL, 'S'},
#  {"target-directory", required_argument, NULL, 't'},
#  {"symbolic", no_argument, NULL, 's'},
#  {"verbose", no_argument, NULL, 'v'},
#  {GETOPT_HELP_OPTION_DECL},
#  {GETOPT_VERSION_OPTION_DECL},
#  {NULL, 0, NULL, 0}


TEMP=`getopt -o "b::FnTfiS:t:sv" \
      --long backup:: --long directory \
      --long no-dereference --long no-target-directory --long force \
      --long interactive --long suffix: --long target-directory: \
      --long symbolic \
      --long verbose --long help --long version \
      -n "$progname"  -- "$@"`

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

no_target_directory=""
target_directory=""
symbolic=""

while true ; do
  case "$1" in
    -n|--no-dereference|-f|--force|-i|--interactive|--help|--version)
      newargs[${#newargs[*]}]="$1"
      shift ;;
    -b|--backup) # 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 directories: '$target_directory' and '$2'" >&2
        exit 1
      fi
      target_directory="$2"
      shift ; shift ;;

    -s|--symbolic)
      symbolic="-s"
      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 [ -z "$target_directory" ] ; then
    # "2nd form": one file and no -t directory.  Thus, "." is target direct.
    target_directory="."
  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: "ln file1 file2".
      file2name="$1"
      shift
    fi
  fi
elif [ $number_of_files -gt 2 ] ; then
  # Differentiates between form 3 (no given target) and form 4 (target given)
  if [ -n "$target_directory" ] ; then
    files[${#files[*]}]="$1"
  else
    target_directory="$1"
  fi
  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 FROM (targets), if there are updated files/dirs there.
# Don't do this for symbolic files, since it's the symbol values themselves
# that we are storing in that case.
if [ "$symbolic" = "" ] ; then
  for i in "${!files[@]}" ; do
    from_dir=`dirname "${files[$i]}"`
    from_redir=`redir_name "$from_dir"`
    if [ "$from_dir" != "$from_redir" ] ; then
      from_fullname="$from_redir/"`basename "${files[$i]}"`
      if [ -r "$from_fullname" ] ; then
        files[$i]="$from_fullname"
      fi
    fi
  done
fi

if [ -n "$file2name" ] ; then
  # Redirect name of file2, if appropriate.
  files[${#files[*]}]=`redir_name --write "$file2name"`
else
  if [ -z "$target_directory" ] ; then
    echo "$err Cannot find a destination." >&2
    exit 1
  else
    redir_target_directory=`redir_name "$target_directory"`
    if [ "$target_directory" != "$redir_target_directory" ] ; then
      /bin/mkdir -p "$redir_target_directory" || exit
    fi
    newargs[${#newargs[*]}]="-t"
    newargs[${#newargs[*]}]="$target_directory"
  fi
fi

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

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

