Page Body

Automating Shell Configuration With Bash

Becoming adept at using the command line interface (CLI) and Bash is one of the most rewarding prospects of being a GNU/Linux user. The initial learning curve is high, but the long-term rewards are manifold.

One of the best aspects of this fluency is the ability to customize the shell environment to fit your needs through various features. For example, Bash offers:

Over time, you may come to rely on your customizations and lament their absence when moving to a new system. Initially, you may have taken some time to optimally configure your shell. Now, you can use the skills that you have acquired to easily initialize a new system's environment.

Note: If you are not familiar with the GNU/Linux command line interface, review the Conventions page before proceeding.

The Setup

A simple way to configure a new system's shell environment is to use shell scripting itself. The solution below assumes the copying of a configuration directory to a new Debian or Fedora GNU/Linux system, where the copied directory has a structure like the following:

$ tree "${HOME}/sh_config/"
/home/amnesia/sh_config/
├── dot_files
│   ├── dbn_bashrc.txt
│   ├── dbn_profile.txt
│   ├── fda_bash_profile.txt
│   ├── fda_bashrc.txt
│   └── vimrc.txt
├── cfg_sh.bash
└── scripts
    ├── script_1.bash
    ├── script_2.bash
    ├── script_3.py
    ├── script_4.py
    └── script_5.sh

2 directories, 11 files

The configuration directory (i.e., /home/amnesia/sh_config/) contains:

  • The shell configuration script (cfg_sh.bash).
  • A directory of files that serve as the basis for the new system's dot files (dot_files/). All files in this directory should be saved with the .txt extension.

    Debian and Fedora-specific files are given special prefixes (dbn_, fda_). Distribution-agnostic files (e.g., vimrc.txt) do not require a special prefix.

  • A directory of scripts (scripts/).

Before running the shell configuration script, five variables need to be set:

  1. dot_files_src The name of the directory that contains the files that serve as the basis for the new system's dot files.
  2. deb_prefix The string prefix that has been chosen to specify files with Debian-specific dot file content.
  3. fed_prefix The string prefix that has been chosen to specify files with Fedora-specific dot file content.
  4. scripts_dir_src The name of the directory that contains the scripts to be copied to the new system.
  5. scripts_dir_dest The destination directory on the new system to copy scripts to.

After the configuration directory is copied to the new system, the shell configuration script is run.

For example, if you saved a sh_config/ configuration directory to the Downloads/ directory of your user's home folder, you could run a cfg_sh.bash configuration script like so:

bash "${HOME}/Downloads/sh_config/cfg_sh.bash"

Alternatively, you could change to the directory that contains cfg_sh.bash and issue the following command:

bash './cfg_sh.bash'

If your system passes the script's verification checks, its dot files will incorporate your custom dot file content, the configuration directory's scripts will be copied to the script directory that you specified for your new system, and the scripts will be made executable.

Script and System Preparation

The first parts of the configuration script evaluate the script arguments and ensure that the script's required variables have been set:

# Evaluate script arguments
if [[ "${#}" -gt 0 ]]; then
    case "${*}" in
        '-h' | '--help')
            display_usage
            exit 0
            ;;
        *)
            display_usage
            exit 1
            ;;
    esac
fi

dot_files_src='dot_files'  # Set dot file directory source
deb_prefix='dbn_'  # Set Debian prefix
fed_prefix='fda_'  # Set Fedora prefix
scripts_dir_src='scripts'  # Set scripts directory source
# Set scripts directory destination for new system
scripts_dir_dest="${HOME}/Documents/Scripts"

# Create required variables array
required_vars=(
    'dot_files_src'
    'deb_prefix'
    'fed_prefix'
    'scripts_dir_src'
    'scripts_dir_dest'
)
# Verify required variables are set
for ((  i=0; i < "${#required_vars[*]}"; i++ )); do

    # Check if required variable is empty using an indirect reference
    if ! [[ "${!var_name}" ]]; then
        missing='yes'
        # Prompt user to set required variable and exit
        message="Set ${var_name} "
        message+='before running the script again.'
        echo "${message}" 1>&2
    fi
done
# Exit script if variables are not set
if [[ "${missing}" ]]; then
    exit 1
