#!/bin/bash
#
# Unpacks one of several different types of archive to the current
# directory.  If there is more than one entry in the top-level
# directory, automatically creates a new subdirectory and unpacks to
# that, in order to avoid filling the current directory with crap.
# In every case, informs the user where the unpacked contents ended up.
#
# Adam Spiers <shell-hacks@adamspiers.org>

me="`basename $0`"

warn () {
    echo >&2 "$*"
}

die () {
    warn "$*"
    exit 1
}

abort () {
    die "$*; aborting."
}

interactive_mode () {
    [ -t 1 ]
}

progress () {
    if interactive_mode; then
        echo "$@"
    fi
}

usage () {
    cat <<EOF >&2
Usage: $me ARCHIVE

Unpacks the given archive.  Supported formats include:

  tar (incl. compressed with gzip, bzip2, xz), zip, 7z, rar,
  apk, xpi, jar, class, gem, box, rpm

Guarantees that extraction will always be to a newly-created directory
rather than polluting an existing one with multiple entries.

If STDOUT is not a tty, only the name of the new directory containing
the unpacked archive will be output to STDOUT.  This can then be
captured and reused programmatically in other scripts.
EOF
    exit 1
}

parse_args () {
    if [ "$1" == -h ] || [ "$1" == --help ] || [ -z "$1" ]; then
        usage
    fi

    archive_path="$1"
    archive="`basename \"$1\"`"

    if ! [ -e "$archive_path" ]; then
        abort "$archive_path does not exist"
    fi
}

get_ideal_dest_dir () {
    ideal_dest_dir=
    case "$archive" in
        *.tar|*.tgz|*.tbz|*.txz|*.zip|*.Zip|*.ZIP|\
        *.7z|*.apk|*.rar|*.xpi|*.jar|*.sar|*.class|\
        *.job|*.pylib|*.gem|*.box|*.rpm)
            ideal_dest_dir="${archive%.*}"
            ;;
        *.tar.gz)
            ideal_dest_dir="${archive%.tar.gz}"
            ;;
        *.tar.bz2)
            ideal_dest_dir="${archive%.tar.bz2}"
            ;;
        *.tar.xz)
            ideal_dest_dir="${archive%.tar.xz}"
            ;;
        *)
            abort "$archive does not have a supported file extension"
            ;;
    esac
}

# Runs external commands, redirecting output as required.
run () {
    if interactive_mode; then
        "$@"
    else
        "$@" >&2
    fi
}

extract () {
    if ! tmpdir=`mktemp -d "$ideal_dest_dir.tmp.XXXXXXXX"`; then
        die "mktemp failed: $!"
    fi

    case "$archive" in
        *.tar)
            run tar -C "$tmpdir" -xvf "$archive_path"
            ;;
        *.tar.gz|*.tgz|*.box)
            run tar -C "$tmpdir" -zxvf "$archive_path"
            ;;
        *.tar.bz2|*.tbz)
            run tar -C "$tmpdir" -jxvf "$archive_path"
            ;;
        *.tar.xz|*.txz)
            run tar -C "$tmpdir" -Jxvf "$archive_path"
            ;;
        *.zip|*.ZIP|*.Zip|*.xpi|*.jar|*.class|*.sar|*.job|*.pylib|*.apk)
            run unzip -d "$tmpdir" "$archive_path"
            ;;
        *.7z)
            run 7za x -o"$tmpdir" "$archive_path"
            ;;
        *.gem)
            run gem unpack --target="$tmpdir" "$archive_path"
            ;;
        *.rar)
            archive_abspath=$( abs "$archive_path" )
            pushd "$tmpdir"
            run unrar x "$archive_abspath"
            popd
            ;;
        *.rpm)
            rpm="$archive_path"
            rpm_name="${rpm%.rpm}"
            rpm_name="${rpm_name##*/}"
            cpio="$rpm_name.cpio"

            progress -n "Extracting cpio to $tmpdir/$cpio ... "
            rpm2cpio "$rpm" > "$tmpdir/$cpio"
            progress "done."

            progress -n "Unpacking cpio into $tmpdir ... "
            mkdir "$tmpdir/$rpm_name"
            cd "$tmpdir/$rpm_name"
            cpio -id < ../"$cpio"
            progress "done."
            rm ../"$cpio"
            ;;
        *)
            abort "$archive is not a supported archive format"
            ;;
    esac

    if [ $? != 0 ]; then
        abort "Unpack of $archive_path failed"
    fi
}

ensure_single_dir () {
    num_dirents=$( ls -A "$tmpdir" | wc -l )

    if [ "$num_dirents" -eq 0 ]; then
        die "$archive was empty?  Aborting"
    fi

    if [ "$num_dirents" -gt 1 ]; then
        # Naughty archive creator!  Would cause a mess if unpacked to cwd.
        progress "$archive had more than one top-level entry"
        unpacked_dir="$tmpdir"
    else
        progress "$archive is clean; everything under a single top-level directory"
        top_dir=$( ls -A "$tmpdir" )
        unpacked_dir="$tmpdir/$top_dir"
        if [ -e "$top_dir" ]; then
            warn "$top_dir already exists"
        else
            if mv "$unpacked_dir" .; then
                dest_dir="$top_dir"
                rmdir "$tmpdir"
                return
            else
                abort "mv $unpacked_dir . failed"
            fi
        fi
    fi

    calc_dest_dir
    rename_to_dest_dir
}

calc_dest_dir () {
    if ! [ -e "$ideal_dest_dir" ]; then
        # safe to use ideal choice of destination directory
        dest_dir="$ideal_dest_dir"
    else
        warn "Ideal destination $ideal_dest_dir already exists"
        dest_dir="$archive.unpacked"
        if [ -e "$dest_dir" ]; then
            warn "$dest_dir also already exists"
            # Couldn't find a better place to move unpacked directory to, so
            # leave it where it already is.
            dest_dir="$unpacked_dir"
        fi
    fi
}

rename_to_dest_dir () {
    if [ "$unpacked_dir" != "$dest_dir" ]; then
        #progress "mv $unpacked_dir $dest_dir OK"
        if ! mv "$unpacked_dir" "$dest_dir"; then
            abort "mv $unpacked_dir $dest_dir failed"
        fi
        [ -d "$tmpdir" ] && rmdir "$tmpdir"
    fi
}

main () {
    parse_args "$@"
    get_ideal_dest_dir
    saved_dir="`pwd`"
    extract
    cd "$saved_dir"
    ensure_single_dir

    progress "Look inside $dest_dir"
    if ! [ -t 1 ]; then
        echo "$dest_dir"
    fi
}

main "$@"

