chrisant996 / clink

Bash's powerful command line editing in cmd.exe
https://chrisant996.github.io/clink/
GNU General Public License v3.0
3.44k stars 135 forks source link

How can I make argmatchers for the arguments to specific perl scripts? #657

Closed mjcarman closed 4 weeks ago

mjcarman commented 1 month ago

My scenario/use case: I wrote a simple parser for perl. Enjoying clink's support for command-line completion suggestions, I then wrote parsers for some Perl scripts that I run via doskey aliases. In order to get the latter working I added :chaincommand() to my perl parser. That worked, but had the very undesirable side-effect of making completions for other perl scripts (ones not using doskey aliases) search my path for executables instead of searching the current working directory for files/directories. e.g. typing perl con<tab> in a folder containing a script named "convert.pl" will suggest perl conhost.exe instead of (really, before) "perl convert.pl."

I experimented with the different chaincommand modes (particularly doskey) without success. I'm requesting a new "files" mode to allow chaining parsers but to otherwise act like the normal file/directory completion.

There may well be a different way to do this that I just haven't been clever enough to figure out yet.

chrisant996 commented 1 month ago

:chaincommand() means "the next word begins a new CMD command line". That doesn't seem relevant for text that comes after perl. I don't think :chaincommand() is useful for whatever you're trying to do, but I'm not sure yet what it is that you're trying to make work.

What version of Clink are you using? What wasn't working in "wrote parsers for some Perl scripts that I run via doskey aliases"?

Can you share a sample that reproduces whatever problems were encountered?

(Also, the exec.path setting is what controls whether command completions include matches from the PATH.)

chrisant996 commented 1 month ago

Are you trying to get the perl parser to show filename completions? If so, then that would be :addarg(clink.filematches) (not :chaincommand()).

mjcarman commented 1 month ago

Are you trying to get the perl parser to show filename completions? If so, then that would be :addarg(clink.filematches) (not :chaincommand()).

No. I'm trying to define completions for a custom Perl script the same way I would for any other command-line executable.

I'm using clink 1.6.20.

:chaincommand() means "the next word begins a new CMD command line". That doesn't seem relevant for text that comes after perl.

That's fair enough with regard to the intent of :chaincommand(). I'm using (abusing?) it for its "use another parser for the rest of the command" behavior.

Scenario:

require("arghelper")
clink.argmatcher("foo")
  :_addexflags({
    { "--help",    "Show built-in documentation"   },
    { "--version", "Print version number and exit" },
    { "--verbose", "Toggle verbose output"         },
  })

Tangential, but important: I added ".PL" to my PATHEXT environment variable (along with creating a file type for perl scripts and associating it with the perl executable). Also, my exec.aliases setting is set to True.

