DE

Bash in Fedora

Bash is the default shell in Fedora. The distribution ships it with sensible presets and additional functionality. This article discusses the relevant configuration files and explains how they can be customized.

Personally I use the Fedora Sway Spin. I wasn’t able to find a repository for the Workstation configuration, but I would assume that the Bash settings are essentially the same for both variants of the distribution. If there are important differences, I would appreciate feedback via email or Mastodon!

The Login Shell and Modern Modes

The main purpose of a shell is to execute applications. A text-based shell runs in a so-called terminal and processes user input entered via the keyboard.

Additionally, for system administration and automation, it’s possible to combine commands into scripts. The process is facilitated by programming constructs such as control structures and variables that are an integral part of the shell language. Scripts behave as self-contained programs and can be parametrized to influence behavior when it’s called from the command line.

For ordinary purposes, shell scripts should be written so that they behave in exactly the same way when executed on different machines. Instead of loading configuration files, the execution environment is defined within the script itself. In the following, I’ll only talk about the interactive use of the shell.

Traditionally, computers were operated entirely through text. Users would log into the operating system and then execute commands via a shell, possibly with inputs through a physical terminal. Terminals as standalone applications (or even multiplexers like Screen) didn’t exist yet; users were limited to the one shell (or shell instance) that greets them after login.

Even back then, there were tools that took up the whole terminal real estate.1 Naturally, you wouldn’t want to close the running application to execute shell commands. Background processes were not yet part of the OS, either. For that reason, programs like ex and vi provided an :sh command that would call up a nested shell. Login shell was coined to refer to the embedding process.

In modern systems the procedure is somewhat similar, since users are generally first prompted to log in with their name and password. Fedora uses SDDM for this stage of the boot sequence. However, this wouldn’t start an interactive shell but a graphical session with the desktop environment or window manager that was selected in the previous step. Terminals still exist, but they’ve changed in nature. Today they are window-based applications on a par with other graphical user interfaces. Unlike the hardware they emulate, the can run multiple instances of the same shell.

When a shell is started with a terminal emulator, it is usually not a login shell in the traditional sense. To still be able to play this role under certain circumstances, Bash can be executed in the login mode. There is the --login flag to manually evoke Bash so that it would simulate the login-shell behavior.

To my mind, it’s no longer obvious which contexts involve a login in the relevant sense. For this reason, it’s harder to decide what special requirements should be placed on the configuration for these environments. Intuitively, the login mode is used for general system administration purposes, and we’ll see that the Fedora configuration reflects this preconception. For a paradigmatic example, think of logging into a system via an SSH connection.

Just as you specify which shell a terminal emulator should use, you can choose the login shell for users of the operating system. Distributions typically use Bash, as does Fedora. But it’s of course possible to set Zsh or Fish, for instance:

sudo chsh --shell <path-to-shell> <user-name> # See /etc/shells for valid options

Bash can run in modes not mentioned here, for instance with POSIX compatibility. The modes are not mutually exclusive: a login shell, for example, is typically operated interactively. The main distinction outlined in this section is special, because different configuration files are sourced depending on the mode in which Bash is running.

In the following sections, I’ll first present an overview of the relevant files and their order of execution. I’ll then discuss the presets you would find in a fresh Fedora installation.

Bash Startup Files

As mentioned in the previous section, Bash automatically loads some configuration files. A distinction must be made between global and user-specific shell settings. The latter are found in the home directory, the former under /etc/.

The Bash man page explains:

  • /etc/profile: “The systemwide initialization file, executed for login shells”
  • ~/.bash_profile: “The personal initialization file, executed for login shells”
  • ~/.bashrc: “The individual per-interactive-shell startup file”
  • /etc/bash.bash_logout: “The systemwide login shell cleanup file, executed when a login shell exits”
  • ~/.bash_logout: “The individual login shell cleanup file, executed when a login shell exits”

The startup behavior can be suppressed by --noprofile and --norc flags, respectively. This affects both the user-specific and the system-wide configuration. With --rcfile, the path to a configuration file can be specified, for instance to use the ~/.config directory (as suggested by the XDG standard).

