Skip to main content

How to create Bash scripts using external variables and embedded scripts

Use external variables and embedded scripts to enhance your Bash programming with interactive scripts.
Image
Linux command line output with green text

Image by joffi from Pixabay

There are times when a script must ask for information that can't be stored in a configuration file or when the number of choices won't allow you to specify every possibility. Bash is pretty good at making interactive scripts to address these kinds of issues.

Ideally, by the end of this article, you should be able to do the following:

  • Write small programs that ask the user questions and save the answers (including sensitive ones, like passwords)
  • Read data from configuration files by using other programs
  • Allow the script to skip asking questions if external variables are defined
  • And as a bonus, write a nice user interface (UI) with text dialogs

Start with a small script to connect to a remote desktop using the RDP protocol.

[ You might also enjoy reading: Using Bash for automation ]

Case study: Connect to a remote server using RDP

On Linux, there are many RDP clients, and a really good one is freerdp. One way to call it is to pass a long line of flags (with confusing short names) like this:

/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:REMOTE_USER /v:MACHINE /p:mynotsosecretpassword

Is there a better way to do this?

Asking questions, learning how to read

So for a first try, I wrote (version 1) a shell wrapper around freerdp that asks for the user, password, and remote machine. I will use the Bash built-in read command:

#!/bin/bash
# author Jose Vicente Nunez
# Do not use this script on a public computer.
tmp_file=$(/usr/bin/mktemp 2>/dev/null) || exit 100
trap '/bin/rm -f $tmp_file' QUIT EXIT INT
/bin/chmod go-wrx "${tmp_file}" > /dev/null 2>&1
read -r -p "Remote RPD user: " REMOTE_USER|| exit 100
test -z "$REMOTE_USER" && exit 100
read -r -s -p "Password for $REMOTE_USER: " PASSWD|| exit 100
test -z "$PASSWD" && exit 100
echo
echo > "$tmp_file"|| exit 100
read -r -p "Remote server: " MACHINE|| exit 100
test -z "$REMOTE_USER" && exit 100
/usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:"$REMOTE_USER" /v:"${MACHINE}" /p:"(/bin/cat ${tmp_file})"

To read (lines 7, 13) into a variable, you just say read variable. To make it more friendly, pass -p (show a custom prompt) and -r (read backslashes if you make a typo).

read also allows you to suppress the characters you write on the screen. The option is called -s (secret) mode (line 9).

One thing that bothers me is that anyone doing a ps -ef can see my password on the command line; to avoid that, I save it into a file, and then, using a subshell, I read it back when xfreerdp needs it. Furthermore, to avoid leaving my password lying around on the disk, I save it into a temporary file, which I ensure gets removed once the script finishes or it is killed.

But still... this script keeps asking some questions over and over. Is there is a way to make it, well, smarter?

You could save some of the defaults, like the remote servers, in a configuration file. If you provide none, then use the default settings.

Also on the topic of code reuse: Put the logic of how to connect to a remote server in a separate file in case you want to reuse some of this logic in other similar situations. So the new library looks like this:

#!/bin/bash
# author Jose Vicente Nunez
# Common logic for RDP connectivity
function remote_rpd {
    local remote_user=$1
    local pfile=$2
    local machine=$3
    test -z "$remote_user" && exit 100
    test ! -f "$pfile" && exit 100
    test -z "$machine" && exit 100
    /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:"$remote_user" /v:"${machine}" /p:"(/bin/cat ${pfile})" && return 0|| return 1
}

The RDP wrapper, version 2 of the original script, is much simpler now:

#!/bin/bash
# author Jose Vicente Nunez
# Do not use this script on a public computer.
# shellcheck source=/dev/null.
. "rdp_common.sh"
tmp_file=$(/usr/bin/mktemp 2>/dev/null) || exit 100
trap '/bin/rm -f $tmp_file' QUIT EXIT INT
/bin/chmod go-wrx "${tmp_file}" > /dev/null 2>&1
read -r -p "Remote RPD user: " REMOTE_USER|| exit 100
read -r -s -p "Password for $REMOTE_USER: " PASSWD|| exit 100
echo
echo "$PASSWD" > "$tmp_file"|| exit 100
read -r -p "Remote server: " MACHINE|| exit 100
remote_rpd "$REMOTE_USER" "$tmp_file" "$MACHINE"

So after this change, how does it look?

$ ./kodegeek_rdp2.sh
Remote RPD user: jose
Password for jose: 
Remote server: myremotemachine.kodegeek.com

