3

My objective is to write a CLI in Typescript/node.js, that uses --experimental-specifier-resolution=node, written in yargs with support for autocompletion.

To make this work, I use this entry.sh file, thanks to this helpful SO anwswer (and the bin: {eddy: "./entry.sh"} options in package.json points to this file)

#!/usr/bin/env bash

full_path=$(realpath $0)
dir_path=$(dirname $full_path)
script_path="$dir_path/dist/src/cli/entry.js"

# Path is made thanks to: https://code-maven.com/bash-shell-relative-path
# Combined with knowledge from: https://stackoverflow.com/questions/68111434/how-to-run-node-js-cli-with-experimental-specifier-resolution-node

/usr/bin/env node --experimental-specifier-resolution=node $script_path "$@"

This works great, and I can use the CLI. However, autocompletion does not work. According to yargs I should be able to get autocompletion by outputting the result from ./entry.sh completion to the ~/.bashrc profile. However this does not seem to work.

Output from ./entry.sh completion:

###-begin-entry.js-completions-###
#
# yargs command completion script
#
# Installation: ./dist/src/cli/entry.js completion >> ~/.bashrc
#    or ./dist/src/cli/entry.js completion >> ~/.bash_profile on OSX.
#
_entry.js_yargs_completions()
{
    local cur_word args type_list

    cur_word="${COMP_WORDS[COMP_CWORD]}"
    args=("${COMP_WORDS[@]}")

    # ask yargs to generate completions.
    type_list=$(./dist/src/cli/entry.js --get-yargs-completions "${args[@]}")

    COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) )

    # if no match was found, fall back to filename completion
    if [ ${#COMPREPLY[@]} -eq 0 ]; then
      COMPREPLY=()
    fi

    return 0
}
complete -o default -F _entry.js_yargs_completions entry.js
###-end-entry.js-completions-###

I tried modifying the completion output, but I don't really understand bash - just yet

Update

Working on a reproducible example (WIP). Repo is here.

Currently one of the big differences is that npm link does not work the same in the 2 different environments. It's only in the repo where I'm trying to reproduce that /usr/local/share/npm-global/bin/ is actually updated. Currently trying to investigate this.

DauleDK
  • 3,313
  • 11
  • 55
  • 98
  • Did you restart Bash after modifying its startup file? – tripleee Dec 29 '21 at 08:43
  • As an aside, you should generally use double quotes around all variables which contain file names. See [When to wrap quotes around a shell variable](https://stackoverflow.com/questions/10067266/when-to-wrap-quotes-around-a-shell-variable) – tripleee Dec 29 '21 at 08:50
  • Hi @tripleee - thanks for giving me that hint. What variable are you thinking about here? The `type_list`? – DauleDK Dec 29 '21 at 09:06
  • 1
    `$0` and `$full_path` are both unquoted. That will work as long as they contain trivial file names, but blow up if you have paths with spaces in them etc. – tripleee Dec 29 '21 at 09:07
  • Can you try providing the full path of entry.js instead of `./dist/src/cli/entry.jsˋ in the completion function ? – Fravadona Dec 29 '21 at 17:56
  • I added quotes and also tried the full path - no luck. I also tried to remove the intermediate bash file, and pointing directly to the js entry file. No luck either.. – DauleDK Dec 29 '21 at 19:14
  • The completions work fine for me with the repo files you provided. Is this expected currently? – Amir Jan 01 '22 at 18:25
  • Hi @Amir - I think I found out why this was not working in Github codespaces. Will update the question/answer soon – DauleDK Jan 01 '22 at 18:32
  • @DauleDK It looks like `scriptName` fixed your issue though? – konsolebox Jan 01 '22 at 19:04

2 Answers2

1

You can try specifying the scriptName in your entry.js file to the name of your wrapper script. This may force generation of completion name using it. I haven't tried it but looking at the source code of yargs, it looks like the $0 parameter can be altered using scriptName, which in turn will affect how the completion-generation function generate the completion code:

In yargs-factor.ts:

  scriptName(scriptName: string): YargsInstance {
    this.customScriptName = true;
    this.$0 = scriptName;
    return this;
  }

In completion.ts:

  generateCompletionScript($0: string, cmd: string): string {
    let script = this.zshShell
      ? templates.completionZshTemplate
      : templates.completionShTemplate;
    const name = this.shim.path.basename($0);

    // add ./ to applications not yet installed as bin.
    if ($0.match(/\.js$/)) $0 = `./${$0}`;

    script = script.replace(/{{app_name}}/g, name);
    script = script.replace(/{{completion_command}}/g, cmd);
    return script.replace(/{{app_path}}/g, $0);
  }

Also I'm not sure how the "bin" configuration works but maybe because of scriptName you'd no longer need a wrapper.

Make sure the version of yargs you use supports this.

Also as a side note I thought about suggesting to modify the generated completion script directly but besides being hackish that might also still lead to the script name being unrecognized during completion. Anyhow I just looked at the right approach first.

The modified version would like this:

_entry.sh_yargs_completions()
{
    local cur_word args type_list

    cur_word="${COMP_WORDS[COMP_CWORD]}"
    args=("${COMP_WORDS[@]}")

    # ask yargs to generate completions.
    type_list=$(/path/to/entry.sh --get-yargs-completions "${args[@]}")

    COMPREPLY=( $(compgen -W "${type_list}" -- ${cur_word}) )

    # if no match was found, fall back to filename completion
    if [ ${#COMPREPLY[@]} -eq 0 ]; then
      COMPREPLY=()
    fi

    return 0
}
complete -o default -F _entry.sh_yargs_completions entry.sh

Another note: If the script name needs to be dynamic based on the name of its caller, you can make it identifiable through an environment variable, so in entry.sh you can declare it like this:

export ENTRY_JS_SCRIPT_NAME=entry.sh
node ...

Then somewhere in entry.js, you can access the variable name through this:

process.env.ENTRY_JS_SCRIPT_NAME

Maybe even just specify $0 or ${0##*/} whatever works:

export ENTRY_JS_SCRIPT_NAME=$0
konsolebox
  • 72,135
  • 12
  • 99
  • 105
  • Thanks for the nice answer - trying everything out. So far no luck - but still trying :) – DauleDK Dec 30 '21 at 12:45
  • You need to update your question on what you tried so far. It needs the right combination to be done right. – konsolebox Dec 30 '21 at 13:07
  • At least post the generated completion script you're now using and make sure it's loaded. – konsolebox Dec 30 '21 at 13:12
  • I'm creating a small reproducible environment - hang on :) – DauleDK Dec 30 '21 at 13:14
  • Updated the question, will continue to post findings. – DauleDK Dec 30 '21 at 13:27
  • I loaded your project. it doesn't seem to have completions enabled yet. To see the current active completions, enter `complete -p`. And then to examine a function, type `type (function_name)`. – konsolebox Dec 30 '21 at 14:15
  • I saw you remove the .sh file and just used the .js file directly. The completion works well if you enable it with `. <(./test-cli.js completion)`. – konsolebox Dec 30 '21 at 14:36
  • Currently I'm struggling with the fact that my reproduction env gets `Linux 11`, but my "real" repo only get's `Linux 10`.... Despite they have the exact same Docker file, and I started-stopped the codespaces... – DauleDK Dec 30 '21 at 14:56
  • Maybe the difference is in the docker versions where supported images vary. Just a guess. – konsolebox Dec 30 '21 at 15:06
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/240571/discussion-between-dauledk-and-konsolebox). – DauleDK Dec 30 '21 at 15:17
  • There is no "Linux 10" or "Linux 11", do you mean Debian 10 / 11? – tripleee Dec 31 '21 at 10:35
1

Thanks, everyone. The solution I ended up with, was 2 fold:

  1. I added a scriptName to the yargs config
  2. In the .sh file "wrapping", I used which node to probably set the --experimental-specifier-resolution=node flags.

test-cli.js

#!/usr/bin/env node

import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import { someOtherModule } from './some-other-module';

someOtherModule();

yargs(hideBin(process.argv))
  .command('curl <url>', 'fetch the contents of the URL', () => {}, (argv) => {
    console.info(argv)
  })
  .command('curlAgain <url>', 'fetch the contents of the URL', () => {}, (argv) => {
    console.info(argv)
  })
  .demandCommand(1)
  .help()
  .completion()
  .scriptName('eddy') // <== Added thanks to konsolebox
  .parse()

test-cli.sh

#!/usr/bin/env bash

full_path="$(realpath "$0")"
dir_path="$(dirname $full_path)"
script_path="$dir_path/test-cli.js"

node_path="$(which node)" # <== Makes it work on github codespaces 

$node_path --experimental-specifier-resolution=node $script_path "$@"

package.json

{
  "name": "lets-reproduce",
  "type": "module",
  "dependencies": {
    "yargs": "^17.3.1"
  },
  "bin": {
    "eddy": "./test-cli.sh"
  }
}

Steps to install autocompletion:

  1. run npm link
  2. run eddy completion >> ~/.bashrc
  3. source ~/.bashrc
  4. profit
DauleDK
  • 3,313
  • 11
  • 55
  • 98
  • I also suggest placing `$node_path` and `$script_path` around double-quotes just to avoid unexpected word splitting or globbing. The necessity to use `node_path="$(which node)"` is odd. Maybe codespaces has an exported node function which you can also bypass by adding `unset -f node`, or simply by running the command with `exec`. So it becomes `exec node --experimental-specifier-resolution=node "$script_path" "$@"`. To check if there's a function, use `type node`. – konsolebox Jan 02 '22 at 19:55