DEV Community

loading...
Cover image for Terminal tricks from my dotfiles

Terminal tricks from my dotfiles

admhlt profile image Adam Hollett Originally published at adamhollett.com ・7 min read

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:

A terminal window showing three multicoloured arrows

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
}
Enter fullscreen mode Exit fullscreen mode

Then we call the function when exporting the PS1 (prompt) variable in .zshrc:

export PS1='$(seasonal_chevrons) '
Enter fullscreen mode Exit fullscreen mode

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, *Mar*|*Apr*|*May* means to match "March OR April OR May". When it finds a match, it reassigns the chevrons variable, colouring them using zsh colour keywords.

At the end of the function, we echo 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 .gitconfig that overrides my configured email address for those directories. It looks like this:

[user]
  name = Adam Hollett
  email = adamh@example.com

[includeIf "gitdir/i:**/workcompany/**"]
  email = adamh@workcompany.com
Enter fullscreen mode Exit fullscreen mode

The includeIf directive in .gitconfig only activates the configuration in that category if its pattern is matched. In this case, it looks for the name workcompany in the current path and sets a work email address if it finds it.

Sort git branches by recency

When you type git branch, 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 .gitconfig:

[branch]
  sort = -committerdate
Enter fullscreen mode Exit fullscreen mode

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

  • hollow purple if any tracked files have been changed:

A terminal prompt featuring a hollow purple circle

  • shaded yellow if changes have been staged for a commit:

A terminal prompt featuring a shaded yellow circle

  • and solid blue if we have any commits ahead of the remote branch:

A terminal prompt featuring a solid blue circle

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 git branch:

# 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
}
Enter fullscreen mode Exit fullscreen mode

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 git status and gives back a state name:

# 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
}
Enter fullscreen mode Exit fullscreen mode

I use these functions in another git_dot function. You can see at the beginning here that if $gitCheck 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:

# 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
}
Enter fullscreen mode Exit fullscreen mode

At the end of the above function we invoke another function called git_status_color to apply a colour to the dot based on the current git status message, using the same git_status function as before:

# 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
}
Enter fullscreen mode Exit fullscreen mode

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:

[alias]
  start = !git checkout master && git pull && git clean -fd
Enter fullscreen mode Exit fullscreen mode

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 $COLUMNS environment variable.

You may have noticed this in the git_dot function in the last example:

if [[ $gitCheck && ! $gitCheck == 'master' && $COLUMNS -lt 100 ]]; then
  echo -en "%F{#616161}⌥%f "
fi
Enter fullscreen mode Exit fullscreen mode

This part of the function checks the current branch name. If it is not master, 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

But if our terminal window is large enough, we can just print the branch name. zsh allows you to assign a variable RPROMPT to right-align part of your prompt, so I assign that in .zshrc:

export RPROMPT='$(git_branch)'
Enter fullscreen mode Exit fullscreen mode
# 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
}
Enter fullscreen mode Exit fullscreen mode

So on larger windows we get this:

A terminal prompt showing the full git branch name, aligned to the right

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, my .editorconfig, and a whole bunch of aliases.

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. 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 on Unsplash.

Discussion (6)

pic
Editor guide
Collapse
gredelston profile image
Greg Edelston • Edited
# 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
}
Enter fullscreen mode Exit fullscreen mode

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.

Let's talk about it!

git_check () {
    ...
}
Enter fullscreen mode Exit fullscreen mode

This defines a function, git_check, which you can call later in the script.

  local gitBranch=$(...)
Enter fullscreen mode Exit fullscreen mode

This defines a local variable, gitBranch, which can only be used within the scope of git_check. The assigned value will be the output of the command within the $(...).

  git branch 2> /dev/null
Enter fullscreen mode Exit fullscreen mode

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 of git 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...

  bar
* foo
  quux
Enter fullscreen mode Exit fullscreen mode

In the above example, we see all the branches in your local checkout, alphabetized; and the active branch is prefixed with *.

Moving on.

  | sed -e "/^[^*]/d" -e "s/* \(.*\)/\1/"
Enter fullscreen mode Exit fullscreen mode

We're piping the output of git branch into the sed command. sed is short for "stream editor"; we'll use it to transform the output of the git branch command, line-by-line. Each -e flag denotes a transformation script. Let's look at the two scripts.

  -e "/^[^*]/d"
Enter fullscreen mode Exit fullscreen mode

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:

  1. ^: Start-of-line.
  2. [^*]: 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 example git 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:

  -e  "s/* \(.*\)/\1/"
Enter fullscreen mode Exit fullscreen mode

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 just foo. It strips away the leading *. If the regex doesn't match, then there is no effect: the line bar would just become bar.

The sum total of our sed statement is that we have eliminated all lines from git 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 the sed 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 variable gitBranch 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.

  if [[ $gitBranch ]]; then
    ...
  fi
Enter fullscreen mode Exit fullscreen mode

This conditional block will execute if (and only if) the gitBranch variable has a non-empty value. Remember that gitBranch 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/031

    echo -en $gitBranch
    return
Enter fullscreen mode Exit fullscreen mode

echo [FLAGS] MESSAGE will print out the message. In this case, we'll print the value of the gitBranch 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, whereas echo -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 -nwill suppress the trailing newline. echo will normally print out a newline at the end of your output. For example:

$ echo "Hello!"
Hello!
$ echo -n "Hello!"
Hello!$ exit
Enter fullscreen mode Exit fullscreen mode

So, that's every part of the code-block! To review, we defined a function git_check, which runs git branch, discards any errors, and extracts the active git branch (if any) into the local variable gitBranch. 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 :)

Collapse
admhlt profile image
Adam Hollett Author

This is incredible! Thank you for taking the time to explain this part!

Collapse
maxpou profile image
Maxence Poutord
[includeIf "gitdir/i:**/workcompany/**"]
  email = adamh@workcompany.com
Enter fullscreen mode Exit fullscreen mode

Love this one! Thanks for the tip!

Collapse
citizen428 profile image
Michael Kohl

Yes, this is a nice one. So far I just set it as a local option but I'll look into includeIf:

git config --local user.email user@example.com
Enter fullscreen mode Exit fullscreen mode
Collapse
pzelnip profile image
Adam Parkin

Some great tips in here I'd never seen before, thanks for sharing!

Collapse
admhlt profile image
Adam Hollett Author

Thanks for reading! Glad you enjoyed them.