Explaining my .bashrc
I have a number of tools in my .bashrc file that make my day-to-day activities fast. The ability to run commands like aws
without needing to remember if I’ve SSOed or connected to the VPN, or to quickly change Kubernetes contexts and namespaces on a per-shell basis, are invaluable to me.
Some have pointed out that switching to zsh, with addons like powerlevel10k, would probably benefit me with these use cases. Indeed, they probably would! But I’m a creature of habit, and have been using bash since I started using shells (though I likely used csh
without knowing it, during my pre-Linux days on IRIX). More generally, there are some nice “features” that being a bash user gives me:
- I don’t find myself lacking for features, or suddenly thrust into a different world, when I SSH to a remote machine without zsh.
- I use my shell as a REPL for bash shell scripts. (Yes, I could run shell scripts under zsh, but then they wouldn’t run anywhere I need to run them).
Many of these scripts are relevant both to my work (macOS) and personal (Fedora) laptops, but some (like the AppleScript bits) are obviously specific to macOS.
Let’s dive in.
Structure
I structure the file so that I know, generally, where to find each part. Each section is alphabetized. The structure (and a table of contents for this post) is:
Aliases
Aliases are a lightweight way to substitute one word for a quick command. Most often, I use aliases to either shorten the name of a program, or to ensure that I run another command before the command I entered.
alias aws="awscheck && aws"
My employer enforces both SSO and VPN connectivity to access our AWS accounts. I don’t want to have to think about having an up-to-date SSO session, or being connected to the VPN. So I run the awscheck
function (see further below) before reaching out to AWS, which handles either or both if they aren’t already done.
alias clear="printf '\033[2J\033[H\033[3J'"
The built-in clear
command in macOS clears the screen, but does not clear the buffer - leaving my shell history and iTerm2 scrollbar in place. I don’t like this behavior, so I instead use ANSI escape codes:
\033[2J
uses the “Erase in Display” control code, to clear the entire screen.\033[H
uses the “Cursor Position” control code to clear the top left corner (the default behavior if coordinates are not given).\033[3J
is the same “Erase in Display” code as before, but this time clears the scrollback.
alias helm='awscheck && helm --namespace $KUBE_NAMESPACE --kube-context $KUBE_CONTEXT'
alias k='awscheck && kubectl --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT'
Many Kubernetes userland tools do not have consistent ways of specifying contexts and namespaces. Furthermore, the default way of changing the current cluster - kubectl config set current-context ...
- leaks across shells, rather than staying sandboxed to the current shell.
These two lines ensure that kubectl and Helm respond to the environment variables KUBE_NAMESPACE
AND KUBE_CONTEXT
, to set the namespace and context on a per-shell basis.
I additionally alias kubectl
to k
, which you should do as well.
alias kgd='awscheck && kubectl get deployments --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT'
alias kgda='awscheck && kubectl get deployments --all-namespaces --context $KUBE_CONTEXT'
alias kgi='awscheck && kubectl get ingresses --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT'
alias kgia='awscheck && kubectl get ingresses --all-namespaces --context $KUBE_CONTEXT'
alias kgn='awscheck && kubectl get nodes --context $KUBE_CONTEXT'
alias kgp='awscheck && kubectl get pods --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT'
alias kgpa='awscheck && kubectl get pods --all-namespaces --context $KUBE_CONTEXT'
alias kgs='awscheck && kubectl get services --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT'
alias kgsa='awscheck && kubectl get services --all-namespaces --context $KUBE_CONTEXT'
These aliases are conveniences for common lookups I do with kubectl
. In general, appending a
after one appends the --all-namespaces
flag in the command (e.g. kgp
and kgpa
differ only by the --all-namespaces
flag).
alias kbash='awscheck && k run -it --rm --image=bash bash'
Sometimes I need to run a quick curl in the context of my cluster - for instance, to test network connectivity. This is an easy way to do it.
alias ksniff='awscheck && kubectl sniff --namespace $KUBE_NAMESPACE --context $KUBE_CONTEXT --privileged'
ksniff is a wonderful plugin that allows capturing network traffic on a live pod, and visualizing it with Wireshark. This is an easy way to run it.
alias ll="exa -h -l --git"
alias ls="exa -h -l --git"
I use exa, which describes itself as “a modern replacement for ls
”. So that I don’t lose the muscle memory on running ls
, I simply alias ls
to exa
. The flags I use are:
-h
adds a header row, so that I can tell easily what I’m looking at.-l
adds extra data about each file, much likels -l
does.--git
shows me Git metadata about each file.
The end result looks like:
alias rl="source ~/.bashrc"
This alias reloads my .bashrc
file. Since I display a flag in my prompt if it’s not up to date, this is helpful for reconciling a stale shell.
Why not just automatically reload it? This makes it much less destructive if I screw up.
alias rst="source ~/.bashrc && cd && printf '\033[2J\033[H\033[3J'"
This alias “resets my shell”, which I define to be:
- Rerunning all of the statements in my
.bashrc
. - Changing back to my home directory, which is where iTerm2 drops me when I create a new window.
- Clearing the screen, according to the same mechanics as the
clear
alias, above.
alias tf=terraform
Terraform is too many letters. tf
will do.
alias open=xdg-open
alias pbcopy="xclip -selection c"
alias pbpaste="xclip -selection c -o"
While I have some well-known disdain for macOS, I do find some of its console builtins useful. These aliases are deployed only to my home (Fedora) laptop, and emulate the behavior of macOS’s ability to open the default handler for a path with open
, and interact with the clipboard from pbcopy
and pbpaste
.
Functions
Functions are small- to medium-sized blocks of code. These could just as easily be binaries in ~/bin
; I have no particular criteria for when something ends up a function, versus a separate shell script.
function ap() {
if grep "\[profile $1" ~/.aws/config > /dev/null 2>&1; then
export_or_default AWS_PROFILE $1 dev
else
echo "No such profile."
fi
}
This sets my AWS_PROFILE
environment variable. If I call it with no arguments, it defaults to the dev
profile. If the profile doesn’t exist, nothing happens, and a warning message is issued.
function aps() {
grep '^\[profile' ~/.aws/config | sed -E 's/\[profile (.*)\]/\1/' | sort
}
This displays a list - one per line - of AWS profiles that I have defined in my AWS config file. As my employer has many AWS accounts, I do not always remember what a particular one is called. This command is easy to compose, so I can run aps | grep foo
if I remember that the profile has foo
in it.
function awscheck() {
if [[ ! $(netstat -nr | grep default | head -n 1) =~ utun ]]; then
osascript -e 'tell application "System Events" to tell process "GlobalProtect"' \
-e 'click menu bar item 1 of menu bar 2' \
-e 'set statusText to name of static text 1 of window 1' \
-e 'if statusText is "Not Connected" then' \
-e 'click button "Connect" of window 1' \
-e 'end if' \
-e 'click menu bar item 1 of menu bar 2' \
-e 'end tell'
while [[ ! $(netstat -nr | grep default | head -n 1) =~ utun ]]; do
sleep 1
done
fi
if [ $(date -jf "%Y-%m-%dT%H:%M:%SZ" $(jq -r .expiresAt ~/.aws/sso/cache/$(/bin/ls -Art ~/.aws/sso/cache | tail -n 1)) +%s) -lt $(date +%s) ]; then
/usr/local/bin/aws sso login
fi
}
This function ensures that I satisfy all of the conditions - being connected to our GlobalProtect VPN, and having a non-expired AWS SSO token - to access AWS. It’s intended to be run before a command that requires access to AWS, e.g. awscheck && aws
.
The first step is to check the system’s default route with netstat
. Our VPN is full-tunnel, so if the VPN is connected, the default route should be to GlobalProtect’s tunnel device. If it’s not, I use an AppleScript script (which was sourced and modified from this Github conversation) to connect. I then wait until GlobalProtect sets the system’s default route to be the VPN.
The second step is to see whether I have a nonexpired AWS SSO token. The AWS CLI stores these tokens in ~/.aws/sso/cache
. I haven’t figured out why I often have multiple files here, so I simply choose the most recent one. This file has an expiresAt
field, which is parsed out with jq
. I then convert it to a UNIX timestamp with date
. If that integer value is less than the integer value of the current time, the token is expired, and I recomplete the auth workflow.
function dlogin() {
awscheck
aws ecr get-login-password | docker login --username AWS --password-stdin $(aws sts get-caller-identity --output text --query Account).dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
}
This function logs into Elastic Container Registry, with the currently configured AWS profile and region.
function export_or_default() {
if [ "$2" == "" ]; then
export $1=$3
else
export $1=$2
fi
}
This workhorse function is referenced from many other aliases and functions. It takes three arguments:
- The name of the variable to set.
- The value to set.
- A default value to set, if a value in #2 was not provided.
function gr() {
local str
current_slashes=$(echo $PWD | tr -cd '\/' | wc -c | tr -d ' ')
toplevel_slashes=$(git rev-parse --show-toplevel | tr -cd '\/' | wc -c | tr -d ' ')
for i in $(seq 1 $(($current_slashes - $toplevel_slashes))); do
str=$str..\/
done
cd $str
}
This function walks a Git path back to its root, and then changes the directory to the root of the repository. This is useful because I’m not always sure how many levels I have to traverse back down, to get to the root, and we have some very deeply nested directories in projects.
I can’t simply cd
to the output of git rev-parse --show-toplevel
directly, because this is an absolute path. I dislike seeing the absolute path to my home directory in my shell prompt, instead opting for ~
, so I intentionally keep the cd
path relative.
function kc() {
export_or_default KUBE_CONTEXT $1 dev
}
function kcs() {
yq .contexts[].name ~/.kube/config
}
function kn() {
export_or_default KUBE_NAMESPACE $1 kube-system
}
function kx() {
export_or_default KUBE_NAMESPACE $(echo $1 | cut -d '@' -f 1) kube-system
export_or_default KUBE_CONTEXT $(echo $1 | cut -d '@' -f 2) dev
}
The first two functions behave like ap
and aps
, but for Kubernetes and contexts in ~/.kube/config
instead of profiles in ~/.aws/config
. However, because Kubernetes does not allow setting the context via environment variable, all tools which depend on it must be aliased (see further above) to pass these environment variables.
kn
merely sets the namespace, again requiring aliasing to function.
kx
is a combination of both kc
and kn
, which allows specifying a namespace and context at the same time, using the format namespace@context
.
The default is the kube-namespace
namespace in the dev
context.
function prompt_command() {
last_command_status=$?
git_status="$(git status 2> /dev/null)"
GIT_BRANCH=$(echo -e "$git_status" | head -n 1 | sed -e 's/On branch //' -e 's/HEAD detached at //')
AWS_THING=""
if [ "$AWS_PROFILE" != "dev" ]; then
AWS_THING=" A$AWS_PROFILE"
fi
BRANCH_THING=""
if [ "$GIT_BRANCH" != "" ]; then
BRANCH_THING=":$GIT_BRANCH"
if [[ ! $git_status =~ "working tree clean" ]]; then
BRANCH_THING="$BRANCH_THING*"
fi
fi
KUBE_THING=""
if [ "$KUBE_NAMESPACE" != "kube-system" -o "$KUBE_CONTEXT" != "dev" ]; then
KUBE_THING=" K$KUBE_NAMESPACE@$KUBE_CONTEXT"
fi
LAST_COMMAND_THING=""
if [ $last_command_status -ne 0 ]; then
LAST_COMMAND_THING=" $last_command_status"
fi
REGION_THING=""
if [ "$AWS_DEFAULT_REGION" != "us-east-1" ]; then
REGION_THING=" R$AWS_DEFAULT_REGION"
fi
STALE_THING=""
if [ "$(md5 -q ~/.bash_profile)" != "$BASHRC_HASH" ]; then
STALE_THING=" STALE"
fi
VENV_THING=""
VENV_NAME=$(basename $VIRTUAL_ENV 2> /dev/null)
if [ "$VENV_NAME" != "" ]; then
VENV_THING=" V$VENV_NAME"
fi
export PS1="[don@work$AWS_THING$KUBE_THING$REGION_THING$VENV_THING$LAST_COMMAND_THING \w$BRANCH_THING$STALE_THING] "
echo -ne "\033]0;$(echo $PWD | sed 's/\/Users\/don.luchiniabacusinsights.com/~/')\007"
}
Every time bash displays the prompt, it invokes the command set in the environment variable PROMPT_COMMAND
. I use this opportunity to reevaluate the state of the world, and display that state in my prompt string:
- First, capture the status of the command that completed, stored in
$?
. This will be displayed in the prompt command, but will get overwritten by subsequent commands in this function. - Determine whether there are any uncommitted files, so that an asterisk can be shown at the end of the branch name if so.
- If the AWS profile is not the default, show it as
A(profile name)
. - If the current directory is a subdirectory of a Git project, show the branch name after a colon (e.g.
:(branch)
). - If the Kubernetes context and/or namespace is not the default, show it as
K(namespace)@(context)
. - If the AWS region is not the default, show it as
R(region)
. - If the md5sum of
~/.bashrc
has changed since~/.bashrc
was last run, display the wordSTALE
. - If a Python virtualenv is active, show it as
V(virtualenv name)
. - Concatenate all of the appropriate bits into the prompt string.
- Finally, set the window title to be the current working directory, again using ANSI escape codes.
I choose to hard-code either home
or work
, based on which laptop I’m using, rather than use the machine hostname.
At its most minimal, the prompt is:
[don@home ~]
An example of a very complex prompt is:
[don@work Aprod Kservice@prod Rus-west-2 Vdeploy 255 ~/git/repo:master* STALE]
It’s unusual to have that many flags set at the same time, so the prompt length is most often not distracting.
function rg() {
export_or_default AWS_DEFAULT_REGION $1 us-east-1
}
This function sets AWS_DEFAULT_REGION
, which is understood by all progams utilizing the AWS SDK. No aliases are needed to make this one work.
function t() {
export_or_default TFENV_TERRAFORM_VERSION $1 1.1.7
}
Terraform projects can use tfenv, plus an appropriate .terraform-version
file, to select a version of Terraform to use. If the variable TFENV_TERRAFORM_VERSION
is set, tfenv ignores this file, and uses this version instead.
function uk() {
aws eks update-kubeconfig --name $1 --alias $1
}
We frequently launch new EKS clusters, requiring new contexts to be loaded into ~/.kube/config
. The AWS CLI has a builtin for doing this by name, but by default it will name the context with the cluster ARN, rather than the cluster name. The --alias
parameter is used to ensure that the actual cluster name is used instead.
function v() {
if [ "$1" == "" ]; then
pyenv deactivate
else
pyenv activate $1
fi
}
This function uses pyenv’s ability to activate and deactivate virtualenvs on demand. Passing a named virtualenv activates it; passing nothing deactivates the currently active virtualenv.
Exports
Exports are mostly used to set defaults on global state, but occasionally change the behavior of tools.
export AWS_DEFAULT_REGION=us-east-1
export AWS_PROFILE=dev
export KUBE_CONTEXT=dev
export KUBE_NAMESPACE=kube-system
These actually do set defaults for variables in the prompt command.
export AWS_SDK_LOAD_CONFIG=1
Some of the AWS SDKs do not attempt to load all possible configuration, like region names, from config files. This forces them to do so.
export BASHRC_HASH=$(md5 -q ~/.bashrc)
This captures a checksum of the ~/.bashrc
file. If the last loaded checksum does not match the current checksum, the STALE
flag is set in the prompt.
export BASH_SILENCE_DEPRECATION_WARNING=1
The version of bash that ships with macOS displays a warning, asking you to upgrade to zsh. This disables the warning, as I have no plans to upgrade.
You may notice that this implies that I’m running the ancient bash 3.2, which Apple ships, because it’s the last GPLv2-licensed bash. This is on purpose! It ensures that I write shell scripts that work on other coworkers’ laptops, even though I lose functionality that’s available in newer versions of bash.
export PROMPT_COMMAND=prompt_command
This runs the prompt_command
shell function each time the prompt is displayed, which in turn changes the prompt.
export PYENV_VIRTUALENV_DISABLE_PROMPT=1
Pyenv, by default, will try to own changing the shell prompt on its own. I do this myself in prompt_command
, and use this variable to turn the feature off.
Code to run
I reserve the last section for imperative statements to run.
source ~/.api_keys
This is cheating, as ~/.api_keys
is a file filled with more export
statements. I keep API keys in a separate file, so as to prevent accidental disclosure. I frequently show my ~/.bashrc
off, including in recorded Teams calls, and don’t want sensitive information being made accessible to others.
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
These statements set up pyenv’s ability to hook cd
, and automatically activate or deactivate a virtualenv when entering a directory with a .python-version
file.
pyenv deactivate 2> /dev/null; true
This statement simply deactivates a Python virtualenv, if one is activated. While this will never happen when a shell is launched for the first time, it allows rereads of the file in rl
and rst
to unset them as if the shell had just been launched.