bash autocompletion with file names - bash

I can't get a simple bash autocompletion function to work. I need to autocomplete file names from a predefined directory so it will look like this:
$ cmd log<TAB><TAB>
file1.log file2.log file3.log
Where files are from /var/log/app.

I don't see the point of using ls when the shell can list files just fine by itself, so here's one using just the shell.
_cmd() {
local files=("/var/log/app/$2"*)
[[ -e ${files[0]} ]] && COMPREPLY=( "${files[#]##*/}" )
}
complete -F _cmd cmd

Put them into ~/.bashrc
_cmd() { COMPREPLY=($(ls /var/log/app)); }
complete -F _cmd cmd
To write a full-featured auto-complete function,
please take a look at /etc/bash_completion.d/python.

I found this to work as needed:
COMPREPLY=( $(compgen -W "$(ls /var/log/app/)" -- $cur) )
Thanks to dogbane in https://unix.stackexchange.com/questions/28283/autocomplete-of-filename-in-directory !

Related

How to remove a single command from bash autocomplete

How do I remove a single "command" from Bash's auto complete command suggestions? I'm asking about the very first argument, the command, in auto complete, not asking "How to disable bash autocomplete for the arguments of a specific command"
For example, if I have the command ls and the system path also finds ls_not_the_one_I_want_ever, and I type ls and then press tab, I want a way to have removed ls_not_the_one_I_want_ever from every being a viable option.
I think this might be related to the compgen -c list, as this seems to be the list of commands available.
Background: WSL on Windows is putting all the .dll files on my path, in addition to the .exe files that should be there, and so I have many dlls I would like to remove in my bash environment, but I'm unsure how to proceed.
Bash 5.0's complete command added a new -I option for this.
According to man bash —
complete -pr [-DEI] [name ...]
[...] The -I option indicates that other supplied options and actions should apply to completion on the initial non-assignment word on the line, or after a command delimiter such as ; or |, which is usually command name completion. [...]
Example:
function _comp_commands()
{
local cur=$2
if [[ $cur == ls* ]]; then
COMPREPLY=( $(compgen -c "$cur" | grep -v ls_not_wanted) )
fi
}
complete -o bashdefault -I -F _comp_commands
Using #pynexj's answer, I came up with the following example that seems to work well enough:
if [ "${BASH_VERSINFO[0]}" -ge "5" ]; then
function _custom_initial_word_complete()
{
if [ "${2-}" != "" ]; then
if [ "${2::2}" == "ls" ]; then
COMPREPLY=($(compgen -c "${2}" | \grep -v ls_not_the_one_I_want_ever))
else
COMPREPLY=($(compgen -c "${2}"))
fi
fi
}
complete -I -F _custom_initial_word_complete
fi

How to configure bash autocomplete for file search and custom search?

I want to have a command autocomplete uploaded scripts that would be run remotely, but the user could also pick a local script to upload. Here is a small example to illustrate the problem I have with the bash complete logic.
_test_complete()
{
local cur prev opts uploaded_scripts
uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
if [[ ${prev} == '-s' ]] ; then
COMPREPLY=( $(compgen -W "${uploaded_scripts}" -- ${cur}) )
return 0
fi
}
complete -F _test_complete remote
The example almost works but it does not autocomplete local file searches anymore.
$ remote -s proc<TAB><TAB>
proc1.sh proc2.sh
$ remote -s proc1.sh ./<TAB><TAB>
Nothing happens when you do the usual file search ./ which should list files in current dir. Any ideas on how you can enable both ?
EDIT: The above example had a problem you could only pick one file with file complete. I hacked a solution which works but if anyone has a better one please leave a comment. Also with the -o default from the accepted answer.
_test_complete()
{
local cur prev opts uploaded_scripts
uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
[[ $COMP_CWORD == '1' ]] && LAST_OPT=''
[[ ${prev:0:1} == '-' ]] && LAST_OPT=${prev}
if [[ ${LAST_OPT} == '-s' ]]; then
COMPREPLY=( $(compgen -o default -W "${uploaded_scripts}" -- ${cur}) )
return 0
fi
}
complete -F _test_complete remote
You can use complete's -o default option (Usually I'd use both -o default and -o bashdefault):
complete -o default -F _test_complete remote
According to man bash:
bashdefault
Perform the rest of the default bash completions if the compspec generates no matches.
default
Use readline's default filename completion if the compspec generates no matches.
You just have to add all files from the local directory to COMPREPLY too. complete -f -- abc generates a list of files starting with abc.
By the way: Instead of "${COMP_WORDS[COMP_CWORD]}" and COMP_CWORD-1 you can also use $2 and $3 which are supplied to any completion function by bash.
But here I completely dropped the if since it seems you want to allow multiple files after -s. Since you don't suggest -s itself, just suggest the files all the time:
_test_complete() {
local cur="$2" prev="$3" uploaded_scripts='proc1.sh proc2.sh'
COMPREPLY=( $(
compgen -W "${uploaded_scripts}" -- "$cur"
compgen -f -- "$cur"
) )
}
complete -F _test_complete remote
Note: COMPREPLY=( $(...) ) is easy to write but has some flaws. Files with spaces in them will be split into multiple suggestions and special symbols like * will expand and generate even more suggestions. To avoid this, either set IFS=$'\n'; set -o noglob or use mapfile -t COMPREPLY < <(...).
After you have done this, you can use complete -o filenames -F ... such that those problematic suggestions are correctly quoted when being inserted too.

