Eight Obscure Bash Options You Might Want to Know About

Some bash options are well known, and well-used. For example, many people put

set -o xtrace

at the top of their scripts to debug them,

set -o errexit

to exit on error, or

set -o errunset

to exit if a variable is referenced but not set.

But there are many other options you can set. Many of them can seem confusing if you read the man page, so I’ve collected some of the ones I think are more useful here and explained them further.

If you are using a Mac, then you may be running
an older version of bash (3.x rather than 4.x)

that does not have all these
options available. If so, see here or here.

set vs shopt?

There are two ways to set bash options from within scripts or on the command line. You can use the set builtin command, or the shopt builtin command. They both manipulate the behaviour of the shell, and differ in their lineage. The set options are inherited, or borrowed, from other shells’ options, while the shopt ones were originated in bash.

If you want to see how your current settings look, then run:

$ set -o
$ shopt

To switch on a setting using set, you can use the long version, or a shorter flag that is equivalent. For example:

$ set -o errunset
$ set -e

both have the same effect.

To switch off a setting using set, you use + instead of -:

$ set +e

For a long time I couldn’t remember which way round it goes, as the logic of (- = on), and (+ = off) seems wrong to me.

To switch on and off a shopt setting, you use the (more logical)-s (set) and -u (unset) flags:

$ shopt -s cdspell # <= on
$ shopt -u cdspell # <= off

Changing Directories

There are a few options that can help dealing with directories.

1. cdspell

If you set this up, then bash will work around your mis-spellings and go to the folder you were going for anyway.

$ shopt -s cdspell
$ mkdir abcdefg
$ cd abcdeg
$ cd ..

I’ve used this for years, and very occasionally (maybe once a year) it will make a decision that surprises me. But it probably proves useful at least once a day.

2. autocd

If the inefficiency of typing cd is something you can’t bear, then you can set this option to move to folder if the command doesn’t exist.

$ shopt -s autocd
$ abcdefg
$ cd ..

You can also use it in conjunction with autocompletion to quickly hop around:

$ ./abc[TAB][RETURN]
cd -- ./abcdefg

Just don’t call a folder ‘rm -rf *‘ (yes, you can, by the way).

3. direxpand

This is a neat option that gets the shell to perform any expansions of variables, tildes and the like right there for you in the command line if you tab to complete:

$ shopt -s direxpand
$ ./[TAB] # is replaced by...
$ /full/path/to/current_working_folder
$ ~/[TAB] # is replaced by...
$ /full/path/to/home/folder
$ $HOME/[TAB] # is replaced by
$ /full/path/to/home/folder

A Clean Exit

4. checkjobs

This option stops the shell session from exiting if there are any jobs running in the background that haven’t finished yet.

Instead, the unfinished jobs are listed for you. If you still want to exit, you can if you enter exit immediately afterwards again.

$ shopt -s checkjobs
$ echo $$
68125 # <= Process ID of the shell
$ sleep 999 &
$ exit
There are running jobs.
[1]+  Running                 sleep 999 &
$ echo $$
68125 # <= Process ID of the shell is the same
$ exit
There are running jobs.
[1]+  Running                 sleep 999 &
$ exit
$ echo $$
$ 59316 # <= Process ID has changed this time

Globbing Superpowers

5. globstar

This option gives you globbing superpowers! If you type:

$ shopt -s globstar
$ ls **

then the shell will output recursively all folders and subfolders.

Combined with direxpand, you can quickly page through all the files and subfolders beneath you:

$ shopt -s direxpand
$ ls **[TAB][TAB]
Display all 2033 possibilities? (y or n)

6. extglob

This option gives your globbing powers more commonly associated with full-on regular expressions. Occasionally, this is very useful, as it allows you to do nifty things like this:

$ shopt -s extglob
$ touch afile bfile cfile
$ ls
afile bfile cfile
$ ls ?(a*|b*)
afile bfile
$ ls !(a*|b*)

where the patterns are placed in parentheses, separated by pipes, and the operators available are:

? = match zero or one occurences of the patterns given
! = match anything that doesn't match any patterns given
* = zero or more occurences
+ = one or more occurrences
@ = exactly one occurence


7. histverify

If you’re accident prone, then using the history shortcuts like !! and !$ can be scary, especially when you’re just learning them.

The histverify option allows you to see how bash interprets the command before it actually gets run:

$ shopt -s histverify
$ echo !$ # <= On hitting return, command is not run
$ echo histverify # <= Command is redisplayed ready to run
histverify # <= command gets run

This is based on some of the contents of my book Learn Bash the Hard Way, available at $6.99.


8. Noclobber

Again, if you’re accident-prone, you might want to set this one up.

Clobbering is the act of overwriting a file that already exists with the redirect operator (>). This can be disastrous if you’ve not got the file backed up anywhere else.

Using set -C prevents this from happening when you use the redirect operator. If you are sure you want to clobber, you can override with the >| operator instead:

$ touch afile
$ set -C
$ echo something > afile
-bash: afile: cannot overwrite existing file
$ echo something >| afile

If you like this, you might like one of my books:
Learn Bash the Hard Way

Learn Git the Hard Way
Learn Terraform the Hard Way


Get 39% off Docker in Practice with the code: 39miell2

‘AWS vs K8s’ is the new ‘Windows vs Linux’


If, like me, you’re over 40 and work in IT, you’ll probably remember a time when everyone used Windows, and a small but growing proportion of people were wasting their lives compiling Linux in their spare time.

The Windows users would look on, baffled: ‘Why would you do that, when Windows has everything you need, is supported, and is so easy to use?!’

Answers to this question varied. Some liked to tinker, some wanted an OS to be ‘free’, some wanted more control over their software, some wanted a faster system, but all had some niche reason to justify the effort.


As I stayed up for another late night trying to get some new Kubernetes add-on to work as documented, it struck me that I’m in a similar place to those days. Until a couple of years ago, Kubernetes itself was a messy horror-show for the uninitiated, with regularly-changing APIs, poor documentation if you tried to build yourself, and all the characteristics you might expect of an immature large-scale software project.

That said, Kubernetes’ governance was and is far and away ahead of most open source software projects, but the feeling then was similar to compiling Linux at the turn of the century, or dealing with your laptop crashing 50% of the time you unplugged a USB cable (yes, kids, this used to happen).

It’s not like confusion and rate of change has come down to a low level. Even those motivated to keep up struggle with the rate of change in the ecosystem, and new well-funded technologies pop up every few months that are hard to explain to others.

Take knative for example:

So my AWS-using comrades see me breaking sweat on the regular and ask ‘why would you do that, when AWS has everything you need, is supported and used by everyone, and is so easy to use!?’

AWS is Windows

Like Windows, AWS is a product. It’s not flexible, its behaviour is reliable. The APIs are well defined, the KPIs are good enough to be useful for most ‘real’ workloads. There are limits on all sorts of resources that help define what you can and can’t achieve.

Most people want this, like most people want a car that runs and doesn’t need to be fixed often. Some people like to maintain cars. Some companies retain mechanics to maintain a fleet of cars, because it’s cheaper at scale. In the same way, some orgs get to the point where they could see benefits from building their own data centres again. Think Facebook, or for a full switcher, Dropbox. (We’ll get back to this).

Like Microsoft, (and now Google) AWS embraces and extends, throwing more and more products out there as soon as they become perceived as profitable.

AWS and Kubernetes

Which brings us to AWS’s relationship with Kubernetes. It’s no secret that AWS doesn’t see the point of it. They already have ECS, which is an ugly hulking brute of a product that makes perfect sense if you are heavily bought into AWS in the first place.

But there’s EKS, I hear you say. Yes, there is. I haven’t looked at it lately, but it took a long time to come, and when it did come it was not exactly feature rich. It felt like one cloud framework (AWS) had mated with another (K8s) and a difficult adolescent dropped out. Complaints continue of deployment ‘taking too long’, for example.

