Learn Docker With My Newest Course

Dive into Docker takes you from "What is Docker?" to confidently applying Docker to your own projects. It's packed with best practices and examples. Start Learning Docker →

Replacing make with a Shell Script for Running Your Project's Tasks


make is a great tool but it has certain characteristics that make it unfriendly for making long commands easier to run.

Quick Jump: Demo Video

I’m a big fan of making things easier for myself, especially if I can do it in such a way that requires not making any compromises. If you’re using a Makefile as a glorified project specific aliases files then in my opinion a shell script suites that job better.

For example, running docker-compose exec web flask db reset --with-testdb is kind of long and it’s a type of command that I don’t want to run by hand all the time while developing my app.

You could use make to wrap this up into a make db-reset command and that would work no problem but what happens when you want to create a generic make flask command , such as being able to run make flask db reset --with-testdb, make flask routes or 8 other make flask <insert_command> commands?

Suddenly everything falls apart because make doesn’t support ${@} which means it can get a bit inconvenient to pass arguments into a make task in a natural way.

You’d have to run make flask ARGS="routes" or ARGS="routes" make flask and then remember to add support for $(ARGS) to every task that uses ARGS.

Alternatively you could create separate make commands but this falls apart when you want to run arbitrary sub-commands of another command that may or may not have flags. You’d have to create a new command for every combo of sub-commands and flags.

But the above issues are easy to solve with a shell script.

In this video we’ll go over how to design a shell script where you can define functions that automatically become commands, and then you can create shortcuts like run flask db reset --with-testdb or run flask routes very easily. The script itself will end up being less lines than a Makefile and IMO even easier to add new tasks too.

We’ll also go over how to create namespaced commands like run ci:test and how to define private functions that don’t get exposed as commands.

It’ll also work in any environment that can run shell scripts (native Linux / macOS / WSL on Windows) and it won’t require installing any dependencies since you already have a shell available (sh, bash, etc.).

Technically you could even use Python, Ruby or other scripting language instead of Bash. It’s up to you.

Demo Video


  • 0:49 – A basic Makefile, using .PHONY and controlling output
  • 5:12 – The main reason why I switched from a Makefile to a shell script
  • 7:35 – Taking a look at a run script that replaces a Makefile
  • 8:14 – A quick comparison between a shell function and make task
  • 9:19 – Using $@ to pass arguments from a command into the script
  • 10:27 – Creating shell functions without arguments
  • 11:07 – Namespacing shell functions with a colon to group up commands
  • 11:31 – Creating private functions to use as helpers in public commands
  • 13:14 – A couple of useful functions you might want in a web application
  • 14:51 – Figuring out this shell script pattern from a Taskfile project on GitHub
  • 15:34 – Creating a run alias so you don’t need to type ./ every time
  • 16:24 – Adding in the idea of having private functions
  • 16:54 – Auto generating a help menu with printf, compgen, grep and cat
  • 20:34 – Using time or eval to convert arguments into runnable functions
  • 22:48 – Any questions? I’m happy to answer them in the comments below

What do you think about this pattern, will you start using it? Let me know below.

Never Miss a Tip, Trick or Tutorial

Like you, I'm super protective of my inbox, so don't worry about getting spammed. You can expect a few emails per month (at most), and you can 1-click unsubscribe at any time. See what else you'll get too.