From 4c821ae145f67d2e6ae0fe80bf751f9fdefed68a Mon Sep 17 00:00:00 2001 From: Tomas Mirchev Date: Thu, 30 Oct 2025 09:57:38 +0200 Subject: [PATCH] feat: bash v1 --- barg.js | 1 + barg.sh | 379 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 380 insertions(+) create mode 100644 barg.sh diff --git a/barg.js b/barg.js index bd8bb35..4587522 100644 --- a/barg.js +++ b/barg.js @@ -306,6 +306,7 @@ const INPUTS = [ "dev stop mycontainer --kill -- not specified one", "dev build --from alpine --image node:20 mybox -- echo hi", "dev build --aaaaa --from alpine --image node:20 --bbbbb orb mybox ccccc -- echo hi", + "dev build mybox --global -qvi=debian", ]; INPUTS.forEach((input) => { diff --git a/barg.sh b/barg.sh new file mode 100644 index 0000000..4ff65a0 --- /dev/null +++ b/barg.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env bash +# Bash 5+ + +# TODO (NOT IMPLEMENTED): +# - repeatable attribute + +# ---------- helpers ---------- +join() { + local out="" sep="" x + for x in "$@"; do + [[ -n "$x" ]] || continue + out+="${sep}${x}" + sep="." + done + printf '%s' "$out" +} + +# ---------- tokenizer ---------- +tokenize() { + local input=$1 + TOKENS=() + read -r -a args <<<"$input" + local root=${args[0]} + TOKENS+=("root::${root}") + args=("${args[@]:1}") + + while ((${#args[@]})); do + local token=${args[0]} + args=("${args[@]:1}") + + if [[ "$token" == "--" ]]; then + TOKENS+=("rest::${args[*]}") + break + fi + + if [[ "$token" == --* ]]; then + local kv=${token#--} + local key=${kv%%=*} + TOKENS+=("narg::${key}") + if [[ "$kv" == *"="* ]]; then + local value=${kv#*=} + TOKENS+=("value::${value}") + fi + continue + fi + + if [[ "$token" == -* ]]; then + local sv=${token#-} + local flags=${sv%%=*} + local value= + if [[ "$sv" == *"="* ]]; then value=${sv#*=}; fi + local i ch + for ((i = 0; i < ${#flags}; i++)); do + ch=${flags:i:1} + TOKENS+=("narg::${ch}") + done + [[ -n "$value" ]] && TOKENS+=("value::${value}") + continue + fi + + TOKENS+=("unk::${token}") + done +} + +# ---------- parser ---------- +declare -A S A + +generate_schema() { + S=() + A=() + local spec_name=$1 + local -n SPEC_REF="$spec_name" + + local _id=100 + local level=-1 + local -a pos=() + local -a cmds=() + + for entry in "${SPEC_REF[@]}"; do + IFS=';' read -r -a parts <<<"$entry" + local type=${parts[0]} + case "$type" in + command) + local id=$_id + _id=$((_id + 1)) + IFS=',' read -r -a aliases <<<"${parts[1]}" + local help="${parts[2]}" + S["$id.entryType"]="command" + S["$id.name"]="${aliases[0]}" + S["$id.help"]="$help" + S["$id.args"]="" + + if ((${#cmds[@]} == 0)); then + A["cmd::root"]=$id + else + local last=$((${#cmds[@]} - 1)) + for alias in "${aliases[@]}"; do + A["$(join "${cmds[$last]}" "cmd::${alias}")"]=$id + done + fi + + level=$((level + 1)) + cmds+=("$id") + pos+=(0) + ;; + end) + level=$((level - 1)) + if ((${#cmds[@]} > 0)); then cmds=("${cmds[@]:0:${#cmds[@]}-1}"); fi + if ((${#pos[@]} > 0)); then pos=("${pos[@]:0:${#pos[@]}-1}"); fi + ;; + argument) + local id=$_id + _id=$((_id + 1)) + IFS=',' read -r -a aliases <<<"${parts[1]}" + local name="${aliases[0]}" + + local dest="$name" + local required="false" + local atype="positional" + local value="" + local help="" + local i kv k v + for ((i = 2; i < ${#parts[@]}; i++)); do + kv=${parts[i]} + if [[ "$kv" == *":"* ]]; then + k=${kv%%:*} + v=${kv#*:} + else + k=$kv + v="true" + fi + case "$k" in + dest) dest="$v" ;; + required) required="$v" ;; + type) atype="$v" ;; + help) help="$v" ;; + default) value="$v" ;; + repeatable) S["$id.repeatable"]="$v" ;; + esac + done + + S["$id.entryType"]="argument" + S["$id.name"]="$name" + S["$id.dest"]="$dest" + S["$id.required"]="$required" + S["$id.type"]="$atype" + [[ -n "$help" ]] && S["$id.help"]="$help" + [[ -n "$value" ]] && S["$id.value"]="$value" + + local last=$((${#cmds[@]} - 1)) + local cmdArgsKey + cmdArgsKey="$(join "${cmds[$last]}" "args")" + if [[ -z "${S[$cmdArgsKey]}" ]]; then + S["$cmdArgsKey"]="$id" + else + S["$cmdArgsKey"]+=",${id}" + fi + + case "$atype" in + positional) + A["$(join "${cmds[$last]}" "pos::${pos[$last]}")"]=$id + pos[$last]=$((pos[$last] + 1)) + ;; + rest) + A["$(join "${cmds[$last]}" "rest")"]=$id + ;; + option | flag) + for alias in "${aliases[@]}"; do + A["$(join "${cmds[$last]}" "narg::${alias}")"]=$id + done + ;; + esac + ;; + *) + printf 'Error: Invalid entry type: "%s"\n' "$type" >&2 + return 1 + ;; + esac + done +} + +print_result() { + local -a COMMANDS=("${!1}") + declare -n VALUES_REF="$2" + + printf 'commands:' + local c + for c in "${COMMANDS[@]}"; do printf ' %s' "$c"; done + printf '\n' + printf 'values:\n' + local k v + for k in "${!VALUES_REF[@]}"; do + v=${VALUES_REF[$k]} + if [[ "$v" == *" "* ]]; then + printf ' %s="%s"\n' "$k" "$v" + else + printf ' %s=%s\n' "$k" "$v" + fi + done +} + +parse_input() { + local spec_name=$1 + local input=$2 + tokenize "$input" + generate_schema "$spec_name" || return 1 + + local -a cmds=() + local -a pos=() + local tagged token tokenTag + + while ((${#TOKENS[@]})); do + tagged=${TOKENS[0]} + TOKENS=("${TOKENS[@]:1}") + tokenTag=${tagged%%::*} + token=${tagged#*::} + + local found=0 + local level start_level entryId id + start_level=$((${#cmds[@]} ? ${#cmds[@]} - 1 : 0)) + + for ((level = start_level; level >= 0; level--)); do + entryId="" + case "$tokenTag" in + root) + entryId="${A["cmd::root"]}" + ;; + rest) + ((${#cmds[@]})) || { + entryId="" + break + } + entryId="${A["$(join "${cmds[$level]}" "rest")"]}" + ;; + narg) + ((${#cmds[@]})) || { + entryId="" + break + } + entryId="${A["$(join "${cmds[$level]}" "$tagged")"]}" + ;; + unk) + ((${#cmds[@]})) || { entryId=""; } # cannot be positional before a command + if [[ -z "$entryId" && ${#cmds[@]} -gt 0 ]]; then + id="${A["$(join "${cmds[$level]}" "cmd::${token}")"]}" + if [[ -z "$id" && $level -eq $((${#pos[@]} - 1)) ]]; then + id="${A["$(join "${cmds[$level]}" "pos::${pos[$level]}")"]}" + [[ -n "$id" ]] && pos[$level]=$((pos[$level] + 1)) + fi + entryId="$id" + fi + ;; + esac + + [[ -z "$entryId" ]] && continue + + local entryType=${S["$entryId.entryType"]} + case "$entryType" in + command) + cmds+=("$entryId") + pos+=(0) + if [[ "$tokenTag" == "root" ]]; then + S["$entryId.name"]="$token" + fi + ;; + argument) + local atype=${S["$entryId.type"]} + local val="" + case "$atype" in + rest | positional) val="$token" ;; + flag) val="true" ;; + option) + if ((${#TOKENS[@]} == 0)); then + printf 'Error: No value provided for option argument: "%s"\n' "$token" >&2 + return 1 + fi + local nTagged=${TOKENS[0]} + local nTag=${nTagged%%::*} + local nTok=${nTagged#*::} + if [[ "$nTag" != "value" && "$nTag" != "unk" ]]; then + printf 'Error: Expected option argument, but got: "%s" (%s). Skipping...\n' "$nTok" "$nTag" >&2 + else + TOKENS=("${TOKENS[@]:1}") + val="$nTok" + fi + ;; + esac + S["$entryId.value"]="$val" + ;; + esac + + found=1 + break + done + + if ((found == 0)); then + printf 'Invalid argument: "%s" (%s). Skipping...\n' "$token" "$tokenTag" + fi + done + + # finalize + local -a COMMANDS=() + declare -A VALUES=() + + local cmdId cmdName cmdArgs argId name dest value required + for cmdId in "${cmds[@]}"; do + cmdName=${S["$cmdId.name"]} + COMMANDS+=("$cmdName") + cmdArgs=${S["$cmdId.args"]} + [[ -z "$cmdArgs" ]] && continue + IFS=',' read -r -a argIds <<<"$cmdArgs" + for argId in "${argIds[@]}"; do + name=${S["$argId.name"]} + dest=${S["$argId.dest"]} + value=${S["$argId.value"]} + required=${S["$argId.required"]} + + if [[ "$required" == "true" && -z "$value" ]]; then + printf 'Error: Argument "%s" is required in command "%s"\n' "$name" "$cmdName" >&2 + return 1 + fi + if [[ -n "${VALUES[$dest]+x}" ]]; then + VALUES["${cmdName}_${dest}"]="$value" + else + VALUES["$dest"]="$value" + fi + done + done + + print_result COMMANDS[@] VALUES +} + +parse_input_wrap() { + local spec_name=$1 + local input=$2 + parse_input "$spec_name" "$input" || printf 'Error: Failed to parse input\n' +} + +# ---------- spec and tests ---------- +SPEC=( + "command;barg;Barg - Bash Argument Parser" + "argument;global;type:flag" + "command;build;Build a dev container" + "argument;from;type:option;dest:fromName;help:Name of the base container" + "argument;name;type:positional;required;help:Name of the container" + "argument;nametwo;help:Name of the container" + "argument;image,i;type:option;required;dest:imageName;help:Base image" + "argument;include,I;type:option;repeatable;dest:includePaths;help:Include paths" + "argument;verbose,v;type:flag;repeatable;default:0;help:Increase verbosity (-v, -vv, ...)" + "argument;quiet,q;type:flag;default:false;help:Suppress all output" + "argument;cmd;type:rest;help:Command and args to run inside the container" + "argument;name;dest:nameOpt;type:option;help:Building container name" + "command;container;Container building" + "argument;dev;type:flag;help:Is dev or not" + "argument;name;required;help:Building container name" + "argument;cmd;type:rest;dest:cmdRest;help:Command and args to run inside the container" + "end" + "end" + "command;stop;Stop a dev container" + "argument;name;type:positional;required;help:Name of the container" + "argument;kill,k;type:flag;default:false;help:Force kill the container" + "end" + "end" +) + +INPUTS=( + "dev build -i myimage buildtwo --from base-dev --image node:18 --include src --include test -v -v -q mycontainer -- npm run start" + "dev build buildname container -i myimage --dev --name webapp externalContainer" + "dev --global build --image debian mybox container --dev --name webapp externalContainer -- echo hi" + "dev stop mycontainer --kill" + "dev stop mycontainer --kill -- not specified one" + "dev build --from alpine --image node:20 mybox -- echo hi" + "dev build --aaaaa --from alpine --image node:20 --bbbbb orb mybox ccccc -- echo hi" +) + +for input in "${INPUTS[@]}"; do + printf "\n\n------------------------\n\n> %s\n" "$input" + parse_input_wrap SPEC "$input" +done