As we’ll see, Fedora uses /etc/bashrc as system-wide configuration file similar to the login-shell case. The file is not mentioned in the man page, and neither is /etc/bash.bashrc (as used by other distributions). I’ll talk about its intended use below, but it might be noted that it’s not part of the offical standard.

It might also be worth noticing that the system-wide profile is sourced even if the local .bash_profile exists. So it doesn’t just serve as a fallback. .bash_login or .profile are possible user-level alternatives, but they are mutually exclusive and .bash_profile takes precedence.

As the man page explains, logout files are used to automatically perform some cleanup tasks at the end of a login-shell session. Fedora doesn’t include these files, which is why I won’t discuss them further.

Startup Files in Fedora

In Fedora, additional files are sourced with Bash by default. The most important difference is that an additional system-wide /etc/bashrc is defined and explicitly imported at the beginning of the user-specific .bashrc. Thus, as with the login shell, the system-wide configuration is loaded first and the user-specific configuration afterwards.

In the configuration for login mode, the settings of a regular terminal session are loaded by importing the local .bashrc in .bash_profile and the global bashrc in /etc/profile. This is different from Bash’s standard behavior, where the configuration of login shells is done independently.

Thus, in a login shell the user has available the same aliases and functions that are familiar from other contexts. If environment variables are set in RC files, this extends or modifies the profile environment.

A safeguard mechanism ensures that the global RC file is loaded only once in a Bash session:

# /etc/bashrc

# Prevent doublesourcing
if [ -z "$BASHRCSOURCED" ]; then
  BASHRCSOURCED="Y"

Without this check, it would be executed in the login context first system-wide (in /etc/profile) and then locally (in .bash_profile).

In both modes, additional configuration scripts (with an *.sh extension) are placed in and sourced from /etc/profile.d/. Contrary to what the naming scheme might suggest, they are manually imported in both cases, i.e. in /etc/profile and /etc/bashrc.

This directory is also intended for your own system-wide configurations, for example in /etc/profile.d/custom.sh. ~/.bashrc.d/ is intended for local configuration modules of a regular shell session. Its scripts are imported in ~/.bashrc, but the directory itself does not exist in Fedora by default.

Intended Purpose of Use

As explained, different configuration files are automatically loaded depending on Bash’s mode of execution. In addition to this, Fedora provides guidelines on what types of settings should be placed in each of them.

The relevant comments are included as headers for /etc/profile and /etc/bashrc:

  • profile is for “System wide environment and startup programs, for login setup”, i.e., for the definition of environment variables and programs that should be executed automatically when Bash starts; and
  • bashrc is for “system wide functions and aliases”.

As “for login setup” indicates, the distinction is only assumed for the login context. For a regular terminal session, the environment defined in /etc/profile files wouldn’t apply without sourcing it explicitly.

Perhaps the distinction is based on the principle that a system-wide environment should only be defined for a login shell, while the specific enfironment for a regular terminal session is best defined at the user level. If so, the rationale for this principle seems reasonable though overly complicated.

Maybe it’s best to keep things simple and exclusively use the user-specific .bashrc (with bashrc.d) for your own configurations, especially on systems with only one active user. If you see no point in the outdated idea of a login shell, you don’t need to touch the local .bash_profile at all. In Fedora, it (only) imports the .bashrc, projecting its settings to the login context.

The system-wide /etc/profile and /etc/bashrc should only be edited in very special cases, as emphasized in a warning at the beginning of the files. Fedora maintainers may make changes to their content at a later point, in which case future updates of the distribution would overwrite your own modifications.

Fedora Presets

The files mentioned so far contain many sensible default settings, which I’ll address in this section. They concern not only Bash itself but also the behavior of certain CLI applications.

In the following subsections, code listings are included. In two or three places, I add less significant remarks as comments. The listings are structured thematically and do not correspond to the exact sequence of the files under discussion.

Environment

When a user starts Bash in a terminal emulator, only ~/.bashrc is sourced by default. We’ve already seen that in Fedora, additional files are included:

# Source global definitions
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi

