Skip to content

Files

Latest commit

de7cc6c · Mar 19, 2025

History

History
1023 lines (845 loc) · 28.9 KB

shell.org

File metadata and controls

1023 lines (845 loc) · 28.9 KB

Shell and terminal emulators settings

Preamble

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.

Register shells with the system

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

Order of shell startup file execution

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.

Profiles

profile

Introduction

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.

Path updates

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

Locale settings

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

Tool settings

Basic

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

Docker

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

Email management

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

NPM

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"

Device specific (local) profile settings

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

bash profile

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

zsh profile

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

Run commands

bash run commands

Header

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

Basic settings

Interactivity check

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

History

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

Window size

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

Completions and the like

# 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

Configure other tools

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)"

Source aliases

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

Set up the prompt

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

zsh run commands

Basic settings

Keybindings

First and foremost: Set Emacs keybindings.

bindkey -e

Locale

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

History

Go crazy with the history; it’s just a plaintext file after all.

HISTFILE=~/.zsh-histfile
HISTSIZE=100000000
SAVEHIST=100000000

Audio

Don’t beep after a successful completion.

unsetopt list_beep

Completion settings

zstyle :compinstall filename '/home/markparmstrong/.zshrc'

autoload -Uz compinit
compinit

Source my aliases

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

Set up the prompt

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

Aliases

Special alias command to display the aliased command before running it

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}
}

Basic commands

Variants on directory listing.

valias lsl='ls -l'
valias lsa='ls -A'
valias lsla='ls -lA'

Include some flags by default

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

Guix

valias gp='guix package'
valias gps='guix package -s'
valias gpi='guix package -i'

VPN

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>'

The Starship prompt

Prompt-wide configuration

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

The shape of the prompt

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.

"""

Module settings

Shell, user and host information

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)"

Dates and times

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)"

Previous command information

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)"

Directory and Git information

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

Final prompt line

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"]

Terminal emulator settings

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.