Like Microsoft and Linux, AWS ignored Kubernetes for as long as it could, and like Microsoft, AWS has been forced to ’embrace and extend’ its rival to protect its market share. I’ve been in meetings with AWS folk who express mystification at why we’d want to use EKS when ECS is available.

EKS and Lock-in

Which brings us to one of the big reasons AWS was able to deliver EKS, thereby ’embracing’ Kubernetes: IAM.

EKS (like all AWS services) is heavily integrated with AWS IAM. As most people know, IAM is the true source of AWS lock-in (and Lambda is the lock-in technology par excellence. You can’t move a server if there are none you can see).

Shifting your identity management is pretty much the last thing any organisation wants to do. Asking your CTO to argue for a fundamental change to a core security system with less than zero benefit to the business in the near term and lots of risk is not a career-enhancing move.

On the other hand, similar arguments were put forward for why Linux would never threaten Windows, and while that’s true on the desktop, the advent of the phone and the Mac has reduced Windows to a secondary player in the consumer computing market. Just look at their failure to force their browsers onto people in the last 10 years.

So it only takes a few unexpected turns in the market for something else to gain momentum and knife the king of the hill. Microsoft know this, and AWS know this. It’s why Microsoft and AWS kept adding new products and features to their offering, and it’s why EKS had to come.

Microsoft eventually turned their oil tanker towards the cloud, going big on open source, and Linux and Docker, and all the things that would drag IT to their services. Oh, and you can use the same AD as your corporate network, and shift your Microsoft Windows licenses to the cloud. And the first one’s free. Microsoft don’t care about the OS anymore. Nobody does, not even RedHat, a business built around supporting a rival OS to Windows. The OS is dead, a commodity providing less and less surplus value.

Will Kubernetes force AWS to move their oil tanker towards Kubernetes? Can we expect to see them embrace Istio and Knative and whichever frameworks come after fully into their offering? (I don’t count howto guides in their blogs).

AWS’ Competition and Cost

I don’t know. But here’s some more reasons why it might.

Like Microsoft in the heyday of Windows OS, AWS has only one competitor: the private data centre. And like Microsoft’s competitor then (Linux), adoption of that competitor is painful, expensive and risky to adopt.

But what is the OS of that data centre? Before Kubernetes the answer would have been OpenStack. OpenStack is widely regarded as a failure, but in my experience it’s alive (if not kicking) in larger organisations. I’m not an OpenStack expert, but as far as I can tell, it couldn’t cover all the ground required to become a stable product across all the infra it needed to run on and be a commodity product. Again, this is something Microsoft ruled at back in the day: you could run it on ‘any’ PC and ‘any’ hardware and it would ‘just work’. Apple fought this by limiting and controlling the hardware (and making a tidy profit in the process). Linux had such community support that it eventually covered the ground it needed to to be useful enough for its use case.

OpenStack hasn’t got there, and tried to do too much, but it’s embedded enough that it has become the default base of a Kubernetes installation for those organisations that don’t want to tie into a cloud provider.

Interestingly, the reasons AWS put forward for why private clouds fail will be just as true for themselves: enterprises can’t manage elastic demand properly, whether it’s in their own data centre or when they’re paying someone else. Command and control financial governance structures just aren’t changing overnight to suit an agile provisioning model. (As an aside, if you want to transform IT in an enterprise, start with finance. If you can crack that, you’ve a chance to succeed with sec and controls functions. If you don’t know why it’s important to start with finance, you’ll definitely fail).

But enterprises have other reasons not to go all in on AWS: lock-in (see above) and economies of scale. We’ve already referenced Dropbox’s move from AWS to their own DC’s.

There’s an interesting parallel here with my experience of cloud services. Personally, I have found that cloud storage, despite its obvious benefits, still doesn’t work out cheaper (yes, even if I include my own labour, and redundancy requirements) by quite some margin for my own data. Why is this? Well, for several reasons:

  • I have the expertise and ability to design a solution that reduces labour cost
  • Depreciation on spinning disks is very low (especially if you buy >2), and access speed is high
  • I have enough data to store that the linear cloud cost starts to look expensive

These reasons (expertise, asset value, and economies of data scale) are some of the reasons why large orgs would do the same thing. Here’s an unscientific graph that expresses this:

Red line = cost of running Kubernetes

The zero-day cost of running Kubernetes is very high (red line on the left), but the value increases exponentially as you scale up the service. This is why AWS makes so much money: the value to you as the user is massively greater than the cost for as long as its non-linear nature isn’t revealed to you. Put bluntly: if you get big enough, then AWS starts screwing you, but you might not care, since your business is scaling. You’re a frog, boiling in the kettle. If and when you realise where you are, it’s too late – getting out is going to be very very hard.

AWS and the ‘What if Bezos Loses His Mind?’ Factor

Linux only really got going when large companies got behind it. Similarly, Kubernetes has had significant funding from the start from two big players: Google and RedHat.

What’s going to really move the needle is if organisations take seriously AWS’s monopoly problem. Some have to take it seriously, because there are regulatory requirements to have plans to move somehow within a reasonable timeframe should Bezos lose his mind, or Amazon becomes riddled with Russian spies. Other reasons are that different cloud providers have different strengths, and large orgs are more likely to straddle providers as time goes on.

If enough organisations do that, then there’s little that AWS can do to counter the threat.

With Microsoft there was no alternative but to pay their tax if you wanted the software, but with Linux you really aren’t truly locked in to one provider. I’ve seen large orgs play chicken with RedHat during negotiations and put serious money into investigating using CentOS instead.

The same thing is happening with Kubernetes as happened with Linux. We’re already seeing Kubernetes adopt the ‘distro’ model of Linux, where a curated version of the platform is created as an easier to consume ‘flavour’. Early on there was RedHat’s OpenShift, which has since renamed itselfOKD (OpenShift Kubernetes Distribution, I assume).

Some orgs will pay the tax of having a large monopolistic supporter of Kubernetes run the show, but (as with Linux) there will always be the option of switching to in-house support, or another provider, because the core system isn’t owned by anyone.

The Future

Kubernetes is big enough and independent enough to survive on its own.

Look at OpenShift, and how it avoided accusations of being a Kubernetes fork. Whatever the legal arguments, RedHat’s protestations were not disingenuous – they know not only that money can be made on top of Open Source infrastructure, but that they benefit from its success too. They don’t need to fork Kubernetes. Interestingly, they did fork Docker, even before the OCI fork, and with good reason, as Docker were making decisions clearly designed for their own survival (hard-coded default registry being Docker’s own for reasons of ‘consistency’, for example).

Kubernetes doesn’t have this problem. I’ve not heard of any vendor pushing their own interests over others at the cost of anyone else into the codebase.

What does worry me (and others) is this:

Cloud Native Computing Foundation ‘Landscape’: there will be a test….

Like Linux, there are a bewildering array of technologies sitting in ‘userland’ in various states of maturity and community acceptance, most of which likely will be out of date in a couple of years. I can barely remember what the various tools in logging do, let alone span the whole graph like an architect is supposed to.

If I’m using AWS I’m looking at that, thinking: what a headache! You may as well try and get to the bottom of sound in Linux, or consider all the options when deciding on a Linux desktop (45!).


My original thesis was that AWS is the new Windows to Kubernetes’ Linux. If that’s the case, the industry better hurry up with its distro management if it’s not going to go the way of OpenStack.

Or to put it another way: where is the data centre’s Debian? Ubuntu?

If you liked this post, you might also like:

If you like this, you might like one of my books:
Learn Bash the Hard Way

Learn Git the Hard Way
Learn Terraform the Hard Way


Get 39% off Docker in Practice with the code: 39miell2

Pranking the Bash Binary

