#!/bin/bash

#-----------------------------------------------------------------------
# Copyright (C) 2001-2003 by Daniel Käps.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# If you do not have a copy of the GNU General Public License write to
# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#-----------------------------------------------------------------------

# Changes:
# - 2000-07-16: now support for spaces and piping, now ACTION must
#   take input from stdin.
# - 2000-08-05: TODO: use cmp (or diff) to eliminate unecessary files 
#   modifications, deal with errors
# - 2000-08-05: TODO: support interactive confirmation, maybe show diff 
#   of what's changed or support calling diff
# - 2001-09-07: 
#   - added parsing and code for options
# - 2001-10-06:
#   - added comparison to check if the resulting file contains any 
#     changes, if there are no changes, nothing is done but removing the 
#     temporary file
#   - added code to trap signals to remove the tempfile
#   - temporary file is now created by the 'mktemp' program, because the 
#     previously used 'tempfile' program isn't installed on many systems
# - 2001-10-07:
#   - corrected some faulty error handling
#   - added code to detect all errors in a piping construct (before, only
#     the error code of the last program in a pipe was used)
#   - added option to preserve file permissions
# - 2001-10-09:
#   - fixed bug with escaping of the "'" char (only needed for the -S 
#     option)
# - 2001-10-10:
#   - changed script interpreter command from /bin/sh to /bin/bash 
#     (because none of the other tested shells seem to have support 
#     for the ${PIPESTATUS[]} variable)
# - 2001-12-05:
#   - added option for a "dry" run
# - 2002-01-20:
#   - added posibility to answer "a" or "all" when confirming
#   - now names of changed files are printed
#   - added option --quiet to suppress output of names of changed files
#   - split up help into short (--help) and long variant (--manual)
# - 2002-04-21:
#   - added option --gzip to treat files compressed with gzip
# - 2002-10-02:
#   - added option '--version' to show version identificator
# - 2002-11-14:
#   - added option '--export-filename' to export the current filename
#     to the environment variable 'MF_FILENAME'
# - 2003-06-06:
#   - added support for modifying the target of symbolic links
#     (option '--modify-symlinks')
#
# Notes:
# - filtering may not work for binary files as expected, in case the
#   media is mounted with special treatment for text files.
#
# Tested:
# - 2001-10-08: - Debian GNU/Linux 2.2; GNU bash version 2.03.0(1)-release
# - 2001-10-09: - SuSE 6.4 Linux; GNU bash 2.03.0(1)-release
# - 2001-10-10: - Win95; Cygwin b20.1; GNU bash version 2.02.1(2)-release
# - 2002-11-28: - Debian GNU/Linux 2.2; GNU bash version 2.03.0(1)-release

#-------------------------------------------------------------

# terminate program in case of any unhandled error:
set -e

IsActionInMultipleArguments=false
IsExportFileName=false
IsInteractive=false
IsDryRun=false
IsPreview=false
IsShowDiff=false
IsPrintModifiedFileNames=true
IsVerbose=false
IsKeepModificationTime=false
IsKeepPermissions=false
IsGzipped=false
IsModifySymlink=false
FileDescription="file"

ProgramName=`basename "$0" .sh`
ProgramVersionString="1.1.0b-02 (2003-06-06)"
UseMktemp=true
TempDir="${TMPDIR:-/tmp}"
TempFileName=""

#-------------------------------------------------------------

CommonHelpString()
{
cat <<.
USAGE
    $0 OPTIONS ACTION FILES
    $0 -S OPTIONS ACTIONARGS -- FILES

DESCRIPTION
    Modify FILES by piping each file of FILES through ACTION in a 
    temporary file before moving back the file to the original location.

OPTIONS
    -d, --diff              do a diff to show changes
    -E, --export-filename   export current filename to the environment 
                            variable 'MF_FILENAME'
    --help                  display short help text
    -i, --interactive       prompt before applying changes
    -l, --less              call 'less' to show the resulting file before
                            making any changes
    --man, --manual         display complete help with examples etc.
    -n, --dry-run           don't actually modify files
    -p, --keep-permissions  keep file permissions
    -q, --quiet             don't print names of modified files
    -S                      use syntax ACTIONARGS -- FILES rather than put 
                            the whole ACTION construct into a single argument
    -t, --keep-mod-time     keep the modification time of the file
    -v, --verbose           be a bit verbose
    -V, --version           output version information and exit
    -y, --modify-symlinks   modify destination/target of symbolic links
                            instead of the contents of files
    -z, --gzip              (experimental:) assume FILES are compressed 
                            with 'gzip'

.
}

#-------------------------------------------------------------

ShortHelpString()
{
CommonHelpString

cat <<.
To display examples, notes and copying conditions, use the "--manual" option.
.
}

#-------------------------------------------------------------

