#!/bin/bash

# xyargs
# by Andrew Stubbs

function print_usage {
	cat <<EOF
Usage: `basename $0` '{actions} [options] <command> <cmd_options> [--] [files]  ....'

 Apply the given command multiple times in the form:
   <command> <cmd_options> file newfile
 where newfile is the result of modifications to file.
 When --redirect is given (or command is 'sed'):
   <command> <cmd_options> file > newfile

`basename $0` will interpret the first non-option as the command name
and, after that, the first existing filename as the start of the file
list to be processed.
Use '--' if the cmd_options contain filenames or if the first filename
does not exist in the filesystem yet (in order to remove ambiguity).

If no files are given on the command line then they are expected
to be given on standard input, one per line (blank lines not allowed)

Actions (filename transformations):
  -p --prefix PREFIX    Add a prefix to each filename.
  -s --suffix SUFFIX    Add a suffix to each filename.
  -c --change PATTERN   How to change each filename.
                        As for bash substitution.
                        e.g. /.s/.o  #.old  %my
  -P --prefix-path PREFIX
  			As for --prefix, but applied to the whole pathname.
  -C --change-path PATTERN
  			As for --change, but applied to the whole pathname.
or
  -n --no-change        Output name = Input name.
                        Using temporary intermediate file.
  -N --really-no-change Output name really = Input name.

Options:
  -r --redirect         Output file via redirect.
  -R --redirect-with OPT
                        Like -r but use OPT instead of '>'.
                        e.g. '-o' for gcc.
  -d --delimiter OPT    Use OPT instead of -- as the delimiter.
  -v --verbose          Print commands used.
  -i --interactive      Prompt before action.
  -t --test-run         Don't actually do anything.
  -S --swap      	Swap filenames round. I.e. new then old
  --help                Print this message

The actions and options may be given in any order, but must
be before the command (which does not begin with '-').
The --prefix, --suffix and --change actions may be combined,
and each may be used multiple times.

Example 1: Convert all makefile to Makefile
  > find . -name makefile | xyargs -c /makefile/Makefile mv
  mv dir/makefile dir/Makefile

Example 2: Convert all instances of WinNT to Win2K without changing
the filenames (note sed command implies --redirect)
  > find . | xyargs -n sed 's/WinNT/Win2K/g'
  sed 's/WinNT/Win2K/g' win.c > temp
  mv temp win.c

Example 3: Compile all .c files to .o files
  > find . -name '*.c' | xyargs -c /.c/.o -R -o gcc -c
  gcc -c main.c -o main.o

Example 4: Copy all .c files in the current directory to .orig
  > xyargs -s .orig cp *.c
  cp main.c main.c.orig

Example 5: Diff one file against many others and keep the output
  > xyargs -s .diff -r diff -u file -- file[0-9]
  diff -u file file0 > file0.diff
(Note that '--' has been used because 'file' exists and would be
just added to the file list)
EOF
	exit 1
}

[ "$1" == "" ] && print_usage

changes=()
nochange=
notemp=
redirect=
delimiter=--
verbose=0
interactive=0
testrun=0
wholepaths=0
swap=0

# Read all arguments up the first that does not start with '-'
while [ "${1#-}" != "$1" ]; do
  case $1 in
  -p | --prefix ) shift; changes=( "${changes[@]}" "prefix:$1" ) ;;
  -s | --suffix | -S ) shift; changes=( "${changes[@]}" "suffix:$1" ) ;;
  -c | --change ) shift; changes=( "${changes[@]}" "change:$1" ) ;;
  -P | --prefix-path ) shift; changes=( "${changes[@]}" "prefix-path:$1" ) ;;
  -C | --change-path ) shift; changes=( "${changes[@]}" "change-path:$1" ) ;;
  -n | --no-change ) nochange=1 ;;
  -N | --really-no-change ) nochange=1; notemp=1 ;;
  -r | --redirect ) redirect='>' ;;
  -R | --redirect-with ) shift; redirect=$1 ;;
  -d | --delimiter ) shift; delimiter=$1 ;;
  -v | --verbose ) verbose=1 ;;
  -i | --interactive ) interactive=1 ;;
  -t | --test-run ) testrun=1; verbose=1 ;;
  -S | --swap ) swap=1 ;;
  --help ) print_usage ;;
  * ) echo "`basename $0`: unknown option '$1'"; exit 1 ;;
  esac
  shift
done

# No more options starting with '-'
# This must be the command name
case "$1" in
sed ) cmd=sed redirect='>' ;;
* ) cmd=$1 ;;
esac
shift

case "$notemp"-"$redirect" in
1-\>* ) echo `basename $0`: --really-no-change cannot be used with a shell redirect 1>&2
       exit 1
       ;;
esac

