[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

another update

Mohammad Bagher Abiyat (Oct 2, 2024, 8:51 PM +0330) 78914e21 1e0ec86b

+363 -15
+334 -15
cac.ts
··· 1 - import cac from 'cac' 1 + import cac, { Command } from "cac"; 2 + 3 + const cli = cac("cac"); 4 + import mri, { Options } from "mri"; 5 + 6 + cli.option("--type [type]", "Choose a project type", { 7 + default: "node", 8 + }); 9 + cli.option("--name <name>", "Provide your name"); 10 + 11 + cli.command("start").option("--port <port>", "your port"); 12 + 13 + cli.command("help"); 14 + 15 + cli.command("help config").option("--foo", "foo option"); 16 + 17 + cli 18 + .command("complete") 19 + .action(() => console.log(genZshComp("my_command", true))); 20 + 21 + cli 22 + .command("__complete [...args]") 23 + .action((_, { ["--"]: args }: { ["--"]: string[] }) => { 24 + const toComplete = args[args.length - 1]; 25 + const trimmedArgs = args.slice(0, args.length - 1); 26 + 27 + // console.log("found", cli.parse(trimmedArgs)) 28 + const parsed = mri(trimmedArgs); 29 + const commandName = parsed._.join(" "); 30 + 31 + let command: Command | null = null; 32 + for (const c of cli.commands) { 33 + if (c.isMatched(commandName)) { 34 + command = c; 35 + } 36 + } 37 + const completions: string[] = []; 38 + 39 + if (toComplete.startsWith("-")) { 40 + const flag = toComplete.slice("--".length); 41 + const options: Command["options"] = [ 42 + ...(command?.options ?? []), 43 + ...cli.globalCommand.options, 44 + ]; 45 + if (flag) { 46 + completions.push( 47 + ...options 48 + .filter((o) => o.names.some((name) => name.includes(flag))) 49 + .map((o) => `--${o.name}`), 50 + ); 51 + } else { 52 + } 53 + 54 + } 55 + 56 + // console.log(args, toComplete, commandName); 57 + // console.log(cli); 58 + console.log(`${completions.join("\n")}\n`); 59 + }); 60 + 61 + cli.version("0.0.0"); 62 + cli.help(); 63 + 64 + // console.log('cli', cli) 65 + 66 + // ShellCompRequestCmd is the name of the hidden command that is used to request 67 + // completion results from the program. It is used by the shell completion scripts. 68 + const ShellCompRequestCmd: string = "__complete"; 69 + 70 + // ShellCompNoDescRequestCmd is the name of the hidden command that is used to request 71 + // completion results without their description. It is used by the shell completion scripts. 72 + const ShellCompNoDescRequestCmd: string = "__completeNoDesc"; 73 + 74 + // ShellCompDirective is a bit map representing the different behaviors the shell 75 + // can be instructed to have once completions have been provided. 76 + enum ShellCompDirective { 77 + // ShellCompDirectiveError indicates an error occurred and completions should be ignored. 78 + ShellCompDirectiveError = 1 << 0, 79 + 80 + // ShellCompDirectiveNoSpace indicates that the shell should not add a space 81 + // after the completion even if there is a single completion provided. 82 + ShellCompDirectiveNoSpace = 1 << 1, 83 + 84 + // ShellCompDirectiveNoFileComp indicates that the shell should not provide 85 + // file completion even when no completion is provided. 86 + ShellCompDirectiveNoFileComp = 1 << 2, 87 + 88 + // ShellCompDirectiveFilterFileExt indicates that the provided completions 89 + // should be used as file extension filters. 90 + // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename() 91 + // is a shortcut to using this directive explicitly. The BashCompFilenameExt 92 + // annotation can also be used to obtain the same behavior for flags. 93 + ShellCompDirectiveFilterFileExt = 1 << 3, 94 + 95 + // ShellCompDirectiveFilterDirs indicates that only directory names should 96 + // be provided in file completion. To request directory names within another 97 + // directory, the returned completions should specify the directory within 98 + // which to search. The BashCompSubdirsInDir annotation can be used to 99 + // obtain the same behavior but only for flags. 100 + ShellCompDirectiveFilterDirs = 1 << 4, 101 + 102 + // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order 103 + // in which the completions are provided. 104 + ShellCompDirectiveKeepOrder = 1 << 5, 105 + 106 + // =========================================================================== 107 + 108 + // All directives using iota (or equivalent in Go) should be above this one. 109 + // For internal use. 110 + shellCompDirectiveMaxValue = 1 << 6, 111 + 112 + // ShellCompDirectiveDefault indicates to let the shell perform its default 113 + // behavior after completions have been provided. 114 + // This one must be last to avoid messing up the iota count. 115 + ShellCompDirectiveDefault = 0, 116 + } 117 + 118 + const execPath = process.execPath; 119 + const args = process.argv.slice(1); 120 + const x = `${execPath} ${process.execArgv.join(" ")} ${args[0]}`; 121 + // console.error(x) 122 + 123 + function genZshComp(name: string, includeDesc: boolean) { 124 + let compCmd = ShellCompRequestCmd; 125 + if (!includeDesc) { 126 + compCmd = ShellCompNoDescRequestCmd; 127 + } 128 + 129 + return `#compdef ${name} 130 + compdef _${name} ${name} 131 + 132 + # zsh completion for ${name} -*- shell-script -*- 133 + 134 + __${name}_debug() { 135 + local file="$BASH_COMP_DEBUG_FILE" 136 + if [[ -n \${file} ]]; then 137 + echo "$*" >> "\${file}" 138 + fi 139 + } 140 + 141 + _${name}() { 142 + local shellCompDirectiveError=${ShellCompDirective.ShellCompDirectiveError} 143 + local shellCompDirectiveNoSpace=${ShellCompDirective.ShellCompDirectiveNoSpace} 144 + local shellCompDirectiveNoFileComp=${ShellCompDirective.ShellCompDirectiveNoFileComp} 145 + local shellCompDirectiveFilterFileExt=${ShellCompDirective.ShellCompDirectiveFilterFileExt} 146 + local shellCompDirectiveFilterDirs=${ShellCompDirective.ShellCompDirectiveFilterDirs} 147 + local shellCompDirectiveKeepOrder=${ShellCompDirective.ShellCompDirectiveKeepOrder} 148 + 149 + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder 150 + local -a completions 151 + 152 + __${name}_debug "\\n========= starting completion logic ==========" 153 + __${name}_debug "CURRENT: \${CURRENT}, words[*]: \${words[*]}" 154 + 155 + # The user could have moved the cursor backwards on the command-line. 156 + # We need to trigger completion from the \$CURRENT location, so we need 157 + # to truncate the command-line (\$words) up to the \$CURRENT location. 158 + # (We cannot use \$CURSOR as its value does not work when a command is an alias.) 159 + words=( "\${=words[1,CURRENT]}" ) 160 + __${name}_debug "Truncated words[*]: \${words[*]}," 161 + 162 + lastParam=\${words[-1]} 163 + lastChar=\${lastParam[-1]} 164 + __${name}_debug "lastParam: \${lastParam}, lastChar: \${lastChar}" 165 + 166 + # For zsh, when completing a flag with an = (e.g., ${name} -n=<TAB>) 167 + # completions must be prefixed with the flag 168 + setopt local_options BASH_REMATCH 169 + if [[ "\${lastParam}" =~ '-.*=' ]]; then 170 + # We are dealing with a flag with an = 171 + flagPrefix="-P \${BASH_REMATCH}" 172 + fi 173 + 174 + # Prepare the command to obtain completions 175 + requestComp="${x} ${compCmd} -- \${words[2,-1]}" 176 + if [ "\${lastChar}" = "" ]; then 177 + # If the last parameter is complete (there is a space following it) 178 + # We add an extra empty parameter so we can indicate this to the go completion code. 179 + __${name}_debug "Adding extra empty parameter" 180 + requestComp="\${requestComp} \"\"" 181 + fi 2 182 3 - const cli = cac('cac') 183 + __${name}_debug "About to call: eval \${requestComp}" 4 184 5 - cli.option('--type [type]', 'Choose a project type', { 6 - default: 'node', 7 - }) 8 - cli.option('--name <name>', 'Provide your name') 185 + # Use eval to handle any environment variables and such 186 + out=\$(eval \${requestComp} 2>/dev/null) 187 + __${name}_debug "completion output: \${out}" 188 + 189 + # Extract the directive integer following a : from the last line 190 + local lastLine 191 + while IFS='\n' read -r line; do 192 + lastLine=\${line} 193 + done < <(printf "%s\n" "\${out[@]}") 194 + __${name}_debug "last line: \${lastLine}" 195 + 196 + if [ "\${lastLine[1]}" = : ]; then 197 + directive=\${lastLine[2,-1]} 198 + # Remove the directive including the : and the newline 199 + local suffix 200 + (( suffix=\${#lastLine}+2)) 201 + out=\${out[1,-\$suffix]} 202 + else 203 + # There is no directive specified. Leave \$out as is. 204 + __${name}_debug "No directive found. Setting to default" 205 + directive=0 206 + fi 207 + 208 + __${name}_debug "directive: \${directive}" 209 + __${name}_debug "completions: \${out}" 210 + __${name}_debug "flagPrefix: \${flagPrefix}" 211 + 212 + if [ \$((directive & shellCompDirectiveError)) -ne 0 ]; then 213 + __${name}_debug "Completion received error. Ignoring completions." 214 + return 215 + fi 9 216 10 - cli.command('lint [...files]', 'Lint files').action((files, options) => { 11 - console.log(files, options) 12 - }) 217 + local activeHelpMarker="%" 218 + local endIndex=\${#activeHelpMarker} 219 + local startIndex=\$((\${#activeHelpMarker}+1)) 220 + local hasActiveHelp=0 221 + while IFS='\n' read -r comp; do 222 + # Check if this is an activeHelp statement (i.e., prefixed with \$activeHelpMarker) 223 + if [ "\${comp[1,\$endIndex]}" = "\$activeHelpMarker" ];then 224 + __${name}_debug "ActiveHelp found: \$comp" 225 + comp="\${comp[\$startIndex,-1]}" 226 + if [ -n "\$comp" ]; then 227 + compadd -x "\${comp}" 228 + __${name}_debug "ActiveHelp will need delimiter" 229 + hasActiveHelp=1 230 + fi 231 + continue 232 + fi 13 233 14 - // Display help message when `-h` or `--help` appears 15 - cli.help() 16 - // Display version number when `-v` or `--version` appears 17 - // It's also used in help message 18 - cli.version('0.0.0') 234 + if [ -n "\$comp" ]; then 235 + # If requested, completions are returned with a description. 236 + # The description is preceded by a TAB character. 237 + # For zsh's _describe, we need to use a : instead of a TAB. 238 + # We first need to escape any : as part of the completion itself. 239 + comp=\${comp//:/\\:} 19 240 20 - console.log('cli', cli) 241 + local tab="\$(printf '\\t')" 242 + comp=\${comp//\$tab/:} 21 243 244 + __${name}_debug "Adding completion: \${comp}" 245 + completions+=\${comp} 246 + lastComp=\$comp 247 + fi 248 + done < <(printf "%s\n" "\${out[@]}") 22 249 250 + # Add a delimiter after the activeHelp statements, but only if: 251 + # - there are completions following the activeHelp statements, or 252 + # - file completion will be performed (so there will be choices after the activeHelp) 253 + if [ \$hasActiveHelp -eq 1 ]; then 254 + if [ \${#completions} -ne 0 ] || [ \$((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then 255 + __${name}_debug "Adding activeHelp delimiter" 256 + compadd -x "--" 257 + hasActiveHelp=0 258 + fi 259 + fi 260 + 261 + if [ \$((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then 262 + __${name}_debug "Activating nospace." 263 + noSpace="-S ''" 264 + fi 265 + 266 + if [ \$((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then 267 + __${name}_debug "Activating keep order." 268 + keepOrder="-V" 269 + fi 270 + 271 + if [ \$((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then 272 + # File extension filtering 273 + local filteringCmd 274 + filteringCmd='_files' 275 + for filter in \${completions[@]}; do 276 + if [ \${filter[1]} != '*' ]; then 277 + # zsh requires a glob pattern to do file filtering 278 + filter="\\*.\$filter" 279 + fi 280 + filteringCmd+=" -g \$filter" 281 + done 282 + filteringCmd+=" \${flagPrefix}" 283 + 284 + __${name}_debug "File filtering command: \$filteringCmd" 285 + _arguments '*:filename:'"\$filteringCmd" 286 + elif [ \$((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then 287 + # File completion for directories only 288 + local subdir 289 + subdir="\${completions[1]}" 290 + if [ -n "\$subdir" ]; then 291 + __${name}_debug "Listing directories in \$subdir" 292 + pushd "\${subdir}" >/dev/null 2>&1 293 + else 294 + __${name}_debug "Listing directories in ." 295 + fi 296 + 297 + local result 298 + _arguments '*:dirname:_files -/'" \${flagPrefix}" 299 + result=\$? 300 + if [ -n "\$subdir" ]; then 301 + popd >/dev/null 2>&1 302 + fi 303 + return \$result 304 + else 305 + __${name}_debug "Calling _describe" 306 + if eval _describe \$keepOrder "completions" completions \${flagPrefix} \${noSpace}; then 307 + __${name}_debug "_describe found some completions" 308 + 309 + # Return the success of having called _describe 310 + return 0 311 + else 312 + __${name}_debug "_describe did not find completions." 313 + __${name}_debug "Checking if we should do file completion." 314 + if [ \$((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then 315 + __${name}_debug "deactivating file completion" 316 + 317 + # We must return an error code here to let zsh know that there were no 318 + # completions found by _describe; this is what will trigger other 319 + # matching algorithms to attempt to find completions. 320 + # For example zsh can match letters in the middle of words. 321 + return 1 322 + else 323 + # Perform file completion 324 + __${name}_debug "Activating file completion" 325 + 326 + # We must return the result of this command, so it must be the 327 + # last command, or else we must store its result to return it. 328 + _arguments '*:filename:_files'" \${flagPrefix}" 329 + fi 330 + fi 331 + fi 332 + } 333 + 334 + # don't run the completion function when being sourced or eval-ed 335 + if [ "\${funcstack[1]}" = "_${name}" ]; then 336 + _${name} 337 + fi 338 + `; 339 + } 340 + 341 + cli.parse()
+4
package.json
··· 10 10 "author": "", 11 11 "license": "ISC", 12 12 "devDependencies": { 13 + "@types/node": "^22.7.4", 13 14 "cac": "^6.7.14", 14 15 "tsx": "^4.19.1" 16 + }, 17 + "dependencies": { 18 + "mri": "^1.2.0" 15 19 } 16 20 }
+25
pnpm-lock.yaml
··· 7 7 importers: 8 8 9 9 .: 10 + dependencies: 11 + mri: 12 + specifier: ^1.2.0 13 + version: 1.2.0 10 14 devDependencies: 15 + '@types/node': 16 + specifier: ^22.7.4 17 + version: 22.7.4 11 18 cac: 12 19 specifier: ^6.7.14 13 20 version: 6.7.14 ··· 161 168 cpu: [x64] 162 169 os: [win32] 163 170 171 + '@types/node@22.7.4': 172 + resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} 173 + 164 174 cac@6.7.14: 165 175 resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 166 176 engines: {node: '>=8'} ··· 178 188 get-tsconfig@4.8.1: 179 189 resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} 180 190 191 + mri@1.2.0: 192 + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 193 + engines: {node: '>=4'} 194 + 181 195 resolve-pkg-maps@1.0.0: 182 196 resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 183 197 ··· 185 199 resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} 186 200 engines: {node: '>=18.0.0'} 187 201 hasBin: true 202 + 203 + undici-types@6.19.8: 204 + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 188 205 189 206 snapshots: 190 207 ··· 260 277 '@esbuild/win32-x64@0.23.1': 261 278 optional: true 262 279 280 + '@types/node@22.7.4': 281 + dependencies: 282 + undici-types: 6.19.8 283 + 263 284 cac@6.7.14: {} 264 285 265 286 esbuild@0.23.1: ··· 296 317 dependencies: 297 318 resolve-pkg-maps: 1.0.0 298 319 320 + mri@1.2.0: {} 321 + 299 322 resolve-pkg-maps@1.0.0: {} 300 323 301 324 tsx@4.19.1: ··· 304 327 get-tsconfig: 4.8.1 305 328 optionalDependencies: 306 329 fsevents: 2.3.3 330 + 331 + undici-types@6.19.8: {}