#!/bin/bash
#
# cleanup -- Maintenance tool to remove unneeded files and directories
#
# Please see usage information in the HERE document defined below.
#
#########################
# $Author: Andreas Spindler$
# $Writestamp: 2011-06-25 22:40:32$
# $Maintained at: http://www.visualco.de$

########################################################################
# Portability / Environment

script=`basename "$0"`
version='1.2.7'

VAR_LOG_DIR=/var/log
E_MISSING=64                                           # Missing programs
E_BADARGS=65                                           # Bad argument format
E_BADDIR=66                                            # Can't change directory
E_NOTROOT=67                                             # Non-root exit error
E_NOTCYGWIN=68                                           # Non-Cygwin-shell
E_NOTGNU=69                                              # GNU utility required

case "`uname`" in
    CYGWIN*)
        under_cygwin=1
        CYGWIN=${CYGWIN:-tty notitle glob nontsec}
        CYGWIN+=" winsymlinks";;
    *)  under_cygwin=0;;
esac

# `Ok' is the normal exit function (success).
# `Panic' function signals runtime errors to the shell.
# `Croak' functions signal logical errors or warnings.

function Ok {
    [ -z "$*" ] || echo "$*"; exit 0
}

function Panic {
    cat <<EOF >&2
Panic! in shell $$, pipe $?, terminal `tty`: $*
$script exits with -1
EOF
    exit -1
}

function CroakFind {
    ((optverbose)) && echo "WARNING: $find exited with code $?"
} >&2

function CroakDir {
    echo "$*: No such directory"; exit $E_BADDIR
} >&2

function CroakArgs {
    Usage; exit $E_BADARGS
}

function CroakUnlessCygwin {
    if ((!$under_cygwin)); then
        echo "WARNING: Not a Cygwin system. Use '-f' to force deletion of Windows files"
        exit $E_NOTCYGWIN
    fi
} >&2

function CroakUnlessGnuFind {
    if ((!$have_gnu_find)); then
        echo "WARNING: GNU find required"
        exit $E_NOTGNU
    fi
} >&2

function CroakUnlessRoot {
    if [ "$UID" -ne "0" ]; then
        echo "WARNING: You must be root to do that."
        exit $E_NOTROOT
    fi
} >&2

########################################################################
# Commandline options
#
# The script will run in dry mode unless one of the following options are
# given:
#     -a -A -e -E -t -g -i -w -C -M

function Version {
    cat <<EOF >&2
$script version $version
EOF
}

function Usage {
    Version; cat <<EOF >&2

Usage:

    $script [-tgwiale] [-CLME] [-l NUM] [-f] [-n] [DIRNAME]
    $script [-h | -V]

EOF
}

