Additional Shell Resources

tl;dr I've learned about a couple of additional resources since writing Stronger Shell, and I wanted to call them out. I've also updated the original post.

Thanks to comments around the web and a little keener attention elsewhere, I've come across a few other good resources that I think are worth sharing. I'll add these to the original post as well, but if you already read that, I wanted to call them out specifically:

  • Unix for the Beginning Mage (pdf) is a tutorial on the basics of shell work done as a story about learning magic. It's delightful, free, and reminds me of the why's (poignant) guide to ruby (which I credit with getting me started in ruby and subsequently web development), but more practical and less overtly weird. Thanks roneesh for pointing it out.
  • Bash (and other shells) has useful options that may make catching errors easier — for example, set -o errexit will "Exit immediately if a command exits with a non-zero status," which can be helpful in testing (and in real scripts). For the full list of options, see the set help page in bash or see the the options index for zsh. Thanks to bdunbar for the reminder.
  • For contextual help at the commandline itself, there's a neat python app: search cmd. It lets you do things like searchcmd find "sort files by size" and will return relevant examples from places like stack overflow. Being able to search without leaving the terminal (and see the results summarized) nicely helps keep context, which is key for feeling productive. Thanks to Pycoders Weekly, which is a generally useful resource.
  • If you're looking for more examples of interesting people things do in their shell, commandlinefu has tons. Like any large repository, the quality is variable, but there's definitely lots of interesting snippets in there that can help you expand how you think about your shell.

Stronger Shell

tl;dr Shell scripting is strange and somewhat forbidding, but will serve you better than most other frameworks or languages you can learn (after your first). I'll explain how I improved and offer resources to improve yourself.


Part of why I love programming is being able to automate repetitive tasks. That's pretty common for programmers; as the quip goes, I'll happily spend 99 hours to automated a task I could do by manually in 100. Strangely though, until I'd been programming professionally for a more than a few years, I was fine typing out many shell commands in a row every day to do repeated tasks.

As my jobs got more complicated though, my environment also did, and it became infeasible to remember everything that I needed to do in the terminal (so many server names to remember!) and too time consuming to keep searching my history all the time. Since scripting my simpler, day-to-day tasks in python wasn't really attractive, I decided to actively work to improve my shell scripting. This has paid off repeatedly.

Forcing Improvement

The trouble for me with improving at shell was bothering to start; although I knew eventually I'd get the time back, the first several scripts were going to take much longer than they'd ever save me.

Image Credit Randall Munroe /

Image Credit Randall Munroe /

Eventually, I decided to follow a rule that my friend Adam Hutton had mentioned: the second time you type a complex command in your terminal, make an alias. Aliases are quick to create and obviously save typing.

As soon as you start trying to do that though, you realize aliases, while wonderful, are pretty limited, and you stumble into shell functions and small scripts. Then, as a programmer, you are going to want flow control and variables, and that thankfully should start you hurtling through your own obsession to actually learning your shell thoroughly.

As my shell skills have improved, I've steadily widened the bar for what I will turn into a function/script/alias — I still apply the "if I type it twice, automate," but the scope of what "if I type it twice" has expanded considerably, because with scripts and functions, you can have arguments, and so automate nearly everything.

Resources for learning

Actually learning did not follow the path I expected; at first, it was hard to find recent books on bash and zsh at the right level for where I was, and shell scripting has quirks that didn't match my expectations, coming from Python/C++/Javascript. I eventually found a number of websites and books though that helped a ton, and collected a few tips below that would have sped me up considerably.

