Embrace Bin Scripts

I recently watched Matt Stauffer’s talk “Patterns That Pay Off” from Laracon 2018. It’s packed with great advice that I’ve been bringing into my own projects.

One piece of advice I’d like to highlight is the use of project-based bin scripts.

Whats the problem?

Have you ever had a project that hasn’t seen active development in years but now needs a small update.

  • “Where do I get Bower from these days?”
  • “What is a PEAR package?”
  • “How do I deploy to Rackspace?”

That quick CSS fix has become a much bigger headache.

Or, a typical PHP Laravel project a local setup might involve these steps:

  1. Switch to PHP 7.4
  2. Install PHP dependencies via Composer
  3. Switch to Node v12.7
  4. Install frontend dependencies via NPM
  5. Start local PHP web server
  6. Start webpack build system
  7. Start a MySQL service
  8. Copy .env variables
  9. Run database migrations and seeders

Thats a lot of steps to remember or fit into your readme, and thats assuming the developer reads the readme at all!

Whats the solution?

What if all your projects had these steps encapsulated in a single bin/install.sh script.

Running the install script on a new project

This script could perform the installation steps and check for project requirements. It could also handle the nuances between macOS, Linux or Windows dev environments.

It doesn’t matter if you are working on a PHP, Node or static web app – you cd into the project and run ./bin/install.sh.

Once you’re ready to commit, run ./bin/precommit.sh to run your tests, static analysis, and formatting.

Finally, run ./bin/deploy.sh to build production assets, update release notes and deploy. How you deploy that particular project doesn’t matter. Docker? Digital Ocean? Heroku? Who cares!

What are the alternatives? Docker? Composer?

You might be thinking “this is the sort of thing Docker and CI solves!” or “can I not just use composer/npm scripts?”.

Docker can standardise your build steps – but not every project needs Docker. I have several projects where Docker would be overkill. For example, sites built using static site generators.

A Docker-based app can still make use of bin scripts. If only to wrap docker-compose up in the familiar bin/run.sh script.

Similarly, a PHP project may not even use Composer – such as a typical WordPress website.

By embracing bin scripts, it doesn’t matter what an individual project’s tech stack is. The interface to install, run and deploy is the same across all your projects.

Most importantly, shell scripts will run out of the box on macOS, Linux and Windows in almost all cases.

Show me the code!

I have a bin directory in my project root which contains my bin scripts. It looks something like this:

$ tree bin
bin
├── install
├── run
└── precommit

Each script is self-contained and includes everything it needs to run. Because of this, they’ll can work in many years from the first git clone.

For example, one of my PHP Laravel apps has the following bin/install.sh:

#!/usr/bin/env bash

# bin/install
# Install the application

command_exists() {
    command -v "[email protected]" > /dev/null 2>&1
}

if ! command_exists fnm; then
    echo "fnm is not installed"
    echo "Visit https://github.com/Schniz/fnm to install"
    exit
fi

if ! command_exists composer; then
    echo "composer is not installed"
    echo "Visit https://getcomposer.org/download/ to install"
    exit
fi

if ! command_exists yarn; then
    echo "yarn is not installed"
    echo "Visit https://yarnpkg.com/lang/en/docs/install/ to install"
    exit
fi

echo "Removing existing ./node_modules folder"
rm -rf ./node_modules
echo "Removing existing ./vendor folder"
rm -rf ./vendor

fnm use
composer install
yarn install

cp .env.example .env
php artisan key:generate
php artisan migrate:fresh --seed

The bin/run.sh script as as follows:

#!/usr/bin/env bash

# bin/run
# Run the local development environment

if [[ "$OSTYPE" == "linux"* ]]; then
    sudo service mysql start
fi

php -S 0.0.0.0:8000 -t public/ & yarn run watch

and finally, this is the bin/precommit.sh:

#!/usr/bin/env bash

# bin/precommit
# Tasks to run pre-comment

php ./vendor/bin/phpunit

The scripts do not need overcomplicating, and can simply be a list of commands. The bin/precommit.sh script only runs my test suite and nothing more.

Crucially, it holds up across all my projects spanning years the tech stacks of their time.

Laravel? WordPress? Static? No problem.

Nice!

Creating your own

I can offer some final words of encouragement, if you’re interested in creating your own.

  • Start small. Don’t worry about adding support for Windows environments if you only ever use macOS. Add as you need.
  • Not everything needs scripting. A printed message to direct the user could be enough.
  • Keep it simple. There’s a temptation to create abstractions from your scripts. Resist adding complexity, duplication is not always a bad thing.
  • Create aliases. I’m a big fan of aliases and saving a keystroke. Once you’ve settled on your script names, you can alias them for even more convenience.