function Help {
    Usage; cat <<EOF >&2
Description:

    Efficiently  and   safely  find  and/or  remove   garbage  files,  compiler
    intermediate files, suspicious files, "core" files, empty files/directories
    and symbolic links that point to nothing. Handles dot files/directories and
    source  repositories  (e.g.  ".svn")  carefully. Works  nicely  on  Windows
    systems under Cygwin.

    Nothing is actually removed unless one  or more of the following options is
    explicitly specified:

        -t -g -w -i -e -E -a -A
        -M -C -L

Options:
    -n      Dry run: do not actually remove any files; just
            print filenames (default).
    -f      Force harder cleaning/release some constraints.
    -V      Print version information.
    -v      Enable verbose mode.
    -h, -H  Print this information.

Remove Options:

    -t      Remove temporary files:
                *~      #*#     .tmp    .temp   .cache  NUL     TAGS
            More precious temporary files require -f:
                .log    .lock

    -g      Remove intermediate files left behind by GNU tools:
                .d      .o      .gch    a.out
            More precious files require -f:
                .elc

    -w      Remove intermediate files left behind by Microsoft's Visual Studio
            and Windows programs:
                .tlog   .dep    .idb    .suo    .ilk    .rsp    .ncb
                .sbr    .pch    .bsc    .clw    .obj    .aps    .exp
                .tlb    .crf    .sdf    .intermediate.manifest
                BuildLog.htm    MSVC.BND
                .unsuccessfulbuild      .lastbuildstate
            More precious Visual Studio files require -f:
                .opt    .pdb    .map    .res
            Remove files left behind by Cygwin tools:
                .exe.stackdump

    -i      Remove files left behind by image processors:
                pspbrwse.jbf    thumbs.db       ExifBrowser.Thumbnails
            Exiftool backup files require -f:
                .*_original

    -e, -0  Remove empty files (zero file size).
    -E      Remove empty directories.

    -a      Short for "-tigw".
    -A      Like before, plus empty files/directories; short for "-tigweE".

    -l      List files only; short for "-An".

Special Cleaning Options:

    -M      Maintainer clean:
                .emacs.desktop.*        *.vcproj.*.user         *.vcxproj.user
            More precious files/directories require -f.
            WARNING: -Mf unties CVS/Subversion/SourceSafe working copies:
                .svn/           .cvs/
                vssver2.scc     mssccprj.scc    *.vspscc

    -C      System maintenance. Remove all "core"-files which are at least 5 days
            old, after asking the user for permission on each file (or use -f).
            Only the root user can do this.
            Furthermore find and print suspicious files (THESE ARE NEVER REMOVED):
                - World writable files.
                - Files with no valid owner and/or group.
                - SetUID files.
                - Files with unusual permissions, sizes, names, dates.
                - Symbolic links that point to nothing.

    -L NUM  Remove "$VAR_LOG_DIR/wtmp" and tailor "$VAR_LOG_DIR/messages" to NUM
            lines (minium 100 or use -f to allow NUM to be less than 100 lines).
            Requires the root user.

Examples:

    Print a list of removable files:
        \$ $script
    As before, but additionally with empty files/directories:
        \$ $script -l
    As before, but print all removable files, i.e. including those that one
    might not discard thoughlessly:
        \$ $script -lf
    Print empty files and directories
        \$ $script -eEn
    Untie CVS/Subversion/SourceSafe working copy:
        \$ $script -Mf

Installation/Prerequisites:

Portable shell script. Works on any UN*X and Windows (Cygwin) system. Copy this
script  into your  path (e.g.  to "/usr/local/bin"  or  "\$HOME/bin"). Requires
uname, getopts, sh and find. Maintained at <http://www.visualco.de>.

Exit codes:

     0  Indicates success to the shell.
    -1  Indicates an unexpected error.
    $E_BADARGS  Bad command-line arguments.
    $E_BADDIR  "-l" could not change to "$VAR_LOG_DIR".
    $E_NOTROOT  Root user required to perform.
    $E_NOTCYGWIN  "-w" requires a Cygwin-driven shell (use "-f")
    $E_NOTGNU  GNU utility required

EOF
}

optdry=1 optforcedry=0 optverbose=0 optforce=0 opttemp=0 optgnujunk=0
optemptyfiles=0 optemptydirs=0 optmsjunk=0 optpixeljunk=0 optcores=0
optloglines=0 optmaintainerclean=0 optdir=

LoadDefaultOpts() {
    optdry=1 opttemp=1 optgnujunk=1 optpixeljunk=1
    ((under_cygwin)) && optmsjunk=1
}

LoadMoreOpts() {
    LoadDefaultOpts
    optemptyfiles=1 optemptydirs=1
}