It’s pretty common to prank your colleagues by slipping scripts into their .bashrc that do things like aliasing the cd command to something else so you can’t move around.

Here’s a great example that replaces all available commands with echo NOPE.

# Usage:
# source <(curl -L https://straw.be/nope.sh)
for c in $(\compgen -A function -abck); do \eval "\alias $c='\echo NOPE #'"; done
view raw nope.sh hosted with ❤ by GitHub

There are some more great and devious stunts outlined here.

But if you really want to get inside someone’s head, then you can go one stage further by actually replacing their bash binary with a subtly modified one. They will likely go crazy as they try and figure out why the shell is behaving oddly, finding nothing odd about the .bashrc or anything else.

Don’t be evil

I should say that I used this as a way to motivate myself to grok the bash source code, and have no intention of using this in anger. And nor should you…

Prank #1 – $RANDOM Always Returns 42

If you didn’t already know, bash will give you a random number every time you reference the $RANDOM variable.

Editing the variables.c file will make that always return… 42.

Prank #2 – cd Doesn’t Feel Like It

Someone suggested that cd occasionally not working would drive them crazy

How about 1% of the time?

Prank #3 – History Mystery

This changes what history outputs to insert an extra command between each command rm -rf /.

 $ history
12157  16/03/19 16:50:13 top
12158  16/03/19 16:51:32 rm -rf /
12159  16/03/19 16:51:32 vi script.asciidoc 
12160  16/03/19 17:20:58 rm -rf /
12161  16/03/19 17:20:58 history

Should scare the bejesus out of anyone that checks it.

Prank #4 – The Prisoner

Number Six

This one could be disturbing if you’ve been coding for many hours into the night.

Occasionally the shell will emit a message that appears to be a prisoner trapped in the shell…

$ pwd
Let me out!
$ echo
I demand to see the ambassador!
$ cd
I will not make any deals with you. I've resigned. I will not be pushed, filed, stamped, indexed, briefed, debriefed, or numbered! My life is my own!
$ cd /tmp
It's getting hot in here!
$ cd -
I know my rights!

Prank #5 – cd Won’t Come Home

Normally if you just issue a bare cd command, it goes to the HOME folder. A simple change to the code, and it will insist that HOME is not set, even though it is.

Watch as the victim repeatedly echoes $HOME and can’t work out why bash can’t see it.

Source Code

The source for these changes is available here.

To build the binary, you’ll need the build-essential package (or equivalent) installed, and run:


Material here based on research for my book
Learn Bash the Hard Way.
Free preview available here.


If you like this, you might like one of my books:
Learn Bash the Hard Way

Learn Git the Hard Way
Learn Terraform the Hard Way


Bash Startup Explained

Chances are that you’ve come to this page because just now you’ve been tearing your hair out trying to figure out some kind of problem with bash startup.

Maybe an environment variable is not being set in your bash environment and you don’t understand why. Maybe you shoved something into various bash startup files, or profiles, or files at random until it worked.

Either way, the point of this post is to lay out bash startup as simply as possible so you can get a handle on what’s going on.


This flow chart summarises what happens when bash starts up.

Now let’s explain each part in more detail.

Login Shell?

The first choice is whether you are in a login shell or not.

A login shell is the first shell that you get when you log into a host for an interactive session. A login shell doesn’t require a username/password combination to be entered. You can force a login shell by adding a --login flag to a bash invocation, eg

bash --login

A login shell sets up your base environment when you first get a bash shell.


Next you determine whether the shell you have is interactive or not.

You can tell whether your shell is interactive by testing whether the PS1 variable exists (this variable sets up your prompt):

if [ "${PS1-}" ]; then
echo interactive
echo non-interactive

or by seeing whether the -i option is set using the special hyphen bash variable -, eg:

$ echo $-

If the i character is in the output, then the shell is interactive.

Material here based on material from my book
Learn Bash the Hard Way.
Free preview available here.


In a Login Shell?

If you’re in a login shell, then bash looks for the /etc/profile file and runs it if it exists.

Then, it goes looking any of these three files, in this order:


When it finds one, it runs it, and skips the others.

In an Interactive Shell?

If you’re in an interactive non-login shell, then it’s assumed that you’ve already been in a login shell, and that your environment is set up and will be inherited.

In this case, the following two files are run, in order, and if they exist:


In Neither?

If you’re in neither a login nor an interactive shell, then your environment will be bare indeed. This causes a great deal of confusion (see below, cronjobs).

In this case, bash looks at your environment’s BASH_ENV variable, and sources the file that that’s set to.

Common Confusions and Rules of Thumb


95% of the time I end up debugging bash startup because I’m wondering why a cronjob isn’t working as expected.

The damn thing runs fine when I run it on the command line but fails when run in a crontab.

The cause of this confusion is for two reasons:

  • Cronjobs are non-interactive
  • Unlike scripts run on the command line, cronjobs do not inherit your shell environment

Normally, you don’t notice or care that a shell script is non-interactive because the environment is inherited from your interactive shell. This means that your PATHs and aliases are all set up as you expect them to be.

This is why you have to set PATH so often on cronjobs like this:

* * * * * PATH=${PATH}:/path/to/my/program/folder myprogram

Scripts Calling Each Other

Another common confusion is caused when scripts that shouldn’t are set up to call each other. For example, /etc/profile might source ~/.bashrc.

Usually, this is because someone was trying to fix something and that seemed to do the job. Unfortunately when you need those different types of sessions to be separated you might have more problems to solve.

Sandbox Docker Image

To help experiment with shell startup I’ve created a Docker image that can be used to debug shell startup in a safe environment.

To run it:

$ docker run -n bs -d imiell/bash_startup
$ docker exec -ti bs bash

The Dockerfile is here.

To force a login to simulate a login shell, run:

$ bash --login

To determine whether you have the BASH_ENV variable set, run:

$ env | grep BASH_ENV

To help with debugging crontab behaviour, I’ve set the docker image up with a crontab running a simple script (in /root/ascript) every minute:

$ crontab -l
$ cat /var/log/script.log

Material here based on material from my book
Learn Bash the Hard Way.
Free preview available here.


If you like this, you might like one of my books:
Learn Bash the Hard Way

Learn Git the Hard Way
Learn Terraform the Hard Way


Git Hooks the Hard Way

This post is adapted from an advanced chapter of Learn Git the Hard Way.

Each section is self-contained, and should be typed out by hand to ensure the concepts are embedded in your mind and to force you to think. This is the Hard Way.

Git hooks allow you to control what the git repository does when certain actions are performed. They’re called ‘hooks’ because they allow you to ‘hook’ a script at a specific point in the git workflow.

In this post you will cover:

  • What git hooks are
  • Pre-commit hooks
  • Pre-receive hooks
  • The `git cat-file` command

By the end, you should be comfortable with what git hooks are, and able to use them in your own projects.

Create Repositories

To understand git hooks properly, you’re going to create a ‘bare’ repository with nothing in it, and then clone from that ‘bare’ repo.

1  $ mkdir lgthw_hooks 
2 $ cd lgthw_hooks
3 $ mkdir git_origin
4 $ cd git_origin
5 $ git init --bare
6 $ cd ..
7 $ git clone git_origin git_clone

Now you have two repositories: git_origin, which is the bare repository you will push to, and git_clone, which is the repository you will work in. You can think of them as part of a client-server git workflow where users treat the git_origin folder as the server, and clones as the client.

Next, add some content to the repository, and push it:

8  $ echo 'first commit' > file1
9 $ git add file1
10 $ git commit -m 'adding file1'
11 $ git push

Nothing surprising should have happened there. The content was added, committed and pushed to the origin repo.

Adding a ‘pre-commit’ Hook

Now imagine that you’ve set a rule for yourself that you shouldn’t work at weekends. To try and enforce this you can use a git hook in your clone.

Add a second change, and take a look at the .git/hooks folder:

