#!/usr/bin/bash

# run-parts - concept taken from Debian
# http://www.unix.com/man-page/linux/8/run-parts/
# with extensions from Ubuntu
# http://manpages.ubuntu.com/manpages/trusty/man8/run-parts.8.html

# keep going when something fails
set +e
# pipefail is required for --report support
set -o pipefail

usage() {
    echo "run-parts  [--test]  [--verbose]  [--report]  [--umask=umask] [--lsbsysinit] [--regex=REGEX]
           [--arg=argument] [--exit-on-error] [--help] [--list] [--reverse]  [--]  DIRECTORY"
}

help() {
    echo "
NAME
       run-parts - run scripts or programs in a directory

SYNOPSIS
       run-parts  [--test]  [--verbose]  [--report]  [--umask=umask] [--lsbsysinit] [--regex=REGEX]
       [--arg=argument] [--exit-on-error] [--help] [--list] [--reverse]  [--]  DIRECTORY

DESCRIPTION
       run-parts runs all the executable files named within constraints described below, found in
       directory directory.  Other files and directories are silently ignored.

       If neither the --lsbsysinit option nor the --regex option is given  then  the  names  must
       consist  entirely of ASCII upper- and lower-case letters, ASCII digits, ASCII underscores,
       and ASCII minus-hyphens.

       If the --lsbsysinit option is given,  then  the  names  must  not  end  in  .dpkg-old   or
       .dpkg-dist  or  .dpkg-new  or  .dpkg-tmp,  and must belong to one or more of the following
       namespaces: the LANANA-assigned namespace (^[a-z0-9]+$); the LSB hierarchical and reserved
       namespaces  (^_?([a-z0-9_.]+-)+[a-z0-9]+$);  and the Debian cron script namespace (^[a-zA-
       Z0-9_-]+$).

       If the --regex option  is  given,  the  names  must  match  the  custom  extended  regular
       expression specified as that option's argument.

       Files  are  run	in  the  lexical sort order of their names unless the --reverse option is
       given, in which case they are run in the opposite order.

OPTIONS
       --test print the names of the scripts which would be run, but don't actually run them.

       --list print the names of the all matching files (not limited to executables),  but  don't
          actually run them. This option cannot be used with --test.

       -v, --verbose
          print the name of each script to stderr before running.

       --report
          similar  to  --verbose,  but  only prints the name of scripts which produce output.
          The script's name is printed to whichever of stdout or stderr the script produces
          output on. The script's name is not printed to stderr if --verbose also specified.

       --reverse
          reverse the scripts' execution order.

       --exit-on-error
          exit as soon as a script returns with a non-zero exit code.

       --umask=umask
          sets  the  umask to umask before running the scripts.  umask should be specified in
          octal.  By default the umask is set to 022.

       --lsbsysinit
          filename must be in one or more of either the LANANA-assigned namespace, the LSB
          namespaces - either hierarchical or reserved - or the Debian cron script namespace.

       --regex=REGEX
          validate filenames against custom extended regular expression REGEX

       -a, --arg=argument
          pass argument to the scripts.  Use --arg once for each argument you want passed.

       --     specifies that this is the end of the options.  Any filename after -- will  be  not
          be interpreted as an option even if it starts with a hyphen.

       -h, --help
          display usage information and exit.
"
}

report-and-pipe() {
    rline="$1"
    while IFS= read -r line; do
        echo -en "$rline" ; echo "$line";
        unset rline;
    done;
}

if [ $# -lt 1 ]; then
    usage
    exit 1
fi

args=""
dir=""
umask=""

for i in "$@"; do
    if [ ${append_arg:-0} = 1 ]; then
        args="$args $i"
        append_arg=0
        continue
    fi
    case $i in
        --list)
            list=1
            ;;
        --test)
            test=1
            ;;
        --verbose|-v)
            verbose=1
            ;;
        --report)
            report=1
            ;;
        --reverse)
            reverse=1
            ;;
        --lsbsysinit)
            lsbsysinit=1
            ;;
        --regex)
            regex="${i#*=}"
            ;;
        --arg=*)
            args="$args ${i#*=}"
            ;;
        -a)
            append_arg=1
            ;;
        --umask=*)
            umask="${i#*=}"
            ;;
        --help|-h)
            help
            exit 0
            ;;
        --exit-on-error)
            exit_on_error=1
            ;;
        --)
            # -- end of options
            ;;
        -*)
            echo Unknown argument: $i > /dev/stderr
            echo Rest of arguments: $* > /dev/stderr
            usage
            exit 1
            ;;
        *)
            # directory
            dir=$i
            break
            ;;
    esac