while getopts 'aAl0eEtigwCL:MfnvhHV' opt
do
    case $opt in
        a) LoadDefaultOpts; optdry=0;;
        A) LoadMoreOpts; optdry=0;;
        l) LoadMoreOpts; optforcedry=1;;
        0) optemptyfiles=1; optdry=0;;
        e) optemptyfiles=1; optdry=0;;
        E) optemptydirs=1; optdry=0;;
        t) opttemp=1; optdry=0;;
        g) optgnujunk=1; optdry=0;;
        i) optpixeljunk=1; optdry=0;;
        w) optmsjunk=1; optdry=0;;
        C) optcores=1; optdry=0;;
        M) optmaintainerclean=1; optdry=0;;
        L) optloglines=$OPTARG; optdry=0;;
        f) optforce=1;;
        n) optforcedry=1;;
        v) optverbose=1;;
        V) Version; Ok;;
        [hH]) Help; Ok;;
        *) CroakArgs;;
    esac
done
shift $(($OPTIND - 1))
optdir=${1:-.}
if [ -n "$2" ]; then
    echo "$script: Only one search directory allowed" >&2
    exit $E_BADDIR
fi

(($optemptyfiles + $optemptydirs + $opttemp +
    $optgnujunk + $optpixeljunk + $optmsjunk + \
    $optcores + $optmaintainerclean + $optloglines)) || \
    LoadDefaultOpts
(($optforcedry)) && optdry=1

(($under_cygwin)) && \
    optdir=`cygpath "$optdir"`
[ "$optdir" != "/" ] || (($under_cygwin)) || \
    CroakUnlessRoot
[ -d "$optdir" ] || \
    CroakDir "$optdir"

(($optmsjunk && !$optforce)) && \
    CroakUnlessCygwin                    # -w requires -f when not under Cygwin

########################################################################
# Test and run find
#

# Get find, test whether '-regextpye' works. A few Windows tools, such as
# find.exe, link.exe and sort.exe, may conflict with the Cygwin versions. Try
# the full path /usr/bin/find then.

find=`which find`
if ((!$under_cygwin)); then
    res=`$find --version`
    if [ "$?" -ne "0" ]; then
        cat <<EOF >&2
WARNING: find is '$find', the native Windows find utility. Possibly the
         Cygwin-bin-directory does not come first in PATH (see "~/.bash_login"
         and "/etc/profile").
EOF
        find=/usr/bin/find res=`$find --version`
        if [ "$?" -ne "0" ]; then
            Panic "find not found"
        else
            cat <<EOF >&2
WARNING: Using '$find' explicitly.
EOF
            exit $E_NOTCYGWIN
        fi
    fi
fi

res=`$find -regextype posix-egrep 2>&1 &>/dev/null`
if [ "$?" -ne "0" ]; then                      # some prehistoric find version?
    cat <<EOF >&2;
WARNING: '$find' is not GNU find, since it does not understand '-regextype'
EOF
    have_gnu_find=0
else
    have_gnu_find=1
fi

if (($have_gnu_find)); then
    regex_expr='-regextype posix-egrep'
    remove_file_expr='-exec rm -fv {} +'
else
    regex_expr=''
    remove_file_expr='-exec rm -fv {} ;'
fi
remove_dir_expr='-exec rmdir -v {} ;'

if (($optdry)); then
    what="    Finding"
    execute_expr="-print"
else
    what="    Removing"
    execute_expr="$remove_file_expr"
fi

# taboo_expr prunes some root directories. Assume "/windows-X-drive" is the
# Windows partition on a dual-boot computer.

taboo_expr="\
        -path /proc -prune \
    -o  -path /sys -prune \
    -o  -path /dev -prune \
    -o -iwholename /windows-*-drive -prune"