# User specific aliases and functions
if [ -d ~/.bashrc.d ]; then
    for rc in ~/.bashrc.d/*; do
        if [ -f "$rc" ]; then
            . "$rc"
        fi
    done
fi
unset rc

Other than that, it only contains the addition of two home directories to the path, namely ~/.local/bin/ and ~/bin/:

# User specific environment
if ! [[ "$PATH" =~ "$HOME/.local/bin:$HOME/bin:" ]]; then
    PATH="$HOME/.local/bin:$HOME/bin:$PATH"
fi
export PATH

As the heading indicates, user-specific environment variables may be defined here. An additional export is necessary so that they are also available in child processes of the shell - scripts and other programs called by it.

For a login shell, an environment with system information is established. It’s defined in the system-wide profile file:

if [ -x /usr/bin/id ]; then
    if [ -z "$EUID" ]; then
        # ksh workaround
        EUID=`/usr/bin/id -u`
        UID=`/usr/bin/id -ru`
    fi
    USER="`/usr/bin/id -un`"
    LOGNAME=$USER
    MAIL="/var/spool/mail/$USER"
fi

HOSTNAME=$(/usr/bin/hostnamectl --transient 2>/dev/null) || \
HOSTNAME=$(/usr/bin/hostname 2>/dev/null) || \
HOSTNAME=$(/usr/bin/uname -n)

export PATH USER LOGNAME MAIL HOSTNAME

The path for this context is defined with a helper function:

pathmunge () {
    case ":${PATH}:" in
        *:"$1":*)
            ;;
        *)
            if [ "$2" = "after" ] ; then
                PATH=$PATH:$1
            else
                PATH=$1:$PATH
            fi
    esac
}
# Path manipulation
if [ "$EUID" = "0" ]; then
    pathmunge /usr/sbin
    pathmunge /usr/local/sbin
else
    pathmunge /usr/local/sbin after
    pathmunge /usr/sbin after
fi

unset -f pathmunge

The sbin directories contain binary files for tools used for system administration. These include applications like ipconfig, fdisk, or shutdown, which should be readily available in a login context.

Interestingly, the sbin components are also found on the path of a regular terminal session. I couldn’t find an entry in the RC files responsible for this behavior.

To make your own changes to the global environment of the login shell, the file /etc/profile.d/sh.local is included. In a newly set up system, it contains only a comment:

#Add any required envvar overrides to this file, it is sourced from /etc/profile

Here you can define environment variables safe from possible overwrites through distribution updates.

Prompt

In a shell, symbols like $ or > indicate that user input is expected. This aspect of what is visible in a terminal window is referred to as a prompt.

In Bash, the prompt by default contains additional components that provide some information about the running system, for example:

bash-5.2$

Visual appearance and content of the prompt can be customized. Fedora overwrites the Bash preset:

# Change the default prompt string
[ "$PS1" = "\\s-\\v\\\$ " ] && PS1="[\u@\h \W]\\$ "

The PS1 environment variable is used to specify the format of the (primary) prompt using special escape sequences. The first string codifies the standard format exemplified above, consisting of shell name (\s), shell version (\v), and prompt character (the literal $).

The line as a whole implies: if the default format is used, use the format in the second string instead. \u stands for username, \h for hostname, and \W for an abbreviated form of the current working directory. Thus, the prompt might look like:

[kai@fedora-laptop www]$

With the PROMPT_COMMAND environment variable, a command can be set that is executed before each display of the prompt. In Fedora, it is defined as follows:

# are we an interactive shell?
if [ "$PS1" ]; then
  if [ -z "$PROMPT_COMMAND" ]; then
    declare -a PROMPT_COMMAND
    case $TERM in
    xterm*)
      if [ -e /etc/sysconfig/bash-prompt-xterm ]; then
        PROMPT_COMMAND=/etc/sysconfig/bash-prompt-xterm
      else
        PROMPT_COMMAND='printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/\~}"'
      fi
      ;;
    screen*)
      if [ -e /etc/sysconfig/bash-prompt-screen ]; then
        PROMPT_COMMAND=/etc/sysconfig/bash-prompt-screen
      else
        PROMPT_COMMAND='printf "\033k%s@%s:%s\033\\" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/\~}"'
      fi
      ;;
    *)
      [ -e /etc/sysconfig/bash-prompt-default ] && PROMPT_COMMAND=/etc/sysconfig/bash-prompt-default
      ;;
    esac
  fi
fi

Following a standard, scripts under /etc/sysconfig/ are executed as prompt commands if they exist. Here, a distinction is made between a regular terminal (the value of $TERM starts with xterm*) and a terminal multiplexer (the value of $TERM starts with screen*).

Fedora doesn’t come with these files included. Instead, a printf command with invisible output is used to set the title of the terminal window. If the application executed in the foreground changes, the window title also changes dynamically each time the prompt reloads. The command could also be used to visibly display information in the line above the prompt. The variable can be overwritten at the user level, as when using the Starship prompt.

Under /etc/profile.d/bash-color-prompt.sh, there is a script that can be used to color-code components of the prompt. An explanation of its use can be found under /usr/share/doc/bash-color-prompt/README.md.

Internationalization

Some CLI applications base their internationalization (i18n) on the values of certain environment variables. This is the case for GNU AWK, for example. Fedora uses /etc/profile.d/lang.sh to ensure proper functioning through sensible fallbacks.

The locale command can be utilized to check whether the LC_* environment variables have been properly defined (for their intended purpose, see the Arch Wiki). Localization problems may arise if the command returns any errors. In this case, the variables are reset to their default values in the lang.sh:

# If unavailable, reset to the default. Do this before reading in any
# explicit user configuration. We simply check if locale emits any
# warnings, and assume that the settings are invalid if it does.
if [ -n "$(/usr/bin/locale 2>&1 1>/dev/null)" ]; then
    [ -z "$LANG" ] || LANG=C.UTF-8
    unset LC_ALL # If it were defined, it would override the other values
    LC_CTYPE="C.UTF-8"
    LC_NUMERIC="C.UTF-8"
    LC_TIME="C.UTF-8"
    LC_COLLATE="C.UTF-8"
    LC_MONETARY="C.UTF-8"
    LC_MESSAGES="C.UTF-8"
    LC_PAPER="C.UTF-8"
    LC_NAME="C.UTF-8"
    LC_ADDRESS="C.UTF-8"
    LC_TELEPHONE="C.UTF-8"
    LC_MEASUREMENT="C.UTF-8"
    LC_IDENTIFICATION="C.UTF-8"
fi

Next, the environment is established as defined in two standard configuration files: /etc/locale.conf and ~/.i18n. The latter file does not exist in Fedora by default, the former contains only a single variable definition. Its value depends on what was set during the installation process, for example:

LANG="en_US.UTF-8"

The import involves a security mechanism:

for config in /etc/locale.conf "${HOME}/.i18n"; do
    if [ -f "${config}" ]; then
        # NOTE: We are using eval & sed here to avoid invoking of any commands & functions from those files.
        if [ -x /usr/bin/sed ]; then
            eval $(/usr/bin/sed -r -e 's/^[[:blank:]]*([[:upper:]_]+)=([[:print:][:digit:]\._-]+|"[[:print:][:digit:]\._-]+")/export \1=\2/;t;d' ${config})
        else
            #but if we don't have sed, let's go old way and source it
            [ -f "${config}" ] && . "${config}"
        fi
    fi
done

This ensures that only lines in the variable=value format are imported. This processing method prevents Bash from unintentionally executing (other) commands. Should sed not be available for whatever reason, the content would be sourced directly in the more usual way.

Another safeguard mechanism in the lang.sh concerns the LC_ALL environment variable. Since it would overwrite all other LC_* values, it should only be set deliberately. This is ensured in the following way:

# The LC_ALL is not supposed to be set in /etc/locale.conf according to 'man 5 locale.conf'.
# If it is set, then we we expect it is user's explicit override (most likely from ~/.i18n file).
# See 'man 7 locale' for more info about LC_ALL.
if [ -n "${LC_ALL}" ]; then
    if [ "${LC_ALL}" != "${LANG}" -a -n "${LANG}" ]; then
        export LC_ALL
    else
        unset LC_ALL
    fi
fi

A final measure concerns so-called virtual consoles, the heirs to the real login shell. They are started with the operating system, and we can enter them with Ctrl+Alt+F<1-9>. They are special since they are also not graphical in nature. That could lead to issues with languages that use certain special character sets, like Japanese, Korean, or Arabic.

# The ${LANG} manipulation is necessary only in virtual terminal (a.k.a. console - /dev/tty*):
if [ -n "${LANG}" ] && [ "${TERM}" = 'linux' ] && /usr/bin/tty | /usr/bin/grep --quiet -e '/dev/tty'; then
    if /usr/bin/grep --quiet -E -i -e '^.+\.utf-?8$' <<< "${LANG}"; then
        case ${LANG} in
            ja*)    LANG=en_US.UTF-8 ;;
            ko*)    LANG=en_US.UTF-8 ;;
            si*)    LANG=en_US.UTF-8 ;;
            zh*)    LANG=en_US.UTF-8 ;;
            ar*)    LANG=en_US.UTF-8 ;;
            fa*)    LANG=en_US.UTF-8 ;;
            he*)    LANG=en_US.UTF-8 ;;
            en_IN*) true             ;;
            *_IN*)  LANG=en_US.UTF-8 ;;
        esac
    else
        case ${LANG} in
            ja*)    LANG=en_US ;;
            ko*)    LANG=en_US ;;
            si*)    LANG=en_US ;;
            zh*)    LANG=en_US ;;
            ar*)    LANG=en_US ;;
            fa*)    LANG=en_US ;;
            he*)    LANG=en_US ;;
            en_IN*) true       ;;
            *_IN*)  LANG=en_US ;;
        esac
    fi

    # NOTE: We are not exporting the ${LANG} here again on purpose.
    #       If user starts GUI session from console manually, then
    #       the previously set LANG should be okay to use.
fi

To avoid weird characters in virtual consoles, the language settings are set to English in these cases.

Bash Options

Bash itself can be configured via environment variables and via some options that can be set with the built-in shopt -s command. The available options and their set values can be displayed by calling shopt without arguments.

It may be argued what options should be in place as sensible defaults. Fedora remains largely neutral on the issue and activates only two such settings:

# /etc/bashrc

# Turn on parallel history
shopt -s histappend
# Turn on checkwinsize
shopt -s checkwinsize

checkwinsize ensures that shell print-out remains legible despite possible changes to the size of the containing terminal window. So, it’s the kind of option that make you go, who wouldn’t want that?

histappend concerns the case where multiple Bash sessions are started simultaneously. Without the shell behaves in such a way that each newly started session overwrites the history file (~/.bash_history). Thus, changes to the hitory (file) would only depend on the last session and its most recent commands. The given option causes the different sessions to append their commands to the history.

The system-wide profile includes two more settings that concern the Bash history:

# /etc/profile
if [ -z "$HISTSIZE" ] ; then
    HISTSIZE=1000 # maximum number of commands stored
fi

if [ "$HISTCONTROL" = "ignorespace" ] ; then
    export HISTCONTROL=ignoreboth
else
    export HISTCONTROL=ignoredups
fi

export HISTSIZE HISTCONTROL

The condition concerns ignorespace and ignoredups. The latter option prevents duplicates: If an executed command is already in the history, it is appended at the end and the earlier entry is removed. The ignorespace option gives users a simple way to execute a command without adding it to the history, namely by prefixing a space.

Both branches imply that the history should not contain duplicates. In the second case the requirement is explicitly stated. In the first case we have the same result, because ignoreboth comprises both ignorespace and ignoredups.

It’s probably reasonable to add checkwinsize and ignoredups to your RC configuration, too. You may also want to set some of the following options:

shopt -s cmdhist # Store multi-line commands as single history entry
shopt -s xpg_echo # Allow escape sequences like `\n` in `echo` (POSIX standard)
shopt -s nullglob # Unmatched globbing patterns expand to nothing (instead of the pattern)
shopt -s nocaseglob # Case-insensitive globbing
shopt -s autocd # Change directory without prepended `cd`
shopt -s cdspell # Bash will fix minor typos in `cd` commands

Aliases

In Fedora, common shell commands like grep and ls are displayed in color. Their respective man pages reveal that this can be done by certain command flags. Interestingly, the result doesn’t depend on actually using those flags. Moreover, additional commands are available, like ll for an ls list view.

The reason for these behaviors is that some aliases reinterpret existing commands. To list them, use alias without any arguments:

> alias
alias egrep='grep -E --color=auto'
alias fgrep='grep -F --color=auto'
alias grep='grep --color=auto'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot'
alias xzegrep='xzegrep --color=auto'
alias xzfgrep='xzfgrep --color=auto'
alias xzgrep='xzgrep --color=auto'
alias zegrep='zegrep --color=auto'
alias zfgrep='zfgrep --color=auto'
alias zgrep='zgrep --color=auto'

The definitions can be found in modules under /etc/profile.d/. For example, grep appears color-coded due to the following setting:

# /etc/profile.d/colorgrep.sh
/usr/libexec/grepconf.sh -c || return

alias grep='grep --color=auto' 2>/dev/null
alias egrep='grep -E --color=auto' 2>/dev/null
alias fgrep='grep -F --color=auto' 2>/dev/null

The file is also responsible for the fact that egrep and fgrep are available. Similar settings for zgrep, xzgrep, and ls can be found in similar modules. colorls.sh also contains definitions for ll and l..

There are other standard tools with a color option. For example, one could add the following file:

# /etc/profile.d/colorip.sh
alias ip='ip -color=auto'

Another alias confers extended functionality to the which command (depending on the shell used):

# Initialization script for bash, sh, mksh and ksh

case "$(basename $(readlink /proc/$$/exe))" in
*ksh*|zsh)
    alias which='alias | /usr/bin/which --tty-only --read-alias --show-tilde --show-dot'
    ;;
bash|sh)
    alias which='(alias; declare -f) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot'
    ;;
*)
    ;;
esac

Normally, which only recognizes binary files on the path. This redefinition first checks whether an alias exists with the name in question. For this reason, which ll is able to show the corresponding alias definition; the ordinary which would merely have pointed out that ll is not on the path.

Configuration for other CLI Applications

There are other application-specific scripts under /etc/profile.d/. For example, gawk.sh defines which libraries are available in AWK without having to enter the full path. GAWKPATH and GAWKLIBPATH work very similarly to the shell’s PATH variable.

less.sh is responsible for ensuring that less can “open” not only text files but also binary files of certain types.

# less initialization script (sh)

if [ -z "$LESSOPEN" ] && [ -x /usr/bin/lesspipe.sh ]; then
    # The '||' here is intentional, see rhbz#1254837.
    export LESSOPEN="||/usr/bin/lesspipe.sh %s"
fi

With LESSOPEN, a command is defined that is executed prior to each less call. The lesspipe.sh filter determines what should happen depending on the file type. Among other things, the script makes it possible to use less to list the contents of archives or to show meta-information of images.

The application-related scripts can be very simple. nano-default-editor.sh is responsible for setting Nano as the value for EDITOR, if the variable has not already been defined or is overwritten later. You may argue that it’s not the most obvious way to set what editor to use. The idea is probably that nano is the most user-friendly text editor included with the distribution (more so than vi). Individual users may either use that, or install their preferred option and redefine EDITOR in their own user-level configuration.

gnupg2.sh is another one-liner, but its effect is less obvious:

export GPG_TTY=$(tty)

The tty command returns the path to the terminal to which the standard output is currently connected. The GPG_TTY variable defines which TTY should be used by pinentry when the user is required to enter a passphrase (in the little GUI window you may be familiar with). This prevents errors that otherwise may occur in certain environments (such as SSH or TMUX sessions).

Finally, debuginfod.sh is a script that provides settings for the service of the same name. I won’t pretend that I understand what’s going on in there.

Autocompletion

Bash is shipped by Fedora with the ability to Tab complete inputs. It may come as a surprise that autocompletion is not one of Bash’s built-in features.

The complex functionality is implemented in /usr/share/bash-completion/bash_completion and sourced in /etc/profile.d/bash_completion.sh. Fedora comes with completions for more than a thousand applications, located under /usr/share/completions/.

New entries can be added to this directory, especially for newer or less widespread applications. The Yazi download, for example, includes completions for its two components (yazi and ya). Their file extension indicates which shell they are intended for.


  1. I’m following the explanations in a post by Zack Weinberg on Mastodon. ↩︎

Article from February 25, 2025.