case / esac
Organize your shell commands and functions in a tree ๐ฒ
Compile your individual .sh
files into a single command ๐พ
# Organize source files
๐ src/
# Use directories to organize commands
๐ salutations
# Use individual source files for each command
๐พ hello.sh
# [hello.sh]
echo "Hello, how are you?"
# Compile your entire source directory into a single file
caseEsac compile --src src/ --out greetings.sh
# Newly compiled shell script file!
๐พ greetings.sh
$ ./greetings.sh salutations hello
# => "Hello, how are you?"
Simpler Source
- Write multiple smaller files to simplify your codebase!
- Reuse duplicated code using
!include
for snippets - Inline other functions using
!inline
for performance
Built for Functions
- Rename
local
variables to avoid variable name collisions! - Shortcut available for accessing original command arguments
!args
- Shortcut available for invoking other compiled functions
!fn
Configurable
- Make it your own by authoring your own source file processors!
- Many flags available
--prefix-locals
--file-header HEADER.sh
- Support for environment variables
CASE_ESAC_PREFIX_LOCALS=true
Getting Started
Installation
Download the latest version by clicking one of the download links above or:
curl -o- https://case-esac/installer.sh | bash
Create your source folder
For a command like myCommand options set [args]
src/
options/
set.sh
# [set.sh]
echo "You called options set with $# arguments: $*"
Run the compiler
./caseEsac compile --src src/ --out myCommand.sh
Run your new command!
$ ./myCommand.sh options set hello world
# => "You called options set with 2 arguments: hello world"
- You can now add new subcommands by adding new files and folders!
Learn More
- Helpers
- Handling Subcommands with
.index.sh
- Configuration Options
- Custom File Parsing
local
Variable Prefixing
Helpers
!fn
!fn
in your code will be replaced with the top-level command name:
# [set.sh]
echo "You called the !fn function"
./myCommand options set
# => "You called the myCommand function"
!command
!command
in your code will be replaced with the full command name:
# [set.sh]
echo "You called the '!command' command"
./myCommand options set
# => "You called the 'myCommand options set' command"
!args
Use the !args
array in your code to reference the original function arguments
# [set.sh]
echo "You called set with $# arguments: $*"
echo "${#!args[@]} original arguments: ${!args[@]}"
./myCommand options set
# => "You called the set with 0 arguments: "
# => "2 original arguments: options set"
./myCommand options set hello
# => "You called the set with 1 arguments: hello"
# => "3 original arguments: options set hello"
!include
Include partial snippets in your code using !include [snippet]
src/
options/
_partial.sh
set.sh
# [partial.sh]
echo "Hello from partial snippet"
# [set.sh]
!include partial
echo "Hello from set"
$ ./myCommand options set
# => "Hello from partial snippet"
# => "Hello from set"
You can provide a path as well.
caseEsac
walks up the source tree from the location of your command to look for the partial,
walking up one directory at a time until it finds the source root directory.
src/
options/
set.sh
partials/
_partial.sh
# [set.sh]
!include partials/partial
echo "Hello from set"
Partial code includes are expected to begin with
_
by default
!inline
Use !inline
to include the source of another function.
src/
hello.sh
goodbye.sh
subcommand/
greetings.sh
# [hello.sh]
echo "Hello"
!inline goodbye
!inline subcommand greetings
# [goodbye.sh]
echo "Goodbye"
# [subcommand/greetings.sh]
echo "Greetings!"
$ ./myCommand hello
# => "Hello"
# => "Goodbye"
# => "Greetings!"
!inline!
is the same as !include
with the following exceptions:
- Commands may be included
- Commands are referenced by command name, not by path
!error
!error
provides errors which:
- Include the original full command name
- Include a stacktrace
# [set.sh]
!error 1 [Error] "Something blew up!"
$ ./myCommand options set
# => "`myCommand options set` [Error] Something blew up!"
# => ""
# => "Stacktrace:"
# => "..."
!error
has flexible syntax for ease-of-use:
!error
- Show a generic error message (return 1
)!error "Message"
Show the provided message (return 1
)!error 2
- Show a generic error message (return 2
)!error 2 "Message"
- Show the provided message (return 2
)!error "Message" "More"
- Show all provided messages (one per line)
Handling Subcommands with .index.sh
In the following example, calling myCommand options
will fail:
src/
options/
set.sh
$ ./myCommand options
# => "myCommand options: requires arguments, no arguments provided"
$ ./myCommand options hello world
# => "myCommand options: unknown command 'hello'"
Add an .index.sh
file to the root of options/
to handle calls to options
:
src/
options/
.index.sh
set.sh
# [index.sh]
echo "You called options with $# arguments: $*"
return 0
./myCommand options
# => "You called options with 0 arguments: "
./myCommand options hello world
# => "You called options with 2 arguments: hello world"
โน๏ธ Note: if you do not
return
from your.index.sh
, subcommand processing will continue.e.g. the same error will be thrown or a matching command may be run!
This provides support for authoring setup code for subcommands.
Code in your
.index.sh
has an impact on subcommand processing.e.g.
shift
will change which subcommandcaseEsac
searches for!
Configuration Options
-f , --flag |
Description |
---|---|
-s , --src |
required Directory containing .sh source files representing a tree of commands and subcommands |
-o , --out |
required Path to file to compile all .sh source files into, this is the end result! |
-f , --fn |
The name of the top-level function in the compiled source binary (defaults to the filename provided by --out ) |
-s , --check-syntax |
This will evaluate the resulting code and display syntax errors (this is enabled by default) |
-r , --reformat |
This will use BASH to reformat the command based on BASHโs preferred syntax (note: this will strip all comments) |
-c , --no-comments |
This will prevent comments from being included in the final source file. |
-e , --no-exe |
This will prevent the resulting source file from being created as an executable (default is to chmod +x the generated file). |
-l , --locals-prefix |
Defines the prefix prepended to local variable names (defaults to __\${FULL_COMMAND[*]// /__}__ , any non alpha-numberic character is translated to a '_' (not configurable)) Read below for more info |
-x , --no-prefix-locals |
Do not prefix local variables (local prefixes are enabled by default) Read below for more info |
-i , --index |
Name of file to use in subcommand folders as an index (defaults to .index.sh ) |
-h , --header |
Name of file to use as a header for the generated file (header is added below the #! ) |
-f , --footer |
Name of file to use as a footer for the generated file (footer is added above the main() code which runs the function) |
-n , --fn-header |
Name of file to use as the header inside the generated function |
-t , --fn-footer |
Name of file to use as the footer inside the generated function (this is only evaluated if commands do not return ) |
-b , --hashbang |
Specify a custom hashbang or โshebangโ, e.g. #! /bin/bash (defaults to #! /usr/bin/env bash ) |
-p , --processor |
Add a processor function Read below for more info |
--keyword-include |
Override the !include keyword to something else |
--keyword-inline |
Override the !inline keyword to something else |
--keyword-error |
Override the !error keyword to something else |
--keyword-fn |
Override the !fn keyword to something else |
--keyword-FN |
Override the !FN keyword to something else |
--keyword-command |
Override the !command keyword to something else |
--keyword-shared |
Override the :shared: keyword to something else |
--keyword-args |
Override the !args keyword to something else |
--variable-args |
Override the variable name used to store !args (defaults to __!fn__args ) |
--partial-prefix |
Override the file prefix used to search for partial files (defaults to _ ) |
--error-not-found |
Override the error message shown when a command is called and no valid subcommand is available (defaults to !error "Command not found '$1'" ) |
--error-no-arguments |
Override the error message shown when a command with no index is called without any arguments (defaults to !error "Arguments are required but none were provided" ) |
--error-generic |
Override the generic error message shown when !error is called without a provided error message (defaults to An error occurred ) |
--error-prefix |
Override the prefix shown with all error messages thrown via !error (defaults to `!command` ) |
--error-silence-variable |
Override the name of the variable which can be configured to true to silence the stacktrace shown by !error (defaults to !FN_SILENCE ) |
--error-silence-loc-variable |
Override the name of the variable which can be configured to true to prevent line of source code from being shown in the stacktrace shown by !error (defaults to !FN_SILENCE_LOC ) |
--error-stacktrace-skip |
Override the number of levels in the stacktrace which are skipped and now shown to the user, e.g. to prevent showing the function which simply throws the error (defaults to 3 ) |
--error-stacktrace-max |
Override the maximum number of levels to show in the stacktrace (defaults to 100 ) |
โน๏ธ All options can alternatively be set using Environment Variables
Custom File Parsing
Perhaps you would like to DRY some redundant code in your source?
Or simply make a little shortcut, e.g. something like !fn
or !command
?
Creating a Custom File Parser is easy, see: Creating a Custom File Parser
local
Variable Prefixing
Why does caseEsac
prefix all local
variables by default?
Well, in BASH, local
variables are available to all functions called by that function.
myProgram() {
local count=10 # this program sets a local
unrelatedFunction # and runs a function
echo "Count: $count"
}
unrelatedFunction() {
count=42 # this function can modify the parent
# function's local variable
# (usually unintentionally)
}
myProgram
# => Count: 42
This happens ALL the TIME
local
variable naming collisions are a very easy mistake to make.
If every function strictly uses local
and avoids common names for global variables,
things are generally OK but Iโve spent hours on bugs where my library code was using
a variable of the same name as an unrelated function and it sucked.
This is why, by default, caseEsac
gives all local
variables very unique names:
local name="Rebecca"
# If this local is in the 'greetings hello' command,
# then the local variable becomes:
local __myFunction__greetings__hello__name="Rebecca"
This is very easy to disable:
./caseEsac --src src/ --out myCommand.sh --no-prefix-locals
# or simply:
./caseEsac --src src/ --out myCommand.sh -x
Prevent Variable Renaming
To prevent an individual local
from being renamed, use :shared:
local name="Rebecca"
local dog="Parker" # :shared:
local __myFunction__greetings__hello__name="Rebecca"
local dog="Parker" # <--- this is not renamed by caseEsac
This is a nice self-documenting option which notes that the variable youโre defining may be used elsewhere (e.g. in child functions)