(($under_cygwin)) && taboo_expr+="\
    -o -iwholename /cygdrive/[a-z]/System?Volume?Information -prune
    -o  -path /cygdrive/[a-z]/\$RECYCLE.BIN/* -prune
    -o  -path /cygdrive -prune"

# Base function running find.

DoFindImpl()
{
    set -f || Panic # disable file pattern expansion; note that the '~'
                    # filename metacharacter is also disabled by this option
    local dotfiles=${1:-0} dotdirs=${2:-0} exec="$3"
    if (($dotfiles)); then
        if (($dotdirs)); then
            if (($optverbose)); then
                echo "WARNING: finding file- and directory names beginning with a dot" >&2
                set -x
            fi
            $find "$optdir" $find_opts $regex_expr \
                \( $taboo_expr \) -o \
                \( \( $conditional_expr \) -a \
                   \( $exec \) \) || CroakFind
        else
            if (($optverbose)); then
                echo "WARNING: finding filenames beginning with a dot (but no dot-directories)" >&2
                set -x
            fi
            $find "$optdir" $find_opts $regex_expr \
                \( $taboo_expr \) -o \
                \( -type d -path '*/.*' -prune \) -o \
                \( \( $conditional_expr \) -a \
                   \( $exec \) \) || CroakFind
        fi
    else
        if (($dotdirs)); then
            if (($optverbose)); then
                echo "WARNING: finding directory names beginning with a dot (but no dot-files)" >&2
                set -x
            fi
            $find "$optdir" $find_opts $regex_expr \
                \( $taboo_expr \) -o \
                \( -type f -name '.*' \) -o \
                \( \( $conditional_expr \) -a \
                   \( $exec \) \) || CroakFind
        else
            # Find no dot files and no directories (default).
            (($optverbose)) && set -x
            $find "$optdir" $find_opts $regex_expr \
                \( $taboo_expr \) -o \
                \( -path '*/.*' \) -o \
                \( \( $conditional_expr \) -a \
                   \( $exec \) \) || CroakFind
        fi
    fi
    set +xf
}

DoFind() {
    DoFindImpl 0 0 "${1:-$execute_expr}"
}

DoFindWithDotDirs() {
    DoFindImpl 0 1 "${1:-$execute_expr}"
}

DoFindWithDotFiles() {
    DoFindImpl 1 0 "${1:-$execute_expr}"
}

DoFindWithDots() {
    DoFindImpl 1 1 "${1:-$execute_expr}"
}

########################################################################
# (0) System files. Truncate $VAR_LOG_DIR/messages to $optloglines (root only).
#

if ((${optloglines:-0})); then
    if ((!$optforce)); then
        [ $optloglines -ge 100 ] || optloglines=100
    fi
    if [ -f "$VAR_LOG_DIR/messages" ]; then
        if (($optdry)); then
            echo "'$VAR_LOG_DIR/messages' lines: $(wc -l \"$VAR_LOG_DIR/messages\")"
            echo "'$VAR_LOG_DIR/messages' words: $(wc -m \"$VAR_LOG_DIR/messages\")"
        else
            echo "    Truncating '$VAR_LOG_DIR/messages' to $optloglines lines"
            CroakUnlessRoot
            tail -n $optloglines "$VAR_LOG_DIR/messages" > "$VAR_LOG_DIR/messages.cleanup" || Panic
            mv -f "$VAR_LOG_DIR/messages.cleanup" "$VAR_LOG_DIR/messages"
        fi
    else
        echo "WARNING: '$VAR_LOG_DIR/messages' not found" >&2
    fi
    if [ -e "$VAR_LOG_DIR/wtmp" ]; then
        if ((optdry)); then
            echo "'$VAR_LOG_DIR/wtmp' lines: $(wc -l \"$VAR_LOG_DIR/wtmp\")"
            echo "'$VAR_LOG_DIR/wtmp' words: $(wc -m \"$VAR_LOG_DIR/wtmp\")"
        else
            echo "    Truncating '$VAR_LOG_DIR/wtmp'"
            CroakUnlessRoot
            cat /dev/null > wtmp
        fi
    else
        echo "WARNING: '$VAR_LOG_DIR/wtmp' not found" >&2
    fi
    Ok
fi

########################################################################
# (1) Print suspicious files, remove "core" files.
#

echo "Directory '$optdir'"
((optdry)) && \
    echo "Dry run, nothing will be deleted"
((optverbose)) && [ "$UID" -eq "0" ] && \
    echo "You are root"

