4
\$\begingroup\$

This is a Bash script I wrote to email me when updates are available for my server. I'm new to bash programming, so I don't know if I've done this well.

#!/bin/bash

# Copyright 2016 FlyingPiMonster
#
# THIS SOFTWARE IS RELEASED "AS IS", WITH NO WARRANTY OR GUARANTEE OF FITNESS FOR A PARTICULAR PURPOSE. THE DEVELOPER IS NOT RESPONSIBLE FOR ANY DAMAGE
# CAUSED BY THE USE OF THIS SOFTWARE.
#
# All rights reserved.
#


# Set up argument variables

reload_cron=false
print_help=false
quiet_mode=false
print_config=false

# Read arguments using getopts

while getopts ":rhqd" opts; do
    case "$opts" in
    r)
        reload_cron=true ;;
    \?|h)
        print_help=true ;;
    q)
        quiet_mode=true ;;
    d)
        print_config=true ;;
    esac
done


# Set up default configuration options
typeset -A config
config=(
    [EmailFrom]="YumNotifier"
    [EmailRecipient]="root@localhost"
    [EmailSubject]="Updates available for your computer"
    [EmailTitle]="Updates Available"
    [EmailGreeting]="YumNotifier has detected updates for some software on your computer."
    [EmailSignature]="Thank you for using YumNotifier!"
    [HideUpdateDetails]="No"
    [EmailHiddenUpdateText]="Please login with SSH and use the yum command to install these updates."
    [RunFrequency]="DAILY"
    [UserId]="root"
    [ErrorBadConfigurationOption]='The following configuration option is not set correctly:'
    [ErrorWritingCrontab]="Could not write to crontab file"
    [ErrorSendingMail]="An error occurred while sending mail"
    [MessageNoUpdatesAvailable]="No updates available"
)

# Read configuration file

while read -r line; do
    if echo "$line" | grep -F = | grep "^[^#]" | &>/dev/null; then
        varname=$(echo "$line" | cut -d '=' -f 1)
        config["$varname"]=$(echo "$line" | cut -d '=' -f 2)
    fi
done < /etc/yumnotifier/yumnotifier.conf


# Check if any flags are set that change the task the program is to do

if [[ "$print_help" == "true" ]]; then
        cat /usr/share/doc/yumnotifier/help.txt
        exit
fi

if [[ "$print_config" == "true" ]]; then
    cat /usr/share/doc/yumnotifier/default.conf
    exit
fi

if [[ "$reload_cron" == "true" ]]; then
    if ! [[ "${config[UserId]}" =~ ^[a-z_][a-z0-9_-]*[$]?$ ]]; then
        if [[ "$quiet_mode" == "false" ]]; then
            echo "${config[ErrorBadConfigurationOption]} UserId (${config[UserId]})"
        fi
        exit 78
    fi

    crontask=''
    case "${config[RunFrequency]}" in
        HOURLY) crontask='0 * * * *' ;;
        DAILY) crontask='* 0 * * *' ;;
        WEEKLY) crontask='* * * * 0' ;;
        MONTHY) crontask='* * 0 * *' ;;
        NEVER) exit ;;
        *)
            if [[ "$quiet_mode" == "false" ]]; then
                echo "${config[ErrorBadConfigurationOption]} RunFrequency"
            fi
            exit 78
            ;;
    esac

    (echo "$crontask ${config[UserId]} yumnotifier" > /etc/cron.d/yumnotifier.cron) &>/dev/null
    code="$?"

    if [[ "$code" != 0 ]]; then
        if [[ "$quiet_mode" == "false" ]]; then
                    echo "${config[ErrorWritingCrontab]}"
                fi
        exit 1
    fi

        exit
fi


# Read from yum check-updates. If HideUpdateDetails is set to Yes, we only need to read the exit code of the command; otherwise, loop through it and print a table.

lines=$(yum --quiet check-update)
code="$?"

# If yum check-update returns 0, there are no updates available
if [[ "$code" == 0 ]]; then
    if [[ "$quiet_mode" == "false" ]]; then
        echo "${config[MessageNoUpdatesAvailable]}"
    fi
    exit
fi

# Create variable for update description
maintext=""

if [[ "${config[HideUpdateDetails]}" == "No" ]]; then
    maintext+="<table>"

    while read -r line; do
            if ! [[ "$line" =~ [A-Za-z]*[\ ]Packages ]]; then
                    name=$(echo "$line" | tr -s '\t' | cut -f 1 | cut -d '.' -f 1)
            maintext+="<tr><th>${name}</th></tr>\n"
            fi
    done <<< "$lines"
    maintext+="</table>"
else
    maintext+="${config[EmailHiddenUpdateText]}"
fi


# Start creating email body

body="<html><head></head><body>"
body+="<h1 style='background-color: black; color: white;'>${config[EmailTitle]}<span style='color: black'>:&nbsp;</span></h1>" # The :&nbsp; is to prevent bad formatting in the text-only summary most mail clients provide
body+="<p>${config[EmailGreeting]}</p>"
body+="$maintext"
body+="<p>${config[EmailSignature]}</p>"
body+="</body></html>"

# Send email

echo -e "Content-Type: text/html\nSubject:${config[EmailSubject]}\n$body" | sendmail -F "${config[EmailFrom]}" "${config[EmailRecipient]}" &>/dev/null

code="$?"

if [[ "$code" != 0 ]]; then
    if [[ "$quiet_mode" == "false" ]]; then
        echo "${config[ErrorSendingMail]}"
    fi
    exit 1;