What I want to do is be able to type (e.g.) foo --v<ctrl+space> and see --version and --verbose as suggestions. I can make that work by adding :chaincommand() to my parser for perl, but doing so means that typing something like perl b<tab> (to complete perl bar.pl (where bar.pl is in the CWD) instead offers completions for executables beginning with "b" anywhere on my PATH.

mjcarman commented 1 month ago

The general case would be clink supporting chaining parsers for runtimes (perl, python, java, etc.) and scripts executed via those runtimes where the overall command might end up looking like this:

perl -Ilib -MMy::Module foo.pl --verbose abc.txt

chrisant996 commented 1 month ago

Scenario:

  • I wrote a script named "foo.pl" that takes some arguments.
  • I created an alias to the script as doskey foo=perl C:\path\to\foo.pl $* so I can easily run it from anywhere.
  • I created a "foo.lua" file in my clink completions folder. e.g.
require("arghelper")
clink.argmatcher("foo")
  :_addexflags({
    { "--help",    "Show built-in documentation"   },
    { "--version", "Print version number and exit" },
    { "--verbose", "Toggle verbose output"         },
  })

Tangential, but important: I added ".PL" to my PATHEXT environment variable (along with creating a file type for perl scripts and associating it with the perl executable). Also, my exec.aliases setting is set to True.

What I want to do is be able to type (e.g.) foo --v<ctrl+space> and see --version and --verbose as suggestions. I can make that work by adding :chaincommand() to my parser for perl, but doing so means that typing something like perl b<tab> (to complete perl bar.pl (where bar.pl is in the CWD) instead offers completions for executables beginning with "b" anywhere on my PATH.

Thank you for describing what you've tried, that helped very much.

I understand what you're trying to do now:

  1. You want foo to delay-load an argmatcher, but you want to delay-load logic to include doing a lookup of the doskey alias name before expanding it. Currently it only tries a lookup of the doskey alias itself if it couldn't find an argmatcher for the command inside the expanded doskey alias.
  2. You also want a way for scriptengine scriptname to look up an argmatcher for the scriptname.

Full solution is at the bottom; it's small and simple.

  • I created a "foo.lua" file in my clink completions folder. e.g.

That's the source of the problem.

If you move the "foo.lua" file to a normal Clink script directory so it's loaded at startup, instead of in a delay-load completions directory, then # 1 isn't needed, because the argmatcher is already loaded and so the delay-load lookup isn't even reached.

Or if you use the approach described further below for dealing with # 2, then # 1 isn't needed.

I can maybe consider exploring the possibility of adding another lookup step which tries using the doskey alias name before expanding it. But it would only partially address your needs, and it isn't needed at all if the following is used, which fully addresses your needs. (And I think it may break other things, so I wouldn't be in a hurry to experiment with it.)

Btw, here is a screenshot showing that if the argmatcher is loaded at startup, instead of delay-load, then the argmatcher for the doskey alias works how you wanted.

image

The general case would be clink supporting chaining parser for runtimes (perl, python, java, etc.) and scripts executed via those runtimes where the overall command might end up looking like this:

perl -Ilib -MMy::Module foo.pl --verbose abc.txt

For # 2 i.e. the "general case" you describe, that's much more problematic for Clink to do automagically. What about python foo.pl or perl taskmgr.exe and other nonsensical combinations of "chain command" input? You would have to somehow tell Clink what things are allowed as script names, to restrict chaining. To try to do that through the :chaincommand() function would get very messy, very quickly.

Instead of trying to use :chaincommand() and restricting it to become a different kind of feature, I would recommend to use clink.filematchesexact(), the onlink callback function, and clink.getargmatcher().

Try this:

First, insert these functions before the clink.argmatcher("perl") line:

local function perl_scripts(word)
    return clink.filematchesexact(word.."*.pl")
end

local function perl_onlink(link, arg_index, word) -- luacheck: no unused
    if word then
        local ext = path.getextension(word)
        if ext and ext:lower() == ".pl" then
            local argmatcher = clink.getargmatcher(word)
            return argmatcher
        end
    end
end

Then, replace the :chaincommand() line with this:

-  :chaincommand()
+  :addarg({ perl_scripts, onlink=perl_onlink })
mjcarman commented 4 weeks ago

Thank you! This is definitely better than what I'd been doing. Is there a way to get filematchesexact() to return directory names as partial matches the way filematches() does so I can incrementally search for scripts in sub-directories? Maybe it would be better to use filematches() and a filter instead, though it isn't clear to me how to hook one in or if I'd have to write a generator.

In onlink, what's the difference between returning nil and returning false? Ideally, after clink sees a script name it should stop using the perl argmatcher even if there isn't an argmatcher for the matched script. I tweaked your solution a bit and am using this at the moment. It works well enough for my purposes but I'm wondering if there's something better.

local argmatcher = clink.getargmatcher(word)
if argmatcher ~= nil then
    return argmatcher
else
    return file_matches
end
chrisant996 commented 4 weeks ago

Thank you! This is definitely better than what I'd been doing. Is there a way to get filematchesexact() to return directory names as partial matches the way filematches() does so I can incrementally search for scripts in sub-directories? Maybe it would be better to use filematches() and a filter instead, though it isn't clear to me how to hook one in or if I'd have to write a generator.

Use clink.dirmatches.

  :addarg({ perl_scripts, clink.dirmatches, onlink=perl_onlink })

In onlink, what's the difference between returning nil and returning false?

I think you're asking what this means:

I had to read the source code to make sure I understand what I meant when I wrote that text. I understand the general concept I had in mind, but I'm not entirely sure specifically what I intended to happen. Anyway, how it actually works is that returning false or nil do exactly the same thing. 🙄 I'll adjust the documentation accordingly.

Ideally, after clink sees a script name it should stop using the perl argmatcher even if there isn't an argmatcher for the matched script. I tweaked your solution a bit and am using this at the moment. It works well enough for my purposes but I'm wondering if there's something better.

local argmatcher = clink.getargmatcher(word)
if argmatcher ~= nil then
    return argmatcher
else
    return file_matches
end

Yes. The sample code I shared just demonstrated how to parse a word and then link to a specific argmatcher.

The tweak you shared will end up making perl script_with_no_argmatcher.pl assume that the named script accepts one argument, which is a file name. Further arguments after that will pop back to the perl argmatcher.

I wonder if you want something more like this:

local file_matches_loop  = clink.argmatcher():addarg(clink.filematches):loop()

and

local argmatcher = clink.getargmatcher(word)
if argmatcher ~= nil then
    return argmatcher
else
    return file_matches_loop
end

The [:loop()]https://chrisant996.github.io/clink/clink.html#_argmatcher:loop() will make it assume that the script accepts multiple file arguments.

If you want it to complete one file argument and then stop without popping back to the perl argmatcher, then use :nofiles() instead of :loop().

local argmatcher = clink.getargmatcher(word)
if argmatcher ~= nil then
    return argmatcher
else
    return file_matches
end

Just a little note about Lua syntax:

That's a perfectly fine way to implement that "if ... else" logic.

It can also be shortened to the following, and behave exactly the same:

return clink.getargmatcher(word) or file_matches

Either way is perfectly fine, but Lua authors often use the more concise version. There is a small difference between them while stepping through code in the Lua debugger, though: the concise form gets executed as a single expression, so you lose the ability to single step through and inspect the value of argmatcher partway through.

See 3.4.4 Logical Operators for details on the logical and, or, not operators in Lua. Lua doesn't have quite the same "short circuit Boolean evaluation" as C/C++ do, but x and y or z is essentially like x ? y : z.

But a subtle but important note: x and y evaluates to x if x is true otherwise y if x is false. It does NOT force the result to be true or false like C/C++ do.

x y x and y
true false false
true true true
true nil nil
true "hello" "hello"
false false false
false true false
false nil false
false "hello" false
nil false nil
nil true nil
nil nil nil
nil "hello" nil
"world" false false
"world" true true
"world" nil nil
"world" "hello" "hello"
mjcarman commented 4 weeks ago

Use clink.dirmatches.

  :addarg({ perl_scripts, clink.dirmatches, onlink=perl_onlink })

Perfect .

how it actually works is that returning false or nil do exactly the same thing. 🙄 I'll adjust the documentation accordingly.

That's what I saw when experimenting, which is why I asked.

I wonder if you want something more like this:

local file_matches_loop  = clink.argmatcher():addarg(clink.filematches):loop()

Yeah, that's better. I threw in the non-looping file matcher as a quick test but hadn't really used it yet. Thanks for the suggestion.

Just a little note about Lua syntax [...]

LOL. When it comes to Lua I'm firmly in the "hack who doesn't know the idioms" category. After looking at the documentation you linked, Lua's and and or operators have the same behavior as Perl's so it's familiar to me, but not something I assume when coding in an unfamiliar language.