Skip to content

cgabriel5/nodecliac

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nodecliac

nodecliac logo

Easy Bash completion for CLI programs

Install

$ bash <(curl -Ls git.io/nodecliac) && source ~/.bashrc
More installation methods

curl Install (explicit defaults):

$ bash <(curl -Ls git.io/nodecliac) --installer= --branch=master --rcfile=~/.bashrc && source ~/.bashrc

wget Install (defaults):

$ bash <(wget -qO- git.io/nodecliac) && source ~/.bashrc

Manual Install: One can also install manually.

  1. First download the GitHub nodecliac repository.
  2. Next unzip the folder via $ unzip nodecliac-*.zip or by right-clicking and using the OS provided extractor utility.
  3. cd into the repository and install: $ sudo chmod +x install.sh && ./install.sh --manual && source ~/.bashrc
  4. Delete the downloaded zip folder, its extracted folder, and start using.

Checksum Install: If desired, the install script file's integrity can be verified before running.

install.sh sha256sum checksum: 2b4f37dae3cbe5cc81a0ce52f55a5d300445e3e3a68d9d7e97040504e1aec9da

Create an executable shell file called install.sh, add the following, and run it.

#!/bin/bash

# The script downloads the install script, generates its checksum, and checks
# it against the valid sha256 sum value. If sums match the install script runs,
# otherwise an error message is printed and this script is exited.

install() {
    url="git.io/nodecliac"
    is="$([[ "$(command -v curl)" ]] && sudo curl -Ls "$url" || sudo wget -qO- "$url")"
    x=($([[ "$OSTYPE" == "darwin"* ]] && shasum -a 256 <<< "$is" || sha256sum <<< "$is"))
    c="2b4f37dae3cbe5cc81a0ce52f55a5d300445e3e3a68d9d7e97040504e1aec9da"
    err="\033[1;31mError\033[0m: Verification failed: checksums don't match."
    [[ "$c" == "$x" ]] && bash <(echo "$is") \
        --installer= \
        --branch=master \
        --rcfile=~/.bashrc \
        && source ~/.bashrc || echo -e "$err" && exit 1
} && install
Installation options
  • --installer: The installer to use. (default: yarn > npm > binary)
    • yarn: Uses yarn to install.
    • npm: Uses Node.js's npm to install.
    • binary: Uses nodecliac's Nim Linux/macOS CLI tools.
  • --branch: An existing nodecliac branch name to install. (default: master)
  • --rcfile: bashrc file to install nodecliac to. (default: ~/.bashrc)
  • --yes: Automate install by saying yes to any prompt(s).
  • --packages: Install collection of pre-made completion packages.
  • --manual: Let's install script to take manual install route.
  • --update: Let's install script to take update router over fresh install route.
Requirements
Uninstall
$ nodecliac uninstall

If a custom rcfile path was used during install provide it again during uninstall.

$ nodecliac uninstall --rcfile=path/to/.bashrc

What Is nodecliac?

bash-completion is awesome. It enhances the user experience by completing paths, file names, commands, flags, etc. Ironically enough, having to use Bash to add it to one's program puts some off from using it.

nodecliac's approach is different. Rather than directly using Bash, nodecliac provides a layer of abstraction. It lets one easily map a program's commands with their flags in an auto-completion map (.acmap) file. Merely write the program's .acmap, compile to .acdef, and let nodecliac handle the rest. That's it.

If Bash is needed, .acmap files are flexible enough to run shell code to generate matches. Better yet, write the necessary completion logic in a familiar language like Perl, Python, Ruby, etc., and use Bash as glue code to tie it all together.

In all, this project aims to 1 minimize the effort needed to add bash-completion so more programs support it, 2 provide a uniform bash-completion experience, and 3 to ultimately build a collection of community made completion packages for all to enjoy.

Why the name nodecliac? Originally it was made in mind for Node.js programs. However, as development continued it proved useful for non Node.js programs as well.

How It Works

The idea is simple. nodecliac uses two file types: auto-completion map (.acmap) and auto-completion definition (.acdef). Those familiar with CSS preprocessors will quickly understand. Similar to how .sass and .less files are compiled to .css — .acmap files must be compiled to .acdef.

Therefore, an .acmap is a user generated file that uses a simple syntax to map a program's commands with their flags. While an .acdef is a nodecliac generated definition file. It's this file nodecliac uses to provide completions.

