Skip to content

Latest commit

 

History

History
834 lines (679 loc) · 29.5 KB

bash.org

File metadata and controls

834 lines (679 loc) · 29.5 KB

Bash Configuration

Profile

When in a multi-user system, you need a way for each user to define their environment independently. The .profile file is one such way. It defines user specific environment variables for an entire session. POSIX-conforming shells will read from this file when used as a login shell. Some graphical interface applications (e.g., login managers, window managers, desktop environments) will also read from the .profile file, since they usually take the place of a login shell.

It’s important to note that this means anything within .profile must conform the POSIX shell.

When running on Windows, I do not want to set any session-wide environment variables. The only thing that would source the .profile file is my UNIX-like shell environment, so all other applications would be in the dark. So instead I inherit from the Windows system and user variables.

[ "$(uname)" != "Linux" ] && return 1

The Executable Path

The PATH environment variable is a comma-delimited list of directories that contain executable files. When running an external command, most applications will read the PATH to find the actual file. This includes the shell, some graphical shells, and applications that use external applications (e.g. Emacs).

export PATH="/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin:/home/max/.local/bin:/home/max/.local/share/cargo/bin"

Text Editors

The EDITOR and VISUAL environment variables defines what command I want executed when an application needs to open a file in an external editor. EDITOR typically points to an editor with a “terminal-friendly” editor, while VISUAL is an editor for a graphical interface.

In my case, I have never been in a situation where I absolutely cannot spawn graphical windows. So I set these to both be emacsclient. This will open the file for editing in an existing Emacs frame connected to a running Emacs daemon. If the daemon is not available, it falls back to calling the command within --alternate-editor. In my case, it falls back to regular Emacs.

export EDITOR='emacsclient --alternate-editor="emacs"'
export VISUAL="$EDITOR"

Terminal & Shell

Applications that run in a terminal environment commonly need to know the capabilities of your default shell and terminal.

SHELL is the absolute path to your chosen shell. In my case (as you can tell by the name if this file), I want to set this to Bash.

export SHELL="/bin/bash"

CLI and TUI applications can look at the TERM environment variable to determine the capabilities available (e.g. acceptable colour code values). I currently use xterm-256color. Not because I use Xterm, but because relevant applications (tmux in particular) understand that to mean all the capabilities in Xterm as well as the ability to output true colours.

export TERM="xterm-256color"

Programming Homes & Paths

Compilers and programming frameworks often look for relevant environment variables as a way to define how you want to configure your build environment. This could be the location of libraries, binaries, or project work spaces.

Clojure

The lein script (used to invoke Leiningen proper) looks for Leiningen’s binaries in the directory specified in the LEIN_HOME environment variable. By default this is $HOME/.lein, but I would rather it follow the XDG standards. So I set LEIN_HOME to a more appropriate location under the XDG data home directory.

export LEIN_HOME="${HOME}/.local/share/lein"

Java

JAVA_HOME is the location of the Java Development Kit base directory. Java frameworks and development environments will use this directory as a starting point to find required libraries and binaries. Since one may have more than one version of the JDK installed, it’s best practice to define the active version as JAVA_HOME. Occasionally, frameworks may also look for a JDK_HOME for the same purpose.

export JAVA_HOME="/usr/lib/jvm/java-8-openjdk-amd64"
export JDK_HOME="${JAVA_HOME}"

JRE_HOME is the location of the Java Runtime Environment base directory. Java frameworks and development environments use this environment variable to find required libraries and binaries for running Java applications. We should reuse the same JRE for the version of the JDK assigned to the JAVA_HOME environment variable.

export JRE_HOME="${JAVA_HOME}/jre"

JAVA_TOOL_OPTIONS allows me to define options that I want given to the Java Virtual Machine by default. In my case, I set the default character-set to UTF-8 like any sane person would want.

export JAVA_TOOL_OPTIONS='-Dfile.encoding="UTF-8"'

Go

GOBIN states the directory go get should install binaries into. I set it to a bin/ directory that is a sibling to the default XDG user data home directory.

export GOBIN="${HOME}/.local/bin"

Rust