Online Resources

  • My first, favorite resource is the BashGuide wiki pages on, which is currently being updated to be a fuller tutorial at
  • Joshua Levy's "The Art of Commandline" is a good, quick introduction that will get you moving
  • Explain Shell is a terrific tool for pasting complex commands and seeing what is going on with them
  • Of course, you can read man bash, which, although turgid, is full of useful wisdom. While you're reading the manual, you'll also want to read man test so that you finally understand what [ -f ~/.bashrc ] means.
  • Unix for the Beginning Mage (pdf) is a tutorial on the basics of shell work done as a story about learning magic. It's delightful, free, and reminds me of the why's (poignant) guide to ruby (which I credit with getting me started in ruby and subsequently web development), but more practical and less overtly weird. Thanks roneesh for pointing it out.
  • For contextual help at the commandline itself, there's a neat python app: search cmd. It lets you do things like searchcmd find "sort files by size" and will return relevant examples from places like stack overflow. Being able to search without leaving the terminal (and see the results summarized) nicely helps keep context, which is key for feeling productive. Thanks to Pycoders Weekly, which is a generally useful resource.
  • If you're looking for more examples of interesting people things do in their shell, commandlinefu has tons. Like any large repository, the quality is variable, but there's definitely lots of interesting snippets in there that can help you expand how you think about your shell.
  • For ZSH (which I love), there's a great bunch of examples and capabilities explained in the ZSH Lovers man page. Additionally, Nacho Caballero's Master Your Z Shell with These Outrageously Useful Tips really expanded my mind as to what was possible in ZSH, and I'm grateful that he wrote it. Via Pycoders Weekly.
  • Finally, your nearest linux or mac computer are FULL of shell scripts, and once you get past reading things like [[ -z $(ssh -T host "exit" 2> /dev/null) ]], you can learn a lot from those scripts. A good habit is to read the scripts that libraries tell you to curl and pipe to sh, like any of the scripts on curlpipe (since you don't want to just run some random shell script from the Internet without knowing what it's going to do, right?).

The other thing that may not be immediately obvious is that bash/zsh/whatever-shell-you-like is your language, and the rest of the POSIX tools are your standard library. With that in mind, it'll definitely be worth your time to pick up even a little grep, tar, awk, sed, curl, head, tail, ssh, and friends.

The DigitalOcean community has actually done a really nice job writing tutorials for awk, sed and a a fair number of other tools. For awk specifically, I'm a fan of the Grymoire awk tutorial which substantially demystified it for me.

If style matters to you (PEP8 has made it matter to me everywhere), the bash hackers wiki style guide is a helpful resource so that your scripts look fluent and avoid obvious errors. Similarly, you should setup a linter, like a linter like shellchck.


No Starch Press has several excellent books for the aspiring shell user. My favorite is The Linux Command Line (affiliate link here and below, thanks!), which gives you a gloriously thorough and well-organized overview of many of the relevant shell commands you might need and, once you have those under your belt, shell scripting. If you're a bit farther along, some chapters may be too basic, but if you're like me, there are probably surprising holes in what you know that the book will fill in nicely.

Along similar lines to The Linux Commandline is Linux Command Line and Shell Scripting Bible, which is well-loved and thorough.

If you are just looking for shell scripting specifically, and you also have decided ZSH is awesome, Oliver Kiddle's From Bash to Z Shell is old but still useful; I refer to it from time to time when picking up new bits of ZSH still.

Finally, as you get more into this stuff, I highly recommend Michael W. Lucas's SSH Mastery (and honestly, any of his other sys admin books); if you're like me, you're probably spending a substantial portion of your shell time SSH'd into a remote host, and it's worth your time to really understand how SSH works and is configured.

Things that I wish I'd known

There are a couple things that I did not infer when I was initially trying to learn Bash from reading examples and experimenting, and I hope they speed someone else up in the future. They are also basically impossible to google, since they are mostly punctuation. Most can probably be picked up with a linter like shellchck, but I didn't have that setup at first either, so let my difficulty be your gain:

  • Bash is whitespace sensitive in a way that's... unexpected. You cannot have spaces around the operator in assignments (so export FOO = "bar" won't work, but export FOO="bar" is correct). You must have spaces inside your test expressions (so [[-z $FOO]] won't work; [[ -z $FOO ]] is correct).
  • There are a number of constructions that are nearly functionally equivalent. The ones that most tripped me up are:
    • [ expression ] and [[ expression ]] have the same function, and in general, if you're writing bash scripts, you should just use the [[ expression ]] version; the single [] version is older and doesn't support as many operations. And, to keep things optimally confusing, [] is the same as the test builtin. See the BashFAQ for the gritty details.
    • Single ticks (`command`) and $(command) are functionally equivalent, but in bash prefer $(command)the BashFAQ explains why.
    • Variable substitution can be done as either $VARNAME or ${VARNAME} — generally I do the latter, but you'll see both.
    • source and . do the same thing, which is execute the sourced file in the context of your current shell (so it can set environment variables in your interactive environment and whatnot).
  • Parens aren't exactly obvious either. (command) runs the command in a subshell (for uses, see the bash guide, or if you're impatient, just remember $(command) gets you the output of a command, so the command has to be running in those parens, right?). However, ((1+2)) does math (and $((1+1)) gets you the result of math). Having parens do something other than group expressions/override precedence was not something I expected.
  • Like our dear friend javascript, variables are global by default. In a function, to scope the variable to the function use the local keyword.
  • Bash (and other shells) has useful options that may make catching errors easier — for example, set -o errexit will "Exit immediately if a command exits with a non-zero status," which can be helpful in testing (and in real scripts). For the full list of options, see the set help page in bash or see the the options index for zsh. Thanks to bdunbar for the reminder.
  • If you switch between linux (GNU) and mac (BSD), many of your basic shell commands will differ; you almost certainly cannot copy some awk magic from stack overflow and expect it to work on both.