if [ "$cmd" == "" ]; then
  echo `basename $0`: No command given 1>&2
  exit 1
fi

# Read command options upto first filename (it must exist)
# or delimiter
# or end of list
options=()
while [ $# -ne 0 ] && [ "$1" != "$delimiter" ] && [ ! -e "$1" ]; do
  options=("${options[@]}" "$1")
  shift
done
[ "$1" == "$delimiter" ] && shift

# Any more words are either files or further options (if -- exists)
files=()
while [ $# -ne 0 ] && [ "$1" != "$delimiter" ]; do
  files=("${files[@]}" "$1")
  shift
done
if [ "$1" == "$delimiter" ]; then
  # We found a delimiter so reclassify any files
  # found upto this point as options.
  options=("${options[@]}" "${files[@]}")
  files=()
  shift
fi
while [ $# -ne 0 ]; do
  files=("${files[@]}" "$1")
  shift
done

# If there were no filenames given then look for them on stdin
[ ${#files[@]} -eq 0 ] && stdin=yes

# Check no mutually exclusive options have been given
if [ "$nochange" == "1" ] && [ "${changes[*]}" != "" ]; then
  echo `basename $0`: --no-change and --really-no-change cannot be used with --prefix, --suffix or --change 1>&2
  exit 1
fi

# Check that at least one action option has been given
if [ "$nochange" == "" ] && [ "${changes[*]}" == "" ]; then
  echo `basename $0`: No action given 1>&2
  exit 1
fi

if [ "$nochange" == 1 ] && [ "$notemp" != 1 ]; then
  # this means that a temporary file will be used in case the command can't
  # read and write the same file (are there any that can?)
  alter_in_place=yes

  if [ "$swap" == 1 ]; then
  	# swap has no meaning with nochange, but will mess up the temp file
	swap=0
  fi
fi

# Initialise nextfile according to the input method.
if [ "$stdin" == yes ]; then
  read -r
  nextfile=$REPLY
else
  i=0
  nextfile=${files[(( i++ ))]}
fi

# Loop until there are no more input files.
while [ "$nextfile" != "" ]; do
  old="$nextfile"

  # Set 'new' to the filename to be created.
  # (Or the temporary file if the name isn't changing.)
  if [ "$alter_in_place" == yes ]; then
    new=/tmp/mycmd.$$
  else
    new=$old
    for change in "${changes[@]}"; do
      dir=`dirname "$new"`/
      dir=${dir#./}
      base=`basename "$new"`
      case $change in
        prefix:* )      new="$dir${change#prefix:}$base" ;;
        prefix-path:* ) new="${change#prefix-path:}$new" ;;
	suffix:* )      new="$new${change#suffix:}" ;;
	change:* )      eval "new=\"$dir\${base${change#change:}}\"" ;;
	change-path:* ) eval "new=\"\${new${change#change-path:}}\"" ;;
      esac
    done
  fi

  # If the command requires the new name first then swap them over.
  if [ $swap == 1 ]; then
    tmp="$new"
    new="$old"
    old="$tmp"
  fi

  # Make sure the filename has actually changed (if it was meant to)
  # to ensure we don't accidentally trash anything.
  if [ "$new" != "$old" ] || [ "$notemp" == 1 ]; then
    # Prompt the user in interactive mode.
    answer=Run
    if [ $interactive == 1 ]; then
      echo "command:" "$cmd" "${options[@]}" "$old" $redirect "$new" 1>&2
      select answer in Run Skip Abort "Run All"; do
        case $answer in
          Run ) break ;;
	  Skip ) break ;;
          Abort ) echo Aborted; exit 1 ;;
	  "Run All" ) interactive=0; answer=Run; break ;;
        esac
      done
    fi

    # Run the command, if the didn't say otherwise, and we aren't in testmode.
    if [ $answer == Run ]; then
      status=0
      [ $verbose == 1 ] && echo "$cmd" "${options[@]}" "$old" $redirect "$new"
      if [ $testrun == 0 ]; then
        eval '"$cmd"' '"${options[@]}"' '"$old"' $redirect '"$new"'
	status=$?
      fi

      # If we are using an intermediate temp file then
      # move the result to the the final destination.
      if [ "$alter_in_place" == yes ]; then
	if [ "$status" != 0 ]; then
	  echo "Command returned failure. '$old' remains unchanged."
	else
	  [ $verbose == 1 ] && echo mv -f "$new" "$old"
	  [ $testrun == 0 ] && mv -f "$new" "$old"
	fi
      fi
    fi
  else
    [ $verbose == 1 ] && echo "No action to '$old'"
  fi

  # Get the next filename and go round the loop again.
  if [ "$stdin" == yes ]; then
    read -r
    nextfile=$REPLY
  else
    nextfile=${files[(( i++ ))]}
  fi
done