Herein are my settings for shells and terminal emulators.
I mostly use zsh
as my shell these days,
but retain and maintain settings for bash
;
mostly as a fallback, as it is the default on a lot of systems.
For a shell to be set as the default shell,
it needs to be registered as a valid shell
in /etc/shells/
.
This seems to be updated automatically using system package managers
(such as apt
on Debian based systems),
but in my usage of Guix I had to update it manually.
Then we can set the default shell using
chsh -s $(which SHELL)
replacing SHELL with the appropriate shell.
Note this operation requires the user password; in the Linux VM on Chromebooks using Crostini, there isn’t a user password by default, so we must set one first using the command
sudo passwd $USER
Before proceeding, it is a good idea to clarify the usage and purpose of the various shell setting files.
In particular, I make use both of “profile” and “run commands” files.
The distinction is that “profile” files are used for (interactive) login shells, and “run commands” for (interactive) non-login shells.
To ensure consistency, the usual practice is to source the relevant “run command file” from the relevant “profile” file.
Just don’t introduce a circular dependency!
See this blog post
on the startup file loading order for bash
and zsh
,
which informed me on the above.
Standard description, inherited from (what I assume is) the default Debian ~~/.profile~.
# ~/.profile: executed by the command interpreter for login shells.
# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
# exists.
# see /usr/share/doc/bash/examples/startup-files for examples.
# the files are located in the bash-doc package.
Package manager binary install locations.
# Guix
if [ -d "$HOME/.guix-profile" ] ; then
# This setup based on that done by the Guix System
for profile in "$HOME/.guix-profile" "$HOME/.config/guix/current"
do
if [ -f "$profile/etc/profile" ]
then
# Load the user profile's settings.
GUIX_PROFILE="$profile" ; \
. "$profile/etc/profile"
else
# At least define this one so that basic things just work
# when the user installs their first package.
export PATH="$profile/bin:$PATH"
fi
done
fi
# Nix
# I had this setting for a Debian machine setup with Nix as a package manager.
# It doesn't work for NixOS, though, and I'm not certain if it is out of date for the package manager as well.
# if [ -d "$HOME/.nix-profile" ] ; then
# source ~/.nix-profile/etc/profile.d/nix.sh
# fi
The ~~/.local/bin~ or /local/bin
,
and sometimes the older convention ~~/bin~,
are often used for user-specific binaries.
(See the XDG base directory specification.)
if [ -d "$HOME/.local/bin" ] ; then
export PATH="$HOME/.local/bin:$PATH"
fi
if [ -d "$HOME/local/bin" ] ; then
export PATH="$HOME/local/bin:$PATH"
fi
if [ -d "$HOME/bin" ] ; then
PATH="$HOME/bin:$PATH"
fi
And some other install locations I’ve encountered. I should occasionally trim these if I stop using some languages.
# Tool install directories
# Haskell tools
if [ -d "/opt/ghc/bin" ] ; then
export PATH="$PATH:/opt/ghc/bin"
fi
if [ -d "$HOME/.cabal/bin" ] ; then
export PATH="$PATH:$HOME/.cabal/bin"
fi
# OCaml package manager
if [ -d "$HOME/.opam/system/bin" ] ; then
export PATH="$PATH:$HOME/.opam/system/bin"
fi
# Rust's package manager
if [ -d "$HOME/.cargo/bin" ] ; then
export PATH="$PATH:$HOME/.cargo/bin"
fi
# The Go path used by Golang by default
if [ -d "$HOME/go/bin" ] ; then
export PATH="$PATH:$HOME/go/bin"
fi
Packages installed via Guix will use the locale data of our host system.
So, we must install one of the locale packages available with Guix
(such as glibc-locales
)
and then define the GUIX_LOCPATH
environment variable.
See the manual.
if [ -d "$HOME/.guix-profile/lib/locale" ] ; then
export GUIX_LOCPATH=$HOME/.guix-profile/lib/locale
fi
Override ls
if gls
(the g
prefixed GNU core utility installation) is available.
This could be done with an alias, but I define other aliases for ls
in my Aliases,
and aliases don’t stack, so it’s better to use a function.
if command -v gls 2>&1 >/dev/null; then
ls() {
if command -v gls 2>&1 >/dev/null; then
gls "${@}"
else
ls "${@}"
fi
}
fi
Some Docker documentation I read recommended avoiding bad habits by disabling legacy commands, so let’s do that.
# Disable some legacy docker commands
export DOCKER_HIDE_LEGACY_COMMANDS=true
I use mbsync
(isync
), mu
, and mu4e
(the Emacs package)
to manage my emails from within Emacs.
My mbsync
settings can be found in ./mbsync.org.
Other than that, Emacs handles most of the work,
except for setting the mail directory; that must be set via mu
.
So we do so here so I never forget to set it.
# Check mu installed and info returns error (indicates not initialized)
if command -v mu >/dev/null && ! mu info >/dev/null 2>/dev/null; then
echo "mu installed but `mu info` returns error; trying to initialize mu"
mu init --maildir="~/.mail/gmail" --my-address="[email protected]"
echo "`mu index` will be handled by mu4e if no action taken"
# else
# echo "mu already running"
fi
Set a specific directory for NPM packages.
Tell npm
where to find it
via npm config set prefix "${HOME}/.npm-packages
.
NPM_PACKAGES="${HOME}/.npm-packages"
export PATH="$PATH:$NPM_PACKAGES/bin"
export MANPATH="${MANPATH-$(manpath)}:$NPM_PACKAGES/share/man"
Some settings only make sense on certain machines.
If the use of those settings is not sensitive information
(I can’t imagine currently why they would be, but just in case)
then I tangle them from this file below,
and then symlink them to .profile_local
.
if [ -f ~/.profile_local ] ; then
source ~/.profile_local
fi
Otherwise, if the use of those settings
should not be shared in my dotfile repo,
I can place them in .profile_private
which I make local only
(but is hence fragile).
if [ -f ~/.profile_private ] ; then
source ~/.profile_private
fi
Just source the relevant files.
No interesting bash
specific setup, at least yet.
if [ -f ~/.profile ]; then
source ~/.profile
fi
if [ -f ~/.bashrc ]; then
source ~/.bashrc
fi
As with bash
, just source the relevant files.
No interesting zsh
specific setup, at least yet.
if [ -f ~/.profile ]; then
source ~/.profile
fi
if [ -f ~/.zshrc ]; then
source ~/.zshrc
fi
Standard, default. I should probably rewrite this eventually.
# ~/.bashrc: executed by bash(1) for non-login shells.
# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc)
# for examples
If not running interactively
(for instance, if this system is the remote
during a data transfer using scp
or sftp
)
then it can be extremely detrimental to apply some of these settings;
in particular, printing to standard output is likely to cause errors.
So we leave in place this (default) check and early return
for the case that we are not running interactively.
See this excellent answer
to a question on StackExchange regarding the necessity of these lines
for more information.
# If not running interactively, don't do anything
case $- in
*i*) ;;
*) return;;
esac
Save lots of history; it’s just a plaintext file, afterall.
We could, in bash
4.3 or later,
instead set these to -1
for unlimited history.
On earlier versions, setting them to an empty string
should have the same effect.
But if we ever reach one hundred million entries,
I think we can safely discard some.
HISTSIZE=100000000
HISTFILESIZE=100000000
Note that HISTSIZE
is the number of lines to store
in memory while running,
whereas HISTFILESIZE
is the number of lines that are allowed
in the history file during session startup.
The ignoreboth
option for HISTCONTROL
causes us to ignore
commands which are prepended by a space
(giving us a way to avoid entering a command into history;
useful if it contains sensitive information such as a password)
and duplicate entries which are entered in succession.
HISTCONTROL=ignoreboth
And finally, we set the histappend
option
to not overwrite history on each session.
shopt -s histappend
Check the window size after each command; not doing this can mess with some terminal software.
# check the window size after each command and, if necessary,
# update the values of LINES and COLUMNS.
shopt -s checkwinsize
# enable programmable completion features (you don't need to enable
# this, if it's already enabled in /etc/bash.bashrc and /etc/profile
# sources /etc/bash.bashrc).
if ! shopt -oq posix; then
if [ -f /usr/share/bash-completion/bash_completion ]; then
. /usr/share/bash-completion/bash_completion
elif [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
fi
The **
pattern for pathname expansion can be useful.
# If set, the pattern "**" used in a pathname expansion context will
# match all files and zero or more directories and subdirectories.
shopt -s globstar
The lesspipe
utility expands the capabilities of less
,
allowing it to better handle various kinds of files,
such as archive files, images, or PDFs.
# make less more friendly for non-text input files, see lesspipe(1)
[ -x $(which lesspipe) ] && eval "$(SHELL=/bin/sh lesspipe)"
I keep my alias
definitions in this non-shell specific file,
using it for all shells.
# Source my alias definitions.
if [ -f ~/.aliases ]; then
. ~/.aliases
fi
I also keep some aliases that are specific to particular machines in files which are then symlinked to ~~/.aliases_local~ on the appropriate machine(s).
# Source my alias definitions.
if [ -f ~/.aliases_local ]; then
. ~/.aliases_local
fi
I now use the cross-shell Starship prompt, created in Rust. See https://starship.rs/, and my settings for it below.
if [ -x $(which starship) ]; then
eval "$(starship init bash)"
fi
First and foremost: Set Emacs keybindings.
bindkey -e
I use Starship to set up a fancy prompt. It involves unicode characters, and the prompt offset often gets calculated incorrectly when, for instance, reverse searching for commands. That causes the command text and the cursor to be desynced, making editing a command near impossible. Setting the locale to UTF-8, to support unicode, fixes that issue.
# Fixes an issue with the starship prompt offset being incorrect due to unicode characters
export LC_ALL=C.UTF-8
Go crazy with the history; it’s just a plaintext file after all.
HISTFILE=~/.zsh-histfile
HISTSIZE=100000000
SAVEHIST=100000000
Don’t beep after a successful completion.
unsetopt list_beep
zstyle :compinstall filename '/home/markparmstrong/.zshrc'
autoload -Uz compinit
compinit
I use the same set of alias definitions here as I do for bash
.
# Source my alias definitions.
if [ -f ~/.aliases ]; then
. ~/.aliases
fi
And as I do for bash
, source the aliases for specific machines
if the file (should be a symlink to a file tangled here) exists.
# Source my alias definitions.
if [ -f ~/.aliases_local ]; then
. ~/.aliases_local
fi
Same as in bash
, use the cross-shell Starship prompt.
See https://starship.rs/
and my settings for it below.
if [ -x $(which starship) ]; then
eval "$(starship init zsh)"
fi
A downside to using aliases is they discourage learning the aliased commands. This can make the developer overly dependent upon their setup, which can be detrimental when collaborating or when migrating systems.
To counteract this, I use this function to define aliases, which makes them automatically print out their definition before executing the command. This way I at least see the underlying command each time I run the alias.
Resources used to develop this function: https://unix.stackexchange.com/questions/53310/splitting-string-by-the-first-occurrence-of-a-delimiter https://unix.stackexchange.com/questions/30903/how-to-escape-quotes-in-the-bash-shell
function valias () {
IFS='=' read -r a c <<< "$1"
alias ${a}="echo -e ' alias '${a}$'=\''${c}$'\'';"${c}
}
Variants on directory listing.
valias lsl='ls -l'
valias lsa='ls -A'
valias lsla='ls -lA'
In particular, if colours are supported, make use of them
in ls
, grep
, and related commands.
if command -v dircolors 2>&1 >/dev/null || command -v gdircolors 2>&1 >/dev/null; then
valias ls='ls --color=auto'
valias dir='dir --color=auto'
valias vdir='vdir --color=auto'
valias grep='grep --color=auto'
valias fgrep='fgrep --color=auto'
valias egrep='egrep --color=auto'
fi
valias gp='guix package'
valias gps='guix package -s'
valias gpi='guix package -i'
A command of this form is useful if connecting to a VPN using OpenConnect, for instance for a workplace.
valias <workplace>vpn='sudo openconnect -bq -u <username> <path>'
I’ve found it takes a little longer than the default timeout of 500ms to set up my prompt when in a Git repository; let’s double that timeout value.
command_timeout = 1000
This is also a prompt-wide configuration setting,
but deserves its own section.
A modification of format
at the top of the configuration
changes the formatting of the whole prompt;
this format string can refer to modules using $module
syntax,
and may include other characters to connect them.
I like to use unicode box drawing characters to connect things.
Define format
as a multiline string.
I break the definition up into several source blocks,
in order to better commentate specific parts.
format="""
The first line of prompt I think of as containing “global” information; the shell in use, the user and hostname, the date and time (that the last command finished) and the return status of the last command along with the time it took.
[┌─⟨$shell⟩──⟨$username$hostname⟩](bold green)\
[──⟨${custom.date}⟩──⟨$time⟩](bold green)\
[──⟨$character$status $cmd_duration⟩](bold green)
The next line contains the current directory path.
[│ $directory](bold green)
Starship has support to show information about the installed versions of several tools. By default this information is shown if the directory contains files indicating it’s relevant. If it’s present, let’s display this information below the path. (Note this whole portion is wrapped in parentheses (and so is the newline to separate it from the directory path); that makes this portion of the string conditional, so it will not display if all the variables referenced here are empty.)
([│ $elm$golang$nodejs$purescript$python$ruby](bold green)\n)\
And if we are in a Git repository, let’s then display Git information.
([│ $git_branch($git_commit)($git_status$git_metrics)](bold green)\n)\
Finally, show the prompt line itself.
I’ve found in the past that I need to adjust this line for zsh
;
see the custom module definitions below.
But I’m trying it out plain again, so this is disabled for now.
[${custom.zsh_prompt}${custom.other_prompt} \\$ ](bold green)
Here’s the plain final line of the prompt.
[└─► \\$ ](bold green)
And end the multiline string.
"""
Show me the shell I’m using.
[shell]
disabled = false
format ="[$indicator](green)"
Always show the username, even if it’s the same one that’s logged in and it’s not root.
[username]
show_always = true
format = "[$user]($style)"
We could also always show the hostname by setting ssh_only
to false;
I’ve chosen not to for now.
[hostname]
format = "@[$hostname](magenta)"
I like the date to be separated from the time in my prompt,
so I need a separate module for date.
Not a problem; the date
command fetches the date for us.
[custom.date]
command = "date +'%a %b %d'"
format = "[$output](bright-blue)"
when = "true"
shell = ["bash", "--noprofile", "--norc"]
So in the time module, we only want the clock time, not the date.
[time]
disabled = false
time_format = "%T"
style = "bold blue"
format = "[$time]($style)"
The character
module shows one of three symbols
based on the result of the last command.
It’s intended to be placed right before the user input area,
but I place it in my first line alongside the status
module.
By only showing the success
symbol, this makes up for the fact
that status
does not have a way to show the command succeeded.
[character]
success_symbol = "✓"
error_symbol = ""
vicmd_symbol = ""
format = "$symbol"
We use status
to report if the command failed,
as it’s more specific;
there are symbols for program error, “file not executable” errors,
“command not found” errors, etc.
[status]
disabled = false
format = "[$symbol $status]($style)"
Also report the time the last command took. Note that even though I set the minimum time to report to 0, commands that take no time still won’t show a time.
[cmd_duration]
min_time = 0
show_milliseconds = true
style = "bold bright-blue"
format = "[$duration]($style)"
Don’t truncate the directory path, unless it’s excessively long.
[directory]
truncation_length = 20
truncate_to_repo = false
truncation_symbol = "…"
Show me metrics for my Git repositories; how many lines have I added and deleted?
[git_metrics]
disabled = false
I’ve found the width of the last line of my prompt
when using unicode box characters and arrow heads
is not detected correctly by zsh
; when I invoke the autocompletion,
the input text position is set incorrectly, causing duplication of text.
So I use two custom modules to format this last line,
with one only displaying when the shell is zsh
,
and the other displaying when the shell is not zsh
.
For the zsh
one, we hardcode the length of those unicode characters
using the %n{...%}
form that zsh
recognizes (but bash
does not).
[custom.zsh_prompt]
format = "[%3{└─►%}](bold green)"
when = '[ "$STARSHIP_SHELL" == "zsh" ]'
shell = ["bash", "--noprofile", "--norc"]
[custom.other_prompt]
format = "[└─►](bold green)"
when = '[ ! "$STARSHIP_SHELL" == "zsh" ]'
shell = ["bash", "--noprofile", "--norc"]
No settings here as of yet. I do as much as I can out of (GUI) Emacs, so the out-of-the-box experience usually suffices for my terminal emulator.