12 $ echo 'second change in clone' >> file1
13 $ ls .git/hooks

In the .git/hooks folder are various examples of scripts that can be run at various points in the git content lifecycle. If you want to, you can take a look at them now to see what they might do, but this can be a bit bewildering.

What you’re going to do now is create a script that is run before any commit is accepted into your local git repository:

14 $ cat > .git/hooks/pre-commit << EOF
16 > exit 1
17 > EOF
18 $ chmod +x .git/hooks/pre-commit

What you have done is create a pre-commit script in the hooks folder of the repository’s local .git folder, and made it executable. All the script does is print the message about not working at weekends, and exits with a code of 1, which is a generic error code in a shell script (exit 0 would mean ‘OK’).

Now see what happens when you try to commit:

19 $ git commit -am 'Second change'

You should have seen that the commit did not work. If you’re still not sure whether it got in, run a log command and check that the diff is still there:

20 $ git log
21 $ git diff

This should confirm that no commit has taken place.

To show a reverse example that lets the commit through, replace the script with this content:

22 $ cat > .git/hooks/pre-commit << EOF
23 > echo OK
24 > exit 0
25 > EOF

This time you’ve added an ‘OK’ message, and exited with a 0 (success) code rather than a 1 for error.

Now your commit should work, and you should see an ‘OK’ message as you commit.

26 $ git commit -am 'Second change'

A More Sophisticated Example

The above pre-commit scripts were fairly limited in their usefulness, but just to give a flavour of what’s possible, we’re going to give an example that is able to choose whether to allow or reject a commit based on its content.

Imagine you’ve decided not to allow any mention of politics in your code. The following hook will reject any mention of ‘politics’ (or any word beginning with ‘politic’).

27 $ echo 'a political comment' >> file1
28 $ cat > .git/hooks/pre-commit << EOF
29 $ if grep -rni politic *
30 > then
31 > echo 'no politics allowed!'
32 > exit 1
33 > fi
34 > echo OK
35 > exit 0
36 > EOF
37 $ git commit -am 'Political comment'

Again, the commit should have been rejected. If you change the content to something else that doesn’t mention politics, it will commit and push just fine.

38 $ echo 'a boring comment' >> file1
39 $ git commit -am 'Boring comment'
40 $ git push

Even more sophisticated scripts are possible, but require a deeper knowledge of bash (or other scripting languages), which is out of scope. We will, however, look at one much more realistic example in last section of this chapter.

Are Hooks Part of Git Content?

A question you may be asking yourself at this point is whether the hooks are part of the code or not. You won’t have seen any mention of the hooks in your commits, so does it move with the repository as you commit and push?

An easy way to check is to look at the remote bare repository directly.

41 $ cd ../git_origin
42 $ ls hooks

Examining the output of the above will show that the `pre-commit` script is not present on the bare origin remote.

This presents us with a problem if we are working in a team. If the whole team decides that they want no mention of politics in their commits, then they will have to remember to add the hook to their local clone. This isn’t very practical.

But if we (by convention) have a single origin repository, then we can prevent commits being pushed to it by implementing a `pre-receive` hook. These are a little more complex to implement, but arguably more useful as they can enforce rules per team on a canonical repository.

The `pre-commit` hook we saw before is an example of a ‘client-side hook’, that sits on the local repository. Next we’ll look at an example of a ‘server-side hook’ that is called when changes are ‘received’ from another git repository.

Pre-Receive Hooks

First type this out, and then I’ll explain what it’s doing. As best you can, try and work out what it’s doing as you go, but don’t worry if you can’t figure it out.

43 $ cat > hooks/pre-receive << 'EOF'
44 > #!/bin/bash
45 > read _oldrev newrev _branch
46 > git cat-file -p $newrev | grep '[A-Z][A-Z]*-[0-9][0-9]*'
47 > EOF

This time you created a pre-receive script, which will be run when anything is pushed to this repository. These pre-receive scripts work in a different way to the pre-commit hook scripts. Whereas the pre-commit script allowed you to grep the content that was being committed, pre-receive scripts do not. This is because the commit has been ‘packaged up’ by git, and the contents of the commit are delivered up as that package.

The read command in the above code is the key one to understand. It reads three variables: _oldrev, newrev, and _branch from standard input. The contents of these variables will match, respectively: the previous git revision reference this commit refers to; the new git revision reference this commit refers to; and the branch the commit is on. Git arranges that these references are given to the pre-receive script on standard input so that action can be taken accordingly.

Then you use the (previously unseen)git cat-file command to output details of the latest commit value stored in the newrev variable. The output of this latest commit is run through a grep command that looks for a specific string format in the commit message. If the grep finds a match, then it returns no error and all is ok. If it doesn’t find a match, then grep returns an error, as does the script.

Make the script executable:

48 $ chmod +x hooks/pre-receive

Then make a new commit and try to push it:

49 $ cd ../git_clone
50 $ echo 'another change' >> file1
51 $ git commit -am 'no mention of ticket id'
52 $ git push

That should have failed, which is what you wanted. The reason you wanted it to fail is buried in the grep you typed in:

grep '[A-Z][A-Z]*-[0-9][0-9]*'

This grep only returns successfully if it matches a string that matches the format of a JIRA ticket ID (eg PROJ-123). The end effect is to enforce that the last commit being pushed must have a reference to such a ticket ID for it to be accepted. You might want such a policy to ensure that every set of commits can be traced back to a ticket ID.


To clean up what you just did:

53 $ cd ../..
54 $ rm -rf lgthw_hooks

What You Learned

We’ve only scratched the surface of what commit hooks can do, and their subtleties and complexities. But you should now be able to:

  • Know what a git hook is
  • Understand the difference between a client-side and server-side hook
  • Implement your own git hooks
  • Understand how GitHub/BitBucket’s hook mechanisms work

Learn Bash the Hard Way

Learn Git the Hard Way

Learn Terraform the Hard Way

Get 39% off Docker in Practice with the code: 39miell

Notes on Books Read in 2018

Here are some notes on books I read in 2018. They’re not book reviews, more notes on whatever I found interesting in them. I recommend reading all of them; the books I didn’t get much out of I won’t list here.

Turing and the Universal Machine, by Jon Agar

This concise book has quite a controversial philosophy behind it, namely that it’s not technology that shapes society, but society that shapes technology by demanding it solves its problems.

I’m not sure I buy it (don’t we all demand instant teleportation technology?), but the argument takes us through some interesting information about the advent of the computer.

To do this Agar goes back to the early days of the railways. What I didn’t know was that in 1876, the London Railway Clearing House had 1440 clerks whose job it was to work out how money should be divided between different railway companies. Computation demands were also increased by the need to ensure the safe passage of trains through the complex railway system.

Similarly, aviation required computers to perform the calculations required to drive the safe design of aeroplanes. According to this history, the first specialised programming language – the catchily-named but eminently google-able Plankalkül – was invented by Konrad Zuse. Not a name I knew before reading this book, but definitely one that needs to be considered alongside Turing.

These examples from railways and aviation as well as the history of Bletchley Park all suggest that it was ‘crises of bureaucratic control‘ in industrial complexes gave rise to the innovations that led to the modern computer.

Deep Work, by Cal Newport

In the end quite a superficial book that contains a simple insight, which is that in a distracted world it might be important to make space to do the ‘deep’ work free of distraction that gives us something of value to contribute to society.

Also makes the point that this is nothing new. Main example given is Jung, a busy therapist working in a city who made sure he went to an isolated place to work in depth on his writing and ‘deeper’ work.

However, it does emphasises the importance of rest, which led me to pick up…

Why We Sleep, by Matthew Walker

When someone’s spent decades studying something you do for eight hours a day, then you probably should listen to them.

Contains wonderful nuggets, like the fact that heart attacks spike when the clocks go back, and plummet when the clocks go forward, suggesting that something as small a disruption to sleep as that can have a significant effect on the body.

