Navigating to directory in ZSH (bash) - shell

I'm using Oh-My-ZSH to create some ailises and functions for easing my repetitive work load.
I need to navigate from anywhere in my computer to my Frontend directory. This is what I have:
frontend(){
cd ~/Desktop/Work/Frontend
cd $1
}
Now this works well when I type frontend or frontend myProject, however, all my project folders are postfixed by something like .m, .tablet, etc.
How can I write things that:
Will let me automatically navigate to a folder that is followed by .something
When there are multiple options (like project.m and project.tablet) will prompt me with options similar to if you hit tab in your terminal and are given multiple options for autocomplete.
I hope my question makes sense.
Thanks.

Find a zsh solution first, followed by a bash solution.
Update: Turns out that a zsh implementation (based on builtin compctl) is much simpler than the bash implementation (based on builtin complete).
Save the code of interest to a file (e.g., frontend) and source it (e.g., . ./frontend); either interactively or, preferably, from your bash/zsh profile.
Once in place, auto-completion of subdirectory names in ~/Desktop/Work/Frontend will work as follows:
Type, for instance, frontend myProject and press TAB.
myProject is then prefix-matched against the names of the subdirectories in ~/Desktop/Work/Frontend:
If there's only 1 match, myProject will instantly expand to the full subdirectory name.
Otherwise, a beep sounds to indicate that there are multiple matches:
zsh: The names of all matching subdirectories are listed right away.
bash: Press TAB again to list the names of all matching subdirectories
Continue typing until the prefix match is unambiguous, then press TAB again.
Note: In bash, to also only require pressing TAB once to list multiple matches, add the following to your shell profile bind "set show-all-if-ambiguous on".
zsh solution:
# Define the shell function.
frontend(){
cd ~/Desktop/Work/Frontend/"${1:-}"
}
# Tell zsh to autocomplete directory names in the same directory as
# the function's when typing a command based on the shell function.
compctl -/ -W ~/Desktop/Work/Frontend frontend
bash solution:
Note: complete -o dirnames doesn't take an argument, unfortunately - it always auto-completes for the current directory. Thus, a custom shell function that returns the potential matches, combined with -o filenames, is required.
# Define the main shell function.
frontend(){
local BASEDIR=~/Desktop/Work/Frontend
cd "$BASEDIR/${1:-}"
}
# Define the custom completion function.
_frontend_completions() {
local BASEDIR=~/Desktop/Work/Frontend
# Initialize the array variable through which
# completions must be passed out.
COMPREPLY=()
# Find all matching directories in the base folder that start
# with the name prefix typed so far and return them.
for f in "$BASEDIR/${COMP_WORDS[COMP_CWORD]}"*; do
[[ -d $f ]] && COMPREPLY+=( "$(basename "$f")" )
done
}
# Tell bash to autocomplete directory names as returned by the
# _frontend_completions() helper functoin when typing a command
# based on the main shell function.
complete -o filenames -F _frontend_completions frontend fe

I strongly recommend you use AutoJump
But if you must, maybe you want to use alias
like in your ~/.zshrc add:
alias fend='cd path/to/frontend'

Related

Can I make tab-completion filter files by extension?