Passing bash argument as string

i have a little and probably very stupid problem..
I'm trying to make an alias for tar and gzip which uses the file name (given as an argument), but it is not converting as expected to the filename in the output.
My alias is:
alias targz='tar -cvzf $1.tar.gz $1'
It works, but the argument stored in $1 is not working when setting the filename, it zips it in a file called ".tar.gz".
I tried just echoing '$1.tar.gz' and the output is '.tar.gz', so, i think it should be something very stupid in the format.
Any help is welcome,
Aliases don't have positional parameters. They're basically macros (an alias gets replaced with the text of the alias when executed).
You could use a function:
targz() {
tar -cvzf "$1".tar.gz "$1"
}
or a script
#!/bin/bash
tar -cvzf "$1".tar.gz "$1"
Personally, I've been using something like the following script to achieve a similar goal (comments added for your convenience):
#!/bin/bash
#support multiple args
for arg in "$#"
do
#strip ending slash if present (tab-completion adds them)
arg=${arg%/}
#compress with pigz (faster on multicore systems)
tar c "$arg" | pigz - > "$arg".tgz
done
In case you want my complete version, I also remove the argument directory if the tarring and compression succeed (similar to what gzip does for individual files)
#!/bin/bash
set -o pipefail
for arg in "$#"
do
arg=${arg%/}
tar c "$arg" | pigz - > "$arg".tgz && rm -rf "$arg"
done
Update:
Credits to #mklement0 for the more succinct and more efficient stripping of ending slashes.
Use an alias to a function for it, something like:
alias targz='function targz_() { tar -cvzf "$1.tar.gz" "$1"; return 0; }; targz_ '
Try to make a script to do that, like this.
echo """
#!/bin/bash if [ -z $1 ]; then
echo "Variable is Null"
else
tar -cvzf $1.tar.gz $1
fi
""" > /usr/local/bin/targz
chmod +x /usr/local/bin/targz

Create shell sub commands by hierarchy

