Skip to content

Migrating from Bash Scripts

Many projects use bash scripts (build.sh, run.sh, Taskfile) as task runners. Jake provides the same functionality with better ergonomics.

A typical bash task script looks like this:

#!/bin/bash
set -euo pipefail
build() {
echo "Building..."
gcc -o app src/*.c
}
test() {
build # Manual dependency
./app --test
}
clean() {
rm -f app *.o
}
help() {
echo "Available tasks:"
compgen -A function | grep -v "^_"
}
${@:-build} # Default task
@default
task build:
echo "Building..."
gcc -o app src/*.c
task test: [build]
./app --test
task clean:
rm -f app *.o

Bash - Manual function calls:

Terminal window
deploy() {
build
test
rsync dist/ server:/app/
}

Jake - Declarative dependencies:

task deploy: [build, test]
rsync dist/ server:/app/

Jake automatically runs dependencies in the right order and skips duplicates.

Bash - Complex PID management:

Terminal window
build() {
build_backend &
PID1=$!
build_frontend &
PID2=$!
wait $PID1 || exit 1
wait $PID2 || exit 1
}

Jake - Just use -j flag:

task build: [build-backend, build-frontend]
echo "Build complete"
task build-backend:
go build ./cmd/server
task build-frontend:
npm run build
Terminal window
jake -j4 build # Run with 4 parallel workers

Bash - No way to skip if unchanged:

Terminal window
compile() {
# Always runs, even if nothing changed
gcc -o app main.c util.c
}

Jake - Smart rebuilds:

file app: main.c util.c
gcc -o app main.c util.c

This only runs if main.c or util.c are newer than app.

Bash - Manual implementation:

Terminal window
help() {
echo "Available tasks:"
compgen -A function | grep -v "^_" | cat -n
}

Jake - Built-in:

Terminal window
jake --list # List all recipes with descriptions
jake -s build # Show detailed info about a recipe

Bash - Platform-specific code:

Terminal window
# macOS vs Linux differences
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' 's/foo/bar/' file
else
sed -i 's/foo/bar/' file
fi

Jake - Platform directives:

@platform macos
task install:
brew install deps
@platform linux
task install:
apt-get install deps

Bash - Manual prompt:

Terminal window
deploy() {
read -p "Deploy to production? (yes/no) " -r
if [ "$REPLY" != "yes" ]; then
exit 0
fi
# deploy...
}

Jake - Built-in directive:

task deploy:
@confirm "Deploy to production?"
./deploy.sh

Use jake -y deploy to auto-confirm.

Bash - Manual loading:

Terminal window
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
fi

Jake - Built-in:

@dotenv
@dotenv ".env.local"
@require DATABASE_URL API_KEY
task deploy:
echo "Using $DATABASE_URL"
#!/bin/bash
set -euo pipefail
PROJECT="myapp"
BUILD_DIR="build"
_log() {
echo "[$(date +'%H:%M:%S')] $1"
}
_check_deps() {
command -v node >/dev/null 2>&1 || { echo "node required"; exit 1; }
command -v go >/dev/null 2>&1 || { echo "go required"; exit 1; }
}
install() {
_log "Installing dependencies..."
npm install
go mod download
}
build-backend() {
_log "Building backend..."
go build -o "${BUILD_DIR}/server" ./cmd/server
}
build-frontend() {
_log "Building frontend..."
npm run build
}
build() {
_check_deps
mkdir -p "${BUILD_DIR}"
build-backend &
local pid1=$!
build-frontend &
local pid2=$!
wait $pid1 || exit 1
wait $pid2 || exit 1
_log "Build complete!"
}
test() {
_log "Running tests..."
go test ./... -v
npm test
}
clean() {
rm -rf "${BUILD_DIR}" dist/ node_modules/
}
deploy() {
local env="${1:-staging}"
if [ "$env" = "production" ]; then
read -p "Deploy to PRODUCTION? " -r
[ "$REPLY" != "yes" ] && exit 0
fi
build
rsync -avz "${BUILD_DIR}/" "deploy@${env}.example.com:/app/"
}
help() {
echo "Usage: $0 <task> [args...]"
echo "Tasks:"
compgen -A function | grep -v "^_" | sort
}
"${@:-help}"
@dotenv
project = "myapp"
build_dir = "build"
@default
task build: [build-backend, build-frontend]
@needs node go
@pre echo "Building {{project}}..."
@post echo "Build complete!"
task build-backend:
mkdir -p {{build_dir}}
go build -o {{build_dir}}/server ./cmd/server
task build-frontend:
mkdir -p {{build_dir}}
npm run build
task install:
@desc "Install dependencies"
npm install
go mod download
task test: [build]
@desc "Run all tests"
go test ./... -v
npm test
task clean:
@desc "Remove build artifacts"
rm -rf {{build_dir}} dist/ node_modules/
task deploy env="staging": [build]
@desc "Deploy to environment"
@if eq({{env}}, "production")
@confirm "Deploy to PRODUCTION?"
@end
rsync -avz {{build_dir}}/ deploy@{{env}}.example.com:/app/
BashJake
./build.shjake
./build.sh buildjake build
./build.sh deploy stagingjake deploy env=staging
./build.sh helpjake --list
N/Ajake -j4 build (parallel)
N/Ajake -w build (watch mode)
N/Ajake -n deploy (dry-run)
  1. Automatic dependency resolution - No manual function calls
  2. Parallel execution - Built-in with -j flag
  3. File-based caching - Skip unchanged builds
  4. Watch mode - Re-run on file changes
  5. Dry-run mode - See what would run
  6. Better help - Auto-generated from recipes
  7. Cross-platform - No bash-specific quirks
  8. Cleaner syntax - Less boilerplate