Ok, 1 but where do these files go? 2 How does nodecliac use them? Good questions. 1 Files will end up in the command's completion package; a folder containing the necessary files needed to provide completions for a command. 2 Completion packages will end up in nodecliac's registry (nodecliac's collection of completion packages).

With that, nodecliac can provide Bash completions for programs by using their respective completion package stored in the registry.

Expand section

nodecliac CLI diagram

With the program's completion package created and stored in the registry the following is possible:

  1. Tab key pressed: Bash completion invokes nodecliac's completion function for the program.

  2. CLI input analysis: Input is parsed for commands, flags, positional arguments, etc.

  3. .acdef lookup: The program's .acdef is compared against the CLI input to return possible completions.

Complete details/events are oversimplified and condensed to get the main points across.

CLI

CLI commands/flags
Commands:

make

Compile .acdef.

  • --source=: (required): Path to .acmap file.
  • --print: Log output to console.
Usage
$ nodecliac make --source path/to/program.acmap # Compile .acmap file to .acdef.
Test/debugging flags (internal)
  • --trace: Trace parsers (for debugging).
  • --test: Log output without file headers (for tests).

format

Format (prettify) .acmap file.

  • --source=: (required): Path to .acmap file.
  • --strip-comments: Remove comments when formatting.
  • --indent="(s|t):Number": Formatting indentation string:
    • s for spaces or t for tabs followed by amount-per-indentation level.
      • t:1: Use 1 tab per indentation level (default).
      • s:2: Use 2 spaces per indentation level.
  • --print: Log output to console.
Usage
# Prettify using 2 spaces per indentation level and print output.
$ nodecliac format --source path/to/program.acmap --print --indent "s:2"
Test/debugging flags (internal)
  • --trace: Trace parsers (for debugging).
  • --test: Log output without file headers (for tests).

init

Starts nodecliac's completion package generator to easily scaffold a completion package.

  • --force: Overwrites existing folder of the same name.
Usage
$ nodecliac init

bin

Prints nodecliac's bin location.

  • No arguments
Usage
$ nodecliac bin # Binary location.

cache

Interact with nodecliac's cache system.

  • --clear: Clears cache.
  • --level=<level>:
    • Without argument it prints the current cache level.
    • With argument it sets cache level to provide level.
      • Levels: 0, 1, 2
Usage
$ nodecliac cache --clear # Clear cache.
$ nodecliac cache --level # Print cache level.
$ nodecliac cache --level 1 # Set cache level to 1.

setup

Setup nodecliac.

  • --force: (required if nodecliac is already setup): Overwrites old nodecliac setup and installs anew.
  • --yes: Automate install by saying yes to any prompt(s).
  • --rcfile: By default ~/.bashrc is used. If another rcfile should be used provide its path.
  • Note: Setup appends ncliac=~/.nodecliac/src/main/init.sh; [ -f "$ncliac" ] && . "$ncliac"; to rcfile.
Usage
$ nodecliac setup # Setup nodecliac.
$ nodecliac setup --force # Force nodecliac setup.
$ nodecliac setup --force --yes # Force nodecliac setup and assume yes to any prompt(s).

status

Returns status of nodecliac (enabled or disabled).

  • --enable: Enables nodecliac.
  • --disable: Disables nodecliac.
Usage
$ nodecliac status # Get nodecliac's status.
$ nodecliac status --enable # Enable nodecliac.
$ nodecliac status --disable # Disable nodecliac.

uninstall

Uninstalls nodecliac.

  • --rcfile: Path of rcfile used in setup to remove changes from.
Usage
$ nodecliac uninstall # Remove nodecliac.

print

Print acmap/def file contents for files in registry.

  • --command=: Name of command (uses available packages in registry).
  • Note: Command is rather pointless and is primarily used to showcase command-strings.
Usage
$ nodecliac print --command=<command> # Print .acdef for given command.

registry

Lists packages in registry.

  • No arguments
Usage
$ nodecliac registry # Print packages in registry.

add

Adds package to registry.

  • --path: Path to completion package.
  • --repo: Repo to install completion package from.
    • Github: Repo only (master): <username>/<repo_name>
    • Github: Repo branch (default: master): <username>/<repo_name><#branch_name>
    • Github: Repo sub-directory: <username>/<repo_name>/trunk/<sub_directory_path>
    • Github: Repo branch + sub-directory: <username>/<repo_name><#branch_name>/trunk/<sub_directory_path>
    • Or GitHub, GitLab, or BitBucket URL to completion package: <repo_url>
      • URL must start with git@ or htttp:// and end with .git.
  • --allow-size: Disables local repo package 10MB size check, letting for any size.
    • Meant as a safeguard to prevent accidentally copying large folders.
  • --allow-structure: Disables valid base completion package structure checks.
  • --allow-overwrite: Disables overwrite warning of same name package in registry.
  • --force: Skip all guards/checks (size, structure, overwrite).
    • --size: Size of local repo is no longer checked when copying to registry.
    • --structure: Basic completion package structure checks are disabled.
    • --overwrite: Same name completion package overwriting is allowed.
Usage
$ nodecliac add # Copies cwd folder (completion package) to registry.
$ nodecliac add --path ~/Desktop/subl # Installs completion package at specified path.

$ nodecliac add --repo cgabriel5/nodecliac # Install completion package from a GitHub repo.
# Install completion package from a specific branch (defaults to master branch).
$ nodecliac add --repo cgabriel5/nodecliac#master
# Install completion package from a specific directory in a GitHub repo.
$ nodecliac add --repo cgabriel5/nodecliac/trunk/resources/packages/yarn
# Install completion package from a specific directory + branch (defaults to master branch).
$ nodecliac add --repo cgabriel5/nodecliac#dev/trunk/resources/packages/yarn

# Install completion package via GitHub, GitLab, BitBucket URL.
$ nodecliac add --repo <repo_url>

remove

Removes package(s) from registry.

  • Takes n-amount of package names as arguments.
  • --all: Removes all packages in registry.
Usage
$ nodecliac remove # Removes cwd folder (completion package) from registry.
$ nodecliac remove --all # Removes all packages from registry.

link

Creates soft symbolic link of package in registry.

  • --path: Path to completion package.
Usage
$ nodecliac link # Symlinks cwd folder (completion package) to registry.
$ nodecliac link --path ~/Desktop/subl # Symlinks completion package at specified path.

unlink

Alias to remove command.

Usage
$ nodecliac unlink # Removes cwd folder (completion package) from registry.
$ nodecliac unlink --all # Removes all packages from registry.

enable

Enables completions for package(s).

  • Takes n-amount of package names as arguments.
  • --all: Enables all packages in registry.
Usage
$ nodecliac enable # Enables disabled package(s).
$ nodecliac enable --all # Enables all disabled packages.

disable

Disables completions for package(s).

  • Takes n-amount of package names as arguments.
  • --all: Disables all packages in registry.
Usage
$ nodecliac disable # Disables enabled package(s).
$ nodecliac disable --all # Disables all enabled packages.

refresh

Fetches and updates the list of nodecliac completion packages.

  • No arguments
Usage
$ nodecliac refresh

CLI quick usage

Compile .acmap files to .acdef.

$ nodecliac make --source path/to/program.acmap

Prettify .acmap file

# Prettify using 2 spaces per indentation level and print output.
$ nodecliac format --source path/to/program.acmap --print --indent "s:2"
CLI anatomy breakdown

nodecliac assumes following CLI program design pathway:

  • program-name → subcommands → short-flags/long-flags → positional-parameters
$ program [subcommand ...] [-a | -b] [--a-opt <Number> | --b-opt <String>] [file ...]
  ^^^^^^^  ^^^^^^^^^^^^^^   ^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   ^^^^^^^^
     |            \             \                      |                   /
  CLI program's   Program        Program          Program long     Program's (flag-less)
  command.        subcommands.   short flags.     flags.           positional parameters.

Syntax

acmap (auto-completion map) is a simple domain-specific language which maps a program's commands to their flags in .acmap files.
Constructs:

Comments

  • Comments begin with a number-sign (#) and continue to the end of the line.
  • Whitespace indentation can precede a comment.
  • Trailing comments are allowed.
  • Multi-line comments are not supported.
# This is a comment.
    # Whitespace can precede comment.
program.command = --flag # A trailing comment.

Settings

  • Settings begin with an at-sign (@) followed by the setting name.
  • Setting values are assigned with = followed by the setting value.
  • Any amount of whitespace before and after = is allowed.
  • Whitespace indentation can precede a setting declaration.
  • Note: Settings can be declared anywhere within your .acmap file.
    • However, it's best if declared at the start of file to quickly spot them.
# Available settings.
@compopt   = "default"
@filedir   = ""
@disable   = false
@placehold = true
Available Settings:
  • @compopt: comp-option (-o) value to Bash's builtin complete function.
    • Values: false (no value), true (default: false)
  • @filedir: Pattern to provide bash-completion's _filedir function.
    • Values: A string value (i.e. "@(acmap), "-d") (default: "")
  • @disable: Disables bash-completion for command.
    • Values: false, true (default: false)
  • @placehold: Placehold long .acdef rows to provide faster file lookups.
    • Values: false, true (default: false)
    • Note: Used only when compiling .acdef files.

Variables

  • Variables begin with a dollar-sign ($) followed by the variable name.
  • Variable name must start with an underscore (_) or a letter (a-zA-Z).
  • Variable values are assigned with = followed by the variable value.
  • A variable's value must be enclosed with quotes.
  • Any amount of whitespace before and after = is allowed.
  • Whitespace indentation can precede a variable declaration.
  • Note: Variables can be declared anywhere within your .acmap.
$scriptpath = "~/path/to/script1.sh"
$scriptpath="~/path/to/script2.sh"
$scriptpath    =   "~/path/to/script3.sh"

# Note: `$scriptpath` gets declared 3 times.
# It's final value is: "~/path/to/script3.sh"
Variable Interpolation

Variable Interpolation

  • Variables are intended to be used inside quoted strings.
  • Interpolation has the following structure:
    • Start with ${ and close with }.
    • Any amount of space between opening/closing syntax is allowed.
    • The string between the closing/starting syntax is the variable name.
$mainscript = "~/.nodecliac/registry/yarn/init.sh"

yarn.remove = default $("${mainscript} remove")
yarn.run = default $("${mainscript} run")
Variable Builtins

Variable Builtins

acmaps provide the following builtin variables:

  • $OS: The user's platform: linux, macosx
  • $HOME: The user's home directory.
  • $COMMAND: The command being completed.
  • $PATH: The command's nodecliac registry path:
    • For example: ~/.nodecliac/registry/<COMMAND>

Command Chains

  • Commands/subcommands should be viewed as chains which read from left to right.
  • They start with the CLI program's name, are followed by any commands/subcommands, and are dot (.) delimited.
  • If a (sub)command happens to use a dot then simply escape the dot. Non escaped dots will be used as delimiters.
  • Whitespace indentation can precede a command chain.

Example: Say the CLI program program has two commands install and uninstall. It's .acmap will be:

program.install
program.uninstall
Command default documentation

Command Chain Default

A command chain's default command-string (a runable shell command string) can be used to dynamically generate auto-completion items. This command-string is run when no completion items (commands/flags) are returned. Think of it as a fallback.

  • Start by using the keyword default followed by a whitespace character.
  • Follow that with the command-string:
    • A command string is denoted with starting $( and closing ).
    • The string between the closing/starting syntax is the command-string.
    • Example: default $("./path/to/script.sh arg1 arg2")
program.command = [
  default $("./path/to/script.sh arg1 arg2")
]
Command-string example

For example, say we are implementing an .acmap file for the dependency manager yarn and would like to return the names of installed packages when removing a package (i.e.$ yarn remove...). Essentially, we want to extract the package.json's dependency and devDependency entries and supply them to nodecliac. Using a command-string one can run a script/shell command to do just that.

yarn.remove = [
  # The command will run on '$ yarn remove [TAB]'. The script 'script.sh' should contain the
  # logic needed to parse package.json to return the installed (dev)dependency package names.
  default $("~/.nodecliac/registry/yarn/script.sh")
]
Command-string escaping

Varying Levels Of Escaping.

  • Level 0: Hypothetical script.sh with the following contents. No extra escaping when running a script.
for f in ~/.nodecliac/registry/yarn/hooks/*.*; do
  [[ "${f##*/}" =~ ^(pre-parse)\.[a-zA-Z]+$ ]] && echo "$f"
done
  • Code Breakdown

    • The code will loop over the ~/.nodecliac/registry/yarn/hooks directory.
    • File names matching the pattern (^(pre-parse).[a-zA-Z]+$) will print to console.
  • Level 1: If bash is one's default shell, copy/paste and run this one-liner in a Terminal:

for f in ~/.nodecliac/registry/yarn/hooks/*.*; do [[ "${f##*/}" =~ ^(pre-parse)\.[a-zA-Z]+$ ]] && echo "$f"; done
  • Level 2: Now say we want to run the same line of code via bash -c. Run the following in a Terminal:
bash -c "for f in ~/.nodecliac/registry/yarn/hooks/*.*; do [[ \"\${f##*/}\" =~ ^(pre-parse)\\.[a-zA-Z]+$ ]] && echo \"\$f\"; done;"
  • Level 3: How about using Perl to run bash -c to execute the command?
perl -e 'print `bash -c "for f in ~/.nodecliac/registry/yarn/hooks/*.*; do [[ \\\"\\\${f##*/}\\\" =~ ^(pre-parse)\\.[a-zA-Z]+\$ ]] && echo \"\\\$f\"; done;"`';

As shown, the more programs involved the more escaping required due to the string being passed from program to program. Escaping can get cumbersome. If so, running the code from a file will be the easiest alternative.

Example: Command-string escaping.

Now let's make a command-string to print all .acdef file names (without extension) in the nodecliac registry:

$ s="";for f in ~/.nodecliac/registry/*/*.acdef; do s="$s$f\n"; done; echo -e "$s" | LC_ALL=C perl -ne "print \"\$1\n\" while /(?! \/)([^\/]*)\.acdef$/g"

Using the following .acmap contents the command-string would be the following:

  • Note: Ensure the | and \ characters are escaped.
# The escaped command-string.
$cmdstr = 's="";for f in ~/.nodecliac/registry/*/*.acdef; do s="$s$f\\n"; done; echo -e "$s" \| LC_ALL=C perl -ne "print \"\$1\\n\" while /(?! \\/)([^\\/]*)\\.acdef$/g"'

nodecliac.print = --command=$('${cmdstr}')

Compiling to .acdef, an .acdef file with the following contents will be generated:

# DON'T EDIT FILE —— GENERATED: Mon Mar 02 2020 14:15:13 (1583187313)

 --
.print --command=|--command=$('s="";for f in ~/.nodecliac/registry/*/*.acdef; do s="$s$f\\n"; done; echo -e "$s" \| LC_ALL=C perl -ne "print \"\$1\\n\" while /(?! \\/)([^\\/]*)\\.acdef$/g"')

Ignoring Options

Letting the completion engine know an option should be ignored (not displayed) is simple. Merely prefix the option with an exclamation-mark (!). This is meant to be used when an option has already been used and therefore doesn't need to be shown again as a possible completion item.

Note: For more information about command-strings take a look at acmap Syntax > Flags > Flag Variants > Flags (dynamic values). The section contains more details for command-strings like special character escaping caveats, dynamic/static arguments, and examples with their breakdowns. Keep in mind that the section uses the term command-flag due it being used for flags but command-flag and command-string are effectively the same thing — just a runable shell command string. The naming (command-{string|flag}) is based on its application (i.e. for command-chains or flags).

Command chain grouping

Command Chain Grouping

Command chains can be grouped. It is not necessary but doing may help condense acmaps.

  • A command group is denoted with starting { and closing }.
  • The commands are found in between the closing/starting syntax.
  • Commands are comma delimited.

For example, take the following:

program.deploy-keys.add
program.deploy-keys.list
program.deploy-keys.rm

Grouping can reduce it to:

program.deploy-keys.{add,list,rm}

Flags

To define flags we need to extend the command chain syntax.

  • Flags are wrapped with = [ and a closing ].
  • The = [ must be on the same line of the command chain.
  • The closing ] must be on its own line and man have any amount of indentation.

Building on the command chain section example, say the install command has the flags: destination/d and force/f. Code can be updated to:

program.install = [
  --destination
  -d
  --force
  -f
]
program.uninstall

However, it can be cleaned up a bit by using the flag alias syntax:

program.install = [
  --destination::d
  --force::f
]
program.uninstall
Flag variants
Types:
Keywords:

Flags (input)

  • If flag requires user input append = to the flag.
program.command = [
  --flag=
]

Flags (boolean)

  • If flag is a switch (boolean) append a ? to the flag to let the completion engine know the flag doesn't require value completion.
program.command = [
  --flag?
]

Flags (multi-flag)

  • Sometimes a flag can be supplied multiple times.
  • Let the completion engine know this by using the multi-flag indicator *.
program.command = [
  # Allow user to provide multiple file paths.
  --file=*

  # Hard-coded values.
  --colors=*(red green yellow)
]

Flags (one liner)

  • This method should be used when the flag value list can be kept to a single line.
  • Note: Values must be delimited with spaces.
  • Note: When a flag has many values a long form list should be used for clarity's sake.
program.command = [
  # Supply 1, "2", false, 4 as hard-coded values.
  --flag=(1 "2" false 4)

  # If multiple values can be supplied to program use the multi-flag indicator '*'.
  # This allows --flag to be used multiple times until all values have been used.
  --flag=*(1 "2" false 4)
]

Flags (long form)

  • Flag long form lists are wrapped with starting =( and a closing ).
  • The =( must be on the same line as the flag.
  • The closing ) must be on its own line and man have any amount of indentation.
  • A flag value option starts with - (a hyphen + a space) followed by the value.
  • Any amount of whitespace indentation can precede the flag value option - sequence.
program.command = [
  --flag=(
    - 1
    - "2"
    - false
    - 4
  )

  # Allow flag to be used multiple times.
  --flag=*(
    - 1
    - "2"
    - false
    - 4
  )
]
program.uninstall

Flags (dynamic values)

Sometimes static values are not enough so a command-flag can be used. A command-flag is just a runnable shell command.

command-flag syntax:

  • Begins with starting $(, followed by command, and ends with closing ).
  • Output: a newline (\n) delimited list is expected.
    • Each completion item should be on its own line.
  • Example: $("cat ~/colors.text")
  • Note: Command must be quoted (double or single).

static or dynamic arguments may be provided.

  • Example: $("cat ~/colors.text", "!red", $"cat ~/names.text"):
    • This provides the static !red and dynamic cat ~/names.text arguments.
    • !red will be argument 0 and the output of cat ~/names.text will be argument 1.
  • Note: dynamic arguments must be dollar-sign prefixed ($).

Escaping: $ and | are used internally so require escaping when used.

  • --flag=$("echo \$0-\$1", $"echo 'john'", "doe"):
    • The $s in the command are escaped.
  • --flag=$("nodecliac registry \| grep -oP \"(?<=─ )([-a-z]*)\""):
    • The | gets escaped here.
    • Note: Inner quotes are also escaped like one would on the command-line.

Example: Showcases dynamic and static values.

program.command = [
  # '*' denotes the flag is a multi-flag.
  --flag=*
  --flag=(
    - index.js
    - ':task:js'
    - "some-thing"
    # Dynamic values get combined with hard-coded values.
    - $("cat ~/values.text")
  )

  # Same as above.
  --flag=*(
    - index.js
    - ':task:js'
    - "some-thing"
    - $("cat ~/values.text")
  )
]
program.uninstall

Keyword (filedir)

When no completion items are found bash-completion's _filedir function is used as a fallback. _filedir performs file/directory completion. By default it returns both file and directory names. However, this can be controlled to only return directory names or files of certain types.

  • Start by using the keyword filedir followed by a whitespace character.
  • Follow that with a string:
    • To only return directories use "-d".
    • To filter file type extensions provide a pattern like "@(acmap)".
    • Example: filedir "@(acmap)"
program.command = [
  filedir "@(acmap)"
]

Note: This filedir usage is per command chain. If this is not needed, a global filedir value can be provided via the @filedir setting like so: @filedir = "@(acmap)". Both can be used but precedence is as follows:

  • If a command uses filedir use that.
  • If not, look for @filedir setting.
  • If neither are provided all files/directories are returned (no filtering).

Keyword (context)

The context keyword provides the ability to disable flags and deal with mutual flag exclusivity.

  • Start by using the keyword context followed by a whitespace character.
  • Follow that with a string:
    • Conditional Example: context "!help: #fge0"
    • Mutual Exclusivity Example: context "{ json | yaml | csv }

Context String (conditional):

Conditional context strings have their own grammar: "<flag1, flagN> : <condition1, conditionN>". If each <condition> results in true the <flags> are enabled/disabled.

Flag grammar
  • A flag is represented without the hyphens.
    • Example: For the flag --help it would just be help.
  • If the flag needs to be disabled, prepend a !.
    • Example: help (If conditions are true flag will be enabled)
    • Example: !help (If conditions are true flag will be disabled)
Condition grammar
  • Check against flag/positional arguments:
    • Format: # + (f)lag|(a)rgument + operator + number
    • Example (flag check): #fge0
    • Example (argument check): #age0
  • Operators:
    • eq: Equal to
    • ne: Not equal to
    • gt: Greater than
    • ge: Greater than or equal to
    • lt: Less than
    • le: Less than or equal to
  • Number:
    • Must be a positive number.
  • Inversion: Tests can be inverted by prepending a !.
Example 1

Disable help and version flags when used flag count is greater or equal to 0.

program.command = [
  --help?
  --version?
  context "!help, !version: #fge0"
]
Example 2

Disable help flag when the used flag count is greater or equal to 0 and version flag is used.

program.command = [
  --help?
  --version?
  context "!help: #fge0, version"
]

Context String (mutual exclusivity):

Mutual exclusivity is represented like so: "{ flag1 | flagN }". Once a grouped flag is used the other(s) are disabled.

Example 1

For example, say the --json, --csv, and --text flags are allowed but the --json flag is used. The remaining flags --text and --csv won't be shown as completion items.

program.command = [
  --json=,
  --csv=,
  --text=(false true)
  context "{ json | csv | text }"
]
Example 2

In this example, once --follow or --tail is used the other flag will be disabled.

program.command = [
  --follow=,
  --tail=(false true)
  context "{follow | tail}"
]

This is equivalent to the previous example.

program.command = [
  --follow=,
  --tail=(false true)
  context "!follow: tail"
  context "!tail: follow"
]

Combine Context Strings

Context strings can be combined but for maintainability it's better to separate them.

Example 1: Separate Context Strings
program.command = [
  --help?
  --version?
  context "!help, !version: #fge0"

  --json=,
  --csv=,
  --text=(false true)
  context "{ json | csv | text }"

  --follow=,
  --tail=(false true)
  context "{follow | tail}"

  --hours=
  --minutes=
  --seconds=
  --service=

  --job-id=
  --target=
  context "{ job-id | target }"
]
Example 1: Combined Context Strings

Context strings can be combined by delimiting them with ;.

program.command = [
  --help?
  --version?

  --json=,
  --csv=,
  --text=(false true)

  --follow=,
  --tail=(false true)

  --hours=
  --minutes=
  --seconds=
  --service=

  --job-id=
  --target=

  context "!help, !version: #fge0; { json | csv | text }; { follow | tail }; { job-id | target }"
]

Note: Context strings are evaluated on every completion cycle. Therefore, using too many may slow down the 'perceived completion feel' as it takes time to evaluate all provided contexts.

Keyword (exclude)

The exclude keyword is only allowed in a wildcard command block. It serves to easily give all command strings the same (universal/shared) flags. Although this can be done manually, this can help reduce the acmap and make it easier to maintain.

Let's look at an example. All command strings but program.cache share the --help flag.

program = [
  --help?
  --version
]

program.make = [
  --help?
  --extensions=*(js html css)
]

program.format = [
  --help?
  --extensions=*(js html css)
  --indentation
]

program.cache = [
  --clear?
]

Now let's use a wildcard block and exclude the program.cache command string.

* = [
  exclude "program.cache"
  --help?
]

program = [
  --version
]

program.make = [
  --extensions=*(js html css)
]

program.format = [
  --extensions=*(js html css)
  --indentation
]

program.cache = [
  --clear?
]

If desired it can even be condensed to.

* = --help?|exclude "program.cache"
program = --version
program.make,
program.format = --extensions=*(js html css)
program.format = --indentation
program.cache = --clear?

acdef (auto-completion definition) is a subset of acmap syntax, is auto-generated, and is used to perform Bash completion lookups.
Constructs:

.acdef Anatomy

The following example .acdef will be used to explain how to read .acdef files.

# DON'T EDIT FILE —— GENERATED: Mon Mar 02 2020 14:15:13 (1583187313)

 --cache-folder|--check-files|--cwd|--disable-pnp
.access --
.add --audit|--dev|--exact|--ignore-workspace-root-check|--optional|--peer|--tilde
.autoclean --force|--init
.bin --
.cache --
.upgrade --caret|--exact|--latest|--pattern|--scope|--tilde
.why --
.workspace --
.workspaces --
.workspaces.info --
.workspaces.run --

.upgrade default $("~/.nodecliac/registry/command/scripts/init.sh upgrade")
.why default $("command list --depth=0 \| perl -wln -e \"/(?! ─ )([-\/_.@(?)a-zA-Z0-9]*)(?=\@)/ and print $&;\"")
.workspace default $("~/.nodecliac/registry/command/scripts/init.sh workspace")
.workspaces.run default $("~/.nodecliac/registry/command/scripts/init.sh run")

Header

  • The first line is the file's header.
  • It is the only comment in the document.
  • It contains a warning to not modify the file and the file's creation information.
# DON'T EDIT FILE —— GENERATED: Mon Mar 02 2020 14:15:13 (1583187313)

...

Commands/Flags

  • The following section contains the command-chains and their respective flags.
  • Each line represents a row which starts with the command chain and is followed by a single space.
  • Whatever comes after the single space are the command's flags.
    • Flags are delimited by pipe (|) characters.
  • Rows that do not have flags will contain two hyphens (--) after the single space character.
...

 --cache-folder|--check-files|--cwd|--disable-pnp
.access --
.add --audit|--dev|--exact|--ignore-workspace-root-check|--optional|--peer|--tilde
.autoclean --force|--init
.bin --
.cache --
.upgrade --caret|--exact|--latest|--pattern|--scope|--tilde
.why --
.workspace --
.workspaces --
.workspaces.info --
.workspaces.run --

...

Note: Command chain lines, lines starting with a single space or a dot (.) character, have the program's name removed. For example, the line .workspaces.run -- can be viewed as command.workspaces.run --.

Command Fallbacks

  • The bottom section of an .acdef file will contain any command chain fallbacks.
...

.upgrade default $("~/.nodecliac/registry/command/scripts/init.sh upgrade")
.why default $("command list --depth=0 \| perl -wln -e \"/(?! ─ )([-\/_.@(?)a-zA-Z0-9]*)(?=\@)/ and print $&;\"")
.workspace default $("~/.nodecliac/registry/command/scripts/init.sh workspace")
.workspaces.run default $("~/.nodecliac/registry/command/scripts/init.sh run")

Placeholders

  • Depending how complex an .acmap is sometimes placeholders are needed. They are used internally to speed up reading, what would otherwise be large, .acdef files.
  • Placeholder syntax:
    • Begin with --p# and are followed by a fixed number of hexadecimal characters.
    • Example: --p#d2eef1

The following example .acdef showcase placeholders.

# DON'T EDIT FILE —— GENERATED: Thu Apr 09 2020 10:4:22 (1586451862)

 --help|--version
.buildIndex --p#07d43e
.c --p#07d43e
.cc --p#07d43e
.check --p#07d43e
.compile --p#07d43e
.compileToC --p#07d43e
.compileToCpp --p#07d43e
.compileToOC --p#07d43e
.cpp --p#07d43e
.ctags --p#07d43e
.doc --p#07d43e
.doc2 --p#07d43e
.dump --p#07d43e
.e --p#07d43e
.genDepend --p#07d43e
.js --p#07d43e
.jsondoc --p#07d43e
.objc --p#07d43e
.rst2html --p#07d43e
.rst2tex --p#07d43e

Registry

Simply put, the registry (~/.nodecliac/registry) is where completion packages are stored. Completion packages are stored in the form: ~/.nodecliac/registry/<command>.

Packages

Package documentation is divided into their own sections.


Create

Add / Link

Remove / Unlink

Enable / Disable

Pre-made completion packages for several programs of varying complexity are available for reference.

Hooks

Hooks are just regular executable shell scripts that run at specific points along a completion cycle when a completion is attempted. They let one modify internal aspects of nodecliac's completion logic and behavior.

Expand hook section

Available Hooks

  1. hooks/pre-parse.sh: Modifies select initialization variables before running completion script.
  2. hooks/post-parse.sh: Modifies final completions before terminating the completion cycle and printing suggestions.

hooks/ Directory

In the command's completion package create a hooks/ directory. All hook scripts will be stored here.

<command>/
  ├── <command>.acmap
  ├── <command>.acdef
  ├── .<command>.config.acdef
  └── hooks/

Environment Variables

Hook scripts are provided parsing information via environment variables.

Bash provided variables but exposed by nodecliac
  • NODECLIAC_COMP_LINE: Original (unmodified) CLI input.
  • NODECLIAC_COMP_POINT: Caret index when Tab key was pressed.
nodecliac provided variables
  • NODECLIAC_MAIN_COMMAND: The command auto completion is being performed for.
  • NODECLIAC_COMMAND_CHAIN: The parsed command chain.
  • NODECLIAC_COMP_INDEX: The index where completion is being attempted.
  • NODECLIAC_LAST: The last parsed word item.
    • Note: Last word item could be a partial word item.
      • This happens when the Tab key gets pressed within a word item. For example, take the following input:$ program command. If theTab key was pressed like so: $ program commTaband, the last word item is comm. Thus a partial word with a remainder string of and. Resulting in finding completions for comm.
  • NODECLIAC_PREV: The word item preceding the last word item.
  • NODECLIAC_INPUT: CLI input from start to caret (Tab key press) index.
  • NODECLIAC_INPUT_ORIGINAL: Original unmodified CLI input.
  • NODECLIAC_INPUT_REMAINDER: CLI input from start to caret index.
  • NODECLIAC_LAST_CHAR: Character before caret.
  • NODECLIAC_NEXT_CHAR: Character after caret.
    • Note: If char is not '' (empty) then the last word item (NODECLIAC_LAST) is a partial word.
  • NODECLIAC_COMP_LINE_LENGTH: Original CLI input's length.
  • NODECLIAC_INPUT_LINE_LENGTH: CLI input length from string beginning to caret position.
  • NODECLIAC_ARG_COUNT: Amount of arguments parsed in NODECLIAC_INPUT string.
  • NODECLIAC_ARG_N: Parsed arguments can be individually accessed with this variable.
    • First argument is NODECLIAC_ARG_0 and will always be the program's command.
    • Because input is variable all other arguments can be retrieved with a loop.
      • Use NODECLIAC_ARG_COUNT as max loop iteration.
    • Example: Given the CLI input: $ yarn remove chalk prettier
      • Arguments would be:
        • NODECLIAC_ARG_0: yarn
        • NODECLIAC_ARG_1: remove
        • NODECLIAC_ARG_2: chalk
        • NODECLIAC_ARG_3: prettier
  • NODECLIAC_USED_DEFAULT_POSITIONAL_ARGS: Collected positional arguments.

Writing Pre Hook Script

Take yarn's pre-parse.sh script as an example:

#!/bin/bash

# Initialization variables:
#
# cline    # CLI input.
# cpoint   # Index of caret position when [TAB] key was pressed.
# command  # Program for which completions are for.
# acdef    # The command's .acdef file contents.

output="$("$HOME/.nodecliac/registry/$command/hooks/pre-parse.pl" "$cline")"

# Remaining lines are package.json's script entries.
mapfile -ts1 lines < <(echo -e "$output")
printf -v output '%s\n' "${lines[@]}" && acdef+=$'\n'"$output"
  • The Bash script is glue code. It runs the Perl script pre-parse.pl to retrieve the cwd package.json scripts and determine whether yarn is being used in a workspace.
  • Using the Perl script's output the Bash script overwrites the cline variable and appends the package.json scripts to the acdef variable. Adding them as their own commands.
  • nodecliac uses the new values to determine completions.

Writing Post Hook Script

Take m-cli's post-parse.sh script as an example:

#!/bin/bash

function completion_logic() {
  COMP_CWORD="$NODECLIAC_COMP_INDEX"
  prev="$NODECLIAC_PREV"
  cmd="$NODECLIAC_ARG_1"
  sub="$NODECLIAC_ARG_2"
  case "$cmd" in
    dir)
      case "$prev" in
        delete) echo -e "empty\ndsfiles"; return ;;
        dsfiles) echo -e "on\noff"; return ;;
      esac
      ;;
    disk)
      case "$sub" in
        # _m_disk
        verify|repair) [[ $COMP_CWORD == 3 ]] && echo -e "disk\nvolume"; return ;;
        format)
          case $COMP_CWORD in
            3) echo -e "ExFAT\nJHFS+\nMS-DOS\nvolume" ;;
            4) [[ "$NODECLIAC_ARG_3" == "volume" ]] && echo -e "ExFAT\nJHFS+\nMS-DOS" ;;
          esac
          return
        ;;
        rename) [[ $COMP_CWORD == 3 ]] && \
        echo -e "$(grep -oE '(disk[0-9s]+)' <<< "$(diskutil list)")"; return ;;

        # _m_dock
        autohide) [[ $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
        magnification) [[ $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
        position) [[ $COMP_CWORD == 3 ]] && echo -e "BOTTOM\nLEFT\nRIGHT"; return ;;
      esac
      ;;
    dock)
      case "$sub" in
        autohide) [[ $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
        magnification) [[ $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
        position) [[ $COMP_CWORD == 3 ]] && echo -e "BOTTOM\nLEFT\nRIGHT"; return ;;
      esac
      ;;
    finder) [[ $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
    screensaver) [[ $sub == "askforpassword" && $COMP_CWORD == 3 ]] && echo -e "YES\nNO"; return ;;
  esac
}
completion_logic
  • The post hook script is written in Bash but any language may be used. As shown, the script makes use of the provided NODECLIAC_* environment variables to determine what completion items to add. Each completion item must be returned on its own line.

Caching

To return quicker results completions are cached.

Expand cache section
Cache Levels:
  • 0: No caching.
  • 1: Cache all but command-string (dynamic) completions. (default)
  • 2: Cache everything.
$ nodecliac cache --clear # Clear cache.
$ nodecliac cache --level 0 # Turn cache off.

Testing

nodecliac provides a way to test completions for your program.

Expand testing section

Creating tests:

Creating tests is done directly from the program's acmap via @test. Start with @test = followed by the test string "<completion string> ; <test1 ; testN>".

  • Test entire completion output (including meta data):
    • Does the output contain format?: @test = "program --; *format*
    • Does the output omit format?: @test = "program --; !*format*
  • Test individual completion items:
    • Do any completion items contain format?: @test = "program --; *:*format*
    • Does the first completion item contain format?: @test = "program --; 1:*format*
    • Does the first completion item start with --for?: @test = "program --; 1:--for*
    • Does the first completion item end with format?: @test = "program --; 1:*format
    • Does the first completion item equal --format?: @test = "program --; 1:--format
  • Test completion items count:
    • Is there at least 1 completion item?: @test = "program --; #cgt0
    • Are there 3 completion items?: @test = "program --; #ceq3
      • Format: # + (c)ount + operator + number
      • Operators:
        • eq: Equal to
        • ne: Not equal to
        • gt: Greater than
        • ge: Greater than or equal to
        • lt: Less than
        • le: Less than or equal to
      • Number:
        • Must be a positive number.
  • Inversion: Any test can be inverted by preceding the test with a !.
Example 1

Take the following example acmap. It contains a couple commands and their respective flags.

program.make = --source
program.format = --source

@test = "program make --; *source*"
@test = "program format --for; *format*"
Example 2

Multiple tests can be provided to test a single completion string. Simply delimit them with ;.

program.make = --source
program.format = --source

@test = "program make --; *source* ; #ceq1"
@test = "program format --for; *format* ; #ceq1"

Running tests:

Running tests is done by running a built in command: $ nodecliac test <command-name>. As an example, try running nodecliac's tests. With nodecliac installed, enter nodecliac test nodecliac into a Terminal and press Enter. Note, for tests to run the program's completion package must exist in the registry to be able to run tests. Running $ nodecliac registry will list installed completion packages.

Debugging

Like with testing completion strings, nodecliac also provides a way to debug completions. This is useful when creating a completion package. To start debugging simply enable it. When enabled pressing the Tab key will output debugging information instead of providing bash completions.

Expand debugging section

Enabling debugging:

Run: $ nodecliac debug --enable

Disabling debugging:

Run: $ nodecliac debug --disable

Picking Debug Script

nodecliac's auto-completion script is written in Nim and Perl. The Nim version supports Linux/macOS while Perl is used as a fallback. When both versions are installed it's possible to use one over the other to debug. This is done with the --script flag like so:

  • Explicitly use Nim script: $ nodecliac debug --enable --script nim
  • Explicitly use Perl script: $ nodecliac debug --enable --script perl

Debug mode

To get the debug mode: $ nodecliac debug

  • 0: Disabled
  • 1: Enabled
  • 2: Enabled + use Perl script
  • 3: Enabled + use Nim script

Support

OS Support
  • Made using Node.js v8.16.0 on a Linux machine running Ubuntu 16.04.5 LTS.
  • Tested and working on:
    • macOS Mojave (v10.14.4).
    • Windows 10 - Untested.
Shell Support
  • nodecliac only works with Bash.
  • Support for other shells (Zsh, Fish, etc.) may be added with increased usage.
Editor Support

Contributing

Contributions are welcome! Found a bug, feel like documentation is lacking/confusing and needs an update, have performance/feature suggestions or simply found a typo? Let me know! :)

See how to contribute here.

License

This project uses the MIT License.