7
\$\begingroup\$

I need to redo a migration occasionally. The workflow is usually something like this:

  1. find or ls | grep the migration using some keyword
  2. copy the timestamp
  3. paste the timestamp in bundle exec rake db:migrate:redo VERSION=123

As I hate repetitive work, I wrote a small bash script that allows running a migration using only a partial name. If there are many matches, it lists them all.

#!/usr/bin/env bash

err() {
  echo "redo: error: $1"
  echo "redo: usage: redo <migration_name>"
  exit 1
}

[[ -d ./db/migrate ]] || err "dir db/migrate not found"

migrations=$(find './db/migrate' -name '*.rb' | grep "$1" | sed 's#^\./db/migrate/##')
declare -A times
for m in $migrations; do
  time=$(echo "$m" | sed 's#\([0-9]*\)_.*.rb#\1#')
  name=$(echo "$m" | sed 's#[0-9]*_\(.*\)\.rb#\1#')
  times["$name"]="$time"
done

if [[ ${#times[@]} == 1 ]]; then
  name="${!times[@]}"
  ver="${times[$name]}"
  echo "redo: executing \`bundle exec rake db:migrate:redo VERSION=$ver\`"
  bundle exec rake db:migrate:redo VERSION="$ver"
  exit 0
fi

echo -e "redo: be more specific, candidates are:\n"
for k in "${!times[@]}"; do
  echo "$k : ${times[$k]}"
done
exit 1

I'd love to get feedback.

\$\endgroup\$
1
\$\begingroup\$

Error and initial check

I recommend to substitute $1 with $@, so that if you ever forget to quote it will still print the complete message.

err ()
{
  echo "redo: error: $@"
  echo "redo: usage: redo <migration_name>"
  exit 1
}

I am not sure why you put the usage in that function, since if there is no database, this does not help anyone.

[[ -d ./db/migrate ]] || err "dir db/migrate not found"

You also may want to check if you have too few/many arguments.

[[ -z $1 ]] && err No argument!
[[ $# -gt 1 ]] && err Two many arguments!

Finding the migrations

I recommend not using sed to truncate your pathnames. I also think it is completely superfluous since your trapping a few things later on and can simply ignore the prefix. You also do not need to use grep, find should be able to do that on his own. Hence instead of

migrations=$(find './db/migrate' -name '*.rb' | grep "$1" | sed 's#^\./db/migrate/##')

simply use

migrations=$(find './db/migrate' -name "*$1*.rb")

At this point I would check if it actually found anything, too.

[[ -z $migrations ]] && err I don't know what you are talking about.

Note that I omitted the quotes to make the point raised above.

Creating the array

I recommend not using times as a variable name, because there is a built-in bash command of the same name.
You can use avoid that echo |sed construct by using the build-in regular expressions comparison operator =~.

Since I don't know the exact form how the migrations are written, I can only assume that a migration comes in the form ./db/migrate/YYYYMMDDHHmmSS_name.rb. Where the timestamp is obviously just numbers, but name can be anything.

declare -A migration_times
for select_migration in $migrations; do
    pattern="([0-9]+)_([^\.]+)\.rb"
    if  [[ $select_migration =~ $pattern ]]; then
        migration_times["${BASH_REMATCH[2]}"]="${BASH_REMATCH[1]}"
    fi
done

That shortens your code quite considerably and should make it more effective since you are not calling another program.

Executing command or listing migrations

If the array does only have one element then you don't need to read the whole pair, you can simply use the value the complete array evaluates to, saving you the lines with reassigning the names. You can also skip the exit 0 statement because that would overwrite the exit status of your external call. You can tie the listing with an else statement to the conditional.
I also used printf to give it a nicer look if the length of the names vary %-10s has to be adjusted to your needs.

if [[ ${#migration_times[@]} == 1 ]]; then
    echo "redo: executing 'bundle exec rake db:migrate:redo VERSION=${migration_times[*]}'"
    # bundle exec rake db:migrate:redo VERSION=${migration_times[*]}
else
    echo "redo: be more specific, candidates are:"
    for element in "${!migration_times[@]}"; do
        printf "%-10s : %s\n" $element ${migration_times[$element]}
    done
    exit 1
fi

I think you can also easily migrate the changes from your answer to this review.

\$\endgroup\$
  • \$\begingroup\$ Thanks, I also allow 0 arguments. In such case the program will simply list all migrations. \$\endgroup\$ – dimid Jun 3 '16 at 13:35
0
\$\begingroup\$

Made the following changes:

  • add flags: u, e
  • make name program dynamic
  • update usage message to reflect that migration_name is optional
  • check bash version
  • check extra args
  • use constants
  • refactor migrations dir to a variable
  • consistent variable names
  • debug the executed command instead of printing it (as recommended in Bash FAQ)

The full diff can be found here.

The new version:

#!/usr/bin/env bash
set -u
set -e
readonly PROG=$(basename $0)

err() {
  echo "$PROG: error: $1"
  echo "$PROG: usage: $PROG [migration_name]"
  exit 1
}

# associative arrays require version >= 4
(( ${BASH_VERSION%%.*} >= 4 )) || err "bash must be at least version 4"
(( $# > 2 )) && err "too many args"
readonly DIR="db/migrate/"
[[ -d $DIR ]] || err "dir $DIR not found"

migs=$(find "$DIR" -name "*.rb" | sed "s#^${DIR}/##")
(( $# == 1 )) && migs=$(echo "$migs" | grep "$1")
declare -A times
for m in $migs; do
  name=$(echo "$m" | sed 's#[0-9]*_\(.*\)\.rb#\1#')
  ver=$(echo "$m" | sed 's#\([0-9]*\)_.*.rb#\1#')
  times["$name"]="$ver"
done

if [[ ${#times[@]} == 1 ]]; then
  name="${!times[@]}"
  ver="${times[$name]}"
  echo "$PROG: executing:"
  set -x
  bundle exec rake db:migrate:redo VERSION="$ver"
  set +x
  exit 0
fi

echo -e "$PROG: be more specific, candidates are:\n"
for t in "${!times[@]}"; do
  echo "$t : ${times[$t]}"
done

set +u
set +e
exit 1
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.