CARGO_HOME specifies the location of cached registry indexes, Git checkouts, and compiled binaries. By default, the location is $HOME/.cargo. To match the Freedesktop standard, I change this location to $HOME/.local/share/cargo. I would prefer to point the cached items to $HOME/.cache/cargo and the binaries to $HOME/.local/bin, but there isn’t a good way to split these up.

export CARGO_HOME="${HOME}/.local/share/cargo"

rustup is a toolchain manager for the Rust language. It acts as a single location to install core tooling (e.g., package manager, compiler, etc.), development tooling (e.g., linters, formatters, language servers, etc.), and to switch between different versions of Rust and the downloaded tooling. A handy tool, but the default location rustup installs toolchains and configuration is $HOME/.rustup. I would rather follow the XDG Base Directories specification, and store related data into the XDG data directory. Setting the RUSTUP_HOME environment variable allows me to do so.

export RUSTUP_HOME="${HOME}/.local/share/rustup"

XDG Base Directories

Freedesktop.org has the XDG Base Directories specification. It defines directories programs should use when searching or storing user-specific program data (i.e., data files, configuration files, cached data, and runtime files/objects). I like this specification. In the past, an application might create a new hidden directory inside the users home director to store this data. Or worse, just dump it all straight into the home directory! This clutter is frustrating. It makes it hard to find application data (since they all effectively do their own thing) if you have deleted an application and want to tidy up everything left behind, for example.

The specification defines a set of environment variables that applications can check to determine where to store their data, as well as a default location in the case the user has not set the environment variables. Even though I set mine, I still use the default location values for simplicity.

(The XDG_RUNTIME_DIR is not set here. The OS sets that for me.)

For user-specific data files, XDG_DATA_HOME defines the base directory to store data in, and XDB_DATA_DIRS defines additional directories to search if the target file is not in the base directory.

export XDG_DATA_HOME="${HOME}/.local/share"
export XDG_DATA_DIRS="/usr/local/share/:/usr/share/"

For user-specific configuration files, XDG_CONFIG_HOME defines the base directory to search and store configuration in, and XDG_CONFIG_DIRS defines additional directories to search if the target file is not in the base directory.

export XDG_CONFIG_HOME="${HOME}/.config"
export XDG_CONFIG_DIRS="/etc/xdg/"

For user-specific cache files, XDG_CACHE_HOME defines the base directory to search and store cached files and objects.

export XDG_CACHE_HOME="${HOME}/.cache"

Bash Profile

When using Bash as a login shell, it reads the user-specific .bash_profile file (technically it does read .profile if .bash_profile and .bash_login does not exist (in that order), but if either do for some reason then it will not. It is safer to be explicit and use the first read file). This file has the same purpose as .profile (to set information that should be immutable session-wide). The major difference being that it is (for the most part) only parsed by Bash itself. This means environment variables set here would not be visible to applications that follow the POSIX standard of only reading .profile.

To make sure Bash still has access the environment variables set in the .profile, I have .bash_profile source it. I also source the .bashrc file as well, so the login shell has access to the Bash configuration before kicking off the interactive shell itself.

[[ -f ~/.profile ]] && source ~/.profile # Get environment variables

[[ -f ~/.bashrc ]] && source ~/.bashrc # Get Bash specific configuration

# If running from tty1 start sway
if [ "$(tty)" = "/dev/tty1" ]; then
  export WOBSOCKET="${XDG_RUNTIME_DIR}/wob"
  mkfifo "${WOBSOCKET}"
  exec sway
fi

Bash Run Configuration

Like most early Linux applications, Bash configures itself at runtime using a “rc” file (“run command” files in early UNIX, “run configuration” in modern UNIX/Linux). The .bashrc file contains Bash configuration specific to interactive shell sessions (unlike .profile or .bash_profile, which are only read during login shell sessions). Anything to do with Bash’s behaviour while I am actively using it belongs in this file.

This is an important distinction, because I do not want my preferences for interactive sessions with Bash to bleed into non-interactive sessions (i.e. running a shell script). This can cause portability problems, and make it harder to debug problems if I did not create the script.

Stop Non-Interactive Sessions