fi

And here's the default config file. Everything is commented out because the values are also hardcoded.

################################################################################
## This is the default configuration file for YumNotifier. All valid
## configuration options are listed here; to edit an option, uncomment the line
## and edit its value. Some options may require running yumnotifier -r to take
## effect.
################################################################################



#### EMAIL SETTINGS

## The string to put in the "To:" field of all emails
#EmailFrom=YumNotifier

## The email address to send notifications to
#EmailRecipient=root@localhost

## The subject line of notification emails
#EmailSubject=Updates are available for your computer

## The heading inside the notification email
#EmailTitle=Updates Available

## A message to put at the beginning of each notification email
#EmailGreeting=YumNotifier has detected updates for some software on your computer.

## A message to put at the end of each notification email
#EmailSignature=Thank you for using YumNotifier!

## If this is set to No, a full list of software to be updated will be sent in
##  the email. If it is set to anything else, the string in
##  EmailHiddenUpdateText will be sent instead of the names of the packages.
#HideUpdateDetails=No

## The text to send in the email instead of a list of packages if
##  HideUpdateDetails is not set to No
#EmailHiddenUpdateText=Please login with SSH and use the yum command to install these updates.



#### RUN SETTINGS

### IMPORTANT: When any of the options in this section is updated, you must run
###  yumnotifier -r for it to take effect.

## How often YumNotifier should check for updates. Must be one of HOURLY, DAILY,
##  WEEKLY, MONTHLY, or NEVER. If NEVER is selected, YumNotifier will only run
##  when invoked directly. This is useful for configuring custom cron jobs.
#RunFrequency=DAILY

## The user ID that yumnotifier should run under. It is recommended that you
##  change this from "root" to a user with fewer privileges.
#UserId=root



#### ERROR AND INFORMATION MESSAGES

## The error message to output if a configuration option is invalid
#ErrorBadConfigurationOption=The following configuration option is not set correctly:

## The error message to output if the crontab file is not accessible
#ErrorWritingCrontab=Could not write to crontab file

## The error message to output if the mailer fails to send a notification
#ErrorSendingMail="An error occurred while sending mail"

## The message to output if no updates are available
#MessageNoUpdatesAvailable=No updates available

I'm mostly concerned about security issues, but I'll take any advice you have.

\$\endgroup\$
2
\$\begingroup\$

No violations on shellcheck.net, nicely done! I do have a couple of recommendations.

Splitting a line in two

This is an inefficient way to split a line into two parts:

varname=$(echo "$line" | cut -d '=' -f 1)
config["$varname"]=$(echo "$line" | cut -d '=' -f 2)

Two subshells, echo and cut processes in each. It would be better to use pattern substitution instead, staying within the same process:

varname=${line%%=*}
config["$varname"]=${line##*=}

But there is a caveat. If there are multiple = signs on the line, this will not take the 2nd column like cut does. However, if that's a serious concern, it's possible to work around that with a few more intermediary substitutions.

The point is to execute as few programs as possible in the process.

Shortening the pipeline

For the same reason as in the previous point, it's good to keep pipelines short. For example here, the two grep commands could be combined:

if echo "$line" | grep -F = | grep "^[^#]" | &>/dev/null; then

Like this:

if echo "$line" | grep "^[^#].*=" | &>/dev/null; then

One less process to execute.

The echo can also be removed by using a here-string instead:

if grep "^[^#].*=" <<< "$line" | &>/dev/null; then

What is that last | doing there before the redirection? It seems unnecessary.

Redirecting stdout and stderr of grep

&>/dev/null after the grep redirects both stdout and stderr to /dev/null. But does grep ever produce something on stderr? I've never seen it. So I think the only practical concern is stdout. There's a flag -q to make that easy, so the line can be simplified to:

if grep -q "^[^#].*=" <<< "$line"; then

The == operator of [[ ... ]]

The == operator is useful for pattern matching. When comparing to a literal string, it's slightly clearer to use = instead. So instead of this:

if [[ "$print_help" == "true" ]]; then

I recommend this:

if [[ "$print_help" = "true" ]]; then

Magic numbers and strings

exit 78 appears twice. I suggest putting 78 in a variable, so that if you ever need to change it, you can do it in one place.

true and false are magic strings. It could be a good idea to put those in variables too.

Inconsistent indentation

The indentation is inconsistent at places. It would be better to make it consistent.

\$\endgroup\$
  • \$\begingroup\$ Thanks! One thing I'm confused about is the == operator. I usually program in Java, so I'm used to ==. Why is = better here? \$\endgroup\$ – FlyingPiMonster Aug 1 '16 at 19:34
  • \$\begingroup\$ = is less powerful than ==, but it's enough in your case. I think it's best to use the simplest tool that does the job. \$\endgroup\$ – janos Aug 1 '16 at 19:38
  • \$\begingroup\$ I think I've implemented most of your recommendations (here's the new file). I didn't know that echo and cut were such performance hits, or anything about substitution. Thanks for reviewing it! \$\endgroup\$ – FlyingPiMonster Aug 2 '16 at 3:30
  • \$\begingroup\$ Executing a process has a non-negligible overhead. In simple scripts with no looping it might not make a difference at all, but if you call several processes in a loop over a few dozen iterations, it becomes very noticeable. It's a good habit to keep process creation to a minimum when you can and easy enough to do. \$\endgroup\$ – janos Aug 4 '16 at 13:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.