I work with a terminal every day. Being able to move around and get things done quickly is a huge boost to my performance and saves me a lot of time. Over the years my personal terminal configuration has evolved to include some neat tricks that I haven't seen many people use.
In case you're not familiar with them: dotfiles are a way of managing the many usually hidden configuration files found in your home directory (most of their filenames begin with a dot). Some developers like to publicly host their dotfiles for anyone to read and use.
A dotfiles repository usually includes a bunch of configuration files and some sort of shell script to copy or symlink them from the repository folder to a user's home directory. It's helpful to have some experience with shell scripting to get into dotfiles, but if you have done any type of programming you'll probably be able to pick up shell scripting or follow along.
Everything in this article comes from my personal dotfiles repository. Most of these are built for zsh but some may work in bash as well — as with any code snippet on the web, your mileage may vary.
Seasonal chevrons
The crown jewel of my dotfiles repo is the set of colourful chevrons at the end of my prompt:
The chevrons are purely decorative, but every three months they change colours to match the season. I always forget about this feature, which means that once every three months I open my terminal and get a little pang of delight when I see that they've changed again.
How it works
Here's the zsh function that returns the chevrons:
seasonal_chevrons () {
local date=$(date)
local chevrons="❯❯❯"
case $date in
# spring
*Mar*|*Apr*|*May*)
chevrons="%F{cyan}❯%F{green}❯%F{yellow}❯%f"
;;
# summer
*Jun*|*Jul*|*Aug*)
chevrons="%F{green}❯%F{yellow}❯%F{red}❯%f"
;;
# fall
*Sep*|*Oct*|*Nov*)
chevrons="%F{yellow}❯%F{red}❯%F{magenta}❯%f"
;;
# winter
*Dec*|*Jan*|*Feb*)
chevrons="%F{magenta}❯%F{cyan}❯%F{green}❯%f"
;;
*)
;;
esac
echo -en $chevrons
}
~~~{% endraw %}
Then we call the function when exporting the {% raw %}`PS1`{% endraw %} (prompt) variable in {% raw %}`.zshrc`{% endraw %}:{% raw %}
~~~ zsh
export PS1='$(seasonal_chevrons) '
~~~{% endraw %}
At the beginning of the function, we assign the date and a set of chevrons as variables. In case our function doesn't work, we'll return those uncoloured chevrons.
The function reads the date and looks for what month it is, in sets of three. For example, {% raw %}`*Mar*|*Apr*|*May*`{% endraw %} means to match "March OR April OR May". When it finds a match, it reassigns the {% raw %}`chevrons`{% endraw %} variable, colouring them using zsh colour keywords.
At the end of the function, we {% raw %}`echo`{% endraw %} whatever we ended up with.
## Dynamic git identity
Sometimes I use my terminal for personal work, like this article. In those cases, I like my git commits to use my personal email address and not my work email.
Luckily, all the repos that I use for work have the company name in their path, so I set up a dynamic include in my {% raw %}`.gitconfig`{% endraw %} that overrides my configured email address for those directories. It looks like this:{% raw %}
~~~ git
[user]
name = Adam Hollett
email = adamh@example.com
[includeIf "gitdir/i:**/workcompany/**"]
email = adamh@workcompany.com
~~~{% endraw %}
The {% raw %}`includeIf`{% endraw %} directive in {% raw %}`.gitconfig`{% endraw %} only activates the configuration in that category if its pattern is matched. In this case, it looks for the name {% raw %}`workcompany`{% endraw %} in the current path and sets a work email address if it finds it.
## Sort git branches by recency
When you type {% raw %}`git branch`{% endraw %}, your branch list is sorted alphabetically by default. This isn't super helpful. To sort your branches by their last commit date, with the most recent at the top, add this to your {% raw %}`.gitconfig`:
~~~ git
[branch]
sort = -committerdate
~~~
## The git status dot
The command I use most often is probably `git status`. This lets me check where I am in the process of writing and committing code and which files I've changed.
Instead of having to type this command over and over again, I wrote some functions to display a "status dot" for four different states:
- solid green for a "clean" state with no changes:
![A terminal prompt featuring a solid green circle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0dy4twpo1y7botdxenrn.png)
- hollow purple if any tracked files have been changed:
![A terminal prompt featuring a hollow purple circle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ofqhu3vu57m551c1t8w.png)
- shaded yellow if changes have been staged for a commit:
![A terminal prompt featuring a shaded yellow circle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0zbwpa05t5mqwvruocia.png)
- and solid blue if we have any commits ahead of the remote branch:
![A terminal prompt featuring a solid blue circle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7fu9hoy0aiomrnmby01.png)
This dot only appears if the current path is a git repo, which has the added bonus of telling me whether I'm currently in a repo.
### How it works
There are a few moving parts to this one. First we have a function called `git_check` that checks whether we're in a git repo by testing the contents of the command {% raw %}`git branch`{% endraw %}:{% raw %}
~~~ zsh
# Return the branch name if we're in a git repo, or nothing otherwise.
git_check () {
local gitBranch=$(git branch 2> /dev/null | sed -e "/^[^*]/d" -e "s/* \(.*\)/\1/")
if [[ $gitBranch ]]; then
echo -en $gitBranch
return
fi
}
~~~{% endraw %}
To be honest I don't exactly know how this works. But if we're in a git repo it outputs the branch name, otherwise it does nothing.
Another function checks the message output by {% raw %}`git status`{% endraw %} and gives back a state name:{% raw %}
~~~ zsh
# Return the status of the current git repo.
git_status () {
local gitBranch="$(git_check)"
if [[ $gitBranch ]]; then
local statusCheck=$(git status 2> /dev/null)
if [[ $statusCheck =~ 'Your branch is ahead' ]]; then
echo -en 'ahead'
elif [[ $statusCheck =~ 'Changes to be committed' ]]; then
echo -en 'staged'
elif [[ $statusCheck =~ 'no changes added' ]]; then
echo -en 'modified'
elif [[ $statusCheck =~ 'working tree clean' ]]; then
echo -en 'clean'
fi
fi
}
~~~{% endraw %}
I use these functions in another {% raw %}`git_dot`{% endraw %} function. You can see at the beginning here that if {% raw %}`$gitCheck`{% endraw %} does not get assigned then most of the body of this function is skipped — there's no need to do any of this if we're not currently in a git repo:{% raw %}
~~~ zsh
# Print a dot indicating the current git status.
git_dot () {
local gitCheck="$(git_check)"
if [[ $gitCheck ]]; then
local gitStatus="$(git_status)"
local gitStatusDot='●'
if [[ $gitStatus == 'staged' ]]; then
local gitStatusDot='◍'
elif [[ $gitStatus == 'modified' ]]; then
local gitStatusDot='○'
fi
if [[ $gitCheck && ! $gitCheck == 'master' && $COLUMNS -lt 100 ]]; then
echo -en "%F{#616161}⌥%f "
fi
echo -en "%F{"$(git_status_color)"}$gitStatusDot%f "
fi
}
~~~{% endraw %}
At the end of the above function we invoke another function called {% raw %}`git_status_color`{% endraw %} to apply a colour to the dot based on the current git status message, using the same {% raw %}`git_status`{% endraw %} function as before:{% raw %}
~~~ zsh
# Return a color based on the current git status.
git_status_color () {
local gitStatus="$(git_status)"
local statusText=''
case $gitStatus in
clean*)
statusText="green"
;;
modified*)
statusText="magenta"
;;
staged*)
statusText="yellow"
;;
ahead*)
statusText="cyan"
;;
*)
statusText="white"
;;
esac
echo -en $statusText
}
~~~{% endraw %}
Looking at this now, I can see that there's a lot of refactoring that could be done here. This doesn't necessarily need four separate functions. One of the great things about writing and maintaining your own dotfiles is being able to learn and improve as you get more practice.
## git start
Here's a quick one: a git alias to return you to the master branch, pull the latest changes, and clean up any stray files. Add this to your `.gitconfig`:
~~~ git
[alias]
start = !git checkout master && git pull && git clean -fd
~~~
Then type `git start` anywhere to run all three commands.
## Responsiveness
I got involved in front-end development right around the time that responsive design was taking over the web. With responsive design, the contents of a web application change to fit the window or device it's being viewed with.
You can do the same thing with your terminal prompt by reacting to the number of columns available using the {% raw %}`$COLUMNS`{% endraw %} environment variable.
You may have noticed this in the {% raw %}`git_dot`{% endraw %} function in the last example:{% raw %}
~~~ zsh
if [[ $gitCheck && ! $gitCheck == 'master' && $COLUMNS -lt 100 ]]; then
echo -en "%F{#616161}⌥%f "
fi
~~~{% endraw %}
This part of the function checks the current branch name. If it is _not_ {% raw %}`master`{% endraw %}, and if the number of columns available is less than 100, then it outputs a grey "option" symbol you may recognize from a Mac keyboard: ⌥. This lets us know that we are on a branch.
![A terminal prompt showing a small git branch icon](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pz5vi2400sitwufpp99n.png)
But if our terminal window is large enough, we can just print the branch name. zsh allows you to assign a variable {% raw %}`RPROMPT`{% endraw %} to right-align part of your prompt, so I assign that in {% raw %}`.zshrc`{% endraw %}:{% raw %}
~~~ zsh
export RPROMPT='$(git_branch)'
~~~{% endraw %}{% raw %}
~~~ zsh
# Print a label for the current git branch if it isn't master.
git_branch () {
local gitBranch="$(git_check)"
if [[ $gitBranch && ! $gitBranch == 'master' && $COLUMNS -gt 79 ]]; then
echo -en "%F{#616161}⌥%f %F{"$(git_status_color)"}$gitBranch%f"
fi
}
~~~{% endraw %}
So on larger windows we get this:
![A terminal prompt showing the full git branch name, aligned to the right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mdmwvjdw6yl96bhsi2qg.png)
The full branch name uses the same colour as the git status dot.
## Try it yourself
There is a lot more useful stuff in my dotfiles, including my [Ruby configuration](https://github.com/adamhollett/dotfiles/tree/master/ruby), my [.editorconfig](https://github.com/adamhollett/dotfiles/blob/master/editor/.editorconfig), and [a whole bunch of aliases](https://github.com/adamhollett/dotfiles/blob/master/shell/aliases.sh).
If you're interested in building your own set of dotfiles, the best place to start is by exploring other peoples' setups. dotfiles.github.io has a [good list of example repositories](https://dotfiles.github.io/inspiration/). Fork someone else's, or start from scratch and build your own piece by piece. If you're new to shell scripting, this is a fantastic way to start.
----
Cover photo by [Arnold Francisca](https://unsplash.com/@clark_fransa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/code?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText).
Top comments (5)
Love this one! Thanks for the tip!
Let's talk about it!
This defines a function,
git_check
, which you can call later in the script.This defines a local variable,
gitBranch
, which can only be used within the scope ofgit_check
. The assigned value will be the output of the command within the$(...)
.This runs the
git branch
command, and sends the stderr to/dev/null
, effectively wiping the error message from existence. Stdout will still be printed, though. Thus, if you are inside a git repository, then you'll see the output ofgit branch
; and if you're not, you won't see any error message.As a reminder, here's what
git branch
output will look like if you're in a git repository...In the above example, we see all the branches in your local checkout, alphabetized; and the active branch is prefixed with
*
.Moving on.
We're piping the output of
git branch
into thesed
command.sed
is short for "stream editor"; we'll use it to transform the output of thegit branch
command, line-by-line. Each-e
flag denotes a transformation script. Let's look at the two scripts.The
/.../d
command is documented as "Delete pattern space. Start next cycle." So, any line matching the regex pattern^[^*]
will be ignored. That regular expression has two components:^
: Start-of-line.[^*]
: Match any character other than the literal*
.Thus, the regex will match any line which begins with a character other than the literal
*
. If you look at the examplegit branch
output I pasted above, this would match the line for any branch except the active branch. Putting it all together,sed -e "/^[^*]/d"
will ignore any line except for the active branch line.Then, we have the other
sed
command:The
s/regexp/replacement/
command will attempt to match the regexp against the input, and if successful, will replace that portion with the replacement. So let's look at the regexp and the replacement.First, let's look at the regex:
* \(.*\)
.*
at the beginning of the regex matches the literal*
.matches the literal space character,
.
\(...\)
creates a capture group.sed
will replace the escaped characters\(
and\)
with parentheses. (If we wanted to capture literal parenthesis characters, we would use\\(foo\\)
)..*
matches any quantity (including zero) of any character(s). It's a wildcard.In short, this regex will match any line starting with the literal
*
, and everything thereafter will be remembered in a capture group.Next, let's look at the replacement,
\1
. This syntax refers to the first capture group: which, recall, is everything after the*
.In summary,
-e "s/* \(.*\)/\1/
would replace a line like* foo
with justfoo
. It strips away the leading*
. If the regex doesn't match, then there is no effect: the linebar
would just becomebar
.The sum total of our
sed
statement is that we have eliminated all lines fromgit branch
except the active branch line, and we have removed the leading*
from that active branch line. All we are left with is the name of the active branch. And, if we are not inside a git repository, then thesed
command would have received no lines for input, and thus would yield no output.Let's check in with our script. Where are we now? We are in a function called
git_check
, and we have just created a local variablegitBranch
whose value is the name of the active branch. If we are not in a git repository, then the value would instead be the empty string.Moving on.
This conditional block will execute if (and only if) the
gitBranch
variable has a non-empty value. Remember thatgitBranch
equals the name of the active git branch, but only if we are inside a git repository. Thus, the conditional block will execute if (and only if) we are inside a git repository.For more info, you might look into the
[[
built-in: mywiki.wooledge.org/BashFAQ/031echo [FLAGS] MESSAGE
will print out the message. In this case, we'll print the value of thegitBranch
variable, and then return from (exit) our function.The
-e
flag enables interpretation of backslash escapes: for example,echo "\t"
will print the literal string\t
, whereasecho -e "\t"
will interpret\t
as a special tab character, and will print a tab. (I don't think this flag is necessary here.)The
-n
will suppress the trailing newline.echo
will normally print out a newline at the end of your output. For example:So, that's every part of the code-block! To review, we defined a function
git_check
, which runsgit branch
, discards any errors, and extracts the active git branch (if any) into the local variablegitBranch
. Then, if that variable is not empty, we printed its contents without any surrounding whitespace.Hope this helps! Thanks for your write-up; I stole several of your nifty tricks :)
This is incredible! Thank you for taking the time to explain this part!
Some great tips in here I'd never seen before, thanks for sharing!
Thanks for reading! Glad you enjoyed them.