3

Before actual question few words about what I'm doing to grasp the idea.

I would want to add another cool feature to my Authentic Theme for Webmin.

I have made Command Shell module look/feel just if it was ordinary shell. However, as it's only a port, it has few limitations. One of them is missing shell autocomplete, when you hit Tab key (or other based on the system: Esc+Esc and/or Ctrl+I).

My point is to make it work natively. I'm going to use an XMLHttpRequest call to the server and pass the part of entered command to the actual shell. The call will be triggered on the event of the Tab key.

For example, when you're in Authentic Theme dropdown shell (you could see it on the video screencast above), and when you type, let's say, xa and hit Tab, the event will be triggered and make a request. Then the server receives a string xa and we're ready to begin.

My questions are is:

1. What is the best way to execute such command in Perl?
2. How to properly escape such command to make sure that it won't be exploitable;
3. How to trigger programmatically Tab key (to run autocomplete) in the system() call;
4. How to grep output.

On the respond, using xa as an example, I expect to get xargs on the results.


I'm aware about possibly of ambiguity of the passed command, for example, when using sys, and when autocomplete wouldn't work on the single Tab key. I think it's better to exclude this from the scope of current question.

Community
  • 1
  • 1
Ilia Ross
  • 13,086
  • 11
  • 53
  • 88
  • 2
    Split it into multiple questions. In its _current form_ it will not attract proper answers. Cheers! – Inian Feb 07 '17 at 08:34
  • @Inian, thank you for your advice but it's all interconnected. However, my real obstacle is number **3** and having an answer for `how to trigger it programmatically`, would be enough for me. – Ilia Ross Feb 07 '17 at 08:39
  • I don't quite understand what you actually need to do. To catch a `TAB`, if that's what you mean, you need to handle keyboard events, for example by [Term::ReadKey](http://search.cpan.org/~jstowe/TermReadKey-2.37/ReadKey_pm.PL). As for handling strings, there are for example [String::Substitution](http://search.cpan.org/~rwstauner/String-Substitution-1.002/lib/String/Substitution.pm) and [String::Interpolate](http://search.cpan.org/~neilb/String-Interpolate-0.32/lib/String/Interpolate.pm), which pays a lot of attention to security – zdim Feb 07 '17 at 08:40
  • @zdim If you go to ordinary shell and try to use autocomplete feature by using `Tab` key, you will see how it works. When you type `xa` and hit tab, you will get `xargs`. My point is to pass `xa` to the shell, trigger tab and get output. It's that fancy because of the currently imposed limitations. – Ilia Ross Feb 07 '17 at 08:43
  • @zdim for example, when using `system()` how would I do that. How would I pass `xa` to it, trigger `Tab` and get `xargs` as a results? – Ilia Ross Feb 07 '17 at 08:44
  • Have a look at [Term::ReadLine::Gnu](https://metacpan.org/pod/Term::ReadLine::Gnu) it provides an interface to the Gnu Readline completion functions. – Håkon Hægland Feb 07 '17 at 08:45
  • 1
    Ah ... how about printing the ANSI sequence for it? If that satisfies the "pass to the shell". I do know and use and love autocompletion, many many times every day :) I sometimes even name my own programs so that it can be done early on while typing their names :) – zdim Feb 07 '17 at 08:45
  • Hm, I don't know how to or whether one can do that. But @HåkonHægland's remark is spot on -- why not write your own readline? The module is excellent (it's a whole suite) – zdim Feb 07 '17 at 08:49
  • @zdim thanks. @HåkonHægland I have never used/heard about `Term::ReadLine::Gnu`. You guess or you sure that it's possible to do using this module? By the way, when type `@usern...` in this comment box, it also has autocomplete feature. :) – Ilia Ross Feb 07 '17 at 08:56
  • @IliaRostovtsev `Term::ReadLine::Gnu::completion_matches()`, or alternatively the Bash command [`compgen`](https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion-Builtins.html) should both be able to give a list of possible completions given the word `xa` for example – Håkon Hægland Feb 07 '17 at 09:03
  • @HåkonHægland, really cool, thanks a lot, **Håkon**. @zdim - thanks for pointing out `Term::ReadKey.` – Ilia Ross Feb 07 '17 at 09:09

1 Answers1

5

If you know the type of completion you want to generate, you can generate completions using the compgen bash builtin.

Most usefully, the -c option will complete command names and -o default will complete filenames through readline. See the options for complete, most of which you can also use with compgen.

Note that system() will pass the command to /bin/sh -c, which may not be the same as bash on your system. So you can do something like:

system('bash', '-c', 'compgen -c -- "$1"', 'bash', 'xa')
# xargs
# xattr
# etc…

or

system('bash', '-c', 'compgen -o default -- "$1"', 'bash', 'file')
# file.txt
# file.pdf
# etc…

If you want to literally the same thing that bash would do given a particular string (and not guess the completion type yourself), you will need to actually invoke the tab-press (or equivalent), which you can do with an expect script. This is also the only way to properly invoke any custom completion functions you may have defined. See for example the way that bash-completion handles its test suite.

Below is my attempt at an expect script to print out the completions that bash would suggest for arbitrary command input:

#!/usr/bin/env expect

log_user 0
set prompt {/@}
set cmd [lindex $argv 0]

# start bash with no startup files for clean env
spawn env INPUTRC=/dev/null PS1=$prompt bash --norc
expect $prompt

# set some readline variables for consistent completion output
send "bind 'set show-all-if-ambiguous on'\r"
expect $prompt
send "bind 'set bell-style none'\r"
expect $prompt
send "bind 'set completion-query-items -1'\r"
expect $prompt
send "bind 'set page-completions off'\r"
expect $prompt
send "bind 'set completion-display-width 0'\r"
expect $prompt

# run the completion
send "$cmd\t $prompt"
expect {
   # multiple matches, printed on separate lines, followed by prompt
   -re "^$cmd\r\n(.*)\r\n$prompt$cmd" { puts $expect_out(1,string) }
   # single match, completed in-place
   -re "^($cmd\[^ \]*)  $prompt" { puts $expect_out(1,string) }
   # single match, completed in-place, nospace
   -re "^($cmd\[^ \]+) $prompt"  { puts $expect_out(1,string) }
   # no match
   -re "^$cmd $prompt" { exit }
}

Calling ./script.exp string will print out the actual completions that bash will produce for string, one per line. If there are no completions suggested, nothing is printed.

Grisha Levit
  • 8,194
  • 2
  • 38
  • 53
  • Yes, that what I was doing right now after reading comments to my question. Thanks, Grisha. :) We definitely need to escape input, e.g. `xa` to prevent injections. The only real problem is that I don't want to specify any params and would want `compgen` to guess, if it's a file or command or anything else (this is what hitting Tab does). Is there a way to do it? – Ilia Ross Feb 07 '17 at 09:22
  • 1
    Sadly there's no way to do that other than literally having bash do it. I've updated the answer with an `expect` script, which is what the bash-completion uses to trigger real completions for testing. – Grisha Levit Feb 07 '17 at 09:54