bashperlshellwebminbash-completion

Trigger programmatically autocomplete event for shell in system call and get output


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.


Solution

  • 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.