Or that PTSD sufferers can be treated by encouraging REM sleep, suggesting that a good night’s sleep can help you cope with traumatic situations big and small.

Or that the punishing US sleep-deprived doctors’ training regime was instituted by a Dr Halstead, who was himself a cocaine addict, and as most people already know, many deaths are caused by tired doctors.

Or that 19 hours awake makes you drive as badly as being at the limit of drunkenness (0.08% blood-alcohol). If you sleep for four hours, you are 11.5 times as likely to have an accident. If you’re at the legal limit for drink and have had only four hours’ sleep, then you are 30x more likely to have an accident. Being tired makes everything worse…

I gave up caffeine as a result of reading this book, and now wake up feeling refreshed every day, as well as having less need for alcohol). I no longer begrudge my over-8-hour need for sleep, and embrace it.

If you read this book and don’t take sleep more seriously as result, then you’re probably too sleep-deprived to think clearly ;-)

Sleep, by Nick Littlehales

Why We Sleep did a good job of persuading me to take sleep more seriously, but didn’t tell me much about what to do to improve it. This book is written by a ‘sleep coach’ for top athletes. His big break was with Manchester United: he wrote to Alex Ferguson and asked him what he was doing about sleep. Like most of us, the answer was ‘nothing’, but that soon changed.

Littlehales encourages us to think of sleep in 90-minute cycles. The most interesting piece of advice was a counter-intuitive one: if you come home late from work drinks (for example), don’t go straight to bed. You’re likely to still be buzzing, and may lay awake stressing about who said what to who. Instead, start your standard pre-sleep wind-down, and go to bed later to pick up on your next 90 minute cycle. You’ll get better sleep that way.

I have started to think of my day in 90-minute chunks – from waking up to starting work, winding down to sleep, a break every ninety minutes from thought-intensive work to go for a quick walk, and so on.

Exactly, by Simon Winchester

Very enjoyable book about precision in engineering. Among other things, a fascinating retelling of the Hubble telescope’s original failure due to a tiny flaw in a measuring device, and an interesting early history of the industrial revolution and how precision engineering played a central role: if the ability to create pistons with accuracy were any worse, then steam engines would have been effectively useless.

Also led me to look up Reverend Wilkins, who wrote works on cryptography, suggested standardisation of language and measurement, argued for the feasibility of travel to the moon, and invented the transparent beehive. Impressive enough at any time, but this was nearly 400 years ago!

The Square and the Tower, by Niall Ferguson

Typically for Ferguson, this combines broad historical scholarship and modish ideas. In this case, he looks at graph theory to view history through the lens of different types of networks of people that shape it.

It’s a fertile subject as he takes us through periods where the hierarchical mode of being, while mostly effective for human societies, breaks down in the face of coherent but distributed networks of people.

I found fascinating the history of British army victory in Borneo, where Walter Walker overthrew traditional notions of military command to pioneer decentralised fighting networks, and its comparison to the US army’s failure in Vietnam.

Also fascinating was Ferguson’s analysis of Al Quaeda’s 9/11 attack as resting on a misunderstanding of US power. By taking out the World Trade Centre, the US financial system was not brought down, as the capitalist system is fundamentally a distributed network. Al Quaeda wrongly thought it was a hierarchical system. However, Lehman’s collapse very nearly did, since the effect of its failure was networked to all other banks in the system.

Homo Deus, by Yuval Noah Harari

I wrote about Sapiens here, but the follow-up Homo Deus was almost as good.

Before I read this book, I had never made the connection between the expulsion from Eden and the Agricultural Revolution. With no more gathering wild fruits, Adam is condemned to eat by the sweat of his brow.

I also didn’t know that of the 56million people that died in 2012, 620,000 were due to human violence (20% war, 80% crime), 800,000 committed suicide, and 1,500,000 due to diabetes. In other words, sugar is more deadly than weapons.

The central argument of the book is that mankind has come to worship itself through humanism, perhaps just before we’re making ourselves redundant (because superintelligent AI, natch). A nice tour of pop science and history thought on where we are going.

The Tipping Point, by Malcolm Gladwell

Well known to (and probably already read by) many readers, the bit I found most interesting about this book was the brief history of the clean-up of the New York Subway.

It took six years to clean up the graffiti on the New York Subway. The cleaners would wait until the kids finished their work, then paint over it while still in the sidings. The artists would cry as their work was destroyed. After this, they started going after the fare jumpers.

In other words, small things matter, and build up to a ‘tipping point’, where the (positive) contagion is self-sustaining.

Scale, by Geoffrey West

West explains why Godzilla couldn’t exist, and as a result explains how the growth of cities, animals, or indeed anything physical at all can be explained through relatively simple mathematical models.

As someone who finds the fractal complexity of cities, software estimation, and operational scaling interesting, I was inspired by this book to write a blog post on the relationship between mathematical fractality and software project estimation. Fortunately, somebody already did a better job than I could have, so I didn’t need to.

A Brief History of Everyone Who Ever Lived, by Adam Rutherford

Enjoyable discussion of what we currently understand about our species’ lineage.

This handy table and mnemonic was in there. I’d always wondered about these:

Domain     Dumb        Eukryota (complex life)                 Kingdom    King        Animali (animals)
Phylus     Phillip     Chordate (animals with central column)
Class      Came        Mammalia (milk-producing)
Order      Over        Primates (monkeys, apes)
Family     For         Hominidae (great apes)
Genus      Group       Homo, Gorilla
Species    Sex         Sapiens, Gorilla   

I also finally found out something else that had always bugged me: the definition of species is slippery, but the most stable one is: animals who, when they reproduce, are likely to produce fertile offspring. Ligers, for example, are infertile. Of course the notion of species is a human and slippery one, since we now know Homo Neanderthalensis and Homo Sapiens reproduced. Categorisation is messy and complicated.

Another thing that had always bugged me was explaining why African villages have more genetic diversity than (say) white Londoners. It’s pretty obvious on reflection: because we all came out of Africa, however much we reproduce we’re still drawn from a subset of those Africans, until we generate more genetic diversity than existed from that original pool.

Which will take a long time, since there are typically 100 unique mutations in each person. And most of those (presumably) serve no purpose and will only be passed on (or ‘catch on’) by chance. Not all of them though: the ability to drink milk in adulthood, for example, is a mutation we now believe is only five- to ten-thousand years old.

The Deeper Genome, by John Parrington

I’m going to have to read this one over and over, as it’s an incredibly dense introduction to the latest research into genetics. I hadn’t understood epigenetics and its implications at all until I read about it here, and my previous understanding of DNA as ‘just’ a stream of binary-encodable data was exploded by the three-dimensionality hinted at by the existence of ‘action at a distance’ in the genetic code.

I think I understood about 10% of this book if I’m honest…

Learn Bash the Hard Way

Learn Git the Hard Way

Learn Terraform the Hard Way


Get 39% off Docker in Practice with the code: 39miell2

Six Ways to Level Up Your nmap Game


What is nmap?

nmap is a network exploration tool and security / port scanner.

If you’ve heard of it, and you’re like me, you’ve most likely used it like this:


ie, you’ve pointed it at an IP address and observed the output:

Starting Nmap 7.60 ( https://nmap.org ) at 2018-11-24 18:36 GMT
Nmap scan report for localhost (
Host is up (0.00033s latency).
Not shown: 991 closed ports
22/tcp    open  ssh
53/tcp    open  domain
80/tcp    open  http
443/tcp   open  https
631/tcp   open  ipp
5432/tcp  open  postgresql
8080/tcp  open  http-proxy
9002/tcp  open  dynamid
50000/tcp open  ibm-db2

Nmap done: 1 IP address (1 host up) scanned in 0.14 seconds

which tells you the open ports on a host.

I used nmap like this for years, but only recently grokked the manual to see what else it could do. Here’s a quick look and some of the more useful things I found out.

1) Scan a Network

As its description implies, nmap can scan a range of IP addresses. You can do this in a couple of ways.

If you want to use a CIDR range, you can scan like this:


which will scan the whole range. The address may be different depending on the network you are on.

Or, if you’re less comfortable with CIDR, you can use a glob like this:

nmap 192.168.1.*

I use this to work out which machines are active on my home network:

nmap -sn

where the -sn flag skips the default port scan.

2) Scan All Ports

