Skip to content

Instantly share code, notes, and snippets.

@roktas
Created October 14, 2023 22:50
Show Gist options
  • Save roktas/895fbedc328e23d98ba7734be344d0ba to your computer and use it in GitHub Desktop.
Save roktas/895fbedc328e23d98ba7734be344d0ba to your computer and use it in GitHub Desktop.
#!/usr/bin/env bash
# Copyright (c) 2020, Recai Oktaş
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[ -n "${BASH_VERSION:-}" ] || { echo >&2 'Bash required.'; exit 1; }
[[ ${BASH_VERSINFO[0]:-} -ge 4 ]] || { echo >&2 'Bash version 4 or higher required.'; exit 1; }
set -Eeuo pipefail; shopt -s nullglob; [[ -z ${TRACE:-} ]] || set -x; unset CDPATH; IFS=$' \t\n'
export LC_ALL=C.UTF-8 LANG=C.UTF-8
# shellcheck disable=2034
declare -gr PROGNAME=${0##*/} # Program name
# shellcheck disable=2120
.cry() {
if [[ $# -gt 0 ]]; then
echo -e >&2 "W: $*"
else
echo >&2 ""
fi
}
# shellcheck disable=2120
.die() {
if [[ $# -gt 0 ]]; then
echo -e >&2 "E: $*"
else
echo >&2 ""
fi
exit 1
}
# shellcheck disable=2120
.haw() {
echo -en "${@-""}" >&2
}
# shellcheck disable=2120
.say() {
echo -e "${@-""}" >&2
}
.available() {
command -v "${1?${FUNCNAME[0]}: missing argument}" &>/dev/null
}
.callable() {
[[ $(type -t "${1?${FUNCNAME[0]}: missing argument}" || true) == function ]]
}
.chmog() {
local mog=${1?${FUNCNAME[0]}: missing argument}; shift
local dst=${1?${FUNCNAME[0]}: missing argument}; shift
local mode owner group
IFS=: read -r mode owner group <<<"$mog"
[[ -z ${mode:-} ]] || chmod "$mode" "$dst"
[[ -z ${owner:-} ]] || chown "$owner" "$dst"
[[ -z ${group:-} ]] || chgrp "$group" "$dst"
}
.cry-() {
local default=${1?${FUNCNAME[0]}: missing argument}; shift
local mesg
while [[ $# -gt 0 ]]; do
case $1 in
--)
shift
mesg=$*
break
;;
esac
shift
done
.cry "${mesg:-$default}"
}
.contains() {
: "${1?${FUNCNAME[0]}: missing argument}"
local element
for element in "${@:2}"; do
if [[ $element = "$1" ]]; then
return 0
fi
done
return 1
}
.contains-() {
local needle="${1?${FUNCNAME[0]}: missing argument}"; shift
local -n contains_="${1?${FUNCNAME[0]}: missing argument}"; shift
local element
for element in "${contains_[@]}"; do
if [[ $element = "$needle" ]]; then
return 0
fi
done
return 1
}
.die-() {
local default=${1?${FUNCNAME[0]}: missing argument}; shift
local mesg
while [[ $# -gt 0 ]]; do
case $1 in
--)
shift
mesg=$*
break
;;
esac
shift
done
.die "${mesg:-$default}"
}
.expired() {
local -i expiry=${1?${FUNCNAME[0]}: missing argument}; shift
case $expiry in
-1) return 1 ;;
0) return 0 ;;
esac
local file
for file; do
local t=d
[[ -d $file ]] || t=f
if [[ -e $file ]] && [[ -z $(find "$file" -maxdepth 0 -type "$t" -mmin +"$expiry" 2>/dev/null) ]]; then
return 1
fi
done
return 0
}
.inside() {
local dir=${1?${FUNCNAME[0]}: missing argument}; shift
[[ $# -gt 0 ]] || return 0
builtin pushd "$dir" >/dev/null || exit
"$@"
builtin popd >/dev/null || exit
if [[ $(type -t "$1" || true) == function ]] && [[ $1 =~ [.]$ ]]; then
unset -f "$1"
fi
}
# Execute command in a temp dir
.intemp() {
[[ $# -gt 0 ]] || return 0
local tmp
tmp=$(mktemp -p "${TMPDIR:-/tmp}" -d "$PROGNAME".XXXXXXXX) || exit
local err
(builtin cd "$tmp" && "$@") || err=$?
rm -rf "$tmp"
if [[ $(type -t "$1" || true) == function ]] && [[ $1 =~ [.]$ ]]; then
unset -f "$1"
fi
return ${err:-0}
}
.interactive() {
[[ -t 1 ]]
}
# Join array with the given separator
.join() {
local IFS=${1?${FUNCNAME[0]}: missing argument}; shift
echo -n "$*"
}
# Join array ref with the given separator
.join-() {
local IFS=${1?${FUNCNAME[0]}: missing argument}; shift
local -n join_=${1?${FUNCNAME[0]}: missing argument}; shift
echo -n "${join_[*]}"
}
# shim.sh - Shims (mostly for UI)
# shellcheck disable=2120
.bye() {
if [[ $# -gt 0 ]]; then
echo -e >&2 "$*"
else
echo >&2 ""
fi
exit 0
}
.bug() {
if [[ $# -gt 0 ]]; then
echo -e >&2 "?: $*"
else
echo >&2 ""
fi
exit 127
}
.calling() {
local message="${1?${FUNCNAME[0]}: missing argument}"; shift
.say "--> $message"
"$@"
}
.getting() {
local message="${1?${FUNCNAME[0]}: missing argument}"; shift
.say "... $message"
"$@"
}
.heading() {
local message="${1?${FUNCNAME[0]}: missing argument}"; shift
.say "==> $message"
"$@"
}
.running() {
local message="${1?${FUNCNAME[0]}: missing argument}"; shift
.say "--> $message"
"$@"
}
# git.sh - Git functions
git.clone() {
local src=${1?${FUNCNAME[0]}: missing argument}; shift
local dst=${1?${FUNCNAME[0]}: missing argument}; shift
local ref=${1:-}
[[ ! -e $dst ]] || .die "Destination already exists: $dst"
local options=(
'--single-branch'
'--quiet'
)
[[ -z ${ref:-} ]] || options+=(
'--branch'
"$ref"
)
.getting "Cloning $src"
git clone "${options[@]}" "$src" "$dst"
if [[ -n ${ref:-} ]] && git.ref.is "$ref" tag; then
git.set.immutable true
fi
}
git.default-branch() {
local repo=${1:-.}
git.must "$repo" sane
git -C "$repo" symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
}
git.is() {
local repo=${1?${FUNCNAME[0]}: missing argument}; shift
local feature=${1?${FUNCNAME[0]}: missing argument}; shift
local func=git.is."$feature"_
.callable "$func" || .bug "No such predicate: $feature"
"$func" "$repo"
}
git.must() {
local repo=${1?${FUNCNAME[0]}: missing argument}; shift
local feature=${1?${FUNCNAME[0]}: missing argument}; shift
git.is "$repo" "$feature" || .die "Repository is not ${feature//-/ }: $repo"
}
git.ref.is() {
local repo=${1?${FUNCNAME[0]}: missing argument}; shift
local ref=${1?${FUNCNAME[0]}: missing argument}; shift
local type=${1?${FUNCNAME[0]}: missing argument}; shift
case $type in
branch)
git -C "$repo" show-ref -q --verify "refs/heads/$ref" &>/dev/null
;;
tag)
git -C "$repo" show-ref -q --verify "refs/tags/$ref" &>/dev/null
;;
remote)
git -C "$repo" show-ref -q --verify "refs/remote/$ref" &>/dev/null
;;
hash|commit)
git -C "$repo" rev-parse --verify "$ref^{commit}" &>/dev/null
;;
*)
.bug "Unrecognized ref type: $type"
return 1
esac
}
git.reset() {
local repo=${1:-.}
git -C "$repo" reset --hard
}
git.set.immutable() {
local repo=${1:-.}
git -C "$repo" config --type bool core.x-immutable true
}
git.set.mutable() {
local repo=${1:-.}
git -C "$repo" config --type bool core.x-immutable false
}
git.switch() {
local repo=${1?${FUNCNAME[0]}: missing argument}; shift
local branch=${1:-}
[[ -n $branch ]] || branch=$(git.default-branch "$repo")
git -C "$repo" checkout --quiet "$branch"
}
git.top() {
local path=${1:-}
git.must.sane "$path"
cd "$(git.topdir "$path")" || exit
}
git.topdir() {
local path=${1:-}
if [[ -n $path ]]; then
[[ -e $path ]] || .die "No such path found: $path"
local d
[[ -d $path ]] || d=${path%/*}
pushd "$d" >/dev/null || exit
fi
local dir
dir=$(git rev-parse --git-dir) && dir=$(cd "$dir" && pwd)/ && echo "${dir%%/.git/*}"
if [[ -n $path ]]; then
popd >/dev/null || exit
fi
}
git.update() {
local repo=${1:-.}
if ! git.is "$repo" immutable; then
.getting "Updating repository"
git pull -q
fi
}
# git - Private functions
git.is.clean_() {
local repo=${1:-.}
git -C "$repo" rev-parse --verify HEAD >/dev/null &&
git -C "$repo" update-index -q --ignore-submodules --refresh &&
git -C "$repo" diff-files --quiet --ignore-submodules &&
git -C "$repo" diff-index --cached --quiet --ignore-submodules HEAD --
}
git.is.git_() {
local repo=${1:-.}
[[ -d $repo/.git ]] && git rev-parse --resolve-git-dir "$repo/.git" &>/dev/null
}
git.is.immutable_() {
local repo=${1:-.}
[[ $(git -C "$repo" config --type bool core.x-immutable 2>/dev/null || true) = true ]]
}
git.is.sane_() {
local repo=${1:-.}
git -C "$repo" rev-parse --is-inside-work-tree &>/dev/null || return 1
git -C "$repo" rev-parse --verify HEAD >/dev/null || return 1
}
git.is.sane-and-clean_() {
git.is.sane_ "$@" && git.is.clean_ "$@"
}
# file.sh - File related operations
file.cp() {
local src=${1?${FUNCNAME[0]}: missing argument}; shift
local dst=${1?${FUNCNAME[0]}: missing argument}; shift
local mog=${1:-}
local dir=${dst%/*}
[[ -d $dir ]] || mkdir -p "$dir"
cp -a "$src" "$dst"
[[ -z ${mog:-} ]] || .chmog "$mog" "$dst"
}
file.hid() {
local src=${1?${FUNCNAME[0]}: missing argument}; shift
local dir; dir=$(dirname "$src")/...
[[ -d $dir ]] || mkdir -p "$dir"
mv "$src" "$dir"
}
file.ln() {
local src=${1?${FUNCNAME[0]}: missing argument}; shift
local dst=${1?${FUNCNAME[0]}: missing argument}; shift
local mog=${1:-}
local dir=${dst%/*}
[[ -d $dir ]] || mkdir -p "$dir"
src=$(realpath -m --relative-base "${dst%/*}" "$src")
ln -sf "$src" "$dst"
[[ -z ${mog:-} ]] || .chmog "$mog" "$dst"
}
file.mv() {
local src=${1?${FUNCNAME[0]}: missing argument}; shift
local dst=${1?${FUNCNAME[0]}: missing argument}; shift
local mog=${1:-}
local dir=${dst%/*}
[[ -d $dir ]] || mkdir -p "$dir"
mv -f "$src" "$dst"
[[ -z ${mog:-} ]] || .chmog "$mog" "$dst"
}
file.upcd() {
local cwd=${1?${FUNCNAME[0]}: missing argument}; shift
cd "$cwd" || exit
while :; do
local try
for try; do
if [[ -e $try ]]; then
return 0
fi
done
# shellcheck disable=2128
if [[ $PWD == "/" ]]; then
break
fi
cd .. || exit
done
}
# shellcheck disable=2154
.dispatch() {
[[ $# -ne 0 ]] || { .usage; .die 'Command required'; }
local help
if [[ $1 = help ]]; then
help=true
shift
[[ $# -ne 0 ]] || { .usage; .die 'Help topic required'; }
fi
local cmd rem=()
.resolve _dispatch_ cmd rem "$@" || .die "Wrong or incomplete command: $*"
local fun=${_dispatch_[$cmd]}
# shellcheck disable=2034
declare -gr CMDNAME=$cmd
if [[ -n ${help:-} ]]; then
.say "${_document_[$fun]:-}" ""
"$fun" -help
else
"$fun" "${rem[@]}"
fi
}
# shellcheck disable=2034
.resolve() {
local -n tab_=${1?${FUNCNAME[0]}: missing argument}; shift
local -n cmd_=${1?${FUNCNAME[0]}: missing argument}; shift
local -n rem_=${1?${FUNCNAME[0]}: missing argument}; shift
local try=()
while [[ $# -gt 0 ]]; do
try+=("$1")
shift
if [[ -n ${tab_[${try[*]}]:-} ]]; then
cmd_=${try[*]}
rem_=("$@")
return 0
fi
done
return 1
}
# shellcheck disable=2120
.usage() {
case ${1:-} in
by-commands|"")
.say "${USAGE:-Usage: $PROGNAME <command>... [-<flag>=<value>...] [<args>...]\n\nCommands:}"
local cmd
# shellcheck disable=2154
for cmd in "${!_dispatch_[@]}"; do
local fun=${_dispatch_[$cmd]}
local doc=${_document_[$fun]:-}
[[ -n $doc ]] || continue
printf "\\t%-24s %s\n" "$cmd" "$doc"
done
;;
by-tasks)
.say "${USAGE:-Usage: $PROGNAME <task>... [<name>=<value>...]\n\nTasks:}"
local fun
# shellcheck disable=2154
for fun in "${!_document_[@]}"; do
local doc=${_document_[$fun]:-}
printf "\\t%-24s %s\n" "$fun" "$doc"
done
;;
*)
.die "Unrecognized listing mode: $1"
;;
esac | sort >&2
}
# flag.sh - Flag handling
.bool() {
local value=${1:-}
value=${value,,}
case $value in
true|t|1|on|yes|y)
return 0
;;
false|f|0|off|no|n|"")
return 1
;;
*)
.bug "Invalid boolean: $value"
esac
}
flag.args() {
local keys=()
mapfile -t keys < <(
for key in "${!_[@]}"; do
[[ $key =~ ^[1-9][0-9]*$ ]] || continue
echo "$key"
done | sort -u
)
local key
if [[ $# -gt 0 ]]; then
# shellcheck disable=2178
local -n _values_=$1
for key in "${keys[@]}"; do
_values_+=("${_[$key]}")
done
else
for key in "${keys[@]}"; do
echo "${_[$key]}"
done
fi
}
flag.env() {
local keys=()
mapfile -t keys < <(
for key in "${!_[@]}"; do
[[ $key =~ ^[[:alpha:]_][[:alnum:]_]*$ ]] || continue
echo "$key"
done | sort -u
)
local key
if [[ $# -gt 0 ]]; then
# shellcheck disable=2178
local -n _values_=$1
for key in "${keys[@]}"; do
_values_+=("$key='${_[$key]}'")
done
else
for key in "${keys[@]}"; do
echo "$key='${_[$key]}'"
done
fi
}
flag.false() {
! flag.true "$@"
}
flag.load() {
local -n _load_src_=${1?${FUNCNAME[0]}: missing argument}; shift
local key
for key in "${!_load_src_[@]}"; do
# shellcheck disable=2034
_[$key]=${_load_src_[$key]}
done
}
flag.nil() {
[[ ${_[$1]:-} = "$NIL" ]]
}
flag.parse_() {
if .contains -help "$@"; then
flag.usage-and-bye
fi
local -A flag_result_
local -i argc=0
while [[ $# -gt 0 ]]; do
local key value
if [[ $1 =~ ^-*[[:alpha:]_][[:alnum:]_]*= ]]; then
key=${1%%=*}; value=${1#*=}
if [[ $key =~ ^-.+$ ]] && [[ ! -v _[$key] ]]; then
.die "Unrecognized flag: $key"
fi
if [[ $key =~ ^-.+$ ]]; then
[[ -v _[$key] ]] || .die "Unrecognized flag: $key"
elif [[ -n ${_[.raw]:-} ]]; then
key=$((++argc)); value=$1
fi
elif [[ $1 == '--' ]] && [[ -z ${_[.dash]:-} ]]; then
shift
break
else
key=$((++argc)); value=$1
fi
# shellcheck disable=2034
flag_result_["$key"]=${value:-${_["$key"]:-}}
shift
done
flag.load flag_result_
flag.validate_ "$argc"
}
flag.peek_() {
if .contains -help "$@"; then
flag.usage-and-bye
fi
local -A flag_result_
local -i argc=0
while [[ $# -gt 0 ]]; do
local key value
if [[ $1 =~ ^-*[[:alpha:]_][[:alnum:]_]*= ]]; then
key=${1%%=*}; value=${1#*=}
elif [[ $1 == '--' ]] && [[ -z ${_[.dash]:-} ]]; then
shift
break
else
key=$((++argc)); value=$1
fi
# shellcheck disable=2034
flag_result_["$key"]=${value:-${_["$key"]:-}}
shift
done
flag.load flag_result_
flag.validate_ "$argc"
}
flag.true() {
.bool "${_[$1]:-}"
}
flag.usage() {
local -a cmdname=("$PROGNAME")
[[ -z ${CMDNAME:-} ]] || cmdname+=("$CMDNAME")
if [[ -n ${_[.desc]:-} ]]; then
# shellcheck disable=2128
.say "Usage: ${cmdname[*]} ${_[.desc]}"
else
# shellcheck disable=2128
.say "Usage: ${cmdname[*]}"
fi
}
flag.usage-and-die() {
flag.usage
.die "$@"
}
# shellcheck disable=2120
flag.usage-and-bye() {
flag.usage
.bye "$@"
}
# flag - Private functions
flag.args_() {
local n=${1?${FUNCNAME[0]}: missing argument}; shift
local argc=${_[.argc]:-0}
[[ $argc != '-' ]] || return 0
local lo hi
if [[ $argc =~ ^[0-9]+$ ]]; then
lo=$argc; hi=$argc
elif [[ $argc =~ ^[0-9]*-[0-9]*$ ]]; then
IFS=- read -r lo hi <<<"$argc"
else
.bug "Incorrect range: $argc"
fi
local message
if [[ -n ${lo:-} ]] && [[ $n -lt $lo ]]; then
message='Too few arguments'
elif [[ -n ${hi:-} ]] && [[ $n -gt $hi ]]; then
message='Too many arguments'
else
return 0
fi
flag.usage-and-die "$message"
}
flag.nils_() {
local required=()
local key
for key in "${!_[@]}"; do
if flag.nil "$key"; then
required+=("$key")
fi
done
[[ ${#required[@]} -eq 0 ]] || .die "Value missing for: ${required[*]}"
}
flag.validate_() {
flag.args_ "$@"
flag.nils_
}
# flag - Init
flag.init_() {
shopt -s expand_aliases
# shellcheck disable=2142,2154
alias flag.parse='flag.parse_ "$@"; local __argv__=() ARGV=("$@"); flag.args __argv__; set -- "${__argv__[@]}"; unset -v __argv__'
# shellcheck disable=2142,2154
alias flag.peek='flag.peek_ "$@"; local __argv__=() ARGV=("$@"); flag.args __argv__; set -- "${__argv__[@]}"; unset -v __argv__'
# shellcheck disable=2034
declare -gr NIL="\0"
}
flag.init_
# shellcheck disable=2154
foreach() {
local packdir=${1?${FUNCNAME[0]}: missing argument}; shift
local func=${1?${FUNCNAME[0]}: missing argument}; shift
local -a repos=()
local dir
for dir in "$packdir"/*/opt/*/*/* "$packdir"/*/start/*; do
if [[ -d $dir ]] && git.is "$dir" git; then
repos+=("$dir")
fi
done
[[ ${#repos[@]} -gt 0 ]] || .bye "No pack found."
for dir in "${repos[@]}"; do
local base=${dir#"$packdir"/}
local -a addr=()
IFS='/' read -ra addr <<< "$base"
# FIXME: Ugly workaround
if [[ ${#addr[@]} -eq 5 ]]; then
local -A _=(
[group]=${addr[0]}
[kind]=${addr[1]}
[provider]=${addr[2]}
[user]=${addr[3]}
[repo]=${addr[4]}
)
elif [[ ${#addr[@]} -eq 3 ]]; then
local -A _=(
[group]=${addr[0]}
[kind]=${addr[1]}
[provider]=''
[user]=''
[repo]=${addr[2]}
)
fi
"$func" "$dir" "$@"
done
}
packdirs() {
local mustexist=${1:-pack}
local -a paths=()
IFS=',' read -ra paths <<<"$(
nvim -u NONE --headless --cmd 'echo &packpath' --cmd q 2>&1
)"
local path
if [[ -z $mustexist ]] || [[ $mustexist = - ]]; then
for path in "${paths[@]}"; do
echo "$path/pack"
done
else
for path in "${paths[@]}"; do
[[ -e $path/$mustexist ]] || continue
echo "$path/pack"
done
fi
}
# update - Update command
# Update plugins
# shellcheck disable=2154
:update() {
# shellcheck disable=2192
local -A _=(
[-prefix]=/usr/local/share
[.desc]='[-prefix=<dir>]'
[.argc]=0
)
flag.parse
local packdir=${_[-prefix]}/nvim/site/pack
[[ -d $packdir ]] || .die "Package directory not found: $packdir"
[[ -w $packdir ]] || .die "Package directory not writable: $packdir"
local prev=
local -a changed=()
up() {
local pack=${1?${FUNCNAME[0]}: missing argument}; shift
local slug="${_[group]}/${_[kind]}"
if [[ $slug != "$prev" ]]; then
.say " > $slug"
prev=$slug
fi
.haw "\t${_[repo]} "
update "$pack" docs || return 0
changed+=("$pack")
}
foreach "$packdir" up
helptags "${changed[@]}"
}
update() {
local dir=${1?${FUNCNAME[0]}: missing argument}; shift
[[ -w $dir ]] || {
.cry "Repository not writable: $dir"
return 1
}
git.is "$dir" sane-and-clean || {
.cry "Repository not clean: $dir"
return 1
}
local here there
here=$(git -C "$dir" rev-parse '@')
git -C "$dir" fetch -q || {
.cry "Error fetching plugin: $dir"
return 1
}
there=$(git -C "$dir" rev-parse '@{u}')
[[ $here != "$there" ]] || {
.say '✗'
return 1
}
git -C "$dir" merge -q || {
.cry "Error updating plugin: $dir"
return 1
}
.say '✓'
return 0
}
helptags() {
local -a docs=()
local dir
for dir; do
local doc=$dir/doc
[[ -d $doc ]] || continue
[[ -w $doc ]] || {
.cry "Directory not writable to generate helptags: $doc"
continue
}
docs+=("$doc")
done
[[ ${#docs[@]} -gt 0 ]] || return 0
nvim -u NONE --cmd "helptags ${docs[*]}" --cmd q || cry "Error updating helptags"
}
declare -Ag _dispatch_=(
['update']=':update'
)
declare -Ag _document_=(
[':update']='Update plugins'
)
main() {
.dispatch "$@"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment