View on GitHub

๐Ÿ—œ๏ธ Command Compiler

case / esac - combine multiple source files into one!

download .ZIPdownload .TGZ

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

Built for Functions

Configurable

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"

Learn More

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:

!error

!error provides errors which:

# [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:

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 subcommand caseEsac 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)