There is more room for improvement, so please keep reading.

Always give users a choice: External variables and more external programs

Say that you use your script to connect to the same machine every day. The chances are that you will not change your remote user, machine, and only the password once in a while. So you can save all those settings in a configuration file, readable only by the current user and nobody else:

(Example of ~/.config/scripts/kodegeek_rdp.json)

{
    "machines": [
        {
            "name": "myremotemachine.kodegeek.com",
            "description": "Personal-PC"
        },
        {
            "name": "vmdesktop1.kodegeek.com",
            "description": "Virtual-Machine"
        }
    ],
    "remote_user": "jose@MYCOMPANY",
    "title" : "Remote desktop settings"
}

Yes, JSON is not the best format for configuration files, but this one is pretty small. Also, notice that you can now store more than one remote machine (for simplicity, use only the first one).

To take advantage of it, modify the library (v2) to look like this:

#!/bin/bash
# author Jose Vicente Nunez
# Common logic for RDP connectivity
if [[ -x '/usr/bin/jq' ]] && [[ -f "$HOME/.config/scripts/kodegeek_rdp.json" ]]; then
    REMOTE_USER="$(/usr/bin/jq --compact-output --raw-output '.remote_user' "$HOME"/.config/scripts/kodegeek_rdp.json)"|| exit 100
    MACHINE="$(/usr/bin/jq --compact-output --raw-output '.machines[0]| join(",")' "$HOME"/.config/scripts/kodegeek_rdp.json)"|| exit 100
    export REMOTE_USER
    export MACHINE
fi


function remote_rpd {
    local remote_user=$1
    local pfile=$2
    local machine=$3
    test -z "$remote_user" && exit 100
    test ! -f "$pfile" && exit 100
    test -z "$machine" && exit 100
    /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:"$remote_user" /v:"${machine}" /p:"(/bin/cat ${pfile})" && return 0|| return 1
}

Did you notice I didn't try to read the password from a configuration file? That's the only credential I will keep asking over and over unless it is encrypted. The rest of the values you get using jq, utilizing a subshell.

And, of course, here is a new version (v3) of the script:

#!/bin/bash
# author Jose Vicente Nunez
# Do not use this script on a public computer.
# shellcheck source=/dev/null
. "rdp_common2.sh" 
tmp_file=$(/usr/bin/mktemp 2>/dev/null) || exit 100
trap '/bin/rm -f $tmp_file' QUIT EXIT INT
/bin/chmod go-wrx "${tmp_file}" > /dev/null 2>&1
if [ -z "$REMOTE_USER" ]; then
    read -r -p "Remote RPD user: " REMOTE_USER|| exit 100
fi
read -r -s -p "Password for $REMOTE_USER: " PASSWD|| exit 100
echo
echo "$PASSWD" > "$tmp_file"|| exit 100
if [ -z "$MACHINE" ]; then
    read -r -p "Remote server: " MACHINE|| exit 100
fi
remote_rpd "$REMOTE_USER" "$tmp_file" "$MACHINE"

Notice that you don't ask for two parameters anymore; just the password:

$ ./kodegeek_rdp2.sh 
Password for jose@MYCOMPANY: 

Is there anything else you can do to enhance this script?

I want a nice text UI: Nothing like a good dialog

Here's how to write an interactive script with an easy tool called Dialog. It asks the user to choose between a variable number of machines (depending on the configuration file) and, of course, the password. However, if the remote user is the same for both machines (which is normal if you connect to the same company), it will not ask for the information each time.

Note that Dialog is not the only player in town. I just happen to like it because it is widely available and because of its simplicity.

Below is version 3 of the script. It is heavily commented. You can see that Dialog works by reading either variables or files to enable/disable options. Give it a shot and run the script to see how each part fits together:

#!/bin/bash
# author Jose Vicente Nunez
# Do not use this script on a public computer.
# https://invisible-island.net/dialog/
SCRIPT_NAME="$(/usr/bin/basename "$0")"
DATA_FILE="$HOME/.config/scripts/kodegeek_rdp.json"
test -f "$DATA_FILE"|| exit 100
: "${DIALOG_OK=0}"
: "${DIALOG_CANCEL=1}"
: "${DIALOG_HELP=2}"
: "${DIALOG_EXTRA=3}"
: "${DIALOG_ITEM_HELP=4}"
: "${DIALOG_ESC=255}"
tmp_file=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file=/tmp/test$$
trap '/bin/rm -f $tmp_file' QUIT EXIT INT
/bin/chmod go-wrx "${tmp_file}" > /dev/null 2>&1

