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 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"
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"
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"
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.
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_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"'
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"
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"
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"
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
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.
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.
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
Bash keeps a log of (almost) all commands. This is useful since:
- Bash gives interactive functions to go through your history
- 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
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
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'
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
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
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:
- Wrapping one-liners
- 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.
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"
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"
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'
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"
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 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:
- Only show the username if it does not contain “max” (my default username is “max”, and it is my full name on Windows)
- Show the username in red if it is “root”
- Only show the hostname if I am in an SSH session
- Show git information (only active when inside a Git repository)
- 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:
- I do not have more than one user
- I never SSH into a machine that I can install fish/this configuration onto
- 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
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.
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
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
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
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.
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)
.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
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
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
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