Assume myprogram only consumes *.data files as command line arguments. In terminal, when we do
$ myprogram <tab>
we want only the *.data files to be listed for tab auto-complete. How is this behavior achieved? The shell being used is Bash.
Option 1
Type the following into your bash shell
complete -f -X '!*.data' myprogram
the -f option tells complete to only complete based on file names, not directories. the -X option allows you to specify the filter pattern.
Option 2
Note: This solution is global. It will affect tab-completion in every directory and on every command (meaning things like cd or rm, as well as myprogram). It works by allowing you to specify file extensions that will not appear in tab-complete. This is not exactly what you asked for, but if there aren't many files other than *.data in your working directory, excluding all the options won't be too much of a pain. For both these reasons this option is probably not what you want but it is still worth noting.
In the file ~/.bash_profile add the line
FIGNORE=".py:.txt:.out:.exe:.c:<etc>"
The syntax there is to create a colon-separated list of the file extensions you want to ignore. After saving the new .bash_profile you must type . ~/.bash_profile for the changes you made to take effect.
Further info
For more info about the complete command check out Programmable Completion Builtins in the Bash manual.
Better use _filedir instead of "raw" complete.
Justification
grep _filedir -r $(pkg-config --variable=completionsdir bash-completion) | wc -l
tilde (~) paths are being expanded
Prefixes are removed for directories, i.e. for /home/tux/Do<TAB><TAB>, the list you get as a reply removes '/home/tux' and is thus much more compact
Easier to implement and more failsafe
MWE
Write a file "completions/myprogram", which you source by . completions/myprogram. Content:
_myprogram()
{
# init bash-completion's stuff
_init_completion || return
# fill COMPREPLY using bash-completion's routine
_filedir '#(data)'
}
complete -F _myprogram myprogram

Autodetect possible arguments in shell script for completion

Here's a shell script that does some stuff according to with what parameter it was called:
if [ $1 = "-add" ]
then
...
elif [ $1 = "-remove" ]
...
else
...
fi
A script is an executable one (a link to it was created in the /usr/bin directory). So, I can call it from shell by specifying the link name added in /usr/bin.
What I want, is auto-detecting the possible arguments of script (in my case they are -add, -remove) during it calling. It means that when I'll type a command, related to script calling, then type -re and press a tab button it will suggest that it's -remove and autofill it for me.
How the arguments need to be defined to reach that?
Tried to create aliases in shell config file or few links in /usr/bin directory for all possible inputs and it was working fine, but I don't think it's a best solution for that.
While it does require some configuration outside of your script, adding autocomplete options is fairly easy.
Here's a simple example of a ~/.bash_completion file that adds auto completion of --add and --remove to command yourscript. In a real world case you'd probably want to generate the options by querying the script directly; they're hard coded here for simplicity.
_yourscript_complete()
{
# list of options for your script
local options="--add --remove"
# current word being completed (provided by stock bash completion)
local current_word="${COMP_WORDS[COMP_CWORD]}"
# create list of possible matches and store to ${COMREPLY[#}}
COMPREPLY=($(compgen -W "${options}" -- "$current_word"))
}
complete -F _yourscript_complete yourscript
Note that the ~/.bash_completion is only sourced during login, so you'll need to spawn another login shell to see your changes in action. You may need to enable sourcing of user bash_completion files on your system, too.
The result:
$ yourscript --<tab><tab>
--add --remove
It seems that bash/zsh completion is a powerful tool that can manage you shell input in the way you want.
In one of the other answers some explanations about how it works in bash were presented.
I'm a user of zsh, so, I think, it wouldn't be superfluous to show how I managed my task there:
Configuring .zshrc file:
adding folder for your autocomplete functions:
fpath=(~/.zsh-completions $fpath)
enabling zsh tab-completion system:
autoload -U compinit & compinit
Note: above accented lines must be added to ~/.zshrc file.
Adding a function for your script:
After configuring .zshrc file, restarting zsh and typing scriptname in it, compinit() function will list all files with underscope from $fpath and find the one with first line that matches #compdef scriptname.
So, a new file _scriptname that will hold a function for our script must be added to ~/.zsh-completions directory. To let compinit() find this file on scriptname typing, as mentioned above, its first line must be: #compdef scriptname.
Let the arguments dancing:
For zsh there are a lot of completion functions examples in /usr/share/zsh/functions/Completion directory. By finding the appropriate one there you can configure your shell input according to your tastes. For auto-detecting the attributes (in my case they are -add and -remove) an _attributes() function in the _scriptname
file could be set in the next way:
_arguments -s \
'(-a --add)'{-a,--add}'[adding a new item to the basket]' \
'(-r --remove)'{-r,--remove}'[removing an item from the basket]'
Eventually, after restarting zsh again, auto-detecting for scriptname is working as below:
scriptname -<TAB> => scriptname -
--add -a -- adding a new item to the basket
--remove -r -- removing an item from the basket
scriptname --a<TAB> => scriptname --add
scriptname --r<TAB> => scriptname --remove