TITLE=$(/usr/bin/jq --compact-output --raw-output '.title' "$DATA_FILE")|| exit 100
REMOTE_USER=$(/usr/bin/jq --compact-output --raw-output '.remote_user' "$DATA_FILE")|| exit 100

# Choose a machine
MACHINES=$(
    tmp_file2=$(/usr/bin/mktemp 2>/dev/null) || declare tmp_file2=/tmp/test$$
    /usr/bin/jq --compact-output --raw-output '.machines[]| join(",")' "$DATA_FILE" > $tmp_file2|| exit 100
    declare -i i=0
    while read -r line; do
        machine=$(echo "$line"| /usr/bin/cut -d',' -f1)|| exit 100
        desc=$(echo "$line"| /usr/bin/cut -d',' -f2)|| exit 100
        toggle=off
        if [ $i -eq 0 ]; then
            toggle=on
            ((i=i+1))
        fi
        echo "$machine" "$desc" "$toggle"
    done < "$tmp_file2"
    /bin/cp /dev/null $tmp_file2
) || exit 100
# shellcheck disable=SC2086
/usr/bin/dialog \
    --clear \
    --title "$TITLE" \
    --radiolist "Which machine do you want to use?" 20 61 2 \
    $MACHINES 2> ${tmp_file}
return_value=$?

case $return_value in
  "$DIALOG_OK")
    remote_machine="$(/bin/cat ${tmp_file})"
    ;;
  "$DIALOG_CANCEL")
    echo "Cancel pressed.";;
  "$DIALOG_HELP")
    echo "Help pressed.";;
  "$DIALOG_EXTRA")
    echo "Extra button pressed.";;
  "$DIALOG_ITEM_HELP")
    echo "Item-help button pressed.";;
  "$DIALOG_ESC")
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

if [ -z "${remote_machine}" ]; then
  /usr/bin/dialog \
      --clear  \
    --title "Error, no machine selected?" --clear "$@" \
           --msgbox "No machine was selected!. Will exit now..." 15 30
  exit 100
fi

# Ask for the password
/bin/rm -f ${tmp_file}
/usr/bin/dialog \
  --title "$TITLE" \
  --clear  \
  --insecure \
  --passwordbox "Please enter your remote password for ${remote_machine}\n" 16 51 2> $tmp_file
return_value=$?
passwd=$(/bin/cat ${tmp_file})
/bin/rm -f "$tmp_file"
if [ -z "${passwd}" ]; then
  /usr/bin/dialog \
      --clear  \
    --title "Error, empty password" --clear "$@" \
           --msgbox "Empty password!" 15 30
  exit 100
fi

# Try to connect
case $return_value in
  "$DIALOG_OK")
    /usr/bin/mkdir -p -v "$HOME"/logs
    /usr/bin/xfreerdp /cert-ignore /sound:sys:alsa /f /u:"$REMOTE_USER" /v:"${remote_machine}" /p:"${passwd}"| \
    /usr/bin/tee "$HOME"/logs/"$SCRIPT_NAME"-"$remote_machine".log
    ;;
  "$DIALOG_CANCEL")
    echo "Cancel pressed.";;
  "$DIALOG_HELP")
    echo "Help pressed.";;
  "$DIALOG_EXTRA")
    echo "Extra button pressed.";;
  "$DIALOG_ITEM_HELP")
    echo "Item-help button pressed.";;
  "$DIALOG_ESC")
    if test -s $tmp_file ; then
      /bin/rm -f $tmp_file
    else
      echo "ESC pressed."
    fi
    ;;
esac

[ Getting started with containers? Check out this free course. Deploying containerized applications: A technical overview. ]

Wrap up

That was a lot of ground to cover in one article. Scripts such as the one developed in this article simplify connections and make the interface easier for users. Here are the things you learned how to do:

  • You can use Bash's built-in read command to get information from your users.
  • You can check if repetitive information is already available to avoid reading from the environment.
  • You don't save passwords without encryption. KeepPassXC and Vault are excellent tools you can use to avoid hardcoding sensitive information in the wrong places.
  • You want a nicer UI, so you can use Dialog and other readily available tools to make it happen.
  • Always validate your inputs and check for errors.
Topics:   Linux   Linux administration   Bash   Scripting  
Author’s photo

Jose Vicente Nunez

Proud dad and husband, software developer and sysadmin. Recreational runner and geek. More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.