I cannot find a source for it, but I remember learning that some Linux distributions (Debian/Ubuntu being an example) will have non-interactive Bash shell sessions still read the .bashrc file. Since I do not want this to happen, I make sure that the configuration exits out if the Bash session reading it is not interactive.

[[ $- == *i* ]] || exit 0 # Either $- contains the character "i", or exit.

Tab-completion

Is able to use completion files to allow users to tab-complete partially entered commands. This means I can enter ema<TAB>, and Bash will expand that to emacs. Some completion files go further, allowing for tab-completion of options and arguments.

Bash comes with a default set of completions. There are two locations where they are commonly stored, so I check both and use whichever exists.

if [[ -f /usr/share/bash-completion/bash_completion ]]; then
  source /usr/share/bash-completion/bash_completion
elif [[ -f /etc/bash_completion ]]; then
  source /etc/bash_completion
fi

I set bash to attempt to “correct” spelling errors when tab-completing a directory name that does not exist.

shopt -s dirspell

If I try to activate tab-complete on an empty line, Bash will search and return possible values from the entire path! I cannot think of a reason to want this, so I disable it by setting the options no_empty_cmd_completion.

shopt -s no_empty_cmd_completion

History

Bash keeps a log of (almost) all commands. This is useful since:

  1. Bash gives interactive functions to go through your history
  2. I can look through the history myself

Bash only writes to the history file on exit, storing the history in memory until then. By default it limits the amount of lines stored in the file and in memory storage to 500. This is far too small, so I increase it to 10,000.

HISTSIZE=10000
HISTFILESIZE=10000

Bash has three options form controlling what it chooses to save to the history.

ignorespace
Ignores any commands that begin with a space. This is useful to

not save commands that contain secrets or sensitive information.

ignoredups
Ignores commands that are exactly the same as the previously saved command.
erasedups
Erases all previous instances of the current command from the history.

Since I value the order of commands when reading history, I do not set erasedups. I do enable both of the ignore options. Bash provides a shorthand value to do this named ignoreboth.

HISTCONTROL="ignoreboth"

I want Bash to store multi-line commands as a single entry. That way, I can grab the entire command from history. To do this, I need to set the cmdhist option.

shopt -s cmdhist

I also do not want Bash to overwrite the history file, which is its default behaviour. histappend will make sure to append to the history file only.

shopt -s histappend

When interactively searching through the Bash history, you can go between forward search and reverse search. Annoyingly, the shortcut for forward search is C-s, which will pause the TTY output. In the age of terminal multiplexers, I have never paused the TTY on purpose. So I disable the XON/XOFF functionality.

stty -ixon

ls Colours

The script lscolors.sh exports the LS_COLORS environment variable. For more information, see the Directory Colours section.

source "${HOME}/.local/share/lscolors.sh" || true

Man Page Colour Support

export LESS_TERMCAP_mb=$'\e[1;30m'
export LESS_TERMCAP_md=$'\e[1;30m'
export LESS_TERMCAP_me=$'\e[0m'
export LESS_TERMCAP_se=$'\e[0m'
export LESS_TERMCAP_so=$'\e[38;2;25;25;25;48;2;224;224;224m'
export LESS_TERMCAP_ue=$'\e[0m'
export LESS_TERMCAP_us=$'\e[01;35m'

Directory Navigation

cdspell will attempt to correct “minor errors” if the directory give to cd does not exist. It will print its “corrected” name first before changing directories.

shopt -s cdspell

Globs, Expansions & Patterns