if (($optcores)); then
    # Find core files. Without -n remove the file; use -f (force) to skip
    # asking the user for permission. The user must be root.

    echo "$what 'core' files:"
    CroakUnlessGnuFind
    core_file_expr="-atime -5 -type f -name core"
    if (($optdry)); then
        $find "$optdir" -noleaf \
            \( $taboo_expr \) -o \
            \( $core_file_expr -print \) || Panic
    else
        if [ "$UID" -ne "0" ]; then
            echo "WARNING: You must be root to remove core files" >&2
        else
            if ((optforce)); then                                  # do not ask
                $find "$optdir" -noleaf \
                    \( $taboo_expr \) -o \
                    \( $core_file_expr $remove_file_expr \) || Panic
            else                                      # ask for user permission
                $find "$optdir" -noleaf \
                    \( $taboo_expr \) -o \
                    \( $core_file_expr -ok rm -v {} \; \) || Panic
            fi
        fi
    fi

    # Print suspicious files (NOT REMOVED).
    #
    # We're using a well-known find expression here. World-writebale (-perm 2),
    # no symlinks, no sockets and no directories with the sticky/text bit set
    # (symlinks, sockets and directories with the sticky bit set are often
    # world-writable and generally not suspicious.) -noleaf is required for
    # filesystems of mounted CD drives.

    if ((!$under_cygwin)); then
        echo "    Finding suspicious files:"
        CroakUnlessGnuFind
        $find "$optdir" -noleaf \
            \( $taboo_expr \) -o \
            \( -perm -2 ! -type l ! -type s ! \( -type d -perm -1000 \) \) -print
    fi

    # Find symbolic links that point to nothing (NOT REMOVED).
    #
    # This is a tip from the Unix Guru Universe (http://www.ugu.com). To find
    # dead symbolic links we let perl determine all links that point to
    # nothing. We may further pipe through "rm -vf" to actually delete these
    # links.

    if ((!$under_cygwin)); then
        echo "    Finding dead symbolic links:"
        $find "$optdir" \
            \( $taboo_expr \) -o \
            -type l -print | perl -nle '-e || print'
    fi
fi

########################################################################
# (2) Maintainer clean
#

if ((optmaintainerclean))
then
    # Find/remove ".emacs.desktop.*", ".svn/*", ".cvs/*" etc.
    echo "$what '.emacs.desktop.*':"
    conditional_expr="\
        -type f
         ( -name .emacs.desktop.*\
        -o -name *.vcproj*.user \
        -o -name *.vcxproj*.user )"
    DoFindWithDotFiles

    # With -f find or remove ".svn/*".
    # WARNING: -Mf unties a Subversion/SourceSafe working copy.
    if (($optforce)); then
        echo "$what Subversion/CVS/SourceSafe files:"
        find_opts="-depth"
        conditional_expr="\
           ( -type d ( -path */.svn -o -path */.cvs ) )\
        -o ( -type f ( -name vssver2.scc -o -name mssccprj.scc -o -name *.vspscc ) )"
        if (($optdry)); then
            DoFindWithDots
        else
            DoFindWithDots "-exec rm -frv {} ;"
        fi
        find_opts=
    fi
fi

########################################################################
# (3) Remove temporary/intermediate/garbage files.
#
# When find does not honor "posix-egrep" use glob patterns.

if (($opttemp)); then
    echo "$what temporary files:"
    if (($have_gnu_find)); then
        conditional_expr+="\
            -regex ^.+/(#.*#|.*~)$ \
        -o -iregex ^.+/(nul|tags)$ \
        -o -iregex ^.+/.*\\.(tmp|temp|cache)$"
        if (($optforce)); then # remove harder: *.lock, *.log
            conditional_expr+=" -o -iregex ^.+/.*\\.(lock|log)$"
        fi
    else                                               # no -regextpye (slower)
        conditional_expr+="\
            -name #*#   -o  -name *~ \
        -o -iname *.cache \
        -o -iname nul   -o -iname tags \
        -o -iname *.tmp -o -iname *.temp"
        if (($optforce)); then
            conditional_expr+="\
        -o  -name *.log -o  -name *.lock"
        fi
    fi
    # Allow dot-files to find (e.g. ".log") but prune dot-directories.
    conditional_expr="-type f ( $conditional_expr )"
    DoFindWithDotFiles
