Managing Shell Configuration

Managing Shell Configuration

Managing a bash or zsh configuration can be tricky or clunky, especially if you work on multiple different machines and want some level of consistency. This is one possible solution to the problem.

14 minute read

Managing a bash or zsh configuration can be tricky or clunky, especially if you work on multiple different machines and want some level of consistency.

This is one possible solution to the problem.

A Brief History

Longer ago than I care to remember I used to, uhm, “sync” my bash setup by always making sure that I had “a copy somewhere” that I could copy from machine to machine, and sometimes even keep them all synced up with the updates.

In late 2010 I had a better idea and wrote a small proof od concept that was loosely based on runlevels and exectable bits being set or not.

For a number of years I was manually cloining my private repo (because Private Tokens) and adding the same block to my .bashrc file:

cat >> ~/.bashrc <<EOF
# part of $HOME/.shellrc.d
if [ -f $HOME/.shellrc.d/source-relevant-files -a -x $HOME/.shellrc.d/source-relevant-files ]; then
    source $HOME/.shellrc.d/source-relevant-files
fi
EOF

Over time I added dotfiles/ which I would softlink into appropriate places, and later add home-bin/ to be a placeholder for common scripts I like to take with me.

I automated it and added some helpers to “link things to places” and pootled along for .. well, longer than I should have.

In 2020 I realised that it was silly to use something called bashrcd to manage a mixture of bash and zsh settings; I’d started my migration to the latter and needed to manage both.

I decided that after almost 10 years it was time to make a proper tool.

shellrcd

Overview

I took my ideas, previous code, experience, and needs to start a new project. Because I wasnted to be generic from the start, I opted for the imaginitively hames shellrcd.

Due to my limited shell experience I only wrote a project that supported bash and zsh, but hopefully in a manner that would be extensible for The Next Great Shell.

The Gist

The idea for the new project was to have a base repo that anyone can use, with support for personalised extensions, and some further support for private repos (to keep those pesky personall access tokens)

  • shellrcd - the top level project written and managed by yours truly
  • shellrcd-extras - a (public) repo written and managed by yourself
  • shellrcd-private - a (private) repo manages by yourself; this gets cloned as a git submodule

Getting Started

One aim for the new project was to make it as easy as possible to get started, as well as staying up to date with any changes pushed to the remote repos.

Getting the most basic setup is as simple as:

# for the sensible, paranoid, people out there, the repo readme also has
# instructions on how to download and inspect the script before running
# anything
sh -c "$(curl -fsSL https://raw.githubusercontent.com/chiselwright/shellrcd/master/tools/install.sh)"

Creating A Test User

To provide a working example of the process I suggest testing with a demo user:

# run this and follow the prompts
sudo adduser --shell /bin/zsh shellbot

become the new user:

sudo su - shellbot

You’ll see a message/warning. For now select q:

(q) Quit and do nothing. The function will be run again next time.

We’re planning on replacing everthing soon enough.

Perform A Basic Installation

Before you do anything make sure you have this, it will save you some extra work later on:

mkdir -p ~/bin

We really ought to update the script to create this on your behalf if it doesn’t exist.

Running the installation command from earlier will perform the appropriate magic and output something resembling the following:

protomolecule% sh -c "$(curl -fsSL https://raw.githubusercontent.com/chiselwright/shellrcd/master/tools/install.sh)"
[shellrcd] /home/shellbot/.shellrc.d is not found. Downloading...
[shellrcd] ...done
[zsh] Looking for an existing zsh config...
[zsh] Creating /home/shellbot/.zshrc and adding shellrcd block...
[bash] Looking for an existing bash config...
[bash] Non-MacOS detected. Using .bashrc.
[bash] Creating /home/shellbot/.bashrc and adding shellrcd block...
[shellrcd] Found /home/shellbot/bin; installing shellrcd-update
[shellrcd] /home/shellbot/bin/shellrcd-update already exists, leaving untouched
           _             _    _                    _
          ( )           (_ ) (_ )                 ( )
      ___ | |__     __   | |  | |  _ __   ___    _| |
    /',__)|  _ `\ /'__`\ | |  | | ( '__)/'___) /'_` |
    \__, \| | | |(  ___/ | |  | | | |  ( (___ ( (_| |
    (____/(_) (_)`\____)(___)(___)(_)  `\____)`\__,_)
                                ....is now installed!

    Please look over /home/shellbot/.zshrc for any glaring errors.

    Check which scripts are active with:
        sh /home/shellbot/.shellrc.d/tools/list-active.sh

    Once happy, open a new shell or:
        source /home/shellbot/.zshrc

If you examine either .bashrc or .zshrc you’ll see the manual block that I mentioned at the start of this article:

# file: ".zshrc"
#!/usr/bin/zsh

## added by shellrcd ##
if [ -f ~/.shellrc.d/source-relevant-files -a -x ~/.shellrc.d/source-relevant-files ]; then
    source ~/.shellrc.d/source-relevant-files
fi
## end of shellrcd block ##

Test it’s working by logging out, then logging back in:

# stop being shellbot
exit

# become shellbot again
sudo su - shellbot

You’ll see something like this:

protomolecule% exitsudo su - shellbot
gpg-agent[25887]: directory '/home/shellbot/.gnupg' created
gpg-agent[25887]: directory '/home/shellbot/.gnupg/private-keys-v1.d' created
gpg-agent[25888]: gpg-agent (GnuPG) 2.2.4 started
protomolecule%

shellrcd does its best to leave existing installations untouched, so you can safely run the installation command again:

protomolecule% sh -c "$(curl -fsSL https://raw.githubusercontent.com/chiselwright/shellrcd/master/tools/install.sh)"
[shellrcd] /home/shellbot/.shellrc.d already exists. Leaving unchanged.
[zsh] Looking for an existing zsh config...
[zsh] shellrcd block already added to /home/shellbot/.zshrc. Nothing to do.
[bash] Looking for an existing bash config...
[bash] Non-MacOS detected. Using .bashrc.
[bash] shellrcd block already added to /home/shellbot/.bashrc. Nothing to do.
[shellrcd] Found /home/shellbot/bin; installing shellrcd-update
[shellrcd] /home/shellbot/bin/shellrcd-update already exists, leaving untouched
           _             _    _                    _
          ( )           (_ ) (_ )                 ( )
      ___ | |__     __   | |  | |  _ __   ___    _| |
    /',__)|  _ `\ /'__`\ | |  | | ( '__)/'___) /'_` |
    \__, \| | | |(  ___/ | |  | | | |  ( (___ ( (_| |
    (____/(_) (_)`\____)(___)(___)(_)  `\____)`\__,_)
                                ....is now installed!

    Please look over /home/shellbot/.zshrc for any glaring errors.

    Check which scripts are active with:
        sh /home/shellbot/.shellrc.d/tools/list-active.sh

    Once happy, open a new shell or:
        source /home/shellbot/.zshrc

shellrcd-update is installed as a softlink, so clearly another bug to raise for ourselves there.

Extending shellrcd

The aim of shellrcd is to be as minimal as possible, and leave you to grow your own suite of startup scripts.

For an example of the format preferred, take a look at the _agnostic/ folder in the base project.

It’s good practice to have startup elements in smaller discrete chunks, so that you can manage them in a more granular fashion.

Create A New Extras Respository

Create a new repository somewhere. We’re using Github for this demo:

new github repo

Update Your Local Project

To keep out of your way shellrcd initialises itself with a non-origin named remote:

protomolecule% git remote -v
shellrcd	git://github.com/chiselwright/shellrcd.git (fetch)
shellrcd	git://github.com/chiselwright/shellrcd.git (push)

leaving you free to add your extras repository as origin:

# make sure you're in the right place for this to work
cd ~/.shellrc.d

# you'll need to replace the repo details for your use case
git remote add origin git@github.com:chiselwright/shellrcd-extras-shellbot.git

# you might need to generate an ssh keypair for your test user
#     ssh-keygen
# and add to your account in Github
git remote update origin

Not much happens, but we’re now in a position for the next step:

git checkout -b extras/firstlast shellrcd/master

will give you some standard git output:

Branch 'extras/firstlast' set up to track remote branch 'master' from 'shellrcd'.
Switched to a new branch 'extras/firstlast'

You’re now ready to test that everything still works .. with no extras, but with the added repository ready to go:

shellrcd-update

will output something like this:

[shellrcd] Switching back to 'master'…
Switched to branch 'master'
Your branch is up to date with 'shellrcd/master'.
[shellrcd] Pulling recent changes into master…
Already up to date.
Current branch master is up to date.
[shellrcd] Switching back to 'extras/firstlast'…
Switched to branch 'extras/firstlast'
Your branch is up to date with 'shellrcd/master'.
[shellrcd] Pulling recent changes into extras/firstlast…
Already up to date.
Current branch extras/firstlast is up to date.
[shellrcd] Rebasing master and extras/firstlast…
Current branch extras/firstlast is up to date.
           _             _    _                    _
          ( )           (_ ) (_ )                 ( )
      ___ | |__     __   | |  | |  _ __   ___    _| |
    /',__)|  _ `\ /'__`\ | |  | | ( '__)/'___) /'_` |
    \__, \| | | |(  ___/ | |  | | | |  ( (___ ( (_| |
    (____/(_) (_)`\____)(___)(___)(_)  `\____)`\__,_)
                                ....is up to date!

    You are running from:
        extras/firstlast

    Updates will activate in a new shell, or if you source your rcfile

shellrcd hasn’t yet been tested with non-master default branches. It might work through sheer luck, but be careful of you want to use something different.

If this works, make sure to push it to the remote:

git push -u origin extras/firstlast

At this point you will have a personal repo that contains an exact copy of shellrcd in the extras/firstlast branch.

Your First Customisation

You’re free to do whatever you wish to extend the setup you have in place now that you have your extras repo in place.

This is a simple example of your forst change, and verifying an update.

echo 'alias just-a-test="echo Just A Test"' > ~/.shellrc.d/_agnostic/alias.test
chmod 0755 ~/.shellrc.d/_agnostic/alias.test

cd ~/.shellrc.d/
git add _agnostic/alias.test
git commit -m 'Add _agnostic/alias.test'

Note - you only have the change locally. This is OK for now.

protomolecule% git log --oneline -n 2
cda1b7d (HEAD -> extras/firstlast) Add _agnostic/alias.test
4cd72d7 (shellrcd/master, shellrcd/HEAD, origin/extras/firstlast, master) Add setup_shellrcd_submodules to install.sh

Test an update:

shellrcd-update

The output will look very similar to the previous time we tested the update.

Test the new script would be picked up either by logging out and back in, or taking the simpler route of resourcing your rcfile:

. ~/.zshrc

for example:

protomolecule% alias |grep test
protomolecule% . ~/.zshrc
protomolecule% alias |grep test
just-a-test='echo Just A Test'

A Shell Specific Customisation

Soemtimes, no matter how hard you try, some things just aren’t shell agnostic enough to live in _agnostic/

If you end up in this situation, simple add your script(s) to bash/ or zsh/, make them executable, and they’ll be included in the shell initialisation.

The shell specific folders are processed after _agnostic/.

Let’s create a quick addition to the zsh/ folder now:

# create the new file
cat <<'EOF' >> ~/.shellrc.d/zsh/50.precmd.tmux
precmd() { if [ -n "$TMUX" ] ; then tmux rename-window "$(basename $PWD)"; fi; }
EOF

# make it executable
chmod 0755 ~/.shellrc.d/zsh/50.precmd.tmux

# get it into git
cd ~/.shellrc.d/
git add zsh/50.precmd.tmux
git commit -m 'Add zsh/50.precmd.tmux'
# no need for fpush as there hasn't been any rebasing since we started
git push

Redeploying

Clearly no one want to repeat all of those individual steps after the initial time preparing the repos.

You can set a couple of variables in your shell and the installer with Do The Right Thing™

export SHELLRCD_EXTRA_BRANCH=extras/firstlast
export SHELLRCD_EXTRA_REPO=git@github.com:chiselwright/shellrcd-extras-shellbot.git

you can then run the installation command:

sh -c "$(curl -fsSL https://raw.githubusercontent.com/chiselwright/shellrcd/master/tools/install.sh)"

We can test this with our demo user by performing some cleanup/removal of our work so far, then running the instructions above:

# file: "remove existing setup"
rm -rf ~/.shellrc.d/

We’ll leave .bashrc and .zshrc alone as we’ve not customised these further than the standard installation process and we’ve already seen that behaves “sensibly”.

# file: "reinstall with information about our extras"
export SHELLRCD_EXTRA_BRANCH=extras/firstlast
export SHELLRCD_EXTRA_REPO=git@github.com:chiselwright/shellrcd-extras-shellbot.git
sh -c "$(curl -fsSL https://raw.githubusercontent.com/chiselwright/shellrcd/master/tools/install.sh)"

which produces the following:

[shellrcd] /home/shellbot/.shellrc.d is not found. Downloading...
[shellrcd] ...done
[shellrcd] Configuring extras/firstlast from git@github.com:chiselwright/shellrcd-extras-shellbot.git
Fetching origin
remote: Enumerating objects: 11, done.
remote: Counting objects: 100% (11/11), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 8 (delta 4), reused 7 (delta 3), pack-reused 0
Unpacking objects: 100% (8/8), done.
From github.com:chiselwright/shellrcd-extras-shellbot
 * [new branch]      extras/firstlast -> origin/extras/firstlast
Branch 'extras/firstlast' set up to track remote branch 'extras/firstlast' from 'origin'.
Switched to a new branch 'extras/firstlast'
First, rewinding head to replay your work on top of it...
Applying: Add _agnostic/alias.test
Applying: Add zsh/50.precmd.tmux
[zsh] Looking for an existing zsh config...
[zsh] shellrcd block already added to /home/shellbot/.zshrc. Nothing to do.
[bash] Looking for an existing bash config...
[bash] Non-MacOS detected. Using .bashrc.
[bash] shellrcd block already added to /home/shellbot/.bashrc. Nothing to do.
[shellrcd] Found /home/shellbot/bin; installing shellrcd-update
[shellrcd] /home/shellbot/bin/shellrcd-update already exists, leaving untouched
           _             _    _                    _
          ( )           (_ ) (_ )                 ( )
      ___ | |__     __   | |  | |  _ __   ___    _| |
    /',__)|  _ `\ /'__`\ | |  | | ( '__)/'___) /'_` |
    \__, \| | | |(  ___/ | |  | | | |  ( (___ ( (_| |
    (____/(_) (_)`\____)(___)(___)(_)  `\____)`\__,_)
                                ....is now installed!

    Please look over /home/shellbot/.zshrc for any glaring errors.

    Check which scripts are active with:
        sh /home/shellbot/.shellrc.d/tools/list-active.sh

    Once happy, open a new shell or:
        source /home/shellbot/.zshrc

We can also examine the project directory and confirm that we do have two remotes (base and extras), with our changes being the most resent in the commit history:

protomolecule% cd ~/.shellrc.d

protomolecule% git remote -v
origin	git@github.com:chiselwright/shellrcd-extras-shellbot.git (fetch)
origin	git@github.com:chiselwright/shellrcd-extras-shellbot.git (push)
shellrcd	git://github.com/chiselwright/shellrcd.git (fetch)
shellrcd	git://github.com/chiselwright/shellrcd.git (push)

protomolecule% git log --oneline -n 5
663f607 (HEAD -> extras/firstlast) Add zsh/50.precmd.tmux
22d6213 Add _agnostic/alias.test
21453e4 (shellrcd/master, shellrcd/HEAD, master) Fix: proper check for "shellrcd-update not a symbolic link"
4e5dac2 Fix: use path to current shell's RCFILE in welcome message
8130fea Explicity unset functions ... just in case

Best Practices

if something doesn’t exist, skip don’t fail

Sometimes you’ll want to run extra commands if something is installed, or you only want to create alises if a certain exectable is available.

A good pattern for this is with type:

if type "someCommand" >/dev/null; then
    # do some stuff
fi

This will extend your setup if you have something installed, and continue merrily if it doesn’t.

Here are a couple of examples:

# file: "_agnostic/20.alias.generate-password_aws"
if type "aws" >/dev/null; then
    # spits out a string, ready to use as a password
    # (no longer requires jq to do waht we can do with the command we already have)
    alias generate-password='aws secretsmanager get-random-password --exclude-punctuation --query "RandomPassword" --output text'
fi

or this one that will attempt to install for you:

# file: "_agnostic/98.glow-markdown"
# do things if NOT installed
if ! type "glow" >/dev/null; then
  # maybe we have brew
  if type "brew" >/dev/null; then
      echo "[Installing 'glow' with 'brew']"
      brew install charmbracelet/homebrew-tap/glow
  # maybe we have golang
  elif type go >/dev/null; then
      echo "[Installing 'glow' with 'go get']"
      go get github.com/charmbracelet/glow
  # ok, nothing, but we feel it's worth knowing about
  else
      echo "glow: can't locate 'brew' or 'go'; skipping installation"
  fi
fi

# if we find it's installed when we get this far, either because we already had
# it, or just installed it above, add an alias that defaults to using --pager
if type "glow" >/dev/null; then
    alias glow='glow --pager'
fi

always --force-with-lease when pushing changes after updates

Because you’ll be rebasing your “local feature branch” from another branch, you’ll be bessing with your commit history.

This is fine, because it’s only you working in the branch (I hope!)

This does mean you will need to force push any changes you make if there have been upstream changes:

git push --force with lease

To make this simpler, set a global alias:

git config --global alias.fpush "push --force-with-lease"

and use:

git fpush

Read more about --force-with-lease in this StackOverflow post.

always --rebase when pulling changes

You’ll get into a world of pain if you git pull from your remote without using the --rebase option. Assuming a version of git >= 1.7.9:

git config --global pull.rebase true

Of course you can avoid this in this project and simply run:

shellrdc-update

Rebasing by default is a generally good practice to use with any git repositories. Read more in “Please, oh please, use git pull –rebase”

Further Information

You can check out the repos mentioned in this article:

Attributions