3
\$\begingroup\$

This is my first Bash script and I decided to use what I've learned so far to help manage stray processes while I develop in my *nix environs.

I've tested the script extensively and it seems to work (there may be some things I have not caught yet).

If I could get some critiques/tips on the script that would be great.

I named the file process and placed it in my ~/bin folder so that I could execute it like any other program.

#!/bin/bash

# kill processes using SIGQUIT signal -- still in testing

PROGNAME=$(basename $0)

function _usage_notice {
    echo "Usage: $PROGNAME [ help | show | destroy ] [ arg(s)... ]"
}

function _usage_summary {
echo "Usage: $PROGNAME [ action ] [ arg(s)... ]

    Summary
    -------
    This script is designed to send a quit signal event to a
    running program. If multiple instances of that program
    exist, they are all sent the same signal.

    Action      Description
    ------      -----------
    show        Shows current processes for current user.

    kill
    destroy     Sends SIGQUIT signal to given process if
            given process exists.

    help        Outputs this help text.
"
}

function _usage_processes {
echo "Usage: $PROGNAME [ show ] [ user | root | unsorted | sorted | less ]

    Examples
    --------
    $PROGNAME show
        Executes 'ps' with the format 'pid,user,comm'
    $PROGNAME show user
        Executes 'ps' and outputs 'user' owned processes
    $PROGNAME show root
        Executes 'ps' and outputs 'root' owned processes
    $PROGNAME show [ unsorted | all ]
        'all' and 'unsorted' are synonymous and displays all processes
    $PROGNAME show sorted
        Executes 'ps' and outputs 'all' processes in sorted order by user
    $PROGNAME show less
        Executes 'ps' and pipes 'all' output to 'less' if 'less' exists
"
}

function _usage_destroy {
echo "Usage: $PROGNAME [ destroy ] [ command(s)... ]

    Examples
    --------
    $PROGNAME destroy nautilus
        Sends the SIGQUIT signal to the process 'nautilus'
    $PROGNAME destroy [ program(s)... ]
        You can pass multiple programs all at once as well.
    $PROGNAME destroy nautilus bash pithos firefox
        Sends the SIGQUIT signal to given processes
"
}

function _usage_help {
echo "Usage: $PROGNAME [ help ] [ show | destroy | help ]

    Examples
    --------
    $PROGNAME
        Outputs basic usage
    $PROGNAME help
        Outputs usage summary
    $PROGNAME help [ show | destroy | help ]
        Outputs usage information for given action
"
}

function _usage_full {
    case $1 in
        "show")         _usage_processes ;;
        "destroy"|"kill")   _usage_destroy ;;
        "help")         _usage_help ;;
        *)          echo "Invalid argument given to action help." ;;
    esac
}

# test if a valid action was given
function _is_valid_action {
    case $1 in
        "destroy"|"kill")   echo valid  ;;
        "show")         echo valid  ;;
        "help")         echo valid  ;;
        *)          echo ""     ;;
    esac
}


# test if action has no arguments
function _input_is_empty {
    while [[ 0 -lt "$#" ]]; do
        if [[ -n $(_is_valid_action $1) ]]; then
            shift
            if [[ -n $(_is_valid_action "$1") ]]; then
                echo valid
                break
            fi
            continue
        elif [[ -n "$1" ]]; then
            echo valid
            break
        else
            echo ""
            break
        fi
    done
}

# show current processes according to given arguments
function _processes {
    case "$1" in
        "user")         ps -U $USER -o pid,tty,user,comm ;;
        "root")         ps -U root -o pid,tty,user,comm ;;
        "all"|"unsorted")   ps -A -o pid,tty,user,comm ;;
        "sorted")       ps -A --sort=user -o pid,tty,user,comm ;;
        "less")         if [[ -n $(which less) ]]; then
                        ps -A --sort=user -o pid,tty,user,comm | less
                    else
                        echo "'less' is not installed."
                    fi ;;
        *)          ps -o pid,user,comm ;;
    esac
}

# send to quit signal to given processes
function _destroy {
    while [[ 0 -lt "$#" ]]; do
        pid=$(pgrep $1)

        if [[ -z "$pid" ]]; then
            echo "Process '$1' does not exist."
            exit 2 
        fi

        for item in $pid; do
            kill -SIGQUIT $pid 
            echo "Destroyed process '$item' named '$1'"
        done

        shift
    done
}

function _prompt {
    echo "Warning! This function uses pattern matching!"
    echo "This is DANGEROUS because multiple processes may be selected!"
    read -p "Are you sure you want to continue? [y/yes/n/no]> " answer
    case $answer in
        "y"|"yes")  clear
                echo "You were Warned! Starting in 5 seconds..."
                sleep 5 
                return ;;
        "n"|"no")   exit 2 ;;
    esac
}

case $1 in
    "show") 
        shift
        _processes "$@"
        ;;

    "destroy"|"kill")   
        if [[ -z $(_input_is_empty "$@") ]]; then
            echo "No arguments were given to destroy."
            exit 1
        fi

        shift
        _prompt
        _destroy "$@"
        ;;

    "help") 
        if [[ -z $(_input_is_empty "$@") ]]; then
            _usage_summary
            exit 0
        fi

        shift
        _usage_full "$@"
        ;;

    *) # default
        _usage_notice 
        exit 1 
        ;;
esac

exit 0
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

First of all, it's very nice that you have separated each distinct functionality into functions, nicely done. Some improvements are possible.

Use the exit code, Luke

This function prints "valid" if the input is valid, or empty string if invalid:

function _is_valid_action {
    case $1 in
        "destroy"|"kill")   echo valid  ;;
        "show")         echo valid  ;;
        "help")         echo valid  ;;
        *)          echo ""     ;;
    esac
}

It would be better to use the exit code instead, like this:

function _is_valid_action {
    case $1 in
        destroy|kill|show|help) return 0 ;;
        *) return 1
    esac
}

You will need to change the callers accordingly. Instead this:

    if [[ -n $(_is_valid_action $1) ]]; then

Change to:

    if _is_valid_action $1; then

Which happens to be simpler.

Adjust _input_is_empty following the same logic.

Double-quote path variables

It's recommended to double-quote all path variables to prevent globbing and word splitting, for example here, put $0 in "$0":

PROGNAME=$(basename $0)

No need to double-quote literal strings

In this code, the double-quoting is a bit awkward for the cases:

    case $1 in
        "show")         _usage_processes ;;
        "destroy"|"kill")   _usage_destroy ;;
        "help")         _usage_help ;;
        *)          echo "Invalid argument given to action help." ;;
    esac

You could write simpler as:

    case $1 in
        show)         _usage_processes ;;
        destroy|kill)   _usage_destroy ;;
        help)         _usage_help ;;
        *)          echo "Invalid argument given to action help." ;;
    esac

Minor things

It's not a problem, it's just a bit unusual to print large multiline blocks of text using echo. It can get troublesome if you need to embed double-quotes. The commonly used alternative is a here document, like this:

cat << EOF
Usage: $PROGNAME [ destroy ] [ command(s)... ]

    Examples
    --------
    $PROGNAME destroy nautilus
        Sends the SIGQUIT signal to the process 'nautilus'
    $PROGNAME destroy [ program(s)... ]
        You can pass multiple programs all at once as well.
    $PROGNAME destroy nautilus bash pithos firefox
        Sends the SIGQUIT signal to given processes
EOF
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.