Awesome
LOcally-configured COmmands with loco
Contents
<!-- toc --> <!-- tocstop -->Overview
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.
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.
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:
loco.frob() { frobulate --cromulently -x 6 "$@"; }
loco.biz() { spizz --bizz "$@"; }
Then running loco biz baz
in any subdirectory of your project will execute spizz --bizz baz
in the project root.
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.
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
:
#!/bin/bash -e
source "$(command -v loco)"
# Pass unrecognized subcommands to docker-compose
loco_exec() { docker-compose "$@"; }
# Do everything else the usual loco way
loco_main "$@"
When you run this new doco
wrapper script, it will:
- look for site configuration in
/etc/doco/config
- look for user configuration in
~/.docorc
, and - look for local configuration in a
.doco
file in or above the current directory - change to the directory where it was found
- expect its first argument to be a command defined as a
doco.commandname()
function - pass its entire command line to
docker-compose
if there wasn't a matching function
You can also change any of these file naming conventions, behaviors, etc., as explained in the sections that follow.
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()
.)
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:
doco.reup() {
# Take all services down and back up again
doco down && doco up -d
}
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.
Installation and Customization
To install loco
, just download bin/loco to some directory on your PATH
, and start making configuration files or wrapper scripts. (If you have basher, you can just basher install bashup/loco
.)
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.)
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
:
LOCO_SITE_CONFIG=/etc/docker/doco.conf
LOCO_RC=doco.conf
LOCO_FILE=docofile
(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.)
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.)
Defining Commands
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
.
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.
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.
Exposed and/or Configurable Variables
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.)
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.
Variable | Default Value | Notes |
---|---|---|
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 |
LOCO_COMMAND | basename $LOCO_SCRIPT | Used in loco_usage() message |
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 . |
LOCO_PWD | directory the script was invoked from | Project directory search begins here |
LOCO_SITE_CONFIG | /etc/$LOCO_NAME/config | Full path of site-wide config file |
LOCO_RC | .${LOCO_NAME}rc | User-level config file name |
LOCO_USER_CONFIG | $HOME/$LOCO_RC | User-level config file full path |
LOCO_LOAD | "source" | Command or function used to read project-level config files |
LOCO_FILE | (".${LOCO_NAME}") | Array of globs matching project-level config files. |
LOCO_PROJECT | $(loco_findproject "$@") | The found path of the project-level config file (not set until just before loco_loadproject is called) |
LOCO_ROOT | $(dirname LOCO_PROJECT) | The project root directory, which loco will cd to before sourcing or reading$LOCO_PROJECT |
LOCO_ARGS | ("$@") | Array of the original arguments passed to loco_main , set by loco_config just before calling loco_preconfig . |
Callable and/or Overrideable Functions
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.
Function | Input(s) | Default Results | Notes |
---|---|---|---|
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 |
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 |
loco_site_config | filename | source filename | Override in a wrapper script to change how the LOCO_SITE_CONFIG file is loaded |
loco_user_config | filename | source filename | Override in a wrapper script or site config to change how the LOCO_USER_CONFIG file is loaded |
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 |
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 |
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. |
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. |
loco_loadproject | project-file | cd $LOCO_ROOT; $LOCO_SOURCE "$LOCO_PROJECT" | Change to the project directory, and load the project file. |
loco_usage | Usage message to stderr; exit errorlevel 64 | Override to provide a more informative message | |
loco_error | message(s) | Outputs message to stderr, exit errorlevel 64 | Used by loco_usage |
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. |
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.) |
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. |
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 "$@" . |
Utility Functions
These functions can be used in your scripts, but must not be redefined, as they are also used internally by loco:
fn_exists
functionname -- succeeds if functionname is the name of an existing bash functionfn_copy
srcfunc newname -- copies the body of srcfunc to a new function named newname; overwrites newname if it already exists.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, setsREPLY
to the full path of the found file, otherwise failure is returned.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.
loco also bundles the realpaths module, so all of its path-manipulation functions are also available.