diff --git a/.config/fish/abbreviations.fish b/.config/fish/abbreviations.fish new file mode 100644 index 00000000..9cddca40 --- /dev/null +++ b/.config/fish/abbreviations.fish @@ -0,0 +1,4 @@ +abbr -a b --function projectdo_build +abbr -a r --function projectdo_run +abbr -a t --function projectdo_test +abbr -a p --function projectdo_tool \ No newline at end of file diff --git a/.config/fish/completions/projectdo.fish b/.config/fish/completions/projectdo.fish new file mode 100644 index 00000000..41413b38 --- /dev/null +++ b/.config/fish/completions/projectdo.fish @@ -0,0 +1,23 @@ +complete -c projectdo --no-files + +set -l commands build run test tool + +# Options +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -s h -l help -d 'Display usage information' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -s d -l 'dry-run' -d 'Do not execute any commands with side-effects' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -s q -l quiet -d 'Do not print commands before execution' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -s v -l version -d 'Dsiplay the version' + +# Actions +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -a build -d 'Build the current project' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -a run -d 'Run the current project' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -a test -d 'Test the current project' +complete -c projectdo -n "not __fish_seen_subcommand_from $commands" \ + -a tool -d 'Invoke the tool corresponding to the current project' diff --git a/.config/fish/config.fish b/.config/fish/config.fish index 848664a7..d3160dcd 100644 --- a/.config/fish/config.fish +++ b/.config/fish/config.fish @@ -1,5 +1,6 @@ source ~/.config/fish/variables.fish source ~/.config/fish/keybinds.fish +source ~/.config/fish/abbreviations.fish if status is-interactive atuin init fish | source @@ -10,4 +11,4 @@ end starship init fish | source zoxide init fish | source -alias cd='z' # Couldn't create it as a function because of zoxide init +alias cd='z' # Couldn't create it as a function because of zoxide init \ No newline at end of file diff --git a/.config/fish/fish_plugins b/.config/fish/fish_plugins index 9ce2b051..92c97b30 100644 --- a/.config/fish/fish_plugins +++ b/.config/fish/fish_plugins @@ -5,3 +5,4 @@ patrickf1/fzf.fish wfxr/forgit danhper/fish-ssh-agent gazorby/fish-abbreviation-tips +paldepind/projectdo diff --git a/.config/fish/fish_variables b/.config/fish/fish_variables index 0e7c767e..324e3421 100644 --- a/.config/fish/fish_variables +++ b/.config/fish/fish_variables @@ -13,16 +13,17 @@ SETUVAR --export XDG_CACHE_HOME:/home/matt/\x2ecache SETUVAR --export XDG_CONFIG_HOME:/home/matt/\x2econfig SETUVAR --export XDG_DATA_HOME:/home/matt/\x2elocal/share SETUVAR --export XDG_SCRIPT_HOME:/home/matt/\x2elocal/script -SETUVAR --export __ABBR_TIPS_KEYS:a__bruh\x1ea__cat\x1ea__cd\x1ea__clock\x1ea__codeinfo\x1ea__disks\x1ea__dots\x1ea__dsize\x1ea__ea\x1ea__ef\x1ea__eg\x1ea__ev\x1ea__f\x1ea__fetch\x1ea__files\x1ea__fsend\x1ea__ga\x1ea__gbd\x1ea__gbl\x1ea__gcb\x1ea__gcf\x1ea__gclean\x1ea__gco\x1ea__gcp\x1ea__gct\x1ea__gd\x1ea__gfetch\x1ea__gfu\x1ea__gi\x1ea__glo\x1ea__gpt\x1ea__grb\x1ea__grc\x1ea__grh\x1ea__gsp\x1ea__gss\x1ea__h\x1ea__info\x1ea__install\x1ea__ip\x1ea__l\x1ea__ld\x1ea__ldh\x1ea__lg\x1ea__lh\x1ea__ls\x1ea__lsh\x1ea__lt\x1ea__lth\x1ea__matrix\x1ea__pages\x1ea__q\x1ea__svn\x1ea__sysproc\x1ea__uninstall\x1ea__update\x1ea__vi\x1ea__vim\x1ea__yP\x1ea__ya\x1ea__yarn\x1ea__yc\x1ea__ym\x1ea__yp\x1ea__yst\x1ea__ysw\x1ea__z\x1ea__zi -SETUVAR --export __ABBR_TIPS_VALUES:genact\x20\x2ds\x204\x1ebat\x1ez\x1etty\x2dclock\x20\x2dsbc\x1escc\x20\x2e/\x1eduf\x1eyadm\x20enter\x20lazygit\x1edua\x20i\x1envim\x20\x7e/\x2econfig/fish/aliases\x2efish\x1envim\x20\x7e/\x2econfig/fish/config\x2efish\x1envim\x20\x7e/\x2egitconfig\x1envim\x20\x7e/\x2econfig/fish/variables\x2efish\x1efzf\x1eneofetch\x1explr\x1efloaterm\x1eforgit\x3a\x3aadd\x1eforgit\x3a\x3abranch\x3a\x3adelete\x1eforgit\x3a\x3ablame\x1eforgit\x3a\x3acheckout\x3a\x3abranch\x1eforgit\x3a\x3acheckout\x3a\x3afile\x1eforgit\x3a\x3aclean\x1eforgit\x3a\x3acheckout\x3a\x3acommit\x1eforgit\x3a\x3acherry\x3a\x3apick\x3a\x3afrom\x3a\x3abranch\x1eforgit\x3a\x3acheckout\x3a\x3atag\x1eforgit\x3a\x3adiff\x1eonefetch\x1eforgit\x3a\x3afixup\x1eforgit\x3a\x3aignore\x1eforgit\x3a\x3alog\x1etgpt\x20\x2di\x1eforgit\x3a\x3arebase\x1eforgit\x3a\x3arevert\x3a\x3acommit\x1eforgit\x3a\x3areset\x3a\x3ahead\x1eforgit\x3a\x3astash\x3a\x3apush\x1eforgit\x3a\x3astash\x3a\x3ashow\x1ezi\x1etldr\x1eyay\x20\x2dS\x20\x1eip\x20\x2dc\x20a\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2dD\x20\x2d\x2dgit\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2dD\x20\x2d\x2dgit\x1elazygit\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dtree\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2d\x2dtree\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eunimatrix\x20\x2ds\x2095\x1enavi\x1eexit\x1esvn\x5c\x5c\x5c\x20\x2d\x2dconfig\x2ddir\x5c\x5c\x5c\x20\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x22\x5c\x5c\x5c\x24XDG_CONFIG_HOME\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x22/subversion\x1esysz\x1eyay\x20\x2dR\x20\x1eyay\x20\x2dSyu\x1envim\x1envim\x1eyadm\x20push\x1eyadm\x20add\x1eyarn\x20\x2d\x2duse\x2dyarnrc\x20\x22\x24XDG_CONFIG_HOME/yarn/config\x22\x1eyadm\x20commit\x20\x2dS\x20\x2da\x20\x2dm\x1eyadm\x20merge\x1eyadm\x20pull\x1eyadm\x20status\x1eyadm\x20switch\x1e__zoxide_z\x1e__zoxide_zi +SETUVAR --export __ABBR_TIPS_KEYS:a__bruh\x1ea__cat\x1ea__cd\x1ea__clock\x1ea__codeinfo\x1ea__disks\x1ea__dots\x1ea__dsize\x1ea__ea\x1ea__ef\x1ea__eg\x1ea__ev\x1ea__f\x1ea__fetch\x1ea__files\x1ea__fsend\x1ea__ga\x1ea__gbd\x1ea__gbl\x1ea__gcb\x1ea__gcf\x1ea__gclean\x1ea__gco\x1ea__gcp\x1ea__gct\x1ea__gd\x1ea__gfetch\x1ea__gfu\x1ea__gi\x1ea__glo\x1ea__gpt\x1ea__grb\x1ea__grc\x1ea__grh\x1ea__gsp\x1ea__gss\x1ea__h\x1ea__info\x1ea__install\x1ea__ip\x1ea__l\x1ea__ld\x1ea__ldh\x1ea__lh\x1ea__ls\x1ea__lsh\x1ea__lt\x1ea__lth\x1ea__matrix\x1ea__q\x1ea__svn\x1ea__sysproc\x1ea__uninstall\x1ea__update\x1ea__vim\x1ea__yP\x1ea__ya\x1ea__yarn\x1ea__yc\x1ea__ym\x1ea__yp\x1ea__yst\x1ea__ysw\x1ea__z\x1ea__zi +SETUVAR --export __ABBR_TIPS_VALUES:genact\x20\x2ds\x204\x1ebat\x1ez\x1etty\x2dclock\x20\x2dsbc\x1escc\x20\x2e/\x1eduf\x1eyadm\x20enter\x20lazygit\x1edua\x20i\x1envim\x20\x7e/\x2econfig/fish/aliases\x2efish\x1envim\x20\x7e/\x2econfig/fish/config\x2efish\x1envim\x20\x7e/\x2egitconfig\x1envim\x20\x7e/\x2econfig/fish/variables\x2efish\x1efzf\x1eneofetch\x1explr\x1efloaterm\x1eforgit\x3a\x3aadd\x1eforgit\x3a\x3abranch\x3a\x3adelete\x1eforgit\x3a\x3ablame\x1eforgit\x3a\x3acheckout\x3a\x3abranch\x1eforgit\x3a\x3acheckout\x3a\x3afile\x1eforgit\x3a\x3aclean\x1eforgit\x3a\x3acheckout\x3a\x3acommit\x1eforgit\x3a\x3acherry\x3a\x3apick\x3a\x3afrom\x3a\x3abranch\x1eforgit\x3a\x3acheckout\x3a\x3atag\x1eforgit\x3a\x3adiff\x1eonefetch\x1eforgit\x3a\x3afixup\x1eforgit\x3a\x3aignore\x1eforgit\x3a\x3alog\x1etgpt\x20\x2di\x1eforgit\x3a\x3arebase\x1eforgit\x3a\x3arevert\x3a\x3acommit\x1eforgit\x3a\x3areset\x3a\x3ahead\x1eforgit\x3a\x3astash\x3a\x3apush\x1eforgit\x3a\x3astash\x3a\x3ashow\x1ezi\x1etldr\x1eyay\x20\x2dS\x20\x1eip\x20\x2dc\x20a\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2dD\x20\x2d\x2dgit\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2dD\x20\x2d\x2dgit\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dicons\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2da\x20\x2d\x2dicons\x20\x2d\x2dtree\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eeza\x20\x2d\x2dlong\x20\x2d\x2dheader\x20\x2d\x2dicons\x20\x2d\x2dtree\x20\x2d\x2dgit\x20\x2d\x2dgroup\x2ddirectories\x2dfirst\x1eunimatrix\x20\x2ds\x2095\x1eexit\x1esvn\x5c\x5c\x5c\x20\x2d\x2dconfig\x2ddir\x5c\x5c\x5c\x20\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x22\x5c\x5c\x5c\x24XDG_CONFIG_HOME\x5c\x5c\x5c\x5c\x5c\x5c\x5c\x22/subversion\x1esysz\x1eyay\x20\x2dR\x20\x1eyay\x20\x2dSyu\x1envim\x1eyadm\x20push\x1eyadm\x20add\x1eyarn\x20\x2d\x2duse\x2dyarnrc\x20\x22\x24XDG_CONFIG_HOME/yarn/config\x22\x1eyadm\x20commit\x20\x2dS\x20\x2da\x20\x2dm\x1eyadm\x20merge\x1eyadm\x20pull\x1eyadm\x20status\x1eyadm\x20switch\x1e__zoxide_z\x1e__zoxide_zi SETUVAR __fish_initialized:3400 SETUVAR _fisher_acomagu_2F_fish_2D_async_2D_prompt_files:\x7e/\x2econfig/fish/conf\x2ed/__async_prompt\x2efish SETUVAR _fisher_danhper_2F_fish_2D_ssh_2D_agent_files:\x7e/\x2econfig/fish/functions/__ssh_agent_is_started\x2efish\x1e\x7e/\x2econfig/fish/functions/__ssh_agent_start\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/fish\x2dssh\x2dagent\x2efish SETUVAR _fisher_franciscolourenco_2F_done_files:\x7e/\x2econfig/fish/conf\x2ed/done\x2efish SETUVAR _fisher_gazorby_2F_fish_2D_abbreviation_2D_tips_files:\x7e/\x2econfig/fish/functions/__abbr_tips_bind_newline\x2efish\x1e\x7e/\x2econfig/fish/functions/__abbr_tips_bind_space\x2efish\x1e\x7e/\x2econfig/fish/functions/__abbr_tips_clean\x2efish\x1e\x7e/\x2econfig/fish/functions/__abbr_tips_init\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/abbr_tips\x2efish SETUVAR _fisher_joehillen_2F_to_2D_fish_files:\x7e/\x2econfig/fish/functions/to\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/to\x2efish\x1e\x7e/\x2econfig/fish/completions/to\x2efish +SETUVAR _fisher_paldepind_2F_projectdo_files:\x7e/\x2econfig/fish/functions/projectdo_build\x2efish\x1e\x7e/\x2econfig/fish/functions/projectdo_run\x2efish\x1e\x7e/\x2econfig/fish/functions/projectdo_test\x2efish\x1e\x7e/\x2econfig/fish/functions/projectdo_tool\x2efish\x1e\x7e/\x2econfig/fish/completions/projectdo\x2efish SETUVAR _fisher_patrickf1_2F_fzf_2E_fish_files:\x7e/\x2econfig/fish/functions/_fzf_configure_bindings_help\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_extract_var_info\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_preview_changed_file\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_preview_file\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_report_diff_type\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_report_file_type\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_directory\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_git_log\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_git_status\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_history\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_processes\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_search_variables\x2efish\x1e\x7e/\x2econfig/fish/functions/_fzf_wrapper\x2efish\x1e\x7e/\x2econfig/fish/functions/fzf_configure_bindings\x2efish\x1e\x7e/\x2econfig/fish/conf\x2ed/fzf\x2efish\x1e\x7e/\x2econfig/fish/completions/fzf_configure_bindings\x2efish -SETUVAR _fisher_plugins:franciscolourenco/done\x1eacomagu/fish\x2dasync\x2dprompt\x1ejoehillen/to\x2dfish\x1epatrickf1/fzf\x2efish\x1ewfxr/forgit\x1edanhper/fish\x2dssh\x2dagent\x1egazorby/fish\x2dabbreviation\x2dtips +SETUVAR _fisher_plugins:franciscolourenco/done\x1eacomagu/fish\x2dasync\x2dprompt\x1ejoehillen/to\x2dfish\x1epatrickf1/fzf\x2efish\x1ewfxr/forgit\x1edanhper/fish\x2dssh\x2dagent\x1egazorby/fish\x2dabbreviation\x2dtips\x1epaldepind/projectdo SETUVAR _fisher_upgraded_to_4_4:\x1d SETUVAR _fisher_wfxr_2F_forgit_files:\x7e/\x2econfig/fish/conf\x2ed/bin\x1e\x7e/\x2econfig/fish/conf\x2ed/forgit\x2eplugin\x2efish\x1e\x7e/\x2econfig/fish/completions/_git\x2dforgit\x1e\x7e/\x2econfig/fish/completions/git\x2dforgit\x2ebash\x1e\x7e/\x2econfig/fish/completions/git\x2dforgit\x2ezsh SETUVAR fish_color_autosuggestion:6e738d diff --git a/.config/fish/functions/projectdo_build.fish b/.config/fish/functions/projectdo_build.fish new file mode 100644 index 00000000..21ce2921 --- /dev/null +++ b/.config/fish/functions/projectdo_build.fish @@ -0,0 +1,3 @@ +function projectdo_build + projectdo -d build +end diff --git a/.config/fish/functions/projectdo_run.fish b/.config/fish/functions/projectdo_run.fish new file mode 100644 index 00000000..2a73e98a --- /dev/null +++ b/.config/fish/functions/projectdo_run.fish @@ -0,0 +1,3 @@ +function projectdo_run + projectdo -d run +end diff --git a/.config/fish/functions/projectdo_test.fish b/.config/fish/functions/projectdo_test.fish new file mode 100644 index 00000000..680e18b1 --- /dev/null +++ b/.config/fish/functions/projectdo_test.fish @@ -0,0 +1,3 @@ +function projectdo_test + projectdo -d test +end diff --git a/.config/fish/functions/projectdo_tool.fish b/.config/fish/functions/projectdo_tool.fish new file mode 100644 index 00000000..3d37d66e --- /dev/null +++ b/.config/fish/functions/projectdo_tool.fish @@ -0,0 +1,3 @@ +function projectdo_tool + projectdo -d tool +end diff --git a/.local/bin/projectdo b/.local/bin/projectdo new file mode 100755 index 00000000..4cc890d4 --- /dev/null +++ b/.local/bin/projectdo @@ -0,0 +1,385 @@ +#!/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 . + +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