Where to go Next

The most important thing to do is write more shell code. The best way to learn is to do, and so, do! If you haven't yet, also pickup one of the great editors, either vim or emacs (or, my personal favorite, both via spacemacs) — once you're spending more time in your terminal, you'll likely want to be able to quickly pop into files to edit them, and it's nice to be able to do so right in the terminal.

Finally, make your shell environment nice. Spend some time with awesome shell (if you use bash) or awesome zsh plugins for ZSH users. Get a decent looking theme for vim. Setup nice completions for commands, and care for your dotfiles by versioning them. Spend a few hours making your environment pleasant and you'll be rewarded for years by that work.


Version-controlling my dotfiles

tl;dr: There are lots of neat ways to version-control your dotfiles. I ended up with homeshick and myrepos.


One of my goals for the last year has been to get more competent with my shell. I've used oh-my-zsh for years and found it helpful, but I'd never really bothered to learn shell-scripting or much about how my shell actually works. I've remedied that a bit (which I'll write about separately), and it's meant that my shell dotfiles have become a lot more useful to me.

Once I started to have a real investment of time in my shell and other command-line configs, I started wanting my usual tools -- version control, history, etc. I also wanted to be able to keep various computers in sync; I have a personal macbook where a lot of my configs originate, then my work computer, then various servers I log into to work periodically.

I'd gotten to a similar place a few years ago when I worked at US News, and I'd followed Brandon Rhodes's method for using git to track your dotfiles; however, my needs have changed, so I decided to see what other people do.


Since I'm doing a good bit of development at home and at work, and I want to share some settings but keep others private to their respective environments. Brandon's method of using a single git repo for all my dotfiles has a lot of benefits, but it doesn't lend itself to this particular setup; I want the work stuff in a private account and my personal stuff in my public github repo.

I also wanted to be able to maximize sharing, which means not requiring that a person take my whole setup to use part. For example, I found Byron Peebles's dotvim really helpful in starting to use vim, and I wanted to be able to share my various settings with colleagues and friends. However, I expect that not everyone is going to want to use my many idiosyncratic aliases and utilities. This means making my dotfiles modular, which is a little tricky --- it's not totally weird to want two separate repos that both end up putting files in .vim; however, doing that with git out of the box isn't really obvious.

For myself, I wanted a reasonably braindead way to bootstrap new machines and keep them in sync. It's not too unusual that I'll be doing a lot of work on a server for a few weeks and then stop touching it for months, only to login again and be frustrated that I've evolved how I work on my laptop and can't use similar shortcuts on the new box. Similarly, I tend to "clean home" every few months and then just work with what I've got for a while, touching-up the edges for a year or two before doing some serious remodeling. That means I need to be able to automate a lot of how my setup works so that it "just works" during the time I'm not focused on sharpening my tools.

The tricky part is that adding modularity makes the bootstraping/sync issue harder; I don't want to have to remember which 10 git repos to pulldown or update on a given box. I played a little with using submodules, but as always happens to me, I ended up making a little mess of my submodules within a few days; this reminded me that that tool doesn't mesh well with how I work.

Finally, because where I can, I try to use the most-specific tool to manage dependencies. For example, in a python project, you could use git submodules and shell scripts to manage all your dependencies, but pip has always been a lot less annoying for me. Similarly, vundle in git handles more than just checking-out and updating git plugins.

Where I ended up