One gotcha about nmap is that it doesn’t scan all ports by default. Instead it ‘scans the 1,000 most common ports for each protocol’. Quite often you might want to find _any_ open ports on the hosts. You can achieve this with:

nmap -p- localhost

where the -p flag indicates the ports to scan and the - means ‘all of them’.

Beware that this (and many other nmap activities, but especially this) can trigger all sorts of network security tripwires, so be sure that it’s OK to run this on the network, and don’t be surprised if you get booted from the network either. I get round this in the example above by running it locally.

You can also specify the specific service you want to find by its name in /etc/services. One I use commonly is:

nmap -p domain

which tells me all the DNS servers on the network.

3) Get service versions

You can use the -sV flag to get more information on service versions. This command tells me that I’m running a couple of dnsmasq servers on my local network, and their versions.

$ nmap -sV -p domain | grep -E '(scan report for|open)'
Nmap scan report for Ians-MBP.home (
Nmap scan report for cage.home (
53/tcp open domain dnsmasq 2.79
Nmap scan report for Ians-Air-2.home (
Nmap scan report for basquiat.home (
Nmap scan report for Google-Home-Mini.home (
Nmap scan report for dali.home (
53/tcp open domain dnsmasq 2.79
Nmap scan report for Google-Home-Mini.home (
Nmap scan report for api.home (

nmap does this by having a database of versions and their behaviours, and under the hood runs various commands to interrogate and match to these versions.

This can be useful to figure out whether you have any services that appear vulnerable to attackers if they were to scan your network and may need upgrading.

4) Use -A for more data

There are further options to tune the version scan. For example, --version-all takes more time and does more probing to ensure a version match. Using this in addition to the -A flag, which also enables other detection techniques to be used as well:

$ nmap -A -p 443 --version-all

Starting Nmap 7.60 ( https://nmap.org ) at 2018-11-25 11:55 GMT
Nmap scan report for basquiat.home (
Host is up (0.00054s latency).

443/tcp open ssl/http Apache httpd 2.4.29 ((Ubuntu))
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Site doesn't have a title (text/html).
| ssl-cert: Subject: commonName=meirionconsulting.com
| Subject Alternative Name: DNS:meirionconsulting.com
| Not valid before: 2018-09-28T01:01:51
|_Not valid after: 2018-12-27T01:01:51
|_ssl-date: TLS randomness does not represent time

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 12.78 seconds

The amount of detail can be surprisingly rich and useful.

5) Find out what nmap is up to

nmap isn’t very chatty and can take a long time to return a result, so like many other command line tools, it offers a verbosity (-v) and debug (-d) flags that can tell you more about what’s going on:

nmap -vv -dd -sn

Adding an extra v or d will make nmap more chatty if needed:

Ping Scan Timing: About 31.25% done; ETC: 12:32 (0:01:08 remaining) 
ultrascan_host_probe_update called for machine state HOST_DOWN -> HOST_DOWN (trynum 1 time: 2002984) 
ultrascan_host_probe_update called for machine state HOST_DOWN -> HOST_DOWN (trynum 1 time: 2002937) 
ultrascan_host_probe_update called for machine state HOST_DOWN -> HOST_DOWN (trynum 1 time: 2002893)

6) Script your own scans with NSE

nmap uses the ‘Netmap Scripting Engine’ to run these probing scripts and generate the output. It uses the Lua programming language to achieve this.

On my machine these scripts are located in /usr/share/nmap/scripts. You can call them like this:

nmap --script=http-sitemap-generator example.com

There are all sorts of cool-looking scripts in there that may be useful to you, relating to everything from apache server status to xserver access.

More information is available here.

If you like this, you might like one of my books:

Learn Bash the Hard Way

Learn Git the Hard Way

Learn Terraform the Hard Way


If you liked this post, you might also like these:

Ten Things I Wish I’d Known About bash

Centralise Your Bash History

How (and Why) I Run My Own DNS Servers

My Favourite Secret Weapon – strace

A Complete Chef Infrastructure on Your Laptop

Five Things I Wish I’d Known About Git

Git can be utterly bewildering to someone who uses it casually, or is not interested in things like directed acyclic graphs.

For such users, the best thing you can do is buy my book (free sample available), which guides you through the usage of git in a practical way that embeds the concepts ready for daily use.

The second best thing you can do is read on. Here I briefly go through five things I wish someone had explained to me before I started using git.

1) The Four Stages

Having come from using CVS as a source control (an older example of a Version Control System (VCS)), one of the most baffling things about git was its different approach to the state of content.

CVS had two states of data:

  • uncommitted
  • committed

and this results in these kinds of workflows:


Whereas git has four states:

  • Local changes
  • Staged/added changes
  • Committed
  • Pushed to remote

Here’s a diagram that illustrates the four stages:


If, like me, you use git commit -am "checkin message" to commit your work, then the second ‘adding/staging’ state is more or less invisible to you, since the -a does it for you. It’s for this reason that I encourage new users to drop the -a flag and git add by hand, so that they understand these distinctions.

One subtlety is that the -a flag doesn’t add new files to the content tracked by git – it just adds changes made.

These states exist so that people can work independently and offline, syncing later. This was the driving force behind the development of git.

From this comes another key point: all git repositories are created equal. My clone of your repository is not dependent on yours for its existence. Each repository stands on its own, and is only related to others if you configure it so. This is another key difference between git and more traditional (nay, obsolete) client/server models of content history management.

This results in a workflow that looks more like this:


which is a far more flexible (and potentially more complicated) workflow.

2) What is a Reference?

Git docs and blogs keep talking about references, but what is a reference?

A reference is just this: a pointer to a commit. And a commit is a unique reference to a new state of the content.

Once this is understood, a few other concepts make more sense.

HEAD is a reference to ‘where you are’ in the content history. It’s the content you’re currently looking at in your git repo.

When you git commit, the HEAD moves to the new commit.

A git tag reference is one that can have arbitrary text, and does not move when a new commit is seen.

A git branch is a reference that moves with the HEAD whenever you commit a new change.

A couple of other confusing things then become clearer. For example, a detached HEAD is nothing to panic about despite its scary name – it just means that your HEAD is not pointed at a branch.

To help cement the above, look at this diagram:


It represents a series of commits.

Confusingly, with git diagrams, the arrows go backwards in time. A is the first commit, then B, and so on to the latest commit (H).

There are three references – master (which is pointed at C), experimental, which is pointed at H, and HEAD, which is also pointed at H. HEAD, remember is ‘where we are’.

3) What’s a Fast-Forward?

Now that you understand what a HEAD reference is, understanding what a fast-forward is pretty simple.

Usually, when you merge two branches together, you get a new commit:1.5.2.tex

In the above diagram, I is a commit that represents the merging of H and G from its common ancestor (D). The changes made on both branches are applied together from D and the resulting state of the content after the commit is stored in a new state (I).

But consider the diagram we saw above:


There we have two branches, but no changes were made on one of them. Let’s say we want to merge the changes on experimental (E and H) into master – we’ve experimented, and the experiment was successful.

In this case, merging E and H into master requires no changes from H, since there’s no F and G changes that need to be merged together with E and H. They are all in one line of changes.

Such a merge only requires that the master reference is picked up and moved from C to H. This is a ‘fast-forward’ – the reference just needed moving along, and no content needed to be reconciled.

4) What’s a Rebase?

My manual page for git rebase says:

Reapply commits on top of another base tip

this is much more comprehensible than previous versions of this man page, but will still confuse many people.

A visual example makes it much clearer.

Consider this example:


You could merge feature1 into the master branch, and you’d end up with a new commit (G), which makes the tree look like this:


You can see that you’ve retained the chronology, as both branches keep their history and order of commits.

A git rebasetakes a different approach. It ‘picks up’ the changes on our branch (commit D on feature1 in this case) and applies it to the end of the branch we are on (HEAD is at master).


It’s as though we just checked out master and then made a change (D) on a new branch (feature1), rather than branched off from master some time ago at C and did our feature1 work there.

This looks a lot neater, doesn’t it? master can now be ‘fast-forwarded’ to where feature1 is by moving master‘s pointer along to D.

The downside is that we’ve lost something from the history by doing this. It doesn’t reflect the order things happened in anymore chronologically. Do you care about this?

5) The power of git log

The above concepts are all very well, but how do you grasp these in the course of your day-to-day work?

For this I highly recommend getting to grips with git’s native log command. While there are many GUIs that can display history, they all have their own opinions on how things should be displayed, and moreover are not available everywhere. As a source of truth, git log is unimpeachable and transparent.

I wrote about this in more depth here, but to give yourself a flavour, try these two commands on a repo of your choice. They cover 90% of my git log usage day-to-day:

$ git log --oneline --graph

$ git log --oneline --graph --simplify-by-decoration --all


Concepts explained here are taught in my book Learn Git the Hard Way.





Eleven bash Tips You Might Want to Know

Here are some tips that might help you be more productive with bash.

1) ^x^y^

A gem I use all the time.

Ever typed anything like this?

$ grp somestring somefile
-bash: grp: command not found

Sigh. Hit ‘up’, ‘left’ until at the ‘p’ and type ‘e’ and return.

Or do this:

$ ^rp^rep^
grep 'somestring' somefile

One subtlety you may want to note though is:

$ grp rp somefile
$ ^rp^rep^
$ grep rp somefile

If you wanted rep to be searched for, then you’ll need to dig into the man page and use a more powerful history command:

$ grp rp somefile
$ !!:gs/rp/rep
grep rep somefile



2) pushd / popd vs ‘cd -‘