Implementing autocompletion to zsh aliases

I am using the awesome zsh framework oh-my-zsh. Every day I hit gc which is an alias for git commit I hit <Tab><Tab> and it gives me the correct files to commit. I am really interested to know how that is implemented, so I can implement my own idea of navigating to sub-directories using an alias.
I mean navigating into ~/workspace/a-repo using this alias -w a<Tab><Tab> which completes to a-repo and gives me some suggestions which are folders inside ~/workspace. I made this alias already but I need to know how to add the correct autocompletion/suggestion to it.
Here is my alias:
-w() { cd ~/workspace/"$*" }
In the case of gc (which I assume is defined as alias gc='git commit -v' in the git plugin of oh-my-zsh) zsh internally substitutes the alias (gc) with the actual command (git commit -v ) before looking for trying for completions. This works for every alias (unless the shell option COMPLETE_ALIASES is set, which would allow to set separate completions for aliases).
As for what you want to do: Seing that -w is actually a function and not an alias, you indeed would have to write your own completion. But there is a much simpler way to go about it: Static named directories.
Named directories are usually just the home directories of the users on the system. The most commonly known is probably ~ for the home directory of the current user. Other users directories are named ~USERNAME. You can also define your own static named directories. There are two ways to do this:
Use hash -d to explicitly define a name, similar to an alias:
hash -d w=~/workspace
Implicitly define it by defining a string shell parameter whose value begins with an / (this also means you cannot use ~ as shortcut for your home directory)
w="${HOME}/workspace"
In both cases you now can use ~w to reference your workspace, in the second case you can also use $w (for example for use in quoted strings). Typing cd ~w/a-repo will get you to ~/workspace/a-repo. Tab-completion works like it would with any other path, so pressing ~w/a<Tab> will present you ~w/a-repo as completion.
Additionally, if you set the shell option AUTO_CD (setopt AUTO_CD), you can switch into a directory just by typing the path (or directory name) without the need for cd.
% w="/tmp"
% cd ~w
% pwd
/tmp
% mkdir 1 2 3
% setopt AUTO_CD
% ~w/<TAB>
1 2 3
% ~w/1
% pwd
/tmp/1

Directory based environment variable scope - how to implement?

