Simple Bash Scripts for Lazy People | Part 2: Rails

This is part 2 of a five-part series:

  • Part One has examples for common daily tasks in Git.
  • Part Two, similar examples for Rails.
  • Part Three, miscellaneous cases.
  • Part Four dissects an example of a failed attempt at a useful script.
  • Part Five concludes with a brief discussion of when to use Bash as opposed to some other scripting language.

TL;DR for Part 2:

  • You don’t have to be a Bash or sed or awk magician to make useful scripts; even knowing only one feature of each of these tools gives you a great deal of power
  • basename and dirname are very handy
  • Prefer absolute paths to relative paths
  • Use one-liners to make something easy to remember for things hard to remember

Rails

rails-server

At Beezwax, devs might have three or four projects they’re actively working on in a given month. When stopping and starting lots of different Rails servers, sometimes I wind up with orphan Spring processes that might block starting the server or cause other problems (amusing: note in the change history for the linked github issue, the original title was “You’re breaking my laptop”). Or I might have a Rails server running on 3000 in another terminal that I forgot about.

So I made a script that quickly checks (and if desired cleans up) those processes before trying to start the Rails server.

This script is given in its entirety but broken up with my comments between sections.

#!/bin/bash

port="3000"
if [[ ! -z "$1" ]]; then
  port="$1"
fi

ip="127.0.0.1"
if [[ ! -z "$2" ]]; then
  ip="$2"
fi
  

This is straightforward — the script can be called with optional anonymous arguments for port and IP address with reasonable defaults.

while read -r -u 3 proc; do
  pid="$(echo "$proc" | awk '{print $2;}')"
  echo "$proc"
  echo -n "kill $pid? [y/N] "
  read -r killit
  if [[ "$killit" == 'y' ]]; then
    kill -9 "$pid"
  else
    echo "port in use"
    exit 1
  fi
done 3< <(lsof -n -P -i:"${port}" | grep LISTEN)
 

Line 24 redirects lsof output for any process listening on the requested port to file descriptor 3 which is read by the while loop.

Why file descriptor 3, I couldn’t say, but there was probably a good reason when I wrote the script; maybe some interaction with the loop internally using 1 and 2?

Inside the loop, it awks out the pid, echos the entire lsof line, and asks me if I want to kill that.

awk is a programming language, but the only thing I know how to do with it is print individual columns from output. Many of these examples could be quite simpler if I were an awk guru, and I’m okay with that — I’ll do the best I can with the tools I have, and learn new features as I need them (or, more likely, when a friend or workmate points something out).

If I respond to the prompt with 'y' it kills the process.

This is a dev script, not a production distribution. I don’t need to test for ‘Y’ or ‘yes’ etc., ‘y’ is fine. There’s an example of more complete testing of input in Part 5.

On line 19, kill -9 is a little aggressive, but this is a dev environment; we can play fast and loose.

If I respond with a blank or enter anything other than 'y', I’m helpfully reminded that the port is in use and the script exits.

while read -r -u 3 proc; do
  pid="$(echo "$proc" | awk '{print $1;}')"
  echo "$proc"
  echo -n "kill $pid? [y/N] "
  read -r killit
  if [[ "$killit" == 'y' ]]; then
    kill -9 "$pid"
  fi
done 3< <(ps -a | grep spring | grep -v grep | sed -e 's/[0-9]*://')
 

Next we do the same thing for anything that looks like a Spring process.

The grep -v is so grep doesn’t match itself, and the bit hacked out with sed is the line number added to ps output because my grep is aliased to grep -n:

$ ps -a | grep tail
5:18536 ttys000 0:00.01 grep -n tail
8:18512 ttys001 0:00.15 tail -f log/error_log

You probably want to keep the grep -v grep but the substitution for the line numbers is likely just me.

TODO: refactor those loop bodies into a function (and maybe doing that would remove the need for the while loop to read on file descriptor 3?)

cmd="rails server --port=${port} --binding=${ip}"
echo "$cmd"
$cmd

And finally, try to start the rails server. I like to save commands I’m going to run as a string and output the content of that string. It makes debugging easier.

link-configs

If you’re like me, you every now and then screw up a local repository to the point you just need a fresh checkout. But if you’re doing things right, database.yml and secrets.yml don’t live in version control (please tell me they don’t live in version control!).

I have a directory at ~/dev/beezwax/configs where I store these instead. When I start a new project, I put those files there as ~/dev/beezwax/configs/my_project_database.yml and ~/dev/beezwax/configs/my_project_secrets.yml, then I can run this script any time I get so messed up that the easiest way forward is rm -dfr my_project && git clone ssh://git@gitlab.beezwax.net/my_project.

#!/bin/bash
app_name="$(basename "$(pwd)")"

cmd="ln -s /Users/colin_l/dev/beezwax/configs/${app_name}_database.yml /Users/colin_l/dev/beezwax/${app_name}/config/database.yml"
echo "$cmd"
$cmd

cmd="ln -s /Users/colin_l/dev/beezwax/configs/${app_name}_secrets.yml /Users/colin_l/dev/beezwax/${app_name}/config/secrets.yml"
echo "$cmd"
$cmd

basename and its friend dirname are super handy tools in your bash toolbox:

$ which rails
/Users/colin_l/.rvm/gems/ruby-2.3.1/bin/rails
$ basename $(which rails)
rails
$ dirname $(which rails)
/Users/colin_l/.rvm/gems/ruby-2.3.1/bin

Line 2 gets the name of the project to use for the symlinks by getting basename of the current working directory:

$ pwd
/Users/colin_l/dev/beezwax/foo
$ basename "$(pwd)"
foo

Note the use of absolute paths in the ln commands. It’s way harder to screw up absolute paths than relative paths, especially in cron jobs and with symlinks, so absolute paths are my default usage.

model-diagram

Sometimes it’s helpful to get a visual representation of your data model, and the railroady gem can do that for you. It has dependencies on dot and neatographviz; both available on Homebrew and MacPorts.

#!/bin/bash
railroady -M | neato -Tjpg > model.jpg

Here’s some example output for a very small project:

UML-like example of railroady datamodel output

Obviously for larger projects, they’re much bigger and more complicated. If your project uses the PaperTrail gem on a lot of models, you’ll get a lovely flower shape with PaperTrail in the center and your models as oddly-sized petals.

This is another example of making something easy to remember (model-diagram) for something hard to remember (railroady -M | neato -Tjpg > model.jpg)

On to Part 3: Other Examples

One thought on “Simple Bash Scripts for Lazy People | Part 2: Rails

Leave a Reply