Grep one-liners as CI tasks Leveraging grep’s exit status and output for simple code checks

January 8, 2022

The ubiquitous grep utility can go a long way in enforcing simple rules in a codebase. Plenty of options and readable output combined with a smart exit status allow for simple one-liners in your continuous integration setup that are easy to write and maintain.

The other day my team encountered an issue that we wanted to prevent via a CI task. We needed a quick way to ensure that our automated Android release builds would not contain any missing translations. These are typically stored in strings.xml files using <string> XML tags.

This grep command was all we needed to quickly1 find such offenses:

$ grep --recursive --line-number --include=strings.xml '>\(""\| *\)</string>$'

The output reads rather nicely and tells us precisely where the offenses are, also thanks to the additional --line-number option:

./app/src/main/res/values/strings.xml:23:    <string name="some_key"></string>
./app/src/main/res/values-fr/strings.xml:147:    <string name="another_key">""</string>

A cool thing about grep is that it has a smart exit status. From GNU grep’s man page:

Normally the exit status is 0 if a line is selected, 1 if no lines were selected, and 2 if an error occurred.

Thus, as long as we have offenses we get an exit status 0, denoting success. Once we fix the offenses no lines will be matched by grep and the exit status will be 1, denoting that it was unsuccessful.

Since we want to fail the CI task in our grep one-liner, we need to invert this exit status. Thus, when no offenses are found, we need to exit with 0, otherwise with 1.

Shell script allows a pipeline of one or more commands such as cmdA or cmdA | cmdB to be prefixed with a ! (followed by a space), i.e. ! cmdA or ! cmdA | cmdB. This alters the exit status as described in the Shell Command Language reference:

If the pipeline does not begin with the ! reserved word, the exit status shall be the exit status of the last command specified in the pipeline. Otherwise, the exit status shall be the logical NOT of the exit status of the last command. That is, if the last command returns zero, the exit status shall be 1; if the last command returns greater than zero, the exit status shall be zero.

Thus, to get the desired inverted exit status for our CI task we just need to prefix grep with !.

If we want to additionally print a success message when no offenses where found, we can do so with an echo "message" which we combine with a logical && operator.

Equipped with this knowledge, we conclude with a generic one-liner that fails the task in case offenses are found and prints a success message otherwise:

! grep [option...] [patterns] [file...] && echo "No offenses found"
  1. I personally prefer ripgrep as it is much faster than grep, but usually that is not available on CI machines.