I have a set of tools which I need to pass parameters depending on the project I'm working on. I'd like to be able to automatically set a couple of environment variables based on the current directory. So when I switched between directories, my commonly used env vars would also change. Example:
Let's current directory is foo, thus if I do:
~/foo$ ./myscript --var1=$VAR1
VAR1 would have some foo based value.
Then, let's say I switched to bar directory. If I do:
~/bar$ ./myscript --var1=$VAR1
VAR1 should now have some bar based value.
Is that possible? How?
the ondir program lets you specify actions to run when you enter and leave directories in a terminal
There is direnv which helps you do this stuff much easily and in an elegant way. Just define a .envrc file in your project directory with all the env variables needed and it will source it once you cd into that folder.
I've written another implementation of this, which is somewhat similar to ondir. I didn't actually know about ondir when I started working on it. There are some key differences that may be useful, however.
smartcd is written entirely in shell, and is fully compatible with bash and zsh, even the more esoteric options
smartcd will run scripts all the way down and up the directory hierarchy down to their common ancestor, not just for the two directories you're entering and leaving. This means you can have a ~/foo script that will execute whether you "cd ~/foo" or "cd ~/foo/bar"
it has "variable stashing" which is a more automatic way of dealing with your environment variables, whereas ondir requires you to explicitly and manually remove and/or reset your variables
smartcd can work with "autocd" turned on by hooking your prompt command (PROMPT_COMMAND in bash, precmd in zsh)
You can find smartcd at https://github.com/cxreg/smartcd
This is not something that is directly supported with the built-in features of bash or any other common shell. However, you can create your own "cd" command that will do whatever you want. For example, you could alias cd to do the cd and then run a special script (eg: ~/bin/oncd). That script could look up the new directory in a database and run some commands, or see if there's a special file (eg: .env) in the directory and load it, etc.
I do this sort of thing a lot. I create several identically named batch files in directories where I need them that only set the variables and call the common script. I even have a batch file that creates the other small files.
This is not pretty, but you can use a combination of exported environment variables and the value of $PWD.
For example:
export VAR1=prefix
export prefix${HOME////_}_foo=42
export prefix${HOME////_}_bar=blah
Then myscript needs only to eval echo \${$VAR1${PWD////_}} to get at the directory based value.
How about wrap your script with a function (the function can be placed either in your bash profile/bashrc file in the system ones to make available for all the users ).
myscript () { case $PWD in
/path/to/foo) path/to/myscript --var1=$VAR1 ;;
/path/to/bar) path/to/myscript --var2=$VAR1 ;;
*) ;;
case
}
Hence the function myscript will call the real "myscript" knowing what to do based on the current working directory.
Take this as an example:
hmontoliu#ulises:/tmp$ myscript () { case $PWD in /tmp) echo I\'m in tmp;; /var) echo I\'m in var;; *) echo I\'m neither in tmp nor in bar; esac; }
hmontoliu#ulises:/tmp$ myscript
I'm in tmp
hmontoliu#ulises:/tmp$ cd /var
hmontoliu#ulises:/var$ myscript
I'm in var
hmontoliu#ulises:/var$ cd /etc
hmontoliu#ulises:/etc$ myscript
I'm neither in tmp nor in bar

Execute a bash function upon entering a directory

I'd like to execute a particular bash function when I enter a new directory. Somethink like:
alias cd="cd $# && myfunction"
$# doesn't work there, and adding a backslash doesn't help. I'm also a little worried about messing with cd, and it would be nice if this worked for other commands which changed directory, like pushd and popd.
Any better aliases/commands?
Aliases don't accept parameters. You should use a function. There's no need to execute it automatically every time a prompt is issued.
function cd () { builtin cd "$#" && myfunction; }
The builtin keyword allows you to redefine a Bash builtin without creating a recursion. Quoting the parameter makes it work in case there are spaces in directory names.
The Bash docs say:
For almost every purpose, shell functions are preferred over aliases.
The easiest solution I can come up with is this
myfunction() {
if [ "$PWD" != "$MYOLDPWD" ]; then
MYOLDPWD="$PWD";
# strut yer stuff here..
fi
}
export PROMPT_COMMAND=myfunction
That ought to do it. It'll work with all commands, and will get triggered before the prompt is displayed.
There are a few other versions of this out there, including
smartcd, which I wrote, and has a ton of features including templating and temporary variable saving
ondir, which is smaller and much simpler
Both of these support both bash and zsh
I've written a ZSH script utilizing the callback function chpwd to source project specific ZSH configurations. I'm not sure if it works with Bash, but I think it'll be worth a try. If it doesn't find a script file in the directory you're cd'ing into, it'll check the parent directories until it finds a script to source (or until it reaches /). It also calls a function unmagic when cd'ing out of the directory, which allows you to clean up your environment when leaving a project.
http://github.com/jkramer/home/blob/master/.zsh/func/magic
Example for a "magic" script:
export BASE=$PWD # needed for another script of mine that allows you to cd into the projects base directory by pressing ^b
ctags -R --languages=Perl $PWD # update ctags file when entering the project directory
export PERL5LIB="$BASE/lib"
# function that starts the catalyst server
function srv {
perl $BASE/script/${PROJECT_NAME}_server.pl
}
# clean up
function unmagic {
unfunction src
unset PERL5LIB
}

Resources