I'm trying to create a system for my scripts -
Each script will be located in a folder, which is the command itself.
The script itself will act as a sub-command.
For example, a script called "who" inside a directory called "git",
will allow me to run the script using git who in the command line.
Also, I would like to create a sub command to a psuedo-command, meaning a command not currently available. E.g. some-arbitrary-command sub-command.
Is that somehow possible?
I thought of somehow extending https://github.com/basecamp/sub to accomplish the task.
EDIT 1
#!/usr/bin/env bash
command=`basename $0`
subcommand="$1"
case "$subcommand" in
"" | "-h" | "--help" )
echo "$command: Some description here" >&2
;;
* )
subcommand_path="$(command -v "$command-$subcommand" || true)"
if [[ -x "$subcommand_path" ]]; then
shift
exec "$subcommand_path" "${#}"
return $?
else
echo "$command: no such command \`$subcommand'" >&2
exit 1
fi
;;
esac
This is currently the script I run for new custom-made commands.
Since it's so generic, I just copy-paste it.
I still wonder though -
can it be generic enough to just recognize the folder name and create the script by its folder name?
One issue though is that it doesn't seem to override the default command name, if it supposed to replace it (E.g. git).
EDIT 2
After tinkering around a bit this is what I came to eventuall:
#!/usr/bin/env bash
COMMAND=`basename $0`
SUBCOMMAND="$1"
COMMAND_DIR="$HOME/.zsh/scripts/$COMMAND"
case "$SUBCOMMAND" in
"" | "-h" | "--help" )
cat "$COMMAND_DIR/help.txt" 2>/dev/null ||
command $COMMAND "${#}"
;;
* )
SUBCOMMAND_path="$(command -v "$COMMAND-$SUBCOMMAND" || true)"
if [[ -x "$SUBCOMMAND_path" ]]; then
shift
exec "$SUBCOMMAND_path" "${#}"
else
command $COMMAND "${#}"
fi
;;
esac
This is a generic script called "helper-sub" I symlink to all the script directories I have (E.g. ln -s $HOME/bin/helper-sub $HOME/bin/ssh).
in my zshrc I created this to call all the scripts:
#!/usr/bin/env bash
PATH=${PATH}:$(find $HOME/.zsh/scripts -type d | tr '\n' ':' | sed 's/:$//')
export PATH
typeset -U path
for aliasPath in `find $HOME/.zsh/scripts -type d`; do
aliasName=`echo $aliasPath | awk -F/ '{print $NF}'`
alias ${aliasName}=${aliasPath}/${aliasName}
done
unset aliasPath
Examples can be seen here: https://github.com/iwfmp/zsh/tree/master/scripts
You can't make a directory executable as a script, but you can create a wrapper that calls the scripts in the directory.
You can do this either with a function (in your profile script or a file in your FPATH) or with a wrapper script.
A simple function might look like:
git() {
local subPath='/path/to/your/git'
local sub="${1}" ; shift
if [[ -x "${subPath}/${1}" ]]; then
"${subPath}/${sub}" "${#}"
return $?
else
printf '%s\n' "git: Unknown sub-command '${sub}'." >&2
return 1
fi
}
(This is the same way that the sub project you linked works, just simplified.)
Of course, if you actually want to create a sub-command for git specifically (and that wasn't just an example), you'll need to make sure that the built-in git commands still work. In that case you could do like this:
git() {
local subPath='/path/to/your/git'
local sub="${1}"
if [[ -x "${subPath}/${sub}" ]]; then
shift
"${subPath}/${sub}" "${#}"
return $?
else
command git "${#}"
return 1
fi
}
But it might be worth pointing out in that case that git supports adding arbitrary aliases via git config:
git config --global alias.who '!/path/to/your/git/who'

How would you bash script to combine cp and cd or mv and cd

I've been shuffling around files and programs and stuff a lot for my research lately, but my bash is so rusty I can't think of how I would do this. For example:
jt ~ $ cp foo.txt arbitrary/folder/destination
jt ~ $ cd arbitrary/folder/destination
jt ~/arbitrary/folder/destination $ //Some command here
Such that I would be able to be where I had just copied it. Is there some way that I could do this with bash regex's (or maybe simipler) in an alias so i could do
jt ~ $ magic foo.txt arbitrary/folder/destination
jt ~/arbitrary/folder/destination $ ls
foo.txt
It would totally help me a lot, and I could learn some bash
Use a function. You can put something like the below in your .bashrc:
cpd() {
cp -- "$1" "$2" || return
if [[ -d "$2" ]]; then
cd -- "$2"
else
case $2 in
?*/*) cd -- "${2%/*}" ;;
/*) cd / ;;
esac
fi
}
...invoked as...
cpd magic.txt arbitrary/directory/destination
or
cpd magic.txt arbitrary/directory/destination/filename.txt
A function is necessary because the alternatives won't work:
An external script can't change it's caller's state (a script could change its own working directory, but not that of the shell that started it).
An alias can't run logic or conditionals, and can't refer to its positional arguments.
An slower (but shorter) variant of #Charles's script
cpd() {
cp -- "$1" "$2" && [[ -d "$2" ]] && cd -- "$2" || cd -- "$(dirname "$2")"
}

Resources