What is the bash variable $COMP_LINE and what does it do? - bash

What is the $COMP_LINE variable in bash scripting? The Bash Reference Manual has the following to say.
$COMP_LINE
The current command line. This variable is available only in shell functions and external commands invoked by the programmable completion facilities (see Programmable Completion).
I don't understand what 'the current command line' means.
I am trying to pick apart this script: to see how it manages to intercept bash commands.
hook() {
echo "$#"
}
invoke_hook() {
[ -n "$COMP_LINE" ] && return
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
local command=`history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//g"`;
hook "$command"
}
trap 'invoke_hook' DEBUG
I am running into trouble figuring out what the following line is supposed to do.
[ -n "$COMP_LINE" ] && return
I assume it some sort of check or test before you run the rest of the script, since [] is an alias for the bash test command, but since I can't read it I can't figure out what it's supposed to be testing.

In Bash, if you have a file myfile.txt, you can edit it with nano myfiTab. This completes the filename automatically to save you typing, turning the command into nano myfile.txt. This is known as filename completion.
However, not all commands accept filenames. You may want to be able to do ssh myhoTab and have it complete to ssh myhostname.example.com.
Since bash can't possibly be expected to maintain this logic for all known and unknown commands across all systems, it has programmable completion.
With programmable completion, you can define a shell function to call that will get all hostnames from .ssh/known_hosts and make them available as completion entries.
When this function is invoked, it can examine the variable $COMP_LINE to see the command line it should give suggestions for. If you have set up complete -F myfunction ssh and type ssh myhoTab, then myfunction will run and $COMP_LINE will be set to ssh myho.
This functionality is used by your snippet to make the interceptor ignore commands run as a result of pressing Tab. Here it is with comments:
# This is a debug hook which will run before every single command executed
# by the shell. This includes the user's command, but also prompt commands,
# completion handlers, signal handlers and others.
invoke_hook() {
# If this command is run because of tab completion, ignore it
[ -n "$COMP_LINE" ] && return
# If the command is run to set up the prompt or window title, ignore it
[ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return
# Get the last command from the shell history
local command=`history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//g"`;
# Run the hook with that command
hook "$command"
}

Related

Save all 'ssh' commands to a file

I log into a lot of servers via ssh. Typically I just use the easy bash history search to scroll through my history of 'ssh' commands and find the one I want. However, eventually my .bash_history reaches its limit and I start losing entries, despite increasing the limits, etc..
I would rather just adjust my $PROMPT_COMMAND so that after every command, it checks to see if I ran a command with ssh, and if so, appends that command to a file somewhere.
I saw some relevant questions asked here and here but I am struggling to understand how to use these in a function that I can source from my .bashrc and add to my $PROMPT_COMMAND that will check if the last command entered started with ssh and copy it to a file.
For example, this does not work:
$ PROMPT_COMMAND="echo; foo; "
$ foo () { echo "command was: $BASH_COMMAND" ; }
$ ssh cn-0030
...(Ctrl-D)...
$ logout
Connection to cn-0030 closed.
command was: echo "command was: $BASH_COMMAND"
This also does not work:
$ foo () { echo !! | grep ssh --color ; }
Because !! gets expanded into the actual last command run immediately and then saved into foo, instead of being evaluated when foo is evaluated

capture the command that launched the running process in my shell

I try to make the title of my windows in GNU screen automatically equal to the path of the working directory PLUS the process running if there is one (e.g: npm start or vim file.js)
for that purpose I added these lines in my .zshrc :
precmd () {
local action = action_to_define
if [[ $TERM == screen* ]]; then
printf -Pn '\ek%~ $action\e\\'
fi
}
this send (somehow) the path as a title to screen (see this post)
and the variable action would print the running program if it exist
I tried local action= $(history | tail -1 | sed 's#[0-9 ]*##') because this select the prompt of the last command in the history (just like history !! would do if the option !! was recognized, which is not for some reason...)
and local action= $(ps -lr | tail -1 | sed 's#^.*:...##') because this select the command of the running process
but it doesn't works, as if the process was not captured neither by history or ps... maybe precmd run before the action is launched, so I tried other functions like preexec or zshaddhistory without any luck...
The precmd hook is only run after a command finished, just before the next prompt is displayed. So it can be used to change the terminal title for when the prompt is shown. In order to change the terminal title when a command is run, you need the preexec hook, which is run after a command was accepted (after hitting Enter, just before the command is run.
When confirming a command on the prompt, this command is then passed to preexec as arguments in three forms
The first argument is the string that was typed, including any new-lines (As long as the history mechanism is active, which it usually is)
The second argument is a single line, size-limited (it is cut off, if it gets too long) version of the command with expanded aliases.
The third argument is the full text of the command that is actually run (with expanded aliases)
So this should do the trick:
preexec () {
local action="$1"
if [[ $TERM == screen* ]]; then
printf -Pn '\ek%~ $action\e\\'
fi
}
As precmd hook is only run, before prompting the next command, it can be simplified:
precmd () {
if [[ $TERM == screen* ]]; then
printf -Pn '\ek%~\e\\'
fi
}
Although it is not the main reason why it would not work, it is important to note that in Zsh (and most other Unix shells) there must be no spaces before or after = when assigning values to parameters.
The exact behavior might differ, depending on where you put spaces and whether you use something like local or set or just plain assignments.
To use the code from the question as example:
putting spaces before and after = with local will usually result in a bad assignment
% local action = action_to_define
zsh: bad assignment
putting spaces only after = with local will assign an empty string to the variable and either create/set another empty variable or result in a context error, depending on whether the intended value would be a viable parameter name:
% local action= "echo"
This empties action and echo
% local action= "echo foo"
local: not valid in this context: echo foo
Here action is emptied, too, but local fails after because echo foo is not a viable parameter name due to the included space.

What does "if [ -t 1 ]" do in shell scripting?

I have code for setting the zsh as default shell:
if [ -t 1 ]; then
exec zsh
fi
What exactly does the command if [ -t 1 ] do here?
I have code for setting the zsh as default shell:
No, you haven't. That's not what your code does, though it produces a similar effect.
if [ -t 1 ]; then
exec zsh
fi
What exactly does the command if [ -t 1 ] do here?
The command [ -t 1 ] is executed. If it exits with status 0 (indicating success, which for the [ command means the condition evaluates to true) then the commands in the body of the if statement are executed.
It may be a bit surprising that [ is a command, rather than part of shell syntax, but that's the case. Your if could be rewritten equivalently to use the test command instead:
if test -t 1; then
# ...
The other key thing, then, is the -t 1 part. You can find out about that in the manual for the test or [ command, but to save you the trouble, it is a conditional expression that evaluates whether file descriptor 1 (the shell's standard output) is connected to a terminal. This is similar to, but not exactly the same thing as, evaluating whether the shell is an interactive one.
Overall, the code presented has the effect of replacing the current (presumably bash) shell with zsh if the standard output is connected to a terminal. This is indirect, and a bit tricky; it would probably be better to genuinely set your login shell to /bin/zsh (or wherever it is installed) via the chsh command.
if command; then other_command; fi runs command and then, if that command exits with a return code of 0 ("success"), runs other_command.
The command [...] is designed to take the place of the Boolean expressions that you find in traditional programming languages. It has a number of options for what goes between the brackets and exits with 0=success if those options evaluate to a true value.
The specific subcommand -t tests a file descriptor to see if it is attached to a terminal. The file descriptor 1 is where the script's output is going (aka "standard output" or "stdout" for short). So -t 1 is true and [ -t 1 ] returns success if and only if the output of the script is going to a terminal (instead of into a file or pipe or something).
In that case, the current shell is replaced (via exec) by a copy of zsh. Which will hopefully not run the same script, since zsh works the same way and will make the same decision and go into an infinite loop execing itself.

bash which OR operator to use - pipe v double pipe

When I'm looking at bash script code, I sometimes see | and sometimes see ||, but I don't know which is preferable.
I'm trying to do something like ..
set -e;
ret=0 && { which ansible || ret=$?; }
if [[ ${ret} -ne 0 ]]; then
# install ansible here
fi
Please advise which OR operator is preferred in this scenario.
| isn't an OR operator at all. You could use ||, though:
which ansible || {
true # put your code to install ansible here
}
This is equivalent to an if:
if ! which ansible; then
true # put your code to install ansible here
fi
By the way -- consider making a habit of using type (a shell builtin) rather than which (an external command). type is both faster and has a better understanding of shell behavior: If you have an ansible command that's provided by, say, a shell function invoking the real command, which won't know that it's there, but type will correctly detect it as available.
There is a big difference between using a single pipe (pipe output from one command to be used as input for the next command) and a process control OR (double pipe).
cat /etc/issue | less
This runs the cat command on the /etc/issue file, and instead of immediately sending the output to stdout it is piped to be the input for the less command. Yes, this isn't a great example, since you could instead simply do less /etc/issue - but at least you can see how it works
touch /etc/testing || echo Did not work
For this one, the touch command is run, or attempted to run. If it has a non-zero exit status, then the double pipe OR kicks in, and tries to execute the echo command. If the touch command worked, then whatever the other choice is (our echo command in this case) is never attempted...

Preserve Bash Completion for all commands that are prepended by a custom command

I have a script which allows to execute Bash processes in the background, i called it "backy". Programs I want to run in background I call like this:
backy long-running-script param1 param2
The problem is now that I loose the Bash completion for long-running-script if I prepend another script.
I want to write a Bash completion file which preserves not only the Bash completion for long-running-script and all of its parameters, also for every other script that I want to call with backy.
I have some experience with Bash completion, but I'm just missing the command which I can insert into my Bash completion script so that it completes with the completion of the script that is to be called. Any ideas?
My completion so far:
have backy &&
_backy_complete()
{
local cur prev goals
COMPREPLY=()
cur=${COMP_WORDS[COMP_CWORD]}
# How to get the completion from the script that is the param for backy,
# in a generic way?
COMPREPLY=( ????? )
return 0
} &&
complete -F _backy_complete backy
EDIT - SOLUTION:
Thanks to Lekensteyn, I replaced the content of my existing bash completion script with just this line:
complete -F _command backy
There is already a bash_completion function for such cases:
complete -F _command backy
It's used for autocompleting the commands after sudo, fakeroot and others. Any arguments passed to backy are ignored like:
backy --whatever --this --is=ignored not ignored anymore

Resources