done

if [[ "x$dir" = "x" && ! -d "$dir" ]]; then
    echo "Not a directory: '$dir'"
    usage
    exit 1
fi

# Ignore *~ and *, scripts
filelist=$(LC_ALL=C; ls -1 "${dir}" | grep -vEe '[~,]$')

if [ ${reverse:-0} = 1 ]; then
    filelist=$(echo "$filelist" | sort -r)
fi

echo "$filelist" | while read bname ; do
    fpath="${dir%/}/${bname}"
    [ -d "${fpath}" ] && continue
    # Don't run *.{disabled,rpmsave,rpmorig,rpmnew,swp,cfsaved} scripts
    [ "${bname%.disabled}" != "${bname}" ] && continue
    [ "${bname%.cfsaved}" != "${bname}" ] && continue
    [ "${bname%.rpmsave}" != "${bname}" ] && continue
    [ "${bname%.rpmorig}" != "${bname}" ] && continue
    [ "${bname%.rpmnew}" != "${bname}" ] && continue
    [ "${bname%.swp}" != "${bname}" ] && continue
    [ "${bname%,v}" != "${bname}" ] && continue

    if [ ${lsbsysinit:-0} = 1 ]; then
        # Don't run *.{dpkg-old,dpkg-dist,dpkg-new,dpkg-tmp} scripts
        [ "${bname%.dpkg-old}" != "${bname}" ] && continue
        [ "${bname%.dpkg-dist}" != "${bname}" ] && continue
        [ "${bname%.dpkg-new}" != "${bname}" ] && continue
        [ "${bname%.dpkg-tmp}" != "${bname}" ] && continue
        # Adhere to LANANA-assigned LSB (hierarchical and reserved) and the Debian cron script namespaces
        [[ ! "${bname}" =~ ^[a-z0-9]+$ ]] && \
        [[ ! "${bname}" =~ ^_?([a-z0-9_.]+-)+[a-z0-9]+$ ]] && \
        [[ ! "${bname}" =~ ^[a-zA-Z0-9_-]+$ ]] && continue
    fi

    if [ "x$regex" != "x" ]; then
        [[ ! "${bname}" =~ $regex ]] && continue
    fi

    if [ -e "${fpath}" ]; then
        if [ -r $dir/whitelist ]; then
            grep -q "^${bname}$" $dir/whitelist && continue
        fi

        if [ ${list:-0} = 1 ]; then
            echo "${fpath}" $args;
            continue
        fi

        if [ -x "${fpath}" ]; then
            if [ ${test:-0} = 1 ]; then
                echo "${fpath}" $args;
                continue
            fi
            if [ "$RANDOMIZE" != "" ]; then
                let "rtime = $RANDOM"
                if [ "$RANDOMTIME" != "" ]; then
                    let "rtime %= $RANDOMTIME"
                else
                    let "rtime %= 300"
                fi
                sleep $rtime
            fi

            # run executable files
            if [ ${verbose:-0} = 1 ]; then
                echo "${fpath}" $args > /dev/stderr
            fi

            if [ "x$umask" != "x" ]; then
                umask $umask
            fi

            if [ ${report:-0} = 1 ]; then
                oline="${fpath}\n"
                # do not report script name over stderr in verbose mode
                # no duplicates are needed
                if [ ${verbose:-0} = 1 ]; then
                    eline=""
                else
                    eline="${fpath}\n"
                fi
                { "${fpath}" $args 2>&1 1>&3 3>&- |
                    # handle stderr redirected to stdout
                    report-and-pipe "$eline"
                } 3>&1 1>&2 |
                    # handle stdout
                    report-and-pipe "$oline"
            else
                "${fpath}" $args
            fi

            rc=${PIPESTATUS[0]}

            if [ ${verbose:-0} = 1 ]; then
                echo "${fpath}" $args exit status $rc > /dev/stderr
            fi

            if [ ${rc:-0} != 0 ]; then
                if [ ${exit_on_error:-0} = 1 ]; then
                    exit $rc
                fi
            fi
        fi
    fi
done

exit 0
