Interactive Linux upgrade script
2018/04/24 edit

I missed an obvious bug: is_linux_mint is not implemented correctly. The results of a return on an empty output of a command substitution, with or without quotes, is implementation-defined:


  The value of the special parameter '?' shall be set to n, an unsigned decimal integer, or to the exit status of the last command executed if n is not specified. If n is not an unsigned decimal integer, or is greater than 255, the results are unspecified.


The fix is to remove the return and dollar-parentheses:

is_linux_mint()
{
    grep -i mint /etc/lsb-release &gt;/dev/null 2&gt;&amp;1
}




#!/bin/sh

printc()
{
    fd=1
    color=0 # Black
    case "$1" in
        -info)
            color=4 # Blue
            ;;
        -ok)
            color=2 # Green
            ;;
        -warning)
            fd=2
            color=3 # Yellow
            ;;
        -error)
            fd=2
            color=1 # Red
            ;;
        *)
            ;;
    esac
    [ "$color" -ne 0 ] &amp;&amp; shift
    printf '%s\n' "$(tput bold)$(tput setaf "$color")$@$(tput sgr0)" &gt;&amp;"$fd"
}

show_error_and_exit()
{
    if [ "$?" -ne 0 ]
    then
        printc -error 'An error has occurred.'
        echo
        exit "$step"
    fi
}

ask_for_sudo_password()
{
    sudo sh -c ':'
}

is_linux_mint()
{
    return "$(grep -i mint /etc/lsb-release &gt;/dev/null 2&gt;&amp;1)"
}

mintupdate()
{
    if is_linux_mint
    then
        while true
        do
            echo 'Do you want to check for kernel upgrade via MintUpdate?'
            read -r answer &lt;/dev/tty
            case "$answer" in
                [Yy])
                    break
                    ;;
                [Nn])
                    exit
                    ;;
                *)
                    echo 'Please answer Y or N.'
                    ;;
            esac
        done
        sudo /usr/bin/python3 /usr/lib/linuxmint/mintUpdate/mintUpdate.py show
    fi
}

echo
printc -info '--------------------'
printc -info 'Vlasta - Laptop Mint'
printc -info '--------------------'
echo
step=0
trap show_error_and_exit EXIT
while IFS='#' read -r command description
do
    printc -ok "Step $step:$description"
    printc -warning "$command"
    step=$(( step + 1 ))
    set -e
    eval "$command"
    set +e
    printc -ok 'Success.'
    echo
done &lt;&lt;'EOF'
ask_for_sudo_password # enter password
sudo dpkg --configure -a # configure packages
sudo apt-get install --fix-broken # fix broken dependencies
sudo apt-get update # update cache
sudo apt-get upgrade # upgrade packages
sudo apt-get dist-upgrade # upgrade packages with possible removals
sudo apt-get --purge autoremove # remove unused packages
sudo apt-get autoclean # clean up
mintupdate # Linux Mint kernel upgrade
EOF


ShellCheck output:

$ shellcheck myscript

Line 26:
    printf '%s\n' "$(tput bold)$(tput setaf "$color")$@$(tput sgr0)" &gt;&amp;"$fd"
                                                     ^-- SC2145: Argument mixes string and array. Use * or separate argument.
                                                                     ^-- SC2039: In POSIX sh, &gt;&amp; is undefined.

Line 31:
    if [ "$?" -ne 0 ]
         ^-- SC2181: Check exit code directly with e.g. 'if mycmd;', not indirectly with $?.



SC2181 is a minor, irrelevant warning: the exit code can't be checked directly in this case.
SC2039 is a false positive for it couldn't detect the value of fd; just make sure that fd is always assigned a valid number here, which I have.
SC2145 is what I disagree with. See What is the difference between $* and $@?, from which I quote:


  "$*" expands to a single word "$1c$2c...". Usually c is a space, but it's actually the first character of IFS, so it can be anything you choose.





  "$@" expands to separate words: "$1" "$2" ...


Of course this depends on how you want to use printc: if you're only giving it strings to print, it's OK to use "$*"; if you're also passing it the format string, or don't want IFS to affect the output presentation, use "$@". Feel free to replace $@ with $* if you feel so inclined to clear this warning.


You forgot to enable interpretation of backslash escapes in echo with the -e option, so I didn't get colorful outputs on my openSUSE where the default for echo is to disable interpretation of backslash escapes. But it doesn't matter since options for echo aren't specified by POSIX. Also, the ANSI escape code is probably less portable, and definitely less readable, than the tput alternative, as a SO user noted.

I grabbed the printc function from my personal arsenal (with minor modifications) and put it here. Originally it didn't have the format string hard-coded in, but you always use it like echo in your code and I'm not about to define another function or add another option to get this behavior, so I sacrificed generality for convenience.

echo without any argument prints a newline, so you don't have to define a function to do just that.

The time frame between each prompt for a password by sudo is dependent on the sudoers policy, specifically the timestamp_timeout option, which can be set to 0 to always prompt for a password. Therefore, I'm not sure of the utility of your ask_for_sudo_password function when one can already type in the password on-demand, unless it is to test if sudo is available as a command, in which case it's better to write it in the same way as your is_it_linux_mint function and rename it to has_sudo or test_sudo.

Compare if is_linux_mint with if is_it_linux_mint, I think is_linux_mint is a better name.

There's no easy way to identify which Linux distribution is running on a user's system. /etc/os-release is a new standard, but for those without it, the most reliable way to find out, in general, is probably by parsing lsb_release -i, provided it is installed on the system.

I see some superfluous code in your case statement:

            [Yy]) :   ; break;;
            [Nn]) exit; break;;


It's probably done for stylistic reasons, but still I want to point out that : and the break after exit are not needed. On a side note, I prefer the shell equivalent of the Allman coding style, so I don't cramp up my case constructs into one line per pattern. But however you write your code, consistency is key.

Last but not least, you're repeating the same patterns throughout each step of the maintenance tasks, so I wrapped them in a while read loop reading from a here document. So now your while read loop, which I grouped inside a new function mintupdate, has to have its input redirected back to the controlling terminal, and rest assured that /dev/tty is required to exist by the POSIX specifications.

After the rewrite, it has become quite easy to adjust the script's behavior: you just add the commands you want to run, potentially wrapped inside functions, in sequential order in the here document. The descriptions are optional, and you can omit them by not adding #-style comments after the commands that they annotate. The # delimiter is chosen so one can easily copy the lines and run them on their terminal.