LongHelpString()
{
CommonHelpString

cat <<.
NOTES
    1 ACTION: input must come from stdin, output must go to stdout
    2 ACTION can be a piping construct
    3 If ACTION fails, no changes will be made
    4 If the ACTIONARGS syntax is used:
      - arguments not prefixed with "@" are put into quotes
      - arguments prefixed with "@" are not put into quotes, and the 
        "@" prefix will be removed from the argument
    5 If the resulting file is identical with the original file, nothing 
      will be done
    6 If the option '--gzip' is used, the filenames must have the extension 
      ".gz", because some needed programs (e.g. zdiff) may require this.

EXAMPLES
    Remove the CR (carriage return) character (0xD, "\\r") in files *.sh 
    (interactive confirmation, keep file modification times):
        $ProgramName -S -i --keep-mod-time tr -d "\\r" -- *.sh

    Change tabulator width of 5 to a tab width of 4 in files *.c:
        $ProgramName "expand -t 5 | unexpand -t 4" *.c

    Same as above, but using the -S syntax:
        $ProgramName -S expand -t 5 "@\\|" unexpand -t 4 -- *.c

    Replace in *.c*, *.h files all strings "<common.h>" with 
    "<lib/common.h>", displaying the differences and using interactive 
    confirmation:
        $ProgramName -d -i -v "sed 's,<common\\\\.h>,<lib/common.h>,g'" \\
            *.c* *.h

    Replace in files *.c* *.h all words "int" with "long" using perl's
    regexp replacement (showing a diff, interactive)
        $ProgramName -i -d "perl -p -e 's/\\\\bint\\\\b/long/g'" *.c* *.h

    Replace in files *.txt all "-chars with "'" (display result using 
    'less', ask user for confirmation)
        $ProgramName --less -i -v --  "sed 's,\",'\'',g'" *.txt

    Add include guards to files *.h (with confirmation):
        ModifyFiles --export-filename -i --less \\
            ' Define=_INCLUDED_\`echo "\$MF_FILENAME" \\
                | sed -e "s,.*/,,g" -e "s,\\.,_,g"\` ;
            echo -e "#ifndef \$Define\n#define \$Define\n" ;
                cat - ; echo -e "\n#endif" ' \\
            *.h

    Modify targets of symlinks from /mnt/local/hda9/* to 
    /mnt/local/hda10/* (don't actually apply changes):
        ModifyFiles -y -d -i -n \\
            'sed "s,^/mnt/local/hda9/,/mnt/local/hda10/,"' \\
            \`find . -type l\`

BUGS
    - only tested with bash
    - (more: see lines in script file marked with BUG)

SEE ALSO
    man perlrun, see option "-i" (for in-place operation);
    RenameFiles

AUTHOR
    Daniel Käps (kaeps AT informatik.uni-leipzig.de)

COPYRIGHT
    This is free software; see the source for copying conditions. 
    There is NO warranty; not even for MERCHANTABILITY or FITNESS 
    FOR A PARTICULAR PURPOSE.

.
}

#-------------------------------------------------------------

verbose_echo()
{
    if $IsVerbose ; then
        echo "$ProgramName: $@"
    fi
}

error_echo()
{
    echo "$ProgramName: $@" >&2
}

#-------------------------------------------------------------

detect_pipe_error_helper()
{
    while [ "$#" != 0 ] ; do
        # there was an error in at least one program of the pipe
        if [ "$1" != 0 ] ; then return 1 ; fi
        shift 1
    done
    return 0
}

detect_pipe_error()
{
    detect_pipe_error_helper "${PIPESTATUS[@]}"
    return $?
}

#-------------------------------------------------------------

create_tempfile()
{
    local FileName="$1"

    if $IsGzipped ; then
        UseMktemp=false
    fi

    if $UseMktemp ; then

        if [ -n "$TempDir" ] ; then
            TempFileNameTemplate="$TempDir/file.XXXXXX"
        else
            TempFileNameTemplate="$FileName.XXXXXX"
        fi

        # NOTE doesn't work: adding a suffix to TempFileNameTemplate 
        # after the 6 Xs (mktemp will fail always with such a
        # filename template)
        # if $IsGzipped ; then
        #    TempFileNameTemplate="$TempFileNameTemplate.gz"
        # fi

        # try creating a temporary file using the 'mktemp' program:
        TempFileName=`mktemp -q "$TempFileNameTemplate"`
        if [ $? != 0 ]; then
            # when we cannot create a temporary file using 'mktemp', 
            # we default to not using the 'mktemp' program
            UseMktemp=false
        fi
    fi
    if ! $UseMktemp ; then
        # needed for e.g. cygwin b20.1, which doesn't come with the
        # 'mktemp' or 'tempfile' program
        TempFileName="$FileName.tmp~"
        if $IsGzipped ; then
            TempFileName="$TempFileName.gz"
        fi

        if [ -e "$TempFileName" ] ; then
            error_echo "temporary file \"$TempFileName\" already exists."
            TempFileName=""
            return 1
        fi
        if ! touch "$TempFileName" ; then
            error_echo "Could not create tempfile \"$TempFileName\"."
            TempFileName=""
            return 1
        fi
    fi
    return 0
}

remove_tempfile()
{
    if [ -n "$TempFileName" ] ; then
        if ! rm "$TempFileName"; then
            error_echo "Could not remove tempfile \"$TempFileName\"."
        fi
    fi
    TempFileName=""
}

tempfile_remove_handler()
{
    if [ -n "$TempFileName" ] ; then
        rm "$TempFileName"
    fi
}

#-------------------------------------------------------------

while [ "$#" != 0 ] ; do
    case "$1" in
        -d|--diff)
            IsShowDiff=true ;;
        -E|--export-filename)
            IsExportFileName=true ;;
        -i|--interactive) 
            IsInteractive=true ;;
        -l|--less)
            IsPreview=true ;;
        -n|--dry-run)
            IsDryRun=true ;;
        -p|--keep-permissions)
            IsKeepPermissions=true ;;
        -q|--quiet)
            IsPrintModifiedFileNames=false ;;
        -S)
            IsActionInMultipleArguments=true ;;
        -t|--keep-mod-time)
            IsKeepModificationTime=true ;;
        -v|--verbose)
            IsVerbose=true ;;
        -y|--modify-symlinks)
            IsModifySymlink=true
            FileDescription="symlink"
            ;;
        -z|--gzip)
            IsGzipped=true ;;
        --help)
            ShortHelpString
            exit 1 ;;
        --man|--manual)
            LongHelpString
            exit 1 ;;
        --version|-V)
            echo -e "$ProgramVersionString\n"
            exit 1 ;;
        --)
            shift 1
            break ;;
        -*)
            error_echo "Unrecognized option: \"$1\""
            ShortHelpString >&2
            exit 1 ;;
        *)
            break ;;
    esac
    shift 1
done

#-------------------------------------------------------------

# the currently used mechanism to preserve file permissions is 
# to copy the file permissions of the original file to those of
# the temporary file.
# however, then the temporary file should be created in the same 
# directory as the original file (if e.g. /tmp was used this might 
# make the file more accesible because of other directory 
# permissions)
# (TempDir="" serves as a flag to create_tempfile to put the 
# temporary file into the same directory as the original file)
if $IsKeepPermissions ; then TempDir="" ; fi

#-------------------------------------------------------------

if $IsActionInMultipleArguments ; then
    while [ "$1" != "--" -a "$#" != 0 ] ; do
        case "$1" in
            @*)
                # don't put arguments prefixed by a "@" into quotes, just remove
                # the "@"
                Argument="${1/@/}"
                ;;
            *)
                # put other arguments into "'" quotation marks and "escape" 
                # "'" characters that are in the argument string
                # was incorrect: Argument="'${1//\'/\'\\\'\'}'"
                COM="'"
                Argument="'${1//$COM/$COM\\$COM$COM}'"
                ;;
        esac
        PipingConstruct="$PipingConstruct$Argument "
        shift 1
    done
    shift 1
else
    PipingConstruct="$1"
    shift 1
fi

#-------------------------------------------------------------

verbose_echo "$FileDescription modification function is: \"$PipingConstruct\""

if [ "$IsModifySymlink" == "false" ] ; then
    if $IsGzipped ; then
        PipingConstruct="$PipingConstruct | gzip -fc"
    fi
fi

PipingConstruct="$PipingConstruct ; detect_pipe_error"

trap tempfile_remove_handler EXIT

#-------------------------------------------------------------

while [ "$#" != 0 ]
do
    FileName="$1"
    if $IsExportFileName ; then
        export MF_FILENAME="$FileName"
    fi

    if $IsModifySymlink ; then

        #------------------------------------------------------------
        # for modifying the targets of symbolic links:

        if [ ! -L "$FileName" ] ; then
            error_echo "\"$FileName\" is not a symbolic link, skipped."
            shift 1
            continue
        fi

        verbose_echo "current $FileDescription is: \"$FileName\""
    
        #------------------------------------------------------------

        # determine current target of symbolic link
        # (could use 'readlink' here as well):
        OldSymlinkTarget=`find "$FileName" -printf "%l"`

        set +e
        NewSymlinkTarget=`echo "$OldSymlinkTarget" | eval ${PipingConstruct} ; detect_pipe_error`
        if [ $? != 0 ] ; then
            set -e
            error_echo "modifying function failed, \"$FileName\" is unchanged."
            shift 1
            continue
        fi
        set -e
        
        #------------------------------------------------------------

        # check whether the modification function would change
        # the symlink target - if not, skip it:
        if [ "$OldSymlinkTarget" = "$NewSymlinkTarget" ] ; then
            # when file is same after applying changes, don't do anything
            verbose_echo "$FileDescription \"$FileName\" isn't changed by modification function."
            shift 1
            continue
        fi

        if [ "$IsShowDiff" == "true" -o "$IsInteractive" == "true" ] ; then
            echo "$FileName: $OldSymlinkTarget -> $NewSymlinkTarget"
        fi

    else

        #------------------------------------------------------------
        # for modifying the contents of files:
    
        if $IsGzipped ; then
            CatCommand=zcat
            CmpCommand=zcmp
            LessCommand=zless
            DiffCommand=zdiff
        else
            CatCommand=cat
            CmpCommand=cmp
            LessCommand=less
            DiffCommand=diff
        fi
    
        if [ -d "$FileName" ] ; then
            error_echo "\"$FileName\" is a directory, skipped."
            shift 1
            continue
        fi
    
        verbose_echo "current $FileDescription is: \"$FileName\""
    
        if ! create_tempfile "$FileName" ; then 
            shift 1
            continue
        fi
    
        if $IsKeepPermissions ; then
            if ! chmod --reference="$FileName" "$TempFileName" ; then
                error_echo "$FileName: keeping permissions failed"
            fi
        fi

        #-------------------------------------------------------------
    
    
        set +e
        $CatCommand "$FileName" | eval ${PipingConstruct} > "$TempFileName" ; detect_pipe_error
        if [ $? != 0 ] ; then
            set -e
            error_echo "modifying function failed, \"$FileName\" is unchanged."
            remove_tempfile
            shift 1
            continue
        fi
        set -e
    
        #-------------------------------------------------------------
    
        if $IsKeepModificationTime ; then
            ! touch --reference="$FileName" "$TempFileName"
        fi
    
        # check whether the modification function has changed 
        # the file; if it is unchanged, skip it:
        if $CmpCommand --quiet "$FileName" "$TempFileName" ; then
            # when file is same after applying changes, don't do anything but
            # removing the temporary file
            verbose_echo "file \"$FileName\" isn't changed by modification function."
            remove_tempfile
            shift 1
            continue
        fi
    
        if $IsPreview ; then 
            ! $LessCommand "$TempFileName"
        fi
    
        if $IsShowDiff ; then
            ! $DiffCommand "$FileName" "$TempFileName"
        fi

    fi

    #-------------------------------------------------------------
    
    if $IsDryRun ; then
        if $IsPrintModifiedFileNames ; then
            echo ">@< $FileName"
        fi

        remove_tempfile
        shift 1
        continue
    fi

    #-------------------------------------------------------------

    # if applicable, ask the user for confirmation to apply changes
    if $IsInteractive ; then
        read -p "$ProgramName: Modify the $FileDescription \"$FileName\" ([y]es/[a]ll/[]no)? " TempInput
        case "$TempInput" in 
            [yY][eE][sS]|[yY]) # "yes or y"
                IsModifyCurrentFile=true ;;

            [aA][lL][lL]|[aA]) # "all or a"
                IsModifyCurrentFile=true
                # assume yes/non-interactive for following files:
                IsInteractive=false
                ;;

            *)
                IsModifyCurrentFile=false ;;
        esac
    else
        IsModifyCurrentFile=true
    fi

    #-------------------------------------------------------------

    if $IsModifyCurrentFile ; then
        if $IsPrintModifiedFileNames ; then
            echo ">@< $FileName"
        fi

        verbose_echo "applying changes to $FileDescription \"$FileName\""

        if $IsModifySymlink ; then

            # relink "$FileName" to "$NewSymlinkTarget"

            # (using -f to force the operation shouldn't be
            # problematic since links always should have 
            # permissions 'rwxrwxrwx' - at least under Linux)
            ln -sf "$NewSymlinkTarget" "$FileName"

        else

            # move back changed file to original location:

            # BUG we should move the original file to a backup file (within
            # the same directory), to be able to recover from failures during
            # "mv" from $TempFileName to $FileName (e.g. out-of-disk space
            # errors or media failures may happen if "mv" involves
            # copying the file contents, e.g. if $TempFileName and $FileName 
            # are on different filesystems)
            if ! mv "$TempFileName" "$FileName" ; then
                error_echo "mv \"$TempFileName\" \"$FileName\" failed, \"$FileName\" is unchanged."
                remove_tempfile
            else
                TempFileName=""
            fi
        fi

    else
        verbose_echo "letting $FileDescription \"$FileName\" unchanged."
        remove_tempfile
    fi

    shift 1
done

#-------------------------------------------------------------
