├── .dkrc ├── .envrc ├── .gitignore ├── LICENSE ├── README.md ├── bin └── loco ├── loco.md ├── package.sh └── script ├── README.md ├── bootstrap ├── cibuild ├── clean ├── console ├── server ├── setup ├── test └── update /.dkrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # --- 3 | # Add your own commands, functions, and variables here. Define defaults first, 4 | # then `dk use:` the desired devkit modules, and then define any overrides to 5 | # the devkit defaults. 6 | # --- 7 | 8 | # Available modules (uncomment to use): 9 | 10 | #dk use: cram # run tests using the "cram" functional test tool 11 | dk use: entr-watch # watch files and re-run tests or other commands 12 | dk use: shell-console # make the "console" command enter a subshell 13 | dk use: shellcheck # support running shellcheck (via docker if not installed) 14 | 15 | # Define overrides, new commands, functions, etc. here: 16 | 17 | require realpaths basher install bashup/realpaths 18 | require mdsh basher install bashup/mdsh 19 | 20 | # SC1090 = dynamic 'source' command 21 | SHELLCHECK_OPTS='-e SC1090' 22 | 23 | on test eval 'dk shellcheck /dev/stdin < <(mdsh --compile loco.md)' 24 | 25 | on build mdsh --out bin/loco --compile loco.md 26 | on build chmod +x bin/loco 27 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # basher installation scheme for dependencies; you can change this if you want, 4 | # so long as all the variables are correct. The .devkit/dk script will clone 5 | # basher to $BASHER_ROOT and look for binaries in $BASHER_INSTALL_BIN. 6 | 7 | export BASHER_PREFIX="$PWD/.deps" 8 | export BASHER_INSTALL_BIN="$BASHER_PREFIX/bin" 9 | export BASHER_INSTALL_MAN="$BASHER_PREFIX/man" 10 | 11 | # Dependencies are checked out here: 12 | export BASHER_PACKAGES_PATH="$BASHER_PREFIX" 13 | export BASHER_ROOT="$BASHER_PACKAGES_PATH/basherpm/basher" 14 | 15 | # Activate virtualenv if present 16 | [[ -f $BASHER_INSTALL_BIN/activate && -f $BASHER_INSTALL_BIN/python ]] && 17 | [[ ! "${VIRTUAL_ENV-}" || $VIRTUAL_ENV != "$BASHER_PREFIX" ]] && 18 | VIRTUAL_ENV_DISABLE_PROMPT=true source $BASHER_INSTALL_BIN/activate 19 | 20 | # $BASHER_INSTALL_BIN must be on PATH to use commands installed as deps 21 | [[ :$PATH: == *:$BASHER_INSTALL_BIN:* ]] || export PATH="$BASHER_INSTALL_BIN:$PATH" 22 | 23 | # You can add other variables you want available via direnv. Configuration 24 | # variables for devkit itself, however, should go in .dkrc unless they need 25 | # to be available via direnv as well. 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .deps 2 | .devkit 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 PJ Eby 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 7 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 13 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 15 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LOcally-configured COmmands with `loco` 2 | 3 | **Contents** 4 | 5 | 6 | 7 | - [Overview](#overview) 8 | - [Installation and Customization](#installation-and-customization) 9 | - [Defining Commands](#defining-commands) 10 | * [Exposed and/or Configurable Variables](#exposed-andor-configurable-variables) 11 | * [Callable and/or Overrideable Functions](#callable-andor-overrideable-functions) 12 | * [Utility Functions](#utility-functions) 13 | 14 | 15 | 16 | ## Overview 17 | 18 | Sometimes, you need some project-specific commands or tasks, like those provided by `npm`, a `Makefile` or `Rakefile`... or maybe just a plain old shell function. 19 | 20 | And sometimes, you want to invoke these commands from a *subdirectory* of your project directory, but still have them execute in the *root* of your project... possibly with some project-specific options. 21 | 22 | So, `loco` is a simple shell script that looks for a project directory at or above your current working directory, reads some project-specific shell variables or functions, and then invokes the remainder of the command line in the matching directory with a customized environment. For example, if you create the following `.loco` file in your project root: 23 | 24 | ```bash 25 | loco.frob() { frobulate --cromulently -x 6 "$@"; } 26 | loco.biz() { spizz --bizz "$@"; } 27 | ``` 28 | 29 | Then running `loco biz baz` in any subdirectory of your project will execute `spizz --bizz baz` in the project root. 30 | 31 | The `.loco` file is sourced in the shell, so it can define functions, run programs, export variables... whatever you need to do to make it work. In addition, you can define certain special functions to change how loco works, define or override various defaults in `~/.locorc` and `/etc/loco/config`, or even rename, symlink, or extend the script to change the names it looks for. 32 | 33 | For example, if you want to use `loco` with `docker-compose`, you might create a `doco` script like this and place it on your `PATH`: 34 | 35 | ```bash 36 | #!/bin/bash -e 37 | source "$(command -v loco)" 38 | 39 | # Pass unrecognized subcommands to docker-compose 40 | loco_exec() { docker-compose "$@"; } 41 | 42 | # Do everything else the usual loco way 43 | loco_main "$@" 44 | ``` 45 | 46 | When you run this new `doco` wrapper script, it will: 47 | 48 | * look for site configuration in `/etc/doco/config` 49 | * look for user configuration in `~/.docorc`, and 50 | * look for local configuration in a `.doco`file in or above the current directory 51 | * change to the directory where it was found 52 | * expect its first argument to be a command defined as a `doco.commandname()` function 53 | * pass its entire command line to `docker-compose` if there wasn't a matching function 54 | 55 | You can also change any of these file naming conventions, behaviors, etc., as explained in the sections that follow. 56 | 57 | But, if all you want to change is the file and function name search patterns, you don't even need a wrapper script: just rename or symlink `loco`, and the resulting script will use its own name to find its configuration files and commands. (e.g., renaming or symlinking `loco` as `foo` will look for `/etc/foo/config`, `~/.foorc`, `.foo` and `foo.commandname()`.) 58 | 59 | In addition, loco automatically defines the script name (`$LOCO_NAME`) as a function (if it doesn't already exist), so that recursive invocation of the script doesn't require a subshell or re-reading all the configuration files. So in the example above, you could define a function like this: 60 | 61 | ```shell 62 | doco.reup() { 63 | # Take all services down and back up again 64 | doco down && doco up -d 65 | } 66 | ``` 67 | 68 | And rather than re-running the script multiple times, the nested `doco` commands will be directly dispatched to the appropriate functions (or `loco_exec()`). Similarly, if you symlinked `loco` as `foo`, and there is no `foo` function already defined by the script or configuration files, `loco_main` will define a `foo` function to prevent recursive invocation. 69 | 70 | ## Installation and Customization 71 | 72 | To install `loco`, just [download bin/loco](https://github.com/bashup/loco/raw/master/bin/loco) to some directory on your `PATH`, and start making configuration files or wrapper scripts. (If you have [basher](https://github.com/basherpm/basher), you can just `basher install bashup/loco`.) 73 | 74 | You can change the naming conventions for the site, user, and local configuration files by setting `LOCO_SITE_CONFIG`, `LOCO_RC`, and `LOCO_FILE` within a wrapper script after sourcing `loco`. (It has to be *after*, since all `LOCO_` variables are unset during the sourcing.) 75 | 76 | For example, if you wanted the `doco` wrapper script to get its site config from `/etc/docker/doco.conf`, user-level config from `~/doco.conf` and project-level config from `docofile` files (instead of the default `~/.docorc` and `.doco`), you could add these lines to your wrapper script after it sources `loco`: 77 | 78 | ```bash 79 | LOCO_SITE_CONFIG=/etc/docker/doco.conf 80 | LOCO_RC=doco.conf 81 | LOCO_FILE=docofile 82 | ``` 83 | 84 | (Note that `loco`'s environment variables and internal functions are *always* named `LOCO_` and `loco_` respectively, regardless of the active script name. Only commands and config file names are based on `loco`'s script name or its wrapper script name.) 85 | 86 | `LOCO_FILE`, by the way, can actually be an array of glob patterns: `LOCO_PROJECT` will be set to the absolute path of the first match. So if our `doco` script set `LOCO_FILE=("*.doco.md" ".doco")`, then each directory would first be checked for any file ending in `.doco.md` before being checked for a `.doco` file. (Of course, the script would need to override `loco_loadproject()` to be able to handle all the different types of `LOCO_PROJECT` -- more on this below.) 87 | 88 | 89 | ## Defining Commands 90 | 91 | By default, you define commands in your project, user, site, or global configuration files by defining functions prefixed with `loco`'s script name. So if your script is named `fudge`, you might define `fudge.melt()` and `fudge.sweeten()` functions which would then be run in your project's root directories (as identified by `.fudge` files), when you type in `fudge melt` or `fudge sweeten`. 92 | 93 | If, however, you type in `fudge mix`, and there is no `fudge.mix` function or command available, the `loco_exec()` function is called with `mix` and any remaining arguments. 94 | 95 | The default implementation of `loco_exec()` emits an error message, but you can override it in any `loco` configuration file to do something different. For example, you could pass the unrecognized command as a subcommand to some other program (such as `make`, `rake`, `gulp`, `docker`, etc.), as shown in the `docker-compose` example above. 96 | 97 | ### Exposed and/or Configurable Variables 98 | 99 | There are a wide variety of variables you can set from your configuration files or wrapper scripts, and use in your functions or commands. When `loco` is initially run or sourced, it unsets all of them, so it's best to source it at the start of your wrapper.) Your wrapper script can then set initial values for these variables, in which case the set value will be used in place of the defaults. (The site, user, and project configuration files can also also set or override these variables, assuming they're loaded as shell scripts.) 100 | 101 | After the default values of everything but `LOCO_PROJECT` and `LOCO_ROOT` have been set, your wrapper script's `loco_postconfig()` function will be called, if it exists. This gives you a chance to *read* the end result of the configuration process prior to the main process execution. 102 | 103 | 104 | 105 | | Variable | Default Value | Notes | 106 | | ------------------ | ---------------------------------------- | ---------------------------------------- | 107 | | `LOCO_SCRIPT` | path to the script (may be relative to `LOCO_PWD`) | If `loco` is sourced, this will be the path to the sourcing script instead | 108 | | `LOCO_COMMAND` | `basename $LOCO_SCRIPT` | Used in `loco_usage()` message | 109 | | `LOCO_NAME` | `$LOCO_COMMAND` | Base name for all configuration files, and command name prefix used by `loco_cmd`. If a function of this name doesn't exist after configuration, `loco_main` will define it as a function alias for `loco_do`. | 110 | | `LOCO_PWD` | directory the script was invoked from | Project directory search begins here | 111 | | `LOCO_SITE_CONFIG` | `/etc/$LOCO_NAME/config` | Full path of site-wide config file | 112 | | `LOCO_RC` | `.${LOCO_NAME}rc` | User-level config file name | 113 | | `LOCO_USER_CONFIG` | `$HOME/$LOCO_RC` | User-level config file full path | 114 | | `LOCO_LOAD` | `"source"` | Command or function used to read project-level config files | 115 | | `LOCO_FILE` | `(".${LOCO_NAME}")` | Array of globs matching project-level config files. | 116 | | `LOCO_PROJECT` | `$(loco_findproject "$@")` | The found path of the project-level config file (not set until just before `loco_loadproject` is called) | 117 | | `LOCO_ROOT` | `$(dirname LOCO_PROJECT)` | The project root directory, which `loco` will `cd` to before sourcing or reading`$LOCO_PROJECT` | 118 | | `LOCO_ARGS` | `("$@")` | Array of the original arguments passed to `loco_main`, set by `loco_config` just before calling `loco_preconfig`. | 119 | 120 | ### Callable and/or Overrideable Functions 121 | 122 | These functions can be called or overridden from your configuration files. If you need to invoke the original implementation of one of these functions, you can do so by adding `_` to the start of the name. For example, if you override `loco_do` , you can invoke `_loco_do` to execute its original behavior. 123 | 124 | | Function | Input(s) | Default Results | Notes | 125 | | ------------------ | -------------- | ---------------------------------------- | ---------------------------------------- | 126 | | `loco_main` | *command line* | invokes `loco_config`, then locates the project root/config file, loads it, and runs the specified subcommand. | Can only be redefined from a wrapper script, since it's already running when config file(s) are loaded | 127 | | `loco_config` | *command line* | invokes `loco_preconfig` and `loco_postconfig` before and after calculating the default config file names and locations and loading them. | Can only be redefined from a wrapper script, since it's already running when config file(s) are loaded | 128 | | `loco_site_config` | *filename* | `source` *filename* | Override in a wrapper script to change how the `LOCO_SITE_CONFIG` file is loaded | 129 | | `loco_user_config` | *filename* | `source` *filename* | Override in a wrapper script or site config to change how the `LOCO_USER_CONFIG` file is loaded | 130 | | `loco_preconfig` | *command line* | no-op | Override in a wrapper script to set initial values of `LOCO_*` variables, before the default values are calculated or configuration files are loaded | 131 | | `loco_postconfig` | *command line* | no-op | Override in a wrapper script or user/site config to read or change the values of `LOCO_*` variables, after the default values have been calculated and any configuration files were loaded | 132 | | `loco_findproject` | *command line* | `findup "$LOCO_PWD" "${LOCO_FILE[@]}" && LOCO_PROJECT=$REPLY` | Set `LOCO_PROJECT` to the project file path. Default implementation uses `findup` and emits an error if the project file isn't found. Override this (anywhere but the project file) to change the way the project file is located. | 133 | | `loco_findroot` | *command line* | `LOCO_ROOT=$(dirname $LOCO_PROJECT)` | Set `LOCO_ROOT` to the project root. Default implementation just uses the directory the project file was found in. Override this to change the way the project directory is located. | 134 | | `loco_loadproject` | *project-file* | `cd $LOCO_ROOT; $LOCO_SOURCE "$LOCO_PROJECT"` | Change to the project directory, and load the project file. | 135 | | `loco_usage` | | Usage message to stderr; exit errorlevel 64 | Override to provide a more informative message | 136 | | `loco_error` | *message(s)* | Outputs message to stderr, exit errorlevel 64 | Used by `loco_usage` | 137 | | `loco_cmd` | *commandname* | `REPLY="$LOCO_NAME.$1"` (e.g. `loco.foo` for an input of `foo`) | Can be overridden to change the subcommand naming convention (e.g. to use a suffix instead of a prefix, `-` instead of `.`, or perhaps pointing to a subdirectory such as `node_modules/.bin`). An empty `$REPLY` will trigger an error message and early termination. | 138 | | `loco_exists` | *commandname* | Return truth if *commandname* is an existing function, alias, command, or shell builtin | Can be overridden to validate command existence some other way, but this is mostly useful to force fallback to `loco_exec()` even if a command exists. (e.g. if you want to only recognize functions, not shell builtins or on-disk commands.) | 139 | | `loco_exec` | *command line* | Error message that command isn't recognized | Override this to pass unrecognized commands to a subcommand of, e.g. `rake`, `python setup.py` `docker`, `gulp`, etc. | 140 | | `loco_do` | *command line* | Translate first arg with `loco_cmd`, check existence with `loco_exists`, then directly execute or pass to `loco_exec` | It can be useful to invoke this when doing option parsing: just define functions like `loco.--arg()` that set a variable, then `shift` and `loco_do "$@"`. | 141 | 142 | 143 | 144 | ### Utility Functions 145 | 146 | These functions can be used in your scripts, but **must not** be redefined, as they are also used internally by loco: 147 | 148 | * `fn_exists` *functionname* -- succeeds if *functionname* is the name of an existing bash function 149 | * `fn_copy` *srcfunc* *newname* -- copies the body of *srcfunc* to a new function named *newname*; overwrites *newname* if it already exists. 150 | * `findup` *dir* *globs...* -- beginning at *dir*, check for the existence of any files matching any of *globs*, and walk upwards to parent directories until a matching file is found or there's nowhere left to go. On success, sets `REPLY` to the full path of the found file, otherwise failure is returned. 151 | * `walkup` *dir cmd args...* -- execute *cmd dir args...* for *dir* and every parent of *dir* until *cmd* returns success. Note that this function does **not** change the current directory (and *cmd* probably shouldn't!) and that when handling the *dir* argument you may need to address the root directory differently than other directories, since it is the only directory argument that will *end* in a slash as well as start with one. 152 | 153 | 154 | loco also bundles the [realpaths](https://github.com/bashup/realpaths) module, so all of its path-manipulation functions are also available. 155 | 156 | -------------------------------------------------------------------------------- /bin/loco: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # --- 3 | # This file is automatically generated from loco.md - DO NOT EDIT 4 | # --- 5 | 6 | # MIT License 7 | # 8 | # Copyright (c) 2017 PJ Eby 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation 11 | # files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, 12 | # modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software 13 | # is furnished to do so, subject to the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 20 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | #!/usr/bin/env bash 23 | 24 | realpath.location(){ realpath.follow "$1"; realpath.absolute "$REPLY" ".."; } 25 | realpath.resolved(){ realpath.follow "$1"; realpath.absolute "$REPLY"; } 26 | realpath.dirname() { if [[ $1 =~ /+[^/]+/*$ ]]; then REPLY="${1%${BASH_REMATCH[0]}}"; REPLY=${REPLY:-/}; else REPLY=.; fi } 27 | realpath.basename(){ if [[ $1 =~ /*([^/]+)/*$ ]]; then REPLY="${BASH_REMATCH[1]}"; else REPLY=/; fi } 28 | 29 | realpath.follow() { 30 | local target 31 | while [[ -L "$1" ]] && target=$(readlink -- "$1"); do 32 | realpath.dirname "$1" 33 | # Resolve relative to symlink's directory 34 | [[ $REPLY != . && $target != /* ]] && REPLY=$REPLY/$target || REPLY=$target 35 | # Break out if we found a symlink loop 36 | for target; do [[ $REPLY == "$target" ]] && break 2; done 37 | # Add to the loop-detect list and tail-recurse 38 | set -- "$REPLY" "$@" 39 | done 40 | REPLY="$1" 41 | } 42 | 43 | realpath.absolute() { 44 | REPLY=$PWD; local eg=extglob; ! shopt -q $eg || eg=; ${eg:+shopt -s $eg} 45 | while (($#)); do case $1 in 46 | /*) REPLY=/; set -- "${1##+(/)}" "${@:2}" ;; 47 | */*) set -- "${1%%/*}" "${1##${1%%/*}+(/)}" "${@:2}" ;; 48 | ''|.) shift ;; 49 | ..) realpath.dirname "$REPLY"; shift ;; 50 | *) REPLY="${REPLY%/}/$1"; shift ;; 51 | esac; done; ${eg:+shopt -u $eg} 52 | } 53 | 54 | realpath.canonical() { 55 | realpath.follow "$1"; set -- "$REPLY" # $1 is now resolved 56 | realpath.basename "$1"; set -- "$1" "$REPLY" # $2 = basename $1 57 | realpath.dirname "$1" 58 | [[ $REPLY != "$1" ]] && realpath.canonical "$REPLY"; # recurse unless root 59 | realpath.absolute "$REPLY" "$2"; # combine canon parent w/basename 60 | } 61 | 62 | realpath.relative() { 63 | local target="" 64 | realpath.absolute "$1"; set -- "$REPLY" "${@:2}"; realpath.absolute "${2-$PWD}" X 65 | while realpath.dirname "$REPLY"; [[ "$1" != "$REPLY" && "$1" == "${1#${REPLY%/}/}" ]]; do 66 | target=../$target 67 | done 68 | [[ $1 == "$REPLY" ]] && REPLY=${target%/} || REPLY="$target${1#${REPLY%/}/}" 69 | REPLY=${REPLY:-.} 70 | } 71 | # For documentation, see https://github.com/bashup/loco 72 | 73 | set -euo pipefail 74 | 75 | fn_exists() { declare -F -- "$1"; } >/dev/null 76 | fn_copy() { REPLY="$(declare -f "$1")"; eval "$2 ${REPLY#$1}"; } 77 | findup() { walkup "${1:-$PWD}" reply_if_exists "${@:2}"; } 78 | 79 | reply_if_exists() { 80 | local pat dir=$1 IFS= ; shift 81 | for pat; do 82 | for REPLY in ${dir%/}/$pat; do [[ -f "$REPLY" ]] && return 0; done 83 | done 84 | return 1 85 | } 86 | 87 | walkup() { 88 | realpath.absolute "$1" 89 | until set -- "$REPLY" "${@:2}"; "$2" "$1" "${@:3}"; do 90 | [[ "$1" != "/" ]] || return 1; realpath.dirname "$1" 91 | done 92 | } 93 | 94 | _loco_usage() { loco_error "Usage: $LOCO_COMMAND command args..."; } 95 | _loco_error() { echo "$@" >&2; exit 64; } 96 | _loco_cmd() { REPLY="$LOCO_NAME.$1"; } 97 | _loco_exec() { loco_error "Unrecognized command: $1"; } 98 | _loco_exists() { type -t "$1"; } >/dev/null 99 | 100 | _loco_do() { 101 | [[ "${1-}" ]] || loco_usage # No command given, exit w/usage 102 | REPLY=""; loco_cmd "$1"; local cmd="$REPLY" 103 | [[ "$cmd" ]] || loco_usage # Unrecognized command, exit w/usage 104 | 105 | if loco_exists "$cmd"; then 106 | # Command, alias, function, or builtin exists 107 | shift; "$cmd" "$@" 108 | else 109 | # Invoke the default command interpreter 110 | loco_exec "$@" 111 | fi 112 | } 113 | 114 | _loco_findproject() { 115 | # shellcheck disable=SC2015 # plain var assign can't be false 116 | findup "$LOCO_PWD" "${LOCO_FILE[@]}" && LOCO_PROJECT=$REPLY || 117 | loco_error "Can't find $LOCO_FILE here"; 118 | } 119 | _loco_preconfig() { true; } 120 | _loco_postconfig() { true; } 121 | _loco_findroot() { realpath.dirname "$LOCO_PROJECT"; LOCO_ROOT=$REPLY; } 122 | _loco_loadproject() { cd "$LOCO_ROOT"; $LOCO_LOAD "$1"; } 123 | _loco_site_config() { source "$1"; } 124 | _loco_user_config() { source "$1"; } 125 | 126 | 127 | # Find our configuration, exposing relevant paths and defaults 128 | 129 | # shellcheck disable=SC2034 # some vars are only used by extending scripts 130 | _loco_config() { 131 | LOCO_ARGS=("$@") 132 | loco_preconfig "$@" 133 | ${LOCO_COMMAND:+:} realpath.basename "$LOCO_SCRIPT"; LOCO_COMMAND="${LOCO_COMMAND-$REPLY}" 134 | LOCO_NAME="${LOCO_NAME-${LOCO_COMMAND}}" 135 | LOCO_PWD="${LOCO_PWD-$PWD}" 136 | 137 | LOCO_SITE_CONFIG="${LOCO_SITE_CONFIG-/etc/$LOCO_NAME/config}" 138 | [ -f "$LOCO_SITE_CONFIG" ] && loco_site_config "$LOCO_SITE_CONFIG" 139 | LOCO_RC="${LOCO_RC-.${LOCO_NAME}rc}" 140 | LOCO_USER_CONFIG="${LOCO_USER_CONFIG-$HOME/$LOCO_RC}" 141 | [ -f "$LOCO_USER_CONFIG" ] && loco_user_config "$LOCO_USER_CONFIG" 142 | 143 | [[ ${LOCO_FILE-} ]] || LOCO_FILE=(".$LOCO_NAME") 144 | LOCO_LOAD="${LOCO_LOAD-source}" 145 | loco_postconfig "$@" 146 | } 147 | 148 | _loco_main() { 149 | loco_config "$@" 150 | fn_exists "$LOCO_NAME" || eval "$LOCO_NAME() { loco_do \"\$@\"; }" 151 | ${LOCO_PROJECT:+:} loco_findproject "$@" 152 | ${LOCO_ROOT:+:} loco_findroot "$@" 153 | loco_loadproject "$LOCO_PROJECT" 154 | loco_do "$@" 155 | } 156 | 157 | # Initialize default function implementations 158 | for f in $(compgen -A function _loco_); do 159 | fn_exists "${f#_}" || fn_copy "$f" "${f#_}" 160 | done 161 | 162 | # Clear all LOCO_* variables before beginning 163 | for lv in ${!LOCO_@}; do unset "$lv"; done 164 | 165 | LOCO_SCRIPT=$0 166 | if [[ $0 == "${BASH_SOURCE-}" ]]; then loco_main "$@"; fi 167 | -------------------------------------------------------------------------------- /loco.md: -------------------------------------------------------------------------------- 1 | ```shell mdsh 2 | @module loco.md 3 | @main loco_main 4 | 5 | @require pjeby/license @comment LICENSE 6 | @require bashup/realpaths cat "$BASHER_INSTALL_BIN/realpaths" 7 | ``` 8 | 9 | ```shell 10 | # For documentation, see https://github.com/bashup/loco 11 | 12 | set -euo pipefail 13 | 14 | fn_exists() { declare -F -- "$1"; } >/dev/null 15 | fn_copy() { REPLY="$(declare -f "$1")"; eval "$2 ${REPLY#$1}"; } 16 | findup() { walkup "${1:-$PWD}" reply_if_exists "${@:2}"; } 17 | 18 | reply_if_exists() { 19 | local pat dir=$1 IFS= ; shift 20 | for pat; do 21 | for REPLY in ${dir%/}/$pat; do [[ -f "$REPLY" ]] && return 0; done 22 | done 23 | return 1 24 | } 25 | 26 | walkup() { 27 | realpath.absolute "$1" 28 | until set -- "$REPLY" "${@:2}"; "$2" "$1" "${@:3}"; do 29 | [[ "$1" != "/" ]] || return 1; realpath.dirname "$1" 30 | done 31 | } 32 | 33 | _loco_usage() { loco_error "Usage: $LOCO_COMMAND command args..."; } 34 | _loco_error() { echo "$@" >&2; exit 64; } 35 | _loco_cmd() { REPLY="$LOCO_NAME.$1"; } 36 | _loco_exec() { loco_error "Unrecognized command: $1"; } 37 | _loco_exists() { type -t "$1"; } >/dev/null 38 | 39 | _loco_do() { 40 | [[ "${1-}" ]] || loco_usage # No command given, exit w/usage 41 | REPLY=""; loco_cmd "$1"; local cmd="$REPLY" 42 | [[ "$cmd" ]] || loco_usage # Unrecognized command, exit w/usage 43 | 44 | if loco_exists "$cmd"; then 45 | # Command, alias, function, or builtin exists 46 | shift; "$cmd" "$@" 47 | else 48 | # Invoke the default command interpreter 49 | loco_exec "$@" 50 | fi 51 | } 52 | 53 | _loco_findproject() { 54 | # shellcheck disable=SC2015 # plain var assign can't be false 55 | findup "$LOCO_PWD" "${LOCO_FILE[@]}" && LOCO_PROJECT=$REPLY || 56 | loco_error "Can't find $LOCO_FILE here"; 57 | } 58 | _loco_preconfig() { true; } 59 | _loco_postconfig() { true; } 60 | _loco_findroot() { realpath.dirname "$LOCO_PROJECT"; LOCO_ROOT=$REPLY; } 61 | _loco_loadproject() { cd "$LOCO_ROOT"; $LOCO_LOAD "$1"; } 62 | _loco_site_config() { source "$1"; } 63 | _loco_user_config() { source "$1"; } 64 | 65 | 66 | # Find our configuration, exposing relevant paths and defaults 67 | 68 | # shellcheck disable=SC2034 # some vars are only used by extending scripts 69 | _loco_config() { 70 | LOCO_ARGS=("$@") 71 | loco_preconfig "$@" 72 | ${LOCO_COMMAND:+:} realpath.basename "$LOCO_SCRIPT"; LOCO_COMMAND="${LOCO_COMMAND-$REPLY}" 73 | LOCO_NAME="${LOCO_NAME-${LOCO_COMMAND}}" 74 | LOCO_PWD="${LOCO_PWD-$PWD}" 75 | 76 | LOCO_SITE_CONFIG="${LOCO_SITE_CONFIG-/etc/$LOCO_NAME/config}" 77 | [ -f "$LOCO_SITE_CONFIG" ] && loco_site_config "$LOCO_SITE_CONFIG" 78 | LOCO_RC="${LOCO_RC-.${LOCO_NAME}rc}" 79 | LOCO_USER_CONFIG="${LOCO_USER_CONFIG-$HOME/$LOCO_RC}" 80 | [ -f "$LOCO_USER_CONFIG" ] && loco_user_config "$LOCO_USER_CONFIG" 81 | 82 | [[ ${LOCO_FILE-} ]] || LOCO_FILE=(".$LOCO_NAME") 83 | LOCO_LOAD="${LOCO_LOAD-source}" 84 | loco_postconfig "$@" 85 | } 86 | 87 | _loco_main() { 88 | loco_config "$@" 89 | fn_exists "$LOCO_NAME" || eval "$LOCO_NAME() { loco_do \"\$@\"; }" 90 | ${LOCO_PROJECT:+:} loco_findproject "$@" 91 | ${LOCO_ROOT:+:} loco_findroot "$@" 92 | loco_loadproject "$LOCO_PROJECT" 93 | loco_do "$@" 94 | } 95 | 96 | # Initialize default function implementations 97 | for f in $(compgen -A function _loco_); do 98 | fn_exists "${f#_}" || fn_copy "$f" "${f#_}" 99 | done 100 | 101 | # Clear all LOCO_* variables before beginning 102 | for lv in ${!LOCO_@}; do unset "$lv"; done 103 | 104 | LOCO_SCRIPT=$0 105 | ``` 106 | 107 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | BINS=bin/loco 2 | -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | ## Scripts To Rule Them All 2 | 3 | The scripts in this directory are a [Scripts To Rule Them All](https://githubengineering.com/scripts-to-rule-them-all/) implementation, powered by [.devkit](https://github.com/bashup/.devkit). They should be run from within the project's root directory, using e.g. `script/test` to run tests, and so on. 4 | 5 | Please check the containing project's documentation for more details, or see the preceding links for more background or reference information. 6 | 7 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if ! [[ -f .dkrc ]]; then 4 | echo "Please run this script from the project root directory." >&2 5 | exit 64 # EX_USAGE 6 | fi 7 | 8 | if ! [[ -d .devkit ]]; then 9 | # Modify this line if you want to pin a particular .devkit revision: 10 | git clone -q --depth 1 https://github.com/bashup/.devkit 11 | fi 12 | 13 | exec ".devkit/dk" "$(basename "$0")" "$@" 14 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/clean: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/setup: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | bootstrap -------------------------------------------------------------------------------- /script/update: -------------------------------------------------------------------------------- 1 | bootstrap --------------------------------------------------------------------------------