Bash supports using “globs” (wildcard characters such as * and ?) within commands. Bash expands globs before executing a command. For example, rm dir/* expands to all contents of the directory dir. This is a powerful tool for writing short Bash commands.

extglob enables the more complex “pattern-list” globs. These globs match based on the occurrences of sub-patterns. This allows the testing of one-to-many patterns within a single pattern.

shopt -s extglob

globstar enables the ** glob. This expands to mean all files, directories, and sub-directories. It is effectively a recursive *.

shopt -s globstar

failglob will fail a command if any patterns fail to expand to at least one file.

shopt -s failglob

Aliases & Functions

An alias is basically a special type of expansion within Bash. Instead of expanding to filenames, an alias expands to a command. I use aliases for two purposes:

  1. Wrapping one-liners
  2. Shadowing existing commands

Defining one-liners in an alias is faster than storing them inside a script that is available on the PATH, as Bash does not have to fork a new job when expanding an alias.

On the same note, I use Bash functions to act as more complex aliases. Because the value of an alias is a string, complex flow control or input parsing can become hard to read. Structuring them as properties simply works better.

By shadowing existing commands, I mean taking an existing command name and using it as an alias or function name so the alias/function runs instead. Usually, this is so I can tack on default arguments or options to these commands. For example, I may alias ls to ls --list so invoking ls always displays in the list mode.

Directory Listing

alias ls="ls \
    --color=auto \
    --group-directories-first \
    --dereference \
    --human-readable"
alias l="ls -C --ignore-backups --classify"
alias la="l --almost-all"
alias ll="l -l"
alias lla="ll --almost-all"
alias lld="ls -l | egrep '^d'"
alias llf="ls -l | egrep -v '^d'"
alias llk="ll --reverse -S"
alias llr="ll --recursive"
alias llx="ll -X"

Docker

As with the Git abbreviations, I haven’t really taken full advantage of the expansion properties to make working with Docker easier. These are just shortcuts.

alias datt="docker attach"
alias dex="docker exec --interactive --tty"
alias dimg="docker images"
alias dip="docker inspect --format '{{ .NetworkSettings.IPAddress }}'"
alias dip="docker push"
alias dstart="docker start"
alias dstop="docker stop"

Editing

To quickly edit a file, I use the e alias. This expands to invoking the Emacs client, resulting in the file opening inside a new or existing Emacs session. This is similar to my EDITOR value, except it returns control back to the shell immediately.

alias e='emacsclient --no-wait --alternate-editor="emacs"'
alias ec="e --create-frame" # Always creates a new frame
alias ebash='e ${HOME}/git/dotfiles/bash.org' # Opens this file in a new frame

I store Vim’s and Tmux’s configuration files within the XDG configuration base directory. To load them, I alias the vim and tmux command to always include the option that sets the configuration file.

alias vim='vim -u ${XDG_CONFIG_HOME}/vim/config.vim'
alias tmux='tmux -f ${XDG_CONFIG_HOME}/tmux/tmux.conf'

Git

While Git is a fantastic source code management solution, it has a sometimes annoying, sometimes awful, interface. Little inconsistencies here and there mixed with an awkward combination of sub-commands and flags can make working with Git on the CLI a hassle. You’ll notice that none of these abbreviations help with the interface issues, because I am a baddie.

alias g="git status"
alias ga="git add"
alias ga.="git add ."
alias gb="git branch"
alias gc="git commit"
alias gc.="git commit"
alias gd="git diff"
alias gf="git fetch"
alias gl="git log"
alias gll="git log --all --decorate --oneline --graph"
alias glp="git log --patch"
alias gllp="git log --all --decorate --oneline --graph --patch"
alias gm="git merge"
alias gp="git push"
alias gpl="git pull"
alias gr="git restore"
alias gre="git reset"
alias greh="git reset --hard"
alias gres="git reset --soft"
alias gs="git switch"
alias gsc="git switch -c"

Utility

alias da='date "+%Y-%m-%dT%TZ%z"'
alias h="history | grep"
alias p="ps aux | grep"
alias grep="grep --colour=auto"
alias q="exit"
alias reload='source ${HOME}/.bashrc'
uu() {
    sudo apt update && sudo apt upgrade && sudo apt autoremove
}

uuu() {
    sudo apt update && sudo apt full-upgrade && sudo apt autoremove
}
fix-mouse() {
    sudo modprobe --remove psmouse && sudo modprobe psmouse
}

The Prompt

The goal of my prompt is to convey important information based on context. If the information is not needed at this given moment, then I do not want to see it. The rules in place are:

  1. Only show the username if it does not contain “max” (my default username is “max”, and it is my full name on Windows)
  2. Show the username in red if it is “root”
  3. Only show the hostname if I am in an SSH session
  4. Show git information (only active when inside a Git repository)
  5. Don’t bother with the username, hostname, or git information if I am on Windows

The reason for not showing most information on Windows is because:

  1. I do not have more than one user
  2. I never SSH into a machine that I can install fish/this configuration onto
  3. Getting the information for the Git prompt is so mind-slaughteringly slow

That last point is also why on Linux I set the PROMPT_COMMAND environment variable instead of PS1. Bash evaluates PROMPT_COMMAND for each instance of the prompt. Windows is terrible in situations like this, where small commands are ran in sequence. It causes massive slowdowns, usually meaning 8 to 10 second delays each time a command exits before the shell is available. This is the same reason for using raw ANSI codes instead of calling tput. Since tput is an external command, Windows just cannot handle calling it more than once.

_pre=
_post=
_os=$(uname)
build_prompt() {
    local -r EXIT="$?"
    local -r ESC="\033"
    local -r BR_RED="\[${ESC}[31;1m\]"
    local -r BR_GREEN="\[${ESC}[32;1m\]"
    local -r BLUE="\[${ESC}[34m\]"
    local -r MAGENTA="\[${ESC}[35m\]"
    local -r RESET="\[${ESC}[39;0m\]"

    local cwd="${BLUE}\w"
    local status_indicator=
    local username=

    if [[ ${EXIT} = "0" ]]; then
        status_indicator="${BR_GREEN}>"
    else
        status_indicator="${BR_RED}>"
    fi

    if [[ $_os = "Linux" ]]; then
        if [[ ${USER} = "root" ]]; then
            username="${BR_RED}root"
        elif [[ ${USER} != "max" ]]; then
            username="${MAGENTA}${USER}"
        fi

        if [[ -n "$SSH_CLIENT" ]]; then
            username="${username}@\h"
        fi

        if [[ -n ${username} ]]; then
            username="${username}:"
        fi
    fi

    _pre="\n${username}${RESET}${cwd}"
    _post="\n${status_indicator} ${RESET}"
}

GIT_PS1_SHOWDIRTYSTATE="true"
GIT_PS1_SHOWUNTRACKEDFILES="true"
GIT_PS1_SHOWUPSTREAM="auto verbose git"
GIT_PS1_STATESEPARATOR="|"

if [[ $_os = "Linux" ]]; then
    PROMPT_COMMAND='build_prompt && __git_ps1 "${_pre}" "${_post}" " [%s]"'
else
    build_prompt
    PS1="${_pre}${_post}"
fi

Input Run Configuration (The Readline)

Readline is a library used by Bash that allows users to manipulate text. This includes moving the cursor, editing text, and completion. Readline looks at the .inputrc file for user-specific configuration and key binds.

Readline is not only used by Bash, so any applications that take advantage of Readline will inherit these settings.

Completion Behaviour

Readline can ignore case when performing filename matching and completion by setting completion-ignore-case to on.

set completion-ignore-case on

Setting completion-map-case makes the Readline treat - and _ as equivalent (given that completion-ignore-case is also on).

set completion-map-case on

If the common prefix for completions is greater than the value of completion-prefix-display-length, replace it with ellipsis. For a value of three, file1.txt and file2.txt would become ...1.txt and ...2.txt.

set completion-prefix-display-length 3

mark-symlinked-directories adds a / after completed names which are symbolic links to a directory.

set mark-symlinked-directories on

By default, ambiguous completions (completions that could have more than one result) and unmodified completions (completions that have no partial completions before a word is complete) do not immediately show possible completions. Instead, it rings the bell. Only on the second completion does it list. I have never once been happy that the bell has rung on my machine ever.

set show-all-if-ambiguous on
set show-all-if-unmodified

Completions performed in the middle of a word can result in duplicated portions of the word following the cursor. skip-completed-text will prevent duplication from occurring.

set skip-completed-text on

Visual Behaviour

Never ring the bell.

set bell-style none

Text editors and IDE can have a quality-of-life feature to highlight or otherwise draw attention to a parenthesis’s opener/closer (if it exists). Readline offers something in the same realm with blink-matching-paren. It will move the cursor quickly to the open parenthesis matching an newly inserted closing parenthesis to visually indicate what the parenthesis is surrounding.

set blink-matching-paren on

Readline can colour code the list of completions. colored-completion-prefix colour codes common prefixes of possible completions, and colored-stats colour codes the file type if the completion is for a file.

set colored-completion-prefix on
set colored-stats on

Similar to colored-stats, visual-stats will tag a character to the end of a filename to denote its file type (e.g., / for directories, * for executable files, etc.).

set visible-stats on

Key Binds

Readline allows users to configure the key bindings that execute functions. Not all of the functions have a bind by default.

Set the up-arrow and down-arrow key to search history. The way this works means that with nothing entered, the arrow keys work as they do by default. With any text entered, then the arrow keys will search for a command in the history containing the text as a suffix.

"\e[A": history-search-backward
"\e[B": history-search-forward

Directory Colours

The ls command (when run with the --color option) will display the names of some files in a colour depending on the files metadata or the files extension. There is a set of default colours, and users can set their own colours by defining them inside the LS_COLORS environment variable.

The format of the LS_COLORS variable is hard to read and edit. Instead, we can write them out using a format that is parceable by the application dircolors. dircolors will read the file, and convert it to the LS_COLORS format.

I based my colours on the Modus Operandi theme for Emacs. I annotated most entries with which colour the entry is using from MO.

Special Files

Most files that are not “regular” files, or those with certain permissions, have a predefined name (e.g., files where the user has execute permissions (that aren’t directories) are EXEC, sockets are SOCK).

BLK                           38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
CAPABILITY                    38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
CHR                           38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
DIR                           38;2;34;63;191;1  # eshell-ls-directory (blue-alt, bold)
DOOR                          38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
EXEC                          38;2;143;0;117    # eshell-ls-executable (magenta-alt)
FIFO                          38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
FILE                          0
LINK                          38;2;0;85;137;4   # eshell-ls-symlink (cyan, underline)
MISSING                       48;2;255;1 36;146 # eshell-ls-missing (red-intense-bg, fg-main)
MULTIHARDLINK                 38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
NORMAL                        0
ORPHAN                        38;2;0;85;137     # eshell-ls-symlink (cyan, underline)
SETGID                        38;2;139;56;0     # eshell-ls-other-priv (yellow)
SETUID                        38;2;139;56;0     # eshell-ls-other-priv (yellow)
SOCK                          38;2;114;16;69;1  # eshell-ls-special (magenta, bold)
STICKY                        38;2;139;56;0     # eshell-ls-other-priv (yellow)
STICKY_OTHER_WRITABLE         38;2;139;56;0     # eshell-ls-other-priv (yellow)

Archives

.arj 38;5;24;88;112;1 # eshell-ls-archive (cyan-alt, bold)
.bz2 38;5;24;88;112;1
.deb 38;5;24;88;112;1
.gz  38;5;24;88;112;1
.lzh 38;5;24;88;112;1
.rpm 38;5;24;88;112;1
.t   38;5;24;88;112;1
.ta  38;5;24;88;112;1
.tar 38;5;24;88;112;1
.taz 38;5;24;88;112;1
.tgz 38;5;24;88;112;1
.xz  38;5;24;88;112;1
.z   38;5;24;88;112;1
.Z   38;5;24;88;112;1
.zip 38;5;24;88;112;1

Backups

Temporary backup or restore files generated by some editors.

.bak 38;5;113;73;0 # eshell-ls-backups (yellow-alt)
*~   38;5;113;73;0
*#   38;5;113;73;0

Clutter

Files left behind by running applications.

*texput 38;5;151;37;0 # eshell-ls-clutter (red-alt)
.log     38;5;151;37;0
*core   38;5;151;37;0
.swp     38;5;151;37;0
.swo     38;5;151;37;0
.tmp     38;5;151;37;0
.sassc   38;5;151;37;0

Products

Files created as the result of compiling a source file. These files should easy to regenerate.

.elc 38;5;93;48;38 # eshell-ls-product (fg-special-warm)
.o   38;5;93;48;38
.obj 38;5;93;48;38
.a   38;5;93;48;38
.lib 38;5;93;48;38
.res 38;5;93;48;38