dotfiles/.local/bin/projectdo

404 lines
8.9 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# projectdo universal project commands.
# Copyright (C) 2019-present Simon Friis Vindum
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
VERSION="0.2.0"
# Global mutable variables.
QUIET=false
DRY_RUN=false
PROJECT_ROOT="${PROJECT_ROOT:-}"
ACTION=""
TOOL_ARGS="" # Arguments to pass along to the tool
IS_TOOL=false # True if ACTION is 'tool' as this action is somewhat special.
has_command() {
command -v "$1" >/dev/null 2>&1
}
# When running the appropriate external tool this function is used which
# evaluates the given command while respecting $QUIET and $DRY_RUN.
execute() {
if [ $QUIET = false ]; then
echo "$1" $TOOL_ARGS
fi
if [ $DRY_RUN = false ]; then
if [ $QUIET = false ]; then
eval "$1" $TOOL_ARGS
else
eval "$1" $TOOL_ARGS >/dev/null
fi
fi
}
# Takes the name of a tool and an action. If the action is `tool` it is
# ignored. It handles the common case where the tool is invoked as `tool
# action`.
execute_command() {
if [ "$2" = tool ]; then
execute "$1"
else
execute "$1 $2"
fi
exit 0
}
# Every supported tool requires a function `try_tool` where `tool` is a name
# indicating what tool the function tries to detect. The function should:
#
# * Return if it does not detect that the tool is appropriate.
# * Read the variables `ACTION` and `IS_TOOL` to determine the correct action
# to run.
# * When running an action is should use the `execute` function and _exit_ when
# it is done.
# Start list of tools
# JavaScript + NodeJS
try_nodejs() {
if [ ! -f package.json ]; then
return 1
fi
if [ -f yarn.lock ]; then
tool=yarn
else
tool=npm
fi
if ! has_command $tool; then
echo "Found a package.json file but '$tool' is not installed."
exit 1
fi
local node_action="$ACTION"
# Only the "run" action need translation, the others match 1-to-1
# if [ "$ACTION" = run ]; then
# node_action=start
# fi
# We check if the package.json file contains an appropriate script. We use
# grep for this. The check is not 100% bulletproof, but it's very close. We
# could've used `npm run` to get the authorative list of the scripts but
# invoking `npm` is two orders of magnitude slower which leads to a
# noticeable delay.
# if ! $IS_TOOL && ! grep -q "^[[:space:]]*\"${node_action}\":" package.json; then
# return 0
# fi
execute_command "$tool" "$node_action"
}
# Rust + Cargo
try_cargo() {
if [ -f Cargo.toml ]; then
execute_command cargo "$ACTION"
fi
}
# CMake
try_cmake() {
if [ -f CMakeLists.txt ] && [ "$ACTION" = test ]; then
if [ -f build ]; then
execute "cmake --build build --target test"
else
execute "cmake --build . --target test"
fi
exit
fi
}
# Haskell + Stack
try_stack() {
if [ -f package.yaml ] && [ -f stack.yaml ]; then
execute_command stack "$ACTION"
fi
}
# Haskell + Cabal
try_cabal() {
cabal_file="$(find ./ -maxdepth 1 -name "*.cabal" 2>/dev/null | wc -l)"
if [ "$cabal_file" -gt 0 ] && [ ! -f stack.yml ]; then
execute_command cabal "$ACTION"
fi
}
# Maven
try_maven() {
if [ -f pom.xml ]; then
case $ACTION in
build)
execute "mvn compile"
exit
;;
test)
execute "mvn test"
exit
;;
run)
echo "projectdo does not know how to run a project with Maven."
exit
;;
esac
fi
}
# Clojure + Leiningen
try_lein() {
if [ -f project.clj ] && [ "$ACTION" = test ]; then
execute "lein test"
fi
}
# Makefile
has_make_target() {
target="${1?}"
output=$(make -n "${target}" 2>&1)
exit_code=$?
if [ $exit_code -ne 0 ]; then
return $exit_code
fi
# If there is a file with the name of the target we're looking for but no
# actual target with that name, make will exit successfully with that
# message. We need to consider that case as a "target not found". Note that
# the way the target is quoted in the output (`test' vs 'test') can differ
# across OSes so we only check a prefix up to the problematic quotes.
if expr "$output" : "make: Nothing to be done for" 1>/dev/null; then
return 1
fi
return 0
}
try_makefile() {
if [ -f Makefile ]; then
if ! has_command "make"; then
echo "Found a Makefile but 'make' is not installed."
exit 1
fi
if $IS_TOOL || [ "$ACTION" = build ]; then
# For make "build" is the default action when running `make`
execute "make"
exit
elif [ "$ACTION" = test ]; then
# Let's see if the makefile contains a test or check target
if has_make_target "test"; then
execute "make test"
exit
elif has_make_target "check"; then
execute "make check"
exit
fi
fi
fi
return 1
}
# Python
try_python() {
if [ -f pyproject.toml ]; then
if grep -q -m 1 "^\[tool.poetry\]$" pyproject.toml; then
case $ACTION in
build)
execute "poetry build"
exit
;;
test)
# TODO: There are other Python test frameworks, it would be nice to
# detect and run the right one.
execute "poetry run pytest"
exit
;;
run)
echo "projectdo does not know how to run a project with Poetry."
exit
;;
esac
else
echo "Found a pyproject.toml file but projectdo is not sure how to run it."
exit
fi
return 1
fi
}
# Go
try_go() {
if [ -f go.mod ] && [ "$ACTION" = test ]; then
# We detect Makefiles before we detect Go, so here we know that the Go
# project is _not_ tested by a Makefile.
# Check if the project uses Mage. A magefile could in theory have any name,
# but `magefile.go` seems to be the convention.
if grep -q -m 1 '^func Check(' magefile.go; then
execute "mage check"
exit
elif grep -q -m 1 '^func Test(' magefile.go; then
execute "mage test"
exit
fi
execute "go test"
exit
fi
}
# LaTeX
try_latex() {
if [ -f Tectonic.toml ] && [ "$ACTION" = build ]; then
execute "tectonic -X build"
exit
fi
}
# End of list of tools
detect_and_run() {
try_nodejs
try_cargo
try_stack
try_cabal
try_cmake
try_maven
try_lein
try_makefile
try_python
try_go
try_latex
}
set_project_root() {
if [ -n "$PROJECT_ROOT" ]; then
return
fi
if has_command git; then
# Find the root of the git repository if we're inside one. If we're in a
# git submodule then the root of the outer git repo is used. If we're not
# in a git repo the git command will not output anything and $PROJECT_ROOT
# is set to the empty string which is fine.
PROJECT_ROOT=$(git rev-parse --show-superproject-working-tree --show-toplevel 2>/dev/null | head -n 1)
fi
}
nothing_found() {
echo "No way to $ACTION found :'("
exit 1
}
display_version() {
echo "$(basename "$0") version $VERSION"
}
display_help() {
echo "Usage: $(basename "$0") [options] [action] [tool-arguments]
Options:
-h, --help Display this help.
-n, -d, --dry-run Do not execute any commands with side-effects.
-q, --quiet Do not print commands as they are about to be executed.
-v, --version Display the version of the program (which is $VERSION).
Actions:
build, run, test Build, run, or test the current project.
tool Invoke the guessed tool for the current project.
Tool arguments:
Any arguments following [action] are passed along to the invoked tool."
}
# Main execution starts here
while getopts dhnqv-: c; do
case $c in
d) DRY_RUN=true ;;
h)
display_help
exit 0
;;
n) DRY_RUN=true ;;
q) QUIET=true ;;
v)
display_version
exit 0
;;
-) case $OPTARG in
help)
display_help
exit 0
;;
dry-run) DRY_RUN=true ;;
quiet) QUIET=true ;;
version)
display_version
exit 0
;;
'') break ;; # "--" should terminate argument processing
*)
echo "Illegal option --$OPTARG" >&2
exit 1
;;
esac ;;
\?) exit 1 ;;
esac
done
shift $((OPTIND - 1)) # Shift away the parsed option arguments
if [ "$1" = test ] ||
[ "$1" = run ] ||
[ "$1" = build ] ||
[ "$1" = tool ]; then
ACTION=$1
if [ "$1" = tool ]; then
IS_TOOL=true
fi
shift 1 # Remove the action from the arguments
TOOL_ARGS=$@
else
if [ -z "$1" ]; then
echo "No action specified."
else
echo "$1 is not a valid action."
fi
echo ""
echo "The valid actions are: build, run, test, tool"
echo ""
echo "Example: projectdo test"
exit 1
fi
set_project_root
while :; do
# We don't want to do anything if we are in the home or root directory
if [ "$PWD" = "$HOME" ] || [ "$PWD" = / ]; then
nothing_found
fi
detect_and_run
# If we didn't detect a tool to run in this directory we go up one directory
# while ensuring that we don't leave the project root
if [ "$PWD" = "$PROJECT_ROOT" ]; then
nothing_found
fi
cd ..
done