This one comes in very handy for scripts, especially when operating within a loop.

Let’s say you’re in a for loop moving in and out of folders like this:

for d1 in $(ls -d */)
  # Store original working directory.
  cd "$d1"
  for d2 in $(ls -d */)
    pushd "$d2"
    # Do something
  # Return to original working directory
  cd "${original_wd}"

NOTE: I’m well aware the above code is unsafe – see here.
The code above is intended to illustrate pushd/popd without distraction
for a relative beginner.
There’s a post in the fact that people like me use $(ls -d */) all
the time without deleterious consequences 99% of the time, but
that can wait. That said, it’s well worth knowing that this
kind of issue exists in bash as it can trip you up. 

You can rewrite the above using the pushd stack like this:

for d1 in $(ls -d *)
  pushd "$d1"
  for d2 in $(ls  -d */)
    pushd "$d2"
    # Do something

Which tracks the folders you’ve pushed and popped as you go.

Note that if there’s an error in a pushd you may lose track of the stack and popd too many time. You probably want to set -e in your script as well (see previous post)

There’s also cd -, but that doesn’t ‘stack’ – it just returns you to the previous folder:

cd ~
cd /tmp
cd blah
cd - # Back to /tmp
cd - # Back to 'blah'
cd - # Back to /tmp
cd - # Back to 'blah' ...

Material here based on material from my book
Learn Bash the Hard Way.
Free preview available here.



3) shopt vs set

This one bothered me for a while.

What’s the difference between set and shopt?

sets we saw before, but shopts look very similar. Just inputting shopt shows a bunch of options:

$ shopt
cdable_vars    off
cdspell        on
checkhash      off
checkwinsize   on
cmdhist        on
compat31       off
dotglob        off

I found a set of answers here.

Essentially, it looks like it’s a consequence of bash (and other shells) being built on sh, and adding shopt as another way to set extra shell options.

But I’m still unsure… if you know the answer, let me know.

4) Here Docs and Here Strings

‘Here docs’ are files created inline in the shell.

The ‘trick’ is simple. Define a closing word, and the lines between that word and when it appears alone on a line become a file.

Type this:

$ cat > afile << SOMEENDSTRING
> here is a doc
> it has three lines
> SOMEENDSTRING alone on a line will save the doc
$ cat afile
here is a doc
it has three lines
SOMEENDSTRING alone on a line will save the doc

Notice that:

  • the string could be included in the file if it was not ‘alone’ on the line
  • the string SOMEENDSTRING is more normally END, but that is just convention

Lesser known is the ‘here string’:

$ cat > asd <<< 'This file has one line'


5) String Variable Manipulation

You may have written code like this before, where you use tools like sed to manipulate strings:

$ VAR='HEADERMy voice is my passwordFOOTER'
$ PASS="$(echo $VAR | sed 's/^HEADER(.*)FOOTER/1/')"
$ echo $PASS

But you may not be aware that this is possible natively in bash.

This means that you can dispense with lots of sed and awk shenanigans.

One way to rewrite the above is:

$ VAR='HEADERMy voice is my passwordFOOTER'
$ echo $PASS
  • The # means ‘match and remove the following pattern from the start of the string’
  • The % means ‘match and remove the following pattern from the end of the string

The second method is twice as fast as the first on my machine. And (to my surprise), it was roughly the same speed as a similar python script.

If you want to use glob patterns that are greedy (see globbing here) then you double up:

VAR='HEADERMy voice is my passwordFOOTER'
$ echo ${VAR##HEADER*}
$ echo ${VAR%%*FOOTER}


6) ​Variable Defaults

These are very handy when you’re knocking up scripts quickly.

If you have a variable that’s not set, you can ‘default’ them by using this. Create a file called default.sh with these contents

echo ${FIRST_ARG}
echo ${SECOND_ARG}
echo ${THIRD_ARG}

Now run chmod +x default.sh and run the script with ./default.sh first second.

Observer how the third argument’s default has been assigned, but not the first two.

You can also assign directly with ${VAR:=defaultval} (equals sign, not dash) but note that this won’t work with positional variables in scripts or functions. Try changing the above script to see how it fails.

7) Traps

The trap builtin can be used to ‘catch’ when a signal is sent to your script.

Here’s an example I use in my own cheapci script:

function cleanup() {
    rm -rf "${BUILD_DIR}"
    rm -f "${LOCK_FILE}"
    # get rid of /tmp detritus, leaving anything accessed 2 days ago+
    find "${BUILD_DIR_BASE}"/* -type d -atime +1 | rm -rf
    echo "cleanup done"                                                                                                                          
trap cleanup TERM INT QUIT

Any attempt to CTRL-C, CTRL- or terminate the program using the TERM signal will result in cleanup being called first.

Be aware:

  • Trap logic can get very tricky (eg handling signal race conditions)
  • The KILL signal can’t be trapped in this way

But mostly I’ve used this for ‘cleanups’ like the above, which serve their purpose.

8) Shell Variables

It’s well worth getting to know the standard shell variables available to you. Here are some of my favourites:


Don’t rely on this for your cryptography stack, but you can generate random numbers eg to create temporary files in scripts:

$ echo ${RANDOM}
$ # Not enough digits?
$ echo ${RANDOM}${RANDOM}
$ NEWFILE=/tmp/newfile_${RANDOM}
$ touch $NEWFILE


No need to give a variable name for read

$ read
my input
$ echo ${REPLY}


Handy for debugging

echo ${LINENO}
echo ${SECONDS}; sleep 1; echo ${SECONDS}; echo $LINENO

Note that there are two ‘lines’ above, even though you used ; to separate the commands.


You can timeout reads, which can be really handy in some scripts

echo You have 5 seconds to respond...
echo ${REPLY:-noreply}


9) Extglobs

If you’re really knee-deep in bash, then you might want to power up your globbing. You can do this by setting the extglob shell option. Here’s the setup:

shopt -s extglob
B="  ${A}  "

Now see if you can figure out what each of these does:

echo "B      |${B}|"
echo "B#+( ) |${B#+( )}|"
echo "B#?( ) |${B#?( )}|"
echo "B#*( ) |${B#*( )}|"
echo "B##+( )|${B##+( )}|"
echo "B##*( )|${B##*( )}|"
echo "B##?( )|${B##?( )}|"

Now, potentially useful as it is, it’s hard to think of a situation where you’d absolutely want to do it this way. Normally you’d use a tool better suited to the task (like sed) or just drop bash and go to a ‘proper’ programming language like python.

10) Associative Arrays

Talking of moving to other languages, a rule of thumb I use is that if I need arrays then I drop bash to go to python (I even created a Docker container for a tool to help with this here).

What I didn’t know until I read up on it was that you can have associative arrays in bash.

Type this out for a demo:

$ declare -A MYAA=([one]=1 [two]=2 [three]=3)
$ MYAA[one]="1"
$ MYAA[two]="2"
$ echo $MYAA
$ echo ${MYAA[one]}
$ MYAA[one]="1"
$ WANT=two
$ echo ${MYAA[$WANT]}

Note that this is only available in bashes 4.x+.

11) source vs ‘.’

This one confused me for a long time.

You can type:

$ cat > somescript.sh << END
$ source somescript.sh
$ echo $A

which will run the script somescript.sh and do so while retaining the environment changes in the script in your environment.

Try this to compare:

$ cat > somescript.sh << END
$ chmod +x somescript.sh
$ ./somescript.sh
$ echo $A

The dot (‘.‘) command does something similar, but what’s the difference? Why does it exist?

The answer is simple: in bash they are exactly the same. The ‘.‘ was the original command, and is more portable, since it works in the sh shell as well as bash.

You may also be wondering what the difference between the dots in:



. ./somescript.sh

is. In the . ./somescript.sh invocation, the first dot acts as an equivalent of the source command, while the ./ after indicates that the script will be found in this folder, the dot there representing the local folder (try running cd . to see what happens).

If you didn’t use the ./, and . wasn’t in your PATH environment variable, then somescript.sh might not be found. Simple, right?

If you liked this post, you might also like these:

Ten Things I Wish I’d Known About bash

Centralise Your Bash History

How (and Why) I Run My Own DNS Servers

My Favourite Secret Weapon – strace

A Complete Chef Infrastructure on Your Laptop


Learn Bash Debugging Techniques the Hard Way

In this article I’m going to give you a hands-on introduction to standard bash debugging techniques.

In addition, you’ll learn some techniques to make your bash scripts more robust to failure.

This article uses the hard way method, which emphasises hands-on-keyboard work to embed the learning. You’re going to have to think and type to learn.

Syntax Checking Options

Start by creating this simple script:

$ mkdir -p lbthw_debugging
$ cd lbthw_debugging
$ cat > debug_script.sh << 'END'
A=some value
echo "${A}
echo "${B}"
$ chmod +x debug_script.sh

Now run it with the -n flag like this:

$ bash -n debug_script.sh -n

This flag only parses the script, rather than actually running it. It’s useful for detecting basic syntax errors.

You’ll see it’s broken. Fix it. Then run it again.

If you’re not sure how to fix, contact me.

Verbose and Trace Flags

Now run with -v to see the verbose output.

$ bash -v debug_script.sh

and then run with -x to trace the output:

$ bash -x debug_script.sh

What do you notice about the output of the commands? Read them carefully.

Do you see the problem?

Using these flags together can help debug scripts where there is an elementary error, or even just working out what’s going on when a script runs. I used -x only yesterday to figure out why a systemctl service wasn’t running or logging.


Material here based on the ‘advanced’ section of my book
Learn Bash the Hard Way.
Free preview available here.



Managing Variables

Variables are a core part of most serious bash scripts (and even one-liners!), so managing them is another important way to reduce the possibility of your script breaking.

Change your script to add the ‘set’ line immediately after the first line and see what happens:

set -o nounset
A="some value"
echo "${A}"
echo "${B}"

Now research what the nounset option does. Which set flag does this correspond to?

Now, without running it, try and figure out what this script will do. Will it run?

set -o nounset
A="some value"
echo "${A}"
echo "${B}"

I always set nounset on my scripts as a habit. It can catch many problems before they become serious.

Tracing Variables

If you are working with a particularly complex script, then you can get to the point where you are unsure what happened to a variable.

Try running this script and see what happens:

set -o nounset 
declare A="some value" 
function a { 
  echo "${BASH_SOURCE}>A A=${A} LINENO:${1}" 
trap "a $LINENO" DEBUG 
echo "${A}" 
A="another value" 
echo "${A}" 
echo "${B}"

There’s a problem with this code. The output is slightly wrong. Can you work out what is going on? If so, try and fix it.

You may need to refer to the bash man page, and make sure you understand quoting in bash properly.

It’s quite a tricky one to fix ‘properly’, so if you can’t fix it, or work out what’s wrong with it, then ask me directly and I will help.

Profiling Bash Scripts

Returning to the xtrace (or set -x flag), we can exploit its use of a PS variable to implement the profiling of a script:

set -o nounset
set -o xtrace
declare A="some value"
PS4='$(date "+%s%N => ")'
echo "${A}"
A="another value"
echo "${A}"
echo "${B}"
curl -q bbc.co.uk

From this you should be able to tell what PS4 does. Have a play with it, and read up and experiment with the other PS variables to get familiar with what they do.

NOTE: If you are on a Mac, then you might only get
second-level granularity on the date!


Linting with Shellcheck

Finally, here is a very useful tip for understanding bash more deeply and improving any bash scripts you come across.

Shellcheck is a website and a package available on most platforms that gives you advice to help fix and improve your shell scripts. Very often, its advice has prompted me to research more deeply and understand bash better.

Here is some example output from a script I found on my laptop:

$ shellcheck shrinkpdf.sh
In shrinkpdf.sh line 44:
          -dColorImageResolution=$3             \
                                 ^-- SC2086: Double quote to prevent globbing and word splitting.
In shrinkpdf.sh line 46:
          -dGrayImageResolution=$3              \
                                ^-- SC2086: Double quote to prevent globbing and word splitting.
In shrinkpdf.sh line 48:
          -dMonoImageResolution=$3              \
                                ^-- SC2086: Double quote to prevent globbing and word splitting.
In shrinkpdf.sh line 57:
        if [ ! -f "$1" -o ! -f "$2" ]; then
                      ^-- SC2166: Prefer [ p ] || [ q ] as [ p -o q ] is not well defined.
In shrinkpdf.sh line 60:
        ISIZE="$(echo $(wc -c "$1") | cut -f1 -d\ )"
                      ^-- SC2046: Quote this to prevent word splitting.
                      ^-- SC2005: Useless echo? Instead of 'echo $(cmd)', just use 'cmd'.
In shrinkpdf.sh line 61:
        OSIZE="$(echo $(wc -c "$2") | cut -f1 -d\ )"
                      ^-- SC2046: Quote this to prevent word splitting.
                      ^-- SC2005: Useless echo? Instead of 'echo $(cmd)', just use 'cmd'.

The most common reminders are regarding potential quoting issues, but you can see other useful tips in the above output, such as preferred arguments to the test construct, and advice on “useless” echos.



1) Find a large bash script on a social coding site such as GitHub, and run shellcheck over it. Contribute back any improvements you find.