fi

if (($optgnujunk)); then
    echo "$what GNU/gcc removable files:"
    if (($have_gnu_find)); then
        conditional_expr="\
           -name a.out \
        -o -regex ^.+/.*\\.(d|o|gch)$"
    else
        conditional_expr="\
           -name a.out \
        -o -name *.d -o -name *.o -o -name *.gch"
    fi
    if (($optforce)); then
        conditional_expr+="\
        -o -name *.elc"
    fi
    conditional_expr="-type f ( $conditional_expr )"
    DoFind
fi

if (($optmsjunk)); then
    # See also "Common File Extensions Used by Visual C++",
    # http://support.microsoft.com/kb/132340/EN-US/
    echo "$what Microsoft/Cygwin removable files:"
    if (($have_gnu_find)); then
        conditional_expr="\
           -name *.exe.stackdump \
        -o -name BuildLog.htm -o -name MSVC.BND \
        -o -name *.intermediate.manifest \
        -o -name *.lastbuildstate -o -name *.unsuccessfulbuild \
        -o -iregex ^.+/.*\\.(obj|rsp|dep|tlb|tlog|aps|exp)$ \
        -o -iregex ^.+/.*\\.(sdf|ncb|clw|cpl|crf|suo|sbr|ilk|pch|mdp|idb|pg[dc]|bsc)$"
    else
        conditional_expr="\
           -name *.exe.stackdump \
        -o -name BuildLog.htm -o -iname MSVC.BND \
        -o -name *.intermediate.manifest \
        -o -name *.lastbuildstate -o -name *.unsuccessfulbuild \
        -o -name *.tlog \
        -o -name *.obj -o -name *.res -o -name *.clw -o -name *.cpl \
        -o -name *.rsp -o -name *.ncb -o -name *.suo -o -name *.sbr \
        -o -name *.ilk -o -name *.pch -o -name *.tlb -o -name *.idb \
        -o -name *.bsc -o -name *.crf -o -name *.mdp -o -name *.aps \
        -o -name *.exp -o -name *.pgd -o -name *.pgc -o -name *.sdf"
    fi
    if (($optforce)); then
        conditional_expr+="\
        -o -iname *.opt -o -iname *.res \
        -o -iname *.map -o -iname *.pdb"
    fi
    conditional_expr="-type f ( $conditional_expr )"
    DoFind
fi

if (($optpixeljunk)); then
    echo "$what image thumbnail removable files:"
    conditional_expr="\
       -iname pspbrwse.jbf \
    -o -iname ExifBrowser.Thumbnails \
    -o -iname thumbs.db"
    if (($optforce)); then               # remove harder: exiftool backup files
        conditional_expr+="\
    -o -name *.*_original"
    fi
    conditional_expr="-type f ( $conditional_expr )"
    DoFind
fi

########################################################################
# (4) Remove empty files and directories.
#

if (($optemptyfiles)); then
    echo "$what empty files:"
    conditional_expr="-type f -empty"
    DoFind
fi

if (($optemptydirs)); then
    echo "$what empty directories:"
    find_opts="-depth"
    conditional_expr="-type d -empty"
    if (($optdry)); then
        DoFind
    else
        DoFind "$remove_dir_expr"
    fi
    find_opts=
fi

((optdry)) && ((optverbose)) && \
    echo "Dry run, nothing was deleted"
Ok

# Local Variables:
# coding: iso-8859-1-unix
# fill-column: 79
# End:

# cleanup ends here