Issue
I am trying to write a simple Bash completion script for a program that runs its arguments as a command. A good example of this is kind of program is the prime-run
script provided by the nvidia-prime
package:
#!/bin/bash
__NV_PRIME_RENDER_OFFLOAD=1 __VK_LAYER_NV_optimus=NVIDIA_only __GLX_VENDOR_LIBRARY_NAME=nvidia "$@"
This script sets a few environment variables, which instructs the prime driver to use the Nvidia dGPU on a hybrid system. The first argument is treated as the command, and all trailing arguments are passed through. So for example you can run prime-run code .
and VSCode will start in the current directory using the dGPU.
Therefore from a completion-script POV, what we want is to basically try to complete as if the prime-run
token isn't there (hence "transparent proxy"-like behaviour). To give a rather contrived example:
> prime-run journalc<TAB>
(completes journalctl)
> prime-run journalctl --us<TAB>
(completes --user)
However I am finding this surprisingly difficult in Bash (not that I know how in other shells). So the question is simple: is it possible and if so how?
Ideas I've (hopelessly) had
- The simple
complete -A command prime-run
: the first argument gets completed as a command as expected (let's call itfoo
), but the following arguments are also completed as commands rather than as arguments tofoo
- Use some combination of
compgen
andcomplete -p
to invoke the completion function offoo
, but AFAIK the completion function for allfoo
is locally defined and thus uncallable
Solution
TL;DR
bash-completion
provides a function named _command_offset
(permalink), which is exactly what I need.
# A meta-command completion function for commands like sudo(8), which need to
# first complete on a command, then complete according to that command's own
# completion definition.
Keep reading if you are interested in how I got here.
So I was daydreaming the other day, when it hit me - doesn't sudo
basically have the exact same behaviour I want? So the task became simple - reverse engineer the completion script for sudo
. Source available here: permalink.
Turns out, most of the code has to do with completing the various options, so it's safe to simply throw most of it out:
- L 8-11, 50-52: Related to
sudo
's edit mode. Safe to ditch. - L 19-24, 27-39, 43-49: These complete
sudo
's options. Safe to ditch.
So we're left with this:
_sudo()
{
local cur prev words cword split
_init_completion -s || return
for ((i = 1; i <= cword; i++)); do
if [[ ${words[i]} != -* ]]; then
local PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin
local root_command=${words[i]}
_command_offset $i
return
fi
done
$split && return
} &&
complete -F _sudo sudo sudoedit
- The
for
andif
block are there to deal withsudo
's options that precede the "guest command". Safe to ditch (after replacing all$i
with1
). - The variable
$split
is only referenced in_init_completion
(permalink), and it seems to be used for handling different argument styles (--foo=bar
v.s.--foo bar
). Same with the-s
flag. Irrelevant. - Appending to
$PATH
and setting$root_command
have to do with privilege escalation. Only relevant tosudo
.
So after the dust has cleared, by process of elimination, I ended up with this simple chunk of code:
_my-script()
{
local cur prev words cword
_init_completion || return
_command_offset 1
} && complete -F _my-script my-script
Declaring these four local variables and calling _init_completion
is standard for all completion scipts, so really it's as simple as one command. Of course someone had to write the massively-complex _command_offset
function so lucky me I guess?
Anyways, thank you for reading the story of me messing around and hopefully this will be helpful to some other person in the future.
Answered By - cyqsimon Answer Checked By - Cary Denson (WPSolving Admin)