Developing a feature rich Natural Language Processor
- Using just jq, Nix & Bash
I've always been drawn to voice commands. Maybe it's because of the extended use cases that come with being blind - you can imagine fumbling around for the TV remote when not being able to see? That sucks! Now I can simply tell the TV to start. Or if I prefer, simply ask where the remote is and the Nvidia Shield Remote plays a sound. Maybe it's just the appeal of being able to multitask with a third hand that doesn't exist. Either way, the idea of speaking to my computer and having it do things has always fascinated me. Voice as an interface feels natural, powerful... and deeply underutilized.
# ๐ฆ says โฎ I use 20x magnification when I code and debug. I use emoji to simplify logs for myself. If you can't handle my code style you can disable most of it on this website by toggling the button in the navbar. Shall duck continue?
Let's rewind a bit. I've always had a soft spot for doing things the stupid way. Not "stupid" as in broken, but "stupid" as in... unorthodox, unnecessary, and definitely not optimized for scale. The kind of stupid that makes you ask: "Wait, you did what in Bash?!"
The Genesis: Bash Meets Nix
Early in this project, no one - not even I - could give a good reason for doing it in Bash. There were no benchmarks to beat, no ecosystem gaps to fill, and definitely no best practices to follow. Maybe it's still a stupid idea. But I kept coming back to one thing: Bash has flexibility.
You can bend it in ways most people can't imagine.
# ๐ฆ says โฎ There is no spoon...
Meanwhile, tools like Home Assistant's Assist were starting to bore me. They worked, sure - but I constantly felt locked in, boxed by high-level abstractions and limited customization. I didn't want a voice assistant that just reacts - I wanted one that obeys.
While being an active contributor within the community, I multiple times got or noticed people getting responses from the dev's "That's not possible." and "No, you can't do that". Which was my breaking point to leave and go my own way.
That's where Nix came in. Going the Nix way - what could be more fun?
Using Nix's declarative configuration model, I realized I could define a CLI interface that held all my custom scripts and logic - the glue that tied intent to action. I could describe sentences declaratively and map them directly to Bash scripts.
No overhead. No dependencies. No noise. Just me, a microphone, and a shell.
I Did Not Mean for It to Get This Complex
What started as a small utility to let me run shell commands by typing natural language sentences quickly spiraled into a full-blown NLP system. With Bash. And Nix. And thousands of dynamically generated regular expressions.
This blog post walks you through the actual code behind a voice-command-esque CLI script framework I built using Nix to organize, parse, cache, and test structured sentence intents to build and execute Shell commands.
๐ฆ๐ HOME via ๐ v3.12.10
01:41:26 โฏ yo do "please stop test auto 5 yes wild card testing here"
โโ(yo-demo)
โ๐ฆ
โโโฎ --action stop
โโโฎ --target test
โโโฎ --mode auto
โโโฎ --duration 5
โโโฎ --confirm yes
โโโฎ --wild wild card testing here
[๐ฆ๐] [01:41:31] โ๏ธDEBUGโ๏ธ โฎ +0.007 s Script Started!
[๐ฆ๐] [01:41:31] โ๏ธDEBUGโ๏ธ โฎ +0.013 s SCript Executed!
[๐ฆ๐] [01:41:31] โ๏ธDEBUGโ๏ธ โฎ +0.020 s Script finished!
Nix Configuration: Declaring Intents
yo = { scripts = { demo = { parameters = [ { name = "action"; description = "Action to perform"; default = "run"; } { name = "target"; description = "Target of the action"; } { name = "intensity"; description = "Level of intensity"; type = "int"; optional = true; } { name = "duration"; description = "Duration in seconds"; type = "int"; optional = true; } { name = "mode"; description = "Mode of operation"; optional = true; } { name = "confirm"; description = "Whether to confirm the action"; optional = true; } { name = "wild"; description = "Wildcard testing"; optional = true; } ]; code = '' echo "Script executed!" ''; }; }; do = { intents = { demo = { data = [{ sentences = [ "please {action} {target} {mode} {duration} {confirm} {wild}" ]; lists = { action.values = [ { "in" = "[run|execute|start]"; out = "RUN"; } { "in" = "[stop|terminate|halt]"; out = "STOP"; } ]; target.values = [ { "in" = "[process|program|sequence]"; out = "PROCESS"; } { "in" = "[test|check|validation]"; out = "TEST"; } ]; mode.values = [ { "in" = "[auto|automatic]"; out = "AUTO"; } { "in" = "[manual|manually]"; out = "MANUAL"; } ]; confirm.values = [ { "in" = "[yes|confirm|sure]"; out = "YES"; } { "in" = "[no|cancel|decline]"; out = "NO"; } ]; intensity.values = builtins.genList (i: { "in" = toString (i + 1); out = toString (i + 1); }) 10; duration.values = builtins.genList (i: { "in" = toString ((i + 1) * 10); out = toString ((i + 1) * 10); }) 6; wild.wildcard = true; }; }]; }; }; }; }; };
Early on, I realized Nix could give me a ton of structure and organization. Since I have been running NixOS system wide for some years now, I have grown comfortable writing Nix expressions. Nix will let user define software configurations declaratively. I'm abusing it for voice logic. I wanted to declaratively define scripts and bind natural language intents to each of them - and Nix is great at producing structured derivations.
Each script gets a name, description, category, and as many parameter options as prefered., Add a set of intent sentences to that.
These are eventually transformed into regular expressions, cached, and tested. Lists can optionally be defined mapping in words to out words. Or set the list value as a wildcard if input for the parameter should be a wildcard and accept any input.
From Sentences to Shell Scripts
The core idea was: take a sentence from the user, figure out what script it maps to, and run it with the correct arguments. Sounds simple? I wouldn't really call it easy, but okay.
# ๐ฆ says โฎ Logical first steps
let scripts = config.yo.scripts; # ๐ฆ says โฎ import all dem scripts scriptNames = builtins.attrNames scripts; # ๐ฆ says โฎ names can be useful too scriptNamesWithIntents = builtins.filter (scriptName: builtins.hasAttr scriptName config.yo.do.intents ) scriptNames; # ๐ฆ says โฎ scripts with no sentences - skippin' dem yo
Intent Parsing + Entity Resolution
To match user input, I needed to preprocess the input sentence, resolve any custom entities, and match it against a giant list of potential phrases. Oh yeah, and some of the words in those phrases were dynamic parameters.
# ๐ฆ says โฎ where da magic dynamic regex iz at makePatternMatcher = scriptName: let dataList = config.yo.do.intents.${scriptName}.data; in '' # ๐ฆ says โฎ diz iz how i pick da script u want match_${scriptName}() { # ๐ฆ says โฎ shushin' da caps โ lowercase life 4 cleaner dyn regex zen โจ local input="$(echo "$1" | tr '[:upper:]' '[:lower:]')" # ๐ฆ says โฎ always show input in debug mode # ๐ฆ says โฎ watch the fancy stuff live in action dt_debug "Trying to match for script: ${scriptName}" >&2 dt_debug "Input: $input" >&2 # ๐ฆ says โฎ duck presentin' - da madnezz ${lib.concatMapStrings (data: lib.concatMapStrings (sentence: lib.concatMapStrings (sentenceText: let # ๐ฆ says โฎ now sentenceText is one of the expanded variants! parts = lib.splitString "{" sentenceText; # ๐ฆ says โฎ diggin' out da goodies from curly nests! Gimme dem {param} nuggets! firstPart = lib.escapeRegex (lib.elemAt parts 0); # ๐ฆ says โฎ gotta escape them weird chars restParts = lib.drop 1 parts; # ๐ฆ says โฎ now we in the variable zone quack? # ๐ฆ says โฎ process each part to build regex and params regexParts = lib.imap (i: part: let split = lib.splitString "}" part; # ๐ฆ says โฎ yeah yeah curly close that syntax shell param = lib.elemAt split 0; # ๐ฆ says โฎ name of the param in da curly โ ex: {user} after = lib.concatStrings (lib.tail split); # ๐ฆ says โฎ anything after the param in this chunk # ๐ฆ says โฎ Wildcard mode! anything goes - duck catches ALL the worms! (.*) isWildcard = data.lists.${param}.wildcard or false; regexGroup = if isWildcard then "(.*)" else "\\b([^ ]+)\\b"; # ๐ฆ says โฎ ^ da regex that gon match actual input text in { regex = regexGroup + lib.escapeRegex after; param = param; } ) restParts; fullRegex = let clean = lib.strings.trim (firstPart + lib.concatStrings (map (v: v.regex) regexParts)); in "^${clean}$"; # ๐ฆ says โฎ mash all regex bits 2gether paramList = map (v: v.param) regexParts; # ๐ฆ says โฎ the squad of parameters in '' local regex='^${fullRegex}$' dt_debug "REGEX: $regex" if [[ "$input" =~ $regex ]]; then # ๐ฆ says โฎ DANG DANG โ regex match engaged ${lib.concatImapStrings (i: paramName: '' # ๐ฆ says โฎ extract match group #i+1 โ param value, come here plz param_value="''${BASH_REMATCH[${toString (i+1)}]}" # ๐ฆ says โฎ if param got synonym, apply the duckfilter if [[ -n "''${param_value:-}" && -v substitutions["$param_value"] ]]; then subbed="''${substitutions["$param_value"]}" if [[ -n "$subbed" ]]; then param_value="$subbed" fi fi ${lib.optionalString ( data.lists ? ${paramName} && !(data.lists.${paramName}.wildcard or false) ) '' # ๐ฆ says โฎ apply substitutions before case matchin' if [[ -v substitutions["$param_value"] ]]; then param_value="''${substitutions["$param_value"]}" fi case "$param_value" in ${makeEntityResolver data paramName} *) ;; esac ''} # ๐ฆ says โฎ declare global param โ duck want it everywhere! (for bash access) declare -g "_param_${paramName}"="$param_value" declare -A params=() params["${paramName}"]="$param_value" matched_params+=("$paramName") '') paramList} # ๐ฆ says โฎ set dat param as a GLOBAL VAR yo! every duck gotta know # ๐ฆ says โฎ build cmd args: --param valu cmd_args=() ${lib.concatImapStrings (i: paramName: '' value="''${BASH_REMATCH[${toString i}]}" cmd_args+=(--${paramName} "$value") '') paramList} dt_debug "REMATCH 1: ''${BASH_REMATCH[1]}" dt_debug "REMATCH 2: ''${BASH_REMATCH[2]}" dt_debug "MATCHED SCRIPT: ${scriptName}" dt_debug "ARGS: ''${cmd_args[@]}" return 0 fi '') (expandOptionalWords sentence) ) data.sentences ) dataList} return 1 } ''; # ๐ฆ says โฎ dat was fun! let'z do it again some time
# ๐ฆ says โฎ Funny side note, before this project, regular expressions was diz duck's absolute worst nightmare. These days diz duck quacktually don't mind it dat much.
The Performance Bomb
As I implemented more features, performance became explosive. I wanted my sentence definitions to have [optional|words] and (required|one|of|these|words) patterns. This led to combinatorial explosion.
cartesianProductOfLists = lists: # ๐ฆ says โฎ if da listz iz empty .. if lists == [] then [ [] ] # ๐ฆ says โฎ .. i gib u empty listz of listz yo got it? else # ๐ฆ says โฎ ELSE WAT?! let # ๐ฆ says โฎ sorry.. i gib u first list here u go yo head = builtins.head lists; # ๐ฆ says โฎ remaining listz for u here u go bro! tail = builtins.tail lists; # ๐ฆ says โฎ calculate combinations for my tail - yo calc wher u at?! tailProduct = cartesianProductOfLists tail; in # ๐ฆ says โฎ for everyy x in da listz .. lib.concatMap (x: # ๐ฆ says โฎ .. letz combinez wit every tail combinationz .. map (y: [x] ++ y) tailProduct ) head; # ๐ฆ says โฎ dang! datz a DUCK COMBO alright! # ๐ฆ says โฎ here i duckie help yo out! makin' yo life eazy sleazy' wen declarative sentence yo typin' expandOptionalWords = sentence: # ๐ฆ says โฎ qucik & simple sentences we quacky & hacky expandin' let # ๐ฆ says โฎ CHOP CHOP! Rest in lil' Pieceez bigg sentence!!1 tokens = lib.splitString " " sentence; # ๐ฆ says โฎ definin' dem wordz in da (braces) taggin' dem' wordz az (ALTERNATIVES) lettin' u choose one of dem wen triggerin' isRequiredGroup = t: lib.hasPrefix "(" t && lib.hasSuffix ")" t; # ๐ฆ says โฎ puttin' sentence wordz in da [bracket] makin' em' [OPTIONAL] when doin' u don't have to be pickin' woooho isOptionalGroup = t: lib.hasPrefix "[" t && lib.hasSuffix "]" t; expandToken = token: # ๐ฆ says โฎ dis gets all da real wordz out of one token (yo!) if isRequiredGroup token then let # ๐ฆ says โฎ thnx 4 lettin' ducklin' be cleanin' - i'll be removin' dem "()" clean = lib.removePrefix "(" (lib.removeSuffix ")" token); alternatives = lib.splitString "|" clean; # ๐ฆ says โฎ use "|" to split (alternative|wordz) yo in # ๐ฆ says โฎ dat's dat 4 dem alternativez alternatives else if isOptionalGroup token then let # ๐ฆ says โฎ here we be goin' again - u dirty and i'll be cleanin' dem "[]" clean = lib.removePrefix "[" (lib.removeSuffix "]" token); alternatives = lib.splitString "|" clean; # ๐ฆ says โฎ i'll be stealin' dat "|" from u in # ๐ฆ says โฎ u know wat? optional means we include blank too! alternatives ++ [ "" ] else # ๐ฆ says โฎ else i be returnin' raw token for yo [ token ]; # ๐ฆ says โฎ now i gib u generatin' all dem combinationz yo expanded = cartesianProductOfLists (map expandToken tokens); # ๐ฆ says โฎ clean up if too much space, smush back into stringz for ya trimmedVariants = map (tokenList: let # ๐ฆ says โฎ join with spaces then trim them suckers raw = lib.concatStringsSep " " tokenList; # ๐ฆ says โฎ remove ALL extra spaces cleaned = lib.replaceStrings [" "] [" "] (lib.strings.trim raw); in # ๐ฆ says โฎ wow now they be shinin' cleaned ) expanded; # ๐ฆ says โฎ and they be multiplyyin'! # ๐ฆ says โฎ throwin' out da empty and cursed ones yo nonEmpty = lib.filter (s: s != "") trimmedVariants; hasFixedText = v: builtins.match ".*[^\\{].*" v != null; # ๐ฆ says โฎ no no no, no nullin' validVariants = lib.filter hasFixedText nonEmpty; in # ๐ฆ says โฎ returnin' all unique variantz of da sentences โ holy duck dat'z fresh lib.unique validVariants;
This is where the bomb went off. The module went from being instant to extremely slow.
Even with a very detailed debugging system in place, when you are generating code with this extensiveness, finding the problematic line can take time, later i found that it was caused by a single intent definition, my timer intent....
I was defining this intent in the most genius way, something similar to:
sentences = [ "(create|set|start|launch) [a] timer [for] {hours} (hour|hours) {minutes} (minute|minutes) {seconds} (second|seconds)" "(create|set|start|launch) [a] timer [for] {minutes} (minute|minutes) [and] {seconds} (second|seconds)" "(create|set|start|launch) [a] timer [for] {minutes} (minute|minutes)" "(create|set|start|launch) [a] timer [for] {seconds} seconds" ]; lists = { hours.values = lib.genList (n: { "in" = lib.concatStringsSep "|" [ (toString n) ("kl " + toString n) (toString n + "h") (builtins.elemAt numberWords n) ]; out = toString n; }) 24; minutes.values = lib.genList (n: { "in" = lib.concatStringsSep "|" [ (toString n) (toString n + "m") ("minut " + toString n) (builtins.elemAt numberWords n) ]; out = toString n; }) 60; seconds.values = lib.genList (n: { "in" = lib.concatStringsSep "|" [ (toString n) (toString n + "s") ("sekund " + toString n) (builtins.elemAt numberWords n) ]; out = toString n; }) 60;
To better paint the picture of why this was a bad idea, I'll do the math
Combinational explosion:
- Sentence 4:
Fixed Variants: 16. Param combos: 240. Total variants: 16 ร 240 = 3,840 - Sentence 3:
Fixed variants: 32. Param combos: 240. Total variants: 32 ร 240 = 7,680 - Sentence 2:
Fixed variants: 128. Param combos: 240 ร 240 = 57,600. Total variants: 128 ร 57,600 = 7,372,800 - Sentence 1:
Fixed variants: 128. Param combos: 96 ร 240 ร 240 = 5,529,600. Total variants: 128 ร 5,529,600 = 707,788,800 (!) - Total number of unique sentence variations:
715,173,120
Yes โ over 715 million possible combinations.
Insane? Definitely.
Useful? Ehh.. I don't think so?
But I still think this example shows how powerful this can be if you'd decide to go that crazy route.
This is where I decided to define with caution, and also where I implemented the next performance feature, the priority system.
Basic functionallity that let user define order of which the intents are processed for the regex patterns.
# ๐ฆ says โฎ priority system 4 runtime optimization scriptRecordsWithIntents = let # ๐ฆ says โฎ calculate priority calculatePriority = scriptName: config.yo.do.intents.${scriptName}.priority or 3; # ๐ฆ says โฎ default medium # ๐ฆ says โฎ create script records metadata makeRecord = scriptName: rec { name = scriptName; priority = calculatePriority scriptName; hasComplexPatterns = let intent = config.yo.do.intents.${scriptName}; patterns = lib.concatMap (d: d.sentences) intent.data; in builtins.any (p: lib.hasInfix "{" p || lib.hasInfix "[" p) patterns; }; in lib.sort (a: b: # ๐ฆ says โฎ primary sort: lower number = higher priority a.priority < b.priority # ๐ฆ says โฎ secondary sort: simple patterns before complex ones || (a.priority == b.priority && !a.hasComplexPatterns && b.hasComplexPatterns) # ๐ฆ says โฎ third sort: alphabetical for determinism || (a.priority == b.priority && a.hasComplexPatterns == b.hasComplexPatterns && a.name < b.name) ) (map makeRecord scriptNamesWithIntents); # ๐ฆ says โฎ generate optimized processing order processingOrder = map (r: r.name) scriptRecordsWithIntents;
Fuzzy Matching
Exact regex matches were cool... until they weren't.
Since I had been getting longer and longer runtime execution, I really wanted something more than just fuzz here.
I decided to go with a trigrams cache solution, with a falling back to Levenshtein distance algoritm.
I think this quickly turned out in my favor both performance and speed wise.
To be honest, what was tricky about this part was, in what way I would use the functions.
I started with the obvious exact matching falling back to fuzzy matching. Which doubled the runtime if a command had no exact match. (At this point about 50 seconds total), which is way to high for my use case.
I wanted to run the fuzzy matching logic async with the exact matching, holding off with the execution of the potential command until exact matching had failed.
Running multiple jobs in the background in Bash is fine, but it pretty much makes you lose control of the actual process and makes you wonder who is actually steering this boat?
trigram_similarity() { local str1="$1" local str2="$2" declare -a tri1 tri2 # ๐ฆ says โฎ creates 3 char substring from str1 for ((i=0; i<''${#str1}-2; i++)); do tri1+=( "''${str1:i:3}" ) done # ๐ฆ says โฎ creates 3 char substring from str2 for ((i=0; i<''${#str2}-2; i++)); do tri2+=( "''${str2:i:3}" ) done local matches=0 # ๐ฆ says โฎ count how many trigrams from str1 appear in str2 for t in "''${tri1[@]}"; do [[ " ''${tri2[*]} " == *" $t "* ]] && ((matches++)) done # ๐ฆ says โฎ calculate total number of trigrams local total=$(( ''${#tri1[@]} + ''${#tri2[@]} )) # ๐ฆ says โฎ no trigrams? (( total == 0 )) && echo 0 && return # ๐ฆ says โฎ return diceโs coefficient similarity ร 100 echo $(( 100 * 2 * matches / total )) # ๐ฆ says โฎ 0-100 scale } levenshtein_similarity() { local a="$1" b="$2" local len_a=''${#a} len_b=''${#b} local max_len=$(( len_a > len_b ? len_a : len_b )) (( max_len == 0 )) && echo 100 && return local dist=$(levenshtein "$a" "$b") local score=$(( 100 - (dist * 100 / max_len) )) # ๐ฆ says โฎ boostz da score for same startin' charizard yo [[ "''${a:0:1}" == "''${b:0:1}" ]] && score=$(( score + 10 )) echo $(( score > 100 ? 100 : score )) }
Bash script workflow
Takes --input "some natural language command" from the user.
Load intent data from a JSON file (includes regex patterns, substitutions, etc.).
Load fuzzy index for fallback fuzzy matching.
For each defined script: Load regex substitutions (entities) and store them in an associative array.
Exact Match Phase (Runs in Background):
Iterate through all defined scripts (in order of priority).
Apply regex substitutions to input text.
Try to match input via corresponding match_script functions.
If matched:
Apply substitutions.
Prepare arguments.
Execute yo-script with those arguments.
Signal fuzzy handler to stop
Fuzzy Match Phase (Runs Concurrently):
Normalize input text.
Compute trigram + Levenshtein similarity scores against defined sentences.
Select best match above the threshold.
Apply the same substitution logic.
Match using match_fuzzy_script function.
If matched:
Wait for exact matcher to finish.
If exact didnโt match, execute yo-script with resolved args.
โถ View the Bash script for the NLP module
in { # ๐ฆ says โฎ YOOOOOOOOOOOOOOOOOO yo.scripts = { # ๐ฆ says โฎ quack quack quack quack quack.... qwack do = { description = "Natural language to Shell script translator with dynamic regex matching and automatic parameter resolutiion"; aliases = ["b"]; category = "โ๏ธ Configuration"; # ๐ฆ says โฎ duckgorize iz zmart wen u hab many scriptz i'd say! logLevel = "WARNING"; autoStart = false; parameters = [ { name = "input"; description = "Text to parse into a yo command"; optional = false; } { name = "fuzzyThreshold"; description = "Minimum procentage for considering fuzzy matching sucessful. (1-100)"; default = "15"; } ]; # ๐ฆ says โฎ run yo do --help to display all defined voice commands helpFooter = '' WIDTH=$(tput cols) # ๐ฆ duck say โฎ Auto detect width cat <0' "$intent_data_file" 2>/dev/null || echo false) if [[ "$has_lists" != "true" ]]; then echo -n "$text" echo "|declare -A substitutions=()" # ๐ฆ says โฎ empty substitutions return fi # ๐ฆ says โฎ dis is our quacktionary yo replacements=$(jq -r '.["'"$script"'"].substitutions[] | "\(.pattern)|\(.value)"' "$intent_data_file") while IFS="|" read -r pattern out; do if [[ -n "$pattern" && "$text" =~ $pattern ]]; then original="''${BASH_REMATCH[0]}" [[ -z "''$original" ]] && continue # ๐ฆ says โฎ duck no like empty string substitutions["''$original"]="$out" substitution_applied=true # ๐ฆ says โฎ rack if any substitution was applied text=$(echo "$text" | sed -E "s/\\b$pattern\\b/$out/g") # ๐ฆ says โฎ swap the word, flip the script fi done <<< "$replacements" echo -n "$text" echo "|$(declare -p substitutions)" # ๐ฆ says โฎ returning da remixed sentence + da whole } for f in "$MATCHER_DIR"/*.sh; do [[ -f "$f" ]] && source "$f"; done scripts_ordered_by_priority=( ${lib.concatMapStringsSep "\n" (name: " \"${name}\"") processingOrder} ) dt_info "$scripts_ordered_by_priority" find_best_fuzzy_match() { local input="$1" local best_score=0 local best_match="" local normalized=$(echo "$input" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]') local candidates mapfile -t candidates < <(jq -r '.[] | .[] | "\(.script):\(.sentence)"' "$YO_FUZZY_INDEX") dt_debug "Found ''${#candidates[@]} candidates for fuzzy matching" for candidate in "''${candidates[@]}"; do IFS=':' read -r script sentence <<< "$candidate" local norm_sentence=$(echo "$sentence" | tr '[:upper:]' '[:lower:]' | tr -d '[:punct:]') local tri_score=$(trigram_similarity "$normalized" "$norm_sentence") (( tri_score < 30 )) && continue local score=$(levenshtein_similarity "$normalized" "$norm_sentence") if (( score > best_score )); then best_score=$score best_match="$script:$sentence" dt_info "New best match: $best_match ($score%)" fi done if [[ -n "$best_match" ]]; then echo "$best_match|$best_score" else echo "" fi } # ๐ฆ says โฎ insert matchers, build da regex empire. yo ${lib.concatMapStrings (name: makePatternMatcher name) scriptNamesWithIntents} # ๐ฆ says โฎ for dem scripts u defined intents for .. exact_match_handler() { for script in "''${scripts_ordered_by_priority[@]}"; do # ๐ฆ says โฎ .. we insert wat YOU sayz & resolve entities wit dat yo resolved_output=$(resolve_entities "$script" "$text") resolved_text=$(echo "$resolved_output" | cut -d'|' -f1) dt_debug "Tried: match_''${script} '$resolved_text'" # ๐ฆ says โฎ we declare som substitutionz from listz we have - duckz knowz why subs_decl=$(echo "$resolved_output" | cut -d'|' -f2-) declare -gA substitutions || true eval "$subs_decl" >/dev/null 2>&1 || true # ๐ฆ says โฎ we hab a match quacky quacky diz sure iz hacky! if match_$script "$resolved_text"; then if [[ "$(declare -p substitutions 2>/dev/null)" =~ "declare -A" ]]; then for original in "''${!substitutions[@]}"; do dt_debug "Substitution: $original >''${substitutions[$original]}"; [[ -n "$original" ]] && dt_info "$original > ''${substitutions[$original]}" # ๐ฆ says โฎ see wat duck did there? done # ๐ฆ says โฎ i hop duck pick dem right - right? fi args=() # ๐ฆ says โฎ duck gettin' ready 2 build argumentz 4 u script for arg in "''${cmd_args[@]}"; do dt_debug "ADDING PARAMETER: $arg" args+=("$arg") # ๐ฆ says โฎ collecting them shell spell ingredients done # ๐ฆ says โฎ final product - hope u like say duck! paramz="''${args[@]}" && echo echo "exact" > "$match_result_flag" # ๐ฆ says โฎ tellz fuzzy handler we done dt_debug "Executing: yo $script $paramz" # ๐ฆ says โฎ EXECUTEEEEEEEAAA โ HERE WE QUAAAAACKAAAOAA exec "yo-$script" "''${args[@]}" return 0 fi done } ${lib.concatMapStrings (name: makeFuzzyPatternMatcher name) scriptNamesWithIntents} # ๐ฆ SCREAMS โฎ FUZZY WOOOO TO THE MOON fuzzy_match_handler() { resolved_output=$(resolve_entities "dummy" "$text") # We'll resolve properly after matching resolved_text=$(echo "$resolved_output" | cut -d'|' -f1) fuzzy_result=$(find_best_fuzzy_match "$resolved_text") [[ -z "$fuzzy_result" ]] && return 1 IFS='|' read -r combined match_score <<< "$fuzzy_result" IFS=':' read -r matched_script matched_sentence <<< "$combined" dt_debug "Best fuzzy script: $matched_script" >&2 # ๐ฆ says โฎ resolve entities agein, diz time for matched script yo resolved_output=$(resolve_entities "$matched_script" "$text") resolved_text=$(echo "$resolved_output" | cut -d'|' -f1) subs_decl=$(echo "$resolved_output" | cut -d'|' -f2-) declare -gA substitutions || true eval "$subs_decl" >/dev/null 2>&1 || true # if (( best_score >= $FUZZY_THHRESHOLD )); then # ๐ฆ says โฎ we hab a match quacky quacky diz sure iz hacky! if match_fuzzy_$matched_script "$resolved_text" "$matched_sentence"; then if [[ "$(declare -p substitutions 2>/dev/null)" =~ "declare -A" ]]; then for original in "''${!substitutions[@]}"; do dt_debug "Substitution: $original >''${substitutions[$original]}"; [[ -n "$original" ]] && dt_info "$original > ''${substitutions[$original]}" # ๐ฆ says โฎ see wat duck did there? done # ๐ฆ says โฎ i hop duck pick dem right - right? fi args=() # ๐ฆ says โฎ duck gettin' ready 2 build argumentz 4 u script for arg in "''${cmd_args[@]}"; do dt_debug "ADDING PARAMETER: $arg" args+=("$arg") # ๐ฆ says โฎ collecting them shell spell ingredients done # ๐ฆ says โฎ wait for exact match to finish # while kill -0 "$pid1" 2>/dev/null; do while [[ ! -f "$match_result_flag" || $(cat "$match_result_flag") != "exact_finished" ]]; do sleep 0.05 done # ๐ฆ says โฎ checkz if exact match succeeded yo if [[ $(cat "$match_result_flag") == "exact" ]]; then dt_debug "Exact match already handled execution. Fuzzy exiting." exit 0 fi # ๐ฆ says โฎ final product - hope u like say duck! paramz="''${args[@]}" && echo dt_info "Executing: yo $matched_script $paramz" # ๐ฆ says โฎ EXECUTEEEEEEEAAA โ HERE WE QUAAAAACKAAAOAA exec "yo-$matched_script" "''${args[@]}" return 0 fi } # ๐ฆ says โฎ if exact match winz, no need for fuzz! but fuzz ready to quack when regex chokes exact_match_handler & pid1=$! fuzzy_match_handler exit ''; };
Comments on this blog post