Bash Command-Line Tab Completion Colon Character - bash

I have successfully implemented bash completions for my custom commands when the parameters contained no special characters in them, with the compgen mechanism:
current=${COMP_WORDS[COMP_CWORD]}
all=$(_get_all_items)
COMPREPLY=( $(compgen -W "$all" -- $current) )
...
complete -F _prog_compl prog
I use the same approach to complete items which start with a colon character: :first, :second, ... But it fails to show me / autocomplete them. I tried escaping colons with backslashes which didn't work either. How should I escape colons in completion?
The items are starting with colon, let's say: :first, :second. If I write progname and start with a colon like this:
$ progname :<Tab here after colon>
I see no completion but two colons ("::") - one of them automatically added to the line I type. If I convert the colon to an ordinary character (let's say 'b') I get completion exactly like I want: bfirst, bsecond...
It is relevant to note that when I press tab it places another ":" next to my previously inserted colon and it becomes "::".
$ bash --version
GNU bash, version 4.1.10(2)-release (i486-slackware-linux-gnu)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Additionally, I have made some experiments in the shell which yield:
$ compgen -W ":aaaa5 :gb2 :cd3" -- ":"
:aaaa5
:gb2
:cd3
Yet it strangely puts another ":" to my sole ":" and makes it "::" after I type : on the command line.
$ complete
complete -F _alp_compl alp.sh
complete -o nospace -F __dbus_send dbus-send
complete -F _myprog_compl myprog
complete -o nospace -F __gdbus gdbus
complete -o nospace -F __gsettings gsettings

After consulting help-bash#gnu.org I got an answer:
The colon breaks words for the completion system (look at the
description of the COMP_WORDBREAKS shell variable), so when you type
progname :[TAB]
the completion system gets an empty word to complete. If all of the
possible completions have `:' as the longest common prefix, then the
completion system will insert the colon into the line.
And my COMP_WORDBREAKS search yield me an answer from stackoverflow.
After fixing (my missing) /etc/bash_completion file from debian bash completion page, now, my colon-initiated completion is working as my will.

Related

Escape slashes in bash complete

I try to use the bash complete builtin to show different options for a command.
I have problems when an option contains a path like in -F/dev/null.
Currently I'm using
#!/bin/bash
_xyz-completion ()
{
local cur
COMPREPLY=() # Array variable storing the possible completions.
cur=${COMP_WORDS[COMP_CWORD]}
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-oOption1 -F/dev/null" -- $cur ) )
;;
esac
return 0
}
complete -F _xyz-completion -o filenames xyz
If -F was already typed, then a Tab completes it successfully.
But if only - was typed, then a Tab shows
null -oOption1
But I expect to see
-F/dev/null -oOption1
I tried already -F\/dev\/null, -F//dev//null, "-F/dev/null" and -F\\\/dev\\\/null
It seems to be only a display problem, as the completion itself works as expected.
I can't see how to appropriate escape the slashes in `-F/dev/null`.
To comment the comments:
1)
Never mind, it's a problem also if -F is replaced by a non-option such as -Q. – Benjamin W.
It's not a problem, that the -F looks like a option for complete itself, as it even fails if I changed it to xOPTION1 xF/dev/null
2)
I'm wondering what compgen -W "-oOption1 -F/dev/null" -- - displays for you.
It displays (as expected)
-oOption1
-F/dev/null
As mentioned, -F completes successfully to -F/dev/null
If you remove the -o filenames option from complete your example works as expected. Which makes some sense as the completions aren't filenames. This is with bash version 5.0.2(1).
So:
#!/bin/bash
_xyz-completion ()
{
local cur
COMPREPLY=() # Array variable storing the possible completions.
cur=${COMP_WORDS[COMP_CWORD]}
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-oOption1 -F/dev/null" -- $cur ) )
;;
esac
return 0
}
complete -F _xyz-completion xyz
It definitely seems like a bug that it would truncate part of the completion when there are slashes. And only when displaying the choices, the actual completion works correctly.
EDIT:
After looking into it a little more, the filenames option is used for escaping strings that could have spaces or other breaking characters. Basically cleaning up file names for the shell. From the Programmable Completion Built-in man page
-o filenames:
Tell Readline that the compspec generates filenames, so it can perform any filename-specific processing (like adding a slash to directory names, quoting special characters, or suppressing trailing spaces). This option is intended to be used with shell functions specified with -F.
Apparently that includes stripping everything before and including the last slash.
EDIT2:
Here's a comment from the readline source that bash uses for file name completion. I got this from the bash repo at https://git.savannah.gnu.org/git/bash.git). The master, so 5.0 patch 3 at time of writing.
./lib/readline/complete.c line 697
/* Return the portion of PATHNAME that should be output when listing
possible completions. If we are hacking filename completion, we
are only interested in the basename, the portion following the
final slash. Otherwise, we return what we were passed. Since
printing empty strings is not very informative, if we're doing
filename completion, and the basename is the empty string, we look
for the previous slash and return the portion following that. If
there's no previous slash, we just return what we were passed. */
static char *
printable_part (char *pathname)
For filename completion, it only wants to print the basename, everything after the last slash.

How to separate outputs of one-line multiple commands?

I have installed some GNU packages on my macOS Sierra, which include bash, coreutils, which, etc. Now I can use which -a bash | xargs -I % echo % "--version" | sh to check all version info of bash, but there is no separation between two version info:
$ which -a bash | xargs -I % echo % "--version" | sh
GNU bash, version 4.4.12(1)-release (x86_64-apple-darwin16.3.0)
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
# There should be one or more blank lines as separation.
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin16)
Copyright (C) 2007 Free Software Foundation, Inc.
I tried ... echo -e % "--version\n" ..., but it can't work. Please help me, thanks.
For a bit more control of the output, use a loop:
which -a bash |
while read cmd; do
printf '%s\t----------\n' "$cmd"
command "$cmd" --version
echo
done
I'd write this as follows:
IFS=: read -r -a path_entries <<<"$PATH"
find "${path_entries[#]}" -maxdepth 1 -name bash -type f -exec '{}' --version ';'
Note:
No use of which. This isn't a shell builtin in bash, so it gives you none of the benefits you'd get from type (ie. awareness of functions and aliases); and because its output is line-oriented, it can't unambiguously emit all possible filenames (such as those containing newlines -- which, yes, are valid).
No reliance on line-oriented content, generally. When the placeholder ({}) is passed as a single token, find -exec substitutes the exact filename returned for that token. Because the domain of possible argv values (all valid C strings, which is to say, no strings containing NULs) matches the domain of possible filenames on common systems, this avoids introducing potential bugs.
No generating text and piping that to an interpreter. This is an extremely error-prone technique, inducing shell-injection vulnerabilities: Consider what would happen if a PATH contained /tmp/$(rm -rf ~), and that directory contained a bash executable.

Can zsh or bash expand history expressions referring to directories?

For example, let's suppose I just copied something:
mv foo_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
and I'd like to use history substitution like so:
mv bar_file.txt !!:2
I'm surprised that zsh is not expanding the !!:2 for me when I hit [tab]. In a more complex reference to a historical argument I might really want the expansion before I hit return, just so I know with certainty that I referred to the correct argument. Is there any way to make it do that? (I would expect that to be the default behavior. Is it the default behavior, that I have somehow inadvertently disabled or broken?)
If zsh can't do it, can bash?
UPDATE: zsh will expand the history expression if it refers to a file, but not a directory:
mv foo_file.txt foo_bar_file.txt
mv bar_file.txt !!:2[TAB]
It will expand it if it is just an arbitrary string:
echo one two three four
echo !!:1[TAB]
But not if you're trying to move something to a directory. It looks more and more like this must be a bug.
I am using zsh in cygwin:
$ zsh --version
zsh 4.3.12 (i686-pc-cygwin)
$ setopt
interactive
monitor
shinstdin
zle
I just tried the following:
$ touch foo_file.txt bar_file.txt
$ mkdir -p ~/to/some/long/path/that/i/do/not/want/to/retype
$ mv foo_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
I then tried the tab completion mentioned above:
$ mv bar_file.txt !!:2[TAB]
and it worked fine, the last argument being expanded as follows:
$ mv bar_file.txt ~/to/some/long/path/that/i/do/not/want/to/retype
You can pseudo-hack it in bash:
$ shopt -s histreedit
$ shopt -s histverify
Then, to actually try an expansion:
$ echo !!:2 [now hit enter]
$ echo histverify
Now you can't do tab expansion in bash. Unequivocally no. That's because of the order in which bash expansion is processed.
Works perfectly for me with zsh 4.3.17. Sounds like you probably have a bug which might be worth reporting on the zsh-user mailing list. However there are at least five other keybindings which should accomplish what you want: C-x * which is by default bound to expand-word, and Esc Space or Meta-Space or Esc ! or Meta-! which are all bound to expand-history by default. (Meta means the Alt key for many people, although it depends on your terminal setup.)
Having said that, Esc . (or Meta-. or Alt-.) is a nicer way of retrieving the last word from the previous line in the history, since it provides instant visual feedback. You can also choose the last word from older lines by repeatedly pressing the keyboard shortcut, or even the n th last word on a previous line by prefixing the shortcut with Alt-n (or Meta-n or Esc n). So for example to retrieve the penultimate word from the 3rd newest line of history, the sequence would be:
Meta-. (goes back one line of history, selecting the last word from that line)
Meta-. (goes back another, again selecting the last word)
Meta-2 Meta-. (goes back another, but this time selects the penultimate word from that line)
I've tried what you've described, and I don't think bash supports this either.
In bash, Alt-. is typically bound to yank-last-arg, which will give you what you want.
Here is a link to the whole list of history related commands that can be bound to keystrokes
http://www.gnu.org/software/bash/manual/bashref.html#Commands-For-History
For example,
ls /etc/passwd /etc/group
cat
# press Alt+. (Alt dot) here, bash adds /etc/group
cat /etc/group
# press space, then press Alt+1 Alt+.
# bash adds the first argument of the previous command /etc/passwd
cat /etc/group /etc/passwd

comments amongst parameters in BASH

I want to include command-parameters-inline comments, e.g.:
sed -i.bak -r \
# comment 1
-e 'sed_commands' \
# comment 2
-e 'sed_commands' \
# comment 3
-e 'sed_commands' \
/path/to/file
The above code doesn't work. Is there a different way for embedding comments in the parameters line?
If you really want comment arguments, can try this:
ls $(
echo '-l' #for the long list
echo '-F' #show file types too
echo '-t' #sort by time
)
This will be equivalent to:
ls -l -F -t
echo is an shell built-in, so does not execute external commands, so it is fast enough. But, it is crazy anyway.
or
makeargs() { while read line; do echo ${line//#*/}; done }
ls $(makeargs <<EOF
-l # CDEWDWEls
-F #Dwfwef
EOF
)
I'd recommend using longer text blocks for your sed script, i.e.
sed -i.bak '
# comment 1
sed_commands
# comment 2
sed_commands
# comment 3
sed_commands
' /path/to/file
Unfortunately, embedded comments in sed script blocks are not universally a supported feature. The sun4 version would let you put a comment on the first line, but no where else. AIX sed either doesnt allow any comments, or uses a different char besides # for comments. Your results may vary.
I Hope this helps.
You could invoke sed multiple times instead of passing all of the arguments to one process:
sed sed_commands | # comment 1
sed sed_commands | # comment 2
sed sed_commands | # comment 3
sed sed_commands # final comment
It's obviously more wasteful, but you may decide that three extra sed processes are a fair tradeoff for readability and portability (to #shellter's point about support for comments within sed commands). Depends on your situation.
UPDATE: you'll also have to adjust if you originally intended to edit the files in place, as your -i argument implies. This approach would require a pipeline.
There isn't a way to do what you seek to do in shell plus sed. I put the comments before the sed script, like this:
# This is a remarkably straight-forward SED script
# -- When it encounters an end of here-document followed by
# the start of the next here document, it deletes both lines.
# This cuts down vastly on the number of processes which are run.
# -- It also does a substitution for XXXX, because the script which
# put the XXXX in place was quite hard enough without having to
# worry about whether things were escaped enough times or not.
cat >$tmp.3 <<EOF
/^!\$/N
/^!\\ncat <<'!'\$/d
s%version XXXX%version $SOURCEDIR/%
EOF
# This is another entertaining SED script.
# It takes the output from the shell script generated by running the
# first script through the second script and into the shell, and
# converts it back into an NMD file.
# -- It initialises the hold space with --#, which is a marker.
# -- For lines which start with the marker, it adds the pattern space
# to the hold space and exchanges the hold and pattern space. It
# then replaces a version number followed by a newline, the marker
# and a version number by the just the new version number, but
# replaces a version number followed by a newline and just the
# marker by just the version number. This replaces the old version
# number with the new one (when there is a new version number).
# The line is printed and deleted.
# -- Note that this code allows for an optional single word after the
# version number. At the moment, the only valid value is 'binary' which
# indicates that the file should not be version stamped by mknmd.
# -- On any line which does not start with the marker, the line is
# copied into the hold space, and if the original hold space
# started with the marker, the line is deleted. Otherwise, of
# course, it is printed.
cat >$tmp.2 <<'EOF'
1{
x
s/^/--#/
x
}
/^--# /{
H
x
s/\([ ]\)[0-9.][0-9.]*\n--# \([0-9.]\)/\1\2/
s/\([ ]\)[0-9.][0-9.]*\([ ][ ]*[^ ]*\)\n--# \([0-9.][0-9.]*\)/\1\3\2/
s/\([ ][0-9.][0-9.]*\)\n--# $/\1/
s/\([ ][0-9.][0-9.]*[ ][ ]*[^ ]*\)\n--# $/\1/
p
d
}
/^--#/!{
x
/^--#/d
}
EOF
There's another sed script in the file that is about 40 lines long (marked as 'entertaining'), though about half those lines are simply embedded shell script added to the output. I haven't changed the shell script containing this stuff in 13 years because (a) it works and (b) the sed scripts scare me witless. (The NMD format contains a file name and a version number separated by space and occasionally a tag word 'binary' instead of a version number, plus comment lines and blank lines.)
You don't have to understand what the script does - but commenting before the script is the best way I've found for documenting sed scripts.
No.
If you put the \ before the # it will escape the comment character and you won't have a comment anymore.
If you put the \ after the # it will be part of the comment and you won't escape the newline anymore.
A lack of inline comments is a limitation of bash that you would do better to adapt to than try and work around with some of the baroque suggestions already put forth.
Although the thread is quite old i did find it for the same question and so will others. Here's my solution for this problem:
You need comments, so that if you look at your code at a much later time you will likely get an idea of what you actually did, when you wrote the code. I am just having the same problem while writing my first rsync script, which has lots of parameters which also have side effects.
Group together your parameter which belong together by topic and put them into a variable, which gets a corresponding name. This makes it easy to identify what the parameter steer. This is your short comment. In addition you can put a comment above the variable declaration to see how you can change the behavior. This is the long version comment.
Call the application with the corresponding parameter variables.
## Options
# Remove --whole-file for delta transfer
sync_filesystem=" --one-file-system \
--recursive \
--relative \
--whole-file \ " ;
rsync \
${sync_filesystem} \
${way_more_to_come} \
"${SOURCE}" \
"${DESTIN}" \
Good overview, easy to edit and like comments in parameters. It takes more effort, but has therefore a higher quality.
I'll suggest another way that works at least in some instances:
Let's say I have the command:
foo --option1 --option2=blah --option3 option3val /tmp/bar`
I can write it this way:
options=(
--option1
--option2=blah
--option3 option3val
)
foo ${options[#]} /tmp/bar
Now let's say I want to temporarily remove the second option. I can just comment it out:
options=(
--option1
# --option2=blah
--option3 option3val
)
Note that this technique may not work when you need extensive escaping or quoting. I have run into some issues with that in the past, but unfortunately I don't recall the details at the moment :(
For most cases, though, this technique works well. If you need embedded blanks in a parameter, just enclose the string in quotes, as normal.

Unix commandline history substitution ^foo^bar (for multiple replacements)

Occasionally I use the bash command to replace string in previous command:
^foo^bar
Today I wanted to make the replacement in the following line for replacing all occurrences of checkbox with `radio:
$ git mv _product_checkbox_buttons.html.erb _product_checkbox_button.html.erb
$ ^checkbox^radio
git mv _product_radio_buttons.html.erb _product_checkbox_button.html.erb
So it only replaces the first occurrence. How do I make it replace all occurrences?
bash --version
GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin10.0)
Copyright (C) 2007 Free Software Foundation, Inc.
man bash says ^old^new is equivalent to !!:s/old/new/. You want !!:gs/old/new/ to globally replace.
An easy way of doing this would be:
fc -s checkbox=radio
I have an alias r defined in my .bashrc:
alias r='fc -s'
Then what you want to do becomes:
r checkbox=radio
See my answer to "hidden features of bash" question for details.

Resources