fi

Above, we evaluate the script's arguments using shell special parameters and a case statement. If help is requested via the use of the -h (--help) option or incorrect arguments are provided, usage information is displayed via a call to the display_usage() function.

Then, we loop through an indexed array of required variable names and check to see if each variable is set using an indirect variable reference (i.e., "${!var_name}"). Here, an indirect reference is necessary because the required_vars array contains the string names of variables.

Without an indirect reference, [[ "${var_name}" ]] would simply check to see if the var_name variable contains a value, which it always will (i.e., var_name="${required_vars[i]}" # Set current required variable name). By using an indirect reference, we instruct the shell to refer to the variable whose name is assigned to the var_name variable.

The following example helps illustrate this concept:

$ protagonist='Nightwing'
$ antagonist='Deathstroke'
$ agonist='protagonist'
$ echo "${agonist} rules!"
protagonist rules!
$ echo "${!agonist} rules!"
Nightwing rules!

If a variable is not set, a message with the variable name is displayed, reminding the user to set the variable before attempting to run the script again.

Once the above criteria are met, the script uses the realpath command and a special parameter to obtain the absolute path of the shell configuration script as it is running on your system:

abs_path="$(realpath "${0}")" # Get absolute path of script

Then, the absolute path of the script configuration directory is extracted from abs_path using a pattern matching operator:

# Get absolute path of script configuration directory
dir_path="${abs_path%/*}"

Configuration

The rest of the script performs the actual configuration steps for the new GNU/Linux system.

The files in the dot files source directory are looped through and, once processed in a distribution-sensitive way, their content is either added to the new system's equivalent dot file (e.g., .bashrc) or used to create a new dot file (e.g., .vimrc).

# Filter out extraneous files, customize command prompt
if grep -iq 'debian' '/etc/os-release'; then
    # Loop through each .txt file in dot_files_src
    for f in ./${dot_files_src}/*; do
        # Filter out Fedora-specific files
        if ! [[ "${f}" =~ ${dot_files_src}/${fed_prefix}.+ ]]; then
            f_base="${f%.txt}"
            # Remove Debian prefix from filename
            if [[ "${f_base}" =~ ${dot_files_src}/${deb_prefix}+ ]]; then
                f_base="${f_base/${deb_prefix}/}"
            fi
            # Create file basename
            f_base="${f_base##*/}"
            # Append file content
            cat "${f}" >> "${HOME}/.${f_base}"
        fi
    done
elif grep -iq 'fedora' '/etc/os-release'; then
    for f in ./${dot_files_src}/*; do
        if ! [[ "${f}" =~ ${dot_files_src}/${deb_prefix}.+ ]]; then
            f_base="${f%.txt}"
            if [[ "${f_base}" =~ ${dot_files_src}/${fed_prefix}.+ ]]; then
                f_base="${f_base/${fed_prefix}/}"
            fi
            f_base="${f_base##*/}"
            cat "${f}" >> "${HOME}/.${f_base}"
        fi
    done
else
    err_msg='\nUnable to verify supported GNU/Linux '
    err_msg+='distribution.\n'
    echo -e "${err_msg}" 1>&2
    exit 1
fi

Finally, the script destination directory on the new system is created (if applicable) and the scripts are copied over. After the scripts are made executable, a message is displayed to make the configuration script's changes to the new system effective:

pass_or_fail="$(val_extant_dir "${scripts_dir_dest}")"
if [[ "${pass_or_fail}" -ne 0 ]]; then
    mkdir "${scripts_dir_dest}"  # Create script directory
fi
# Copy over scripts
for f in ${dir_path}/${scripts_dir_src}/*; do
    cp -f "${f}" "${scripts_dir_dest}"
done
chmod -R 700 "${scripts_dir_dest}"  # Make scripts executable

# Display log out message
echo -e '\nLog out and log back in to see changes take effect.\n'

The full script is available below:

The Virtuous Cycle

Adopting Free/Libre Open Source Software (FLOSS) creates a virtuous cycle. By ensuring the freedoms of its users, individuals and their communities have a chance to use what they learn to further improve FLOSS and their lives.

Enjoyed this post?

Subscribe to the feed for the latest updates.