It turns out, there are a ton of tools for managing your dotfiles, and github has even curated them. Initially, I was really interested in rcs and the python dotfiles package. But with rcs, my modularity goals were tricky to think through, and with the python package, given that I want all my python packages in virtualenvs, and part of that setup depends on my dotfiles, I decided to try to keep things simpler if I could.

I ended up using a set of shell scripts called homeshick to manage the dotfiles themsleves and myrepos to manage the many git repos. Homeshick handles symlinking files from a gitrepo into your home. It depends on bash, which means I can just clone it and be on my way. Symlinking has its own downsides, but the approach homeshick takes allows you to have a bunch of repos that all link into the same directories in your home, which solves my modularity problems. Then, myrepos handles cloning and bootstrapping the several repos where I keep things. I can have separate configs for myrepos for work and home (and even have one include the other), which is really useful.

I use these utilities to bootstrap tool-specific package managers, like zgen for manage zsh plugins or vundle for vim plugins.

What's cool

Now, I can run mr --trust-all bootstrap in my home and have all my basic tooling setup. Not only does this pull my personal tools, but also external dependencies like homeshick or spacemacs (which I've been playing with). Because myrepos is well-designed, at work I can have a .mrconfig that includes my home one and extends it. This allows for the sort of modularity and layering that I wanted.

I was able to pretty easily separate out my personal base setup from work-specific settings and keep all of this in sync. I can run homeshick pull and mr pull and have my tooling updated.

Also, with automated-setups, I'm starting to think about ways to make my own tooling more useful; for example, since I can create more "layers" of setup, I might want to break my .vimrc into some basic settings I want absolutely everywhere (like, even on my router), and then all the plugins I like using for programming and writing. I've got to think on this more though, since it will add complexity that I need to remember later.

What's uncool

First, I'm using many tools; maybe too many. The domain-specific plugin managers are useful since they usually do more than just manage git repos, but they also mean thinking about a bunch of tools in a specific order if I want to make sure I'm using the same vim plugins everywhere. This means it's not braindead enough yet; for example, if I've added some new zsh plugins, it's not always even obvious to me which tools I should run in which order to make sure they're updated in a given environment. I should be able to solve this with a shell-script or two, but I haven't done so yet (I didn't think of it until writing this, so +1 blogging I guess).

Having so many repos also makes sharing somehow harder; it's hard to point people to any one specific place if they want to use my tools. This blog post is in part to help with that, but I probably need to write another once my setup has settled down a little more to give a better overview.

Also, the homeshick approach of using symbolic links for everything means it can be hard to tell at a glance if I have untracked configs sitting around in my home. This is another thing that I can almost certainly fix with some shell scripting, but I haven't yet, and it's another place I can trip. Then there's all the problems that come with symbolic links generally; part of why I picked homeshick is that they have documented how to work around some of these problems, but just reading the docs should give you an idea of the woes you're headed for with the symlinking approach.

Finally, ironically, homeshick itself makes it slightly harder to share my configs, since homeshick requires that all the configs be in a REPO/home layout, which I don't think other people necessarily want. But, outside of work (where I don't think people will really mind), I don't really expect people to "fork" my dotfiles as much as copy them and use them for their own purposes, in which case, a slightly-weird directory-layout isn't so bad. And, on the positive, it makes it obvious where to put docs, if I ever write any.

What's next

Foremost, I need to just live with this for a few more weeks; I'm pretty happy with how things are working, but only time will tell whether the complexity is really worth it.

In the meantime, I want to smooth out some of the "what's uncool" — the biggest things are

  • making it easy to get an environment totally updated without having to run a ton of commands
  • being able to see which dotfiles aren't currently tracked somewhere
  • coming up with some sort of documentation for the various "castles" (homeshick's name for a set of tracked files) I have tracked

Resources and my repos

As I worked through this, I found the following really helpful:

I've got the following repos available for now:

  • .zsh -- zsh plugins and customization. This deserves its own blog post, because there's a fair bit of work in here.
  • .vim -- a (now pretty distant) fork of Byron Peebles's vim config, with about a million plugins, because I sure do like plugins
  • .myrepos -- the home of everything else
  • .git -- basic git configs and Tim Pope's automation for generating ctags

I still need to get my screen and tmux configs and a few others in, but they'll come before long.

Working with Developers

If you have never developed software, the process can seem opaque and confusing. Understanding what motivates developers and what they sweat can help you achieve better results when you are collaborating with them.

Although I do a couple of different things at work, I spend the lion's share of my time as a web developer. I do not work at a software development company though, but instead as a part of a larger company that develops software to accomplish other ends. This means I regularly work with lots of people who have never developed software before (and maybe never wanted to). My goal here is to give non-developers a better sense of what developers do so that projects go more smoothly, cost less, and finish with higher quality. I feel like, for a person who has never written software, developers may be a little too mysterious, and so I hope to clear up some of the mystery here by explaining

  • what we love about developing software
  • what are a developer's inner conflicts that s/he is trying to balance against; these conflicts can tip your project from awesome to nightmare if not mitigated
  • what being a developer does to your brain; that is, where do developers fall down
  • what you can do to make your projects more successful when working with developers

Of course, I have a few caveats. This is heavily influenced by the specific setting of my work experience and the teams I've been on; I can't claim this post is universal. In writing this out, I've also come up with some improvements and projects for myself and my team, and in that respect, I recommend other developers try writing something similar up. Finally, as always, I speak only for myself here, and not for my employer or colleagues.

Why We Love Developing

Developing software is not like normal life activities; it allows you to translate your thoughts and needs into tangible devices that realize your intentions and solve your problems. Software developers, at least ones like me in small shops, are not disconnected from the product of their labor. In the software development classic The Myth of the Man-Month, Frederick P Brooks, Jr. explains

The programmer, like the poet, works only slightly removed from pure thought-stuff. He builds his castles in the air, from air, creating by exertion of the imagination. Few media of creation are so flexible, so easy to polish and rework, so readily capable of realizing grand conceptual structures. (Ch. 1)

And it is this, I think, that is so intoxicating. The day-to-day of software development is mentally taxing, often frustrating, and a little lonely. But the result is the ability to solve complicated problems almost out of thin air. Even better, the things we make are virtually tangible; we do not usually create processes and memos of understanding, but instead systems with interfaces and outputs that people actually interact with. Having a product that you can point to with capabilities that you can describe is strikingly satisfying.

Problem solving is important to the developers I know. We are not exactly artists (at least not all of us); we do not create things just to delight and evoke. We like to make things that make life easier, things that are used, things that people care about. We hate inefficiency and people doing tasks that machines can do, and we like making machines do repetitive tasks. We also like to learn something in the process; once you're hooked on developing software, there's a strong urge to get better at doing it (and to make it less frustrating, mentally taxing, and time consuming). 

What Are Developers' Inner Conflicts

Developers are, like all creative people, tormented by several sets of tradeoffs (or inner conflicts) and systemic problems in their craft. The most frustrating (and, paradoxically, enriching) can be pithily summarized:

  • Software is an asset, source code is a liability (thanks to Alan Cooper for the formulation)
  • Change is inevitable, and potentially very expensive
  • You have to use a computer to run a program
  • Adding people to a late project makes it later (Brooks's Law)

Software is an asset, source code is a liability

Developers work to create software. Software is the stuff you use on your computer: your web browser, word processor, favorite game, and websites like Twitter and Google. Software is clearly an asset; it makes your computer do valuable work and makes your life easier. To make software, we write source code (code for short) in a programming language, which is then translated eventually into software you can use. It's at this making software part that things go to hell.

The trouble is, every line of code we write is fraught with problems:

  • The more code, the more room for bugs; we translate fuzzy ideas into very concrete logic — sometimes, that logic is imperfect, especially when it is very complicated
  • The fastest, most secure code is the code you never wrote; the more complicated the code, the more room for performance and security problems
  • We're in our programs for the long hull — the amount of time we spend actually writing new code is small, compared to the time we spend maintaining existing programs; every new line is another line you need to understand 14 months from now
  • Writing code forces concrete decisions, many of which we make on the fly; every line is a potentially misunderstood intention, which we will later have to delete and rewrite
  • Code rots; we upgrade systems, add features, and fix bugs, any of which can break some older code that we haven't even touched

As such, we want the simplest design that will meet your needs. We love consistency; every special case means more code of questionable long-term value. (Special cases are the most expensive when someone is trying to understand code a few months or years later, or when we're trying to make a major change — like responding to a real-life customer need). When planning software, we want people to justify that the features they want will be used, because once we write the code, we have to live with how it affects the rest of our programs for as long as they're used. Worse, unused features can block us from quickly implementing new, actually useful ones.

Change is inevitable, and potentially very expensive

We know much of the code we initially write will be changed. We will imperfectly understand a client's requirements. The client will misunderstand much of his or her own needs until s/he sees them made concrete as software. The situation surrounding the software will change, requiring the software to pivot to meet reality. 

The trouble is, change is often expensive. There are two types of change: ones we planned for and ones we did not. With anticipated changes, we can typically make a change without disrupting the system; we've made ourselves places to configure the change, or ways to plug into the existing code without disrupting existing parts of the system. In unanticipated changes, most anything could happen — the change could be very simple, or it could require we rewrite 80% of the existing system.

An example could make this more concrete, using one of my favorite sites I've developed: DC Green Scene, a calendar of energy and environment events in DC. The calendar has a variety of filtering options, such as filtering by date, event type, and event topic. We anticipated the topics would evolve over time, and so we made it very simple to add new ones; adding and removing topics doesn't require a programmer. We actually capture a lot of information we could filter on (such as event location and cost), and adding these as options wouldn't be terribly complicated either.

However, imagine that we realized we were missing some critical variable in our filtering, such as the event size (to differentiate small meetings from conferences). This we did not anticipate, and it requires the following types of change (with the language required to make the change in parentheses):

  • Add the size field to the database, and figure out a way to add event-size data to the existing events in the database (SQL)
  • Add size filtering options to the code that actually executes the filters (Python)
  • Add the size information to the event's display on the homepage and on the event's page (HTML, CSS)
  • Add code to execute the size filtering dynamically (Javascript) 

So for this relatively simple change, we end up changing about seven files and using five different "languages." This doesn't count the design and usability work required to make the new feature work with the existing look. And that first change, where we need to collect new data about old events, is potentially a nightmare of busy work, or a confusing usability problem as we try to signal that size filter doesn't always work.

The natural question is, of course, why not make the system infinitely flexible, so that every change is simple? We certainly strive for simple, flexible systems, but often flexibility simply adds complexity to the code. A developer will sometimes say a system is over-engineering: this is when a system has been designed to be highly flexible in ways that no one will ever use. In such a system, simple tasks can become very complicated, because you use the same machinery for simple tasks as you would for very complex ones. Worse, you end up with lots of extra, unused code, which I'm sure you understand is undesirable after reading this far. 

Thankfully, there's a mitigation here: the earlier you realize you've made a mistake, the easier it is to fix the problem. When we first started writing DC Green Scene, we could easily add new fields to capture data and filter that data; there wasn't nearly as much code to upset at the beginning as there is now. Paying attention to mockups, prototypes, and early demos is essential; I know they usually look strange and it's hard to get past the interface, but seriously working with a prototype or half-functional product can save a ton of time down the road.

You have to use a computer to run a program

The essence of a developer's job is translate a human's ideas into instructions for a computer. Computers, however, are very unlike humans. Computers are terrible with assumptions. They cannot be given a vague picture of what you want with a little context and understand it. Everything must be described in minute detail. This means

  • every little detail requires a decision — what order things should be listed in, whether punctuation is bold or not, everything
  • when we tell the computer to do something stupid, it does exactly what we told it to

Sometimes, we use systems that provide functionality for us (e.g., when we use a content management system or some other customizable application). Unfortunately, this really just means that someone else made a lot of decisions for us, which might be great and might be totally wrong. In the end though, someone has to sweat the details, or they won't exist in the software at all.

Adding people to a late project makes it later

I used to hate estimating the timelines and costs of software projects. The trouble is, if you haven't written a type software before (e.g., someone asks you how long it would be to make a facebook competitor, but you've never written social networking software), it can be very tricky to have any clue where the pain points will be — and it's in the pain points that your project starts to slip. What always made this process more difficult is the realization that whatever we spec at the beginning will change, but deadlines typically won't.


From the outside, both of these problems look like they could be solved by simply bringing in additional staff. If we get behind due to some unexpected technical trouble, or if the client realizes they need some difficult-to-implement realtime prediction system, we'll just bring in some freelancers. Unfortunately, adding additional developers to a late project will just make it later. The logic here is that once you have a substantial amount of code, it can take longer to bring someone up to speed than it would to implement that person's features. The problem is so well understood, it even has a name and a wikipedia entry: Brooks's Law

Brooks's law has implications: the initial team has a huge effect on the final delivery of the software, and new features added later in the development process likely mean the removal of some other, not-yet-implemented features. If the scope of a project expands substantially beyond its initial description, it will likely be impossible to meet the initial deadline.

A bonus conflict: Writing code requires a ton of focus

Finally, there's a problem that I couldn't pithily summarize but that feels too important to leave out: writing code requires an excess of focus. The number of things you have to remember to write a web application is absurd; I once read programming is the art of balancing an impossible number of details long enough to get an idea into your text editor.

Writing complicated code requires a specific state of mind akin to writing a flowing narrative; you need focus that excludes most everything else, and if someone breaks that focus, it can take hours to get it back. Paul Graham writes about the implications of the state of mind required to write code in Maker's Schedule, Manager's Schedule:

Most powerful people are on the manager's schedule. ... But there's another way of using time that's common among people who make things, like programmers and writers. They generally prefer to use time in units of half a day at least. You can't write or program well in units of an hour. That's barely enough time to get started.

This means many little interruptions through a day or meetings dotting the morning or afternoon can cause sizable delays by making it hard to get the necessary concentration to implement large features.

For my team, there are additional scheduling considerations: We divide all our work into two phases, development mode and deployment mode:

  • During the development cycle, we tend to work more slowly and carefully, while at the same time taking bigger risks with the code. These bigger risks mean that we can make bigger changes that are actually maintainable.
  • When we're in deployment mode, we expect we're going to be making our software available generally very soon; thus, we are looking to break as little as possible and work as quickly as possible — so we cut corners and add quick and dirty fixes, which we hope to resolve during the next development cycle.

The trouble comes in when we're in contant deployment mode (because the schedule was unrealistically agressive, the scope crept, or the content or approvals didn't land). When that happens, if we're rushing and just piling features in without adequate planning because deployment is "any minute now," we may end up making a mess of our own codebase, making later features much more expensive.  

Where We Fall Down

There are a few areas where I have noticed my developer friends and I fall down, I think likely by the nature of our jobs. As every developer is different, any one might be able to combat some of these deficiencies, but they're worth looking out for as a non-developer.

  • It's easy for us to get the "new/shiny's," wherein we find neat new technologies that solve some problem we've recently had; unfortunately, this is how a two person development team ends up with eight products written in six entirely different languages — making it very hard to staff properly in the future
  • We have trouble keeping the really big picture in mind; developing is a lot of very detailed work; unfortunately, that doesn't leave a ton of room for keeping the master plan in mind, which makes it progressively harder to give high level feedback on projects as we get into them
  • We can't know for sure if what we've made meets your needs; only you know what you really had in mind, which means your attention and testing are crucial for knowing whether we've correctly implemented a project.
  • The last 10% of any feature is hideously boring: it turns out that the last 10% of a feature is usually figuring out all the little ways that the other 90% break or are hard to understand, and so is really tedious, time consuming, really important — and we might need a little push to get through it.
  • We over-rely on technological solutions: I know I often assume that we need to automate some process that no one expects or even wants to be automated; I assume people should do as little work as possible, even if it's very time consuming (and thereby expensive) to get the computer to do some simple piece of person work.

The valuable thing about realizing these weaknesses is that you can then combat them; knowing is, after all, half the battle. For example, at home I'm constantly playing with new applications, frameworks, and languages, but at work I'm pretty conservative about allowing new technologies into our stack; the more technologies you have to know, the less time you have to know any one of them well. 

What Can You Do To Help?

Armed with an understanding of what motivates developers, what makes them crazy, and where they're prone to weakness, you're hopefully brimming with productive changes you can make to improve the software products your company produces. Hopefully, you're thinking that going forward, you'll

  • think hard about what you want, finding good examples of similar products to clearly communicate your desires
  • think hard about the value of new features that you ask for during development, and think about which already-agreed-upon features you can live without to get your new features
  • sweat the details of projects together with your development team
  • pay close attention to prototypes and early demos to catch misunderstandings early
  • embrace technology — I'm not kidding; the more conversant you are with technology, the more likely you will be able to think about your problems in a concrete way that we can actually solve
  • remember that the most flexible system is not necessarily the best system
  • be realistic with your deadlines, knowing now that constant deployment leads to long-term panic