How to run a function when the cd command is run - shell

I am kind of new to Linux and am just learning to make the computer do the work that I want.
So my wish is that whenever I use the cd
command, I want it to change directory and then list all the files present in them. If there are no arguments passed, I want the pwd command to run.
This is what I have done so far.
function cd {
if [ $# -eq 0 ]
then
pwd
else
cd "$1"; ls -l
fi
}
When I run this, it works fine when there are no arguments passed and it runs the pwd command. However, when I pass an argument, it does not display anything and it closes the terminal, which is'nt what I want.
When I changed the function name to ca though, and ran ca, it worked as expected.
Why is this so? Are there a list of aliases I am not allowed to use? How can I make it work?

If it exists, chpwd_functions is an array of function names, each of which will be called, in order, whenever the working directory changes. In your case, it could be used as follows:
foo () {
if [[ $PWD == $HOME ]]; then
pwd
else
ls -l
fi
}
chpwd_functions+=(foo)

You recursively call your function instead of calling the cd builtin.
In ZSH the builtin command can be used to execute a builtin explicitly suppressing shell function lookup. This is exactly what you need to implement a function that has the same name as a shell builtin
function cd {
if [ $# -eq 0 ]
then
pwd
else
builtin cd "$1" ; ls -l
fi
}
This applies to BASH as well.
Regarding the command builtin
In BASH you could use the command builtin to execute an external command or a builtin. That is where BASH is different from ZSH as in ZSH command executes external commands only.
Only in BASH
command cd works the same as builtin cd (assuming /bin/cd does not exist)
In ZSH
command cd would probably fail with cd: command not found unless /bin/cd exists

The "cd" command in Bash can take options, so using just $1 will drop the directory name. This passes everything:
function cd {
if [ $# -eq 0 ]
then
pwd
else
cd "$#"; ls -l
fi
}

I was having a similar issue having switched from bash to zsh. Example function...
go() {
if [[ $# == "code" ]]; then command cd "$CODE";
}
The solution for me was to 1. prefix my function with function and 2. replace command with builtin
function go() {
if [[ $# == "code" ]]; then builtin cd "$CODE";
}

Related

Is it possible to CD into a file?

I find a list of files that I need to cd to (obviously to the parent directory).
If I do cd ./src/components/10-atoms/fieldset/package.json I get the error cd: not a directory:, which makes sense.
But isn't there a way to allow for that? Because manipulating the path-string is pretty cumbersome and to me that would make total sense to have an option for that, since cd is a directory function and it would be cool that if the path would not end up in a file, it would recursively jump higher and find the "first dir" from the given path.
So cd ./src/components/10-atoms/fieldset/package.json would put me into ./src/components/10-atoms/fieldset/ without going on my nerves, telling me that I have chosen a file rather than a dir.
You could write a shell function to do it.
cd() {
local args=() arg
for arg in "$#"; do
if [[ $arg != -* && -e $arg && ! -d $arg ]]; then
args+=("$(dirname "$arg")")
else
args+=("$arg")
fi
done
builtin cd ${args[0]+"${args[#]}"}
}
Put it in your ~/.bashrc if you want it to be the default behavior. It won't be inherited by shell scripts or other programs so they won't be affected.
It modifies cd's arguments, replacing any file names with the parent directory. Options with a leading dash are left alone. command cd calls the underlying cd builtin so we don't get trapped in a recursive loop.
(What is this unholy beast: ${args[0]+"${args[#]}"}? It's like "${args[#]}", which expands the array of arguments, but it avoids triggering a bash bug with empty arrays on the off chance that your bash version is 4.0-4.3 and you have set -u enabled.)
This function should do what you need:
cdd() { test -d "$1" && cd "$1" || cd $(dirname "$1") ; }
If its first argument "$1" is a directory, just cd into it,
otherwise cd into the directory containing it.
This function should be improved to take into account special files such as devices or symbolic links.
You can if you enter a bit longer line (or create dedicated shell script)
cd $(dirname ./src/components/10-atoms/fieldset/package.json)
If you add it in script it can be :
cd $(dirname $1)
but you need to execute it on this way:
. script_name ./src/components/10-atoms/fieldset/package.json
You can put this function in your ~/.bashrc:
function ccd() {
TP=$1 # destination you're trying to reach
while [ ! -d $TP ]; do # if $TP is not a directory:
TP=$(dirname $TP) # remove the last part from the path
done # you finally got a directory
cd $TP # and jump into it
}
Usage: ccd /etc/postfix/strangedir/anotherdir/file.txt will get you to /etc/postfix.

Override a builtin command with an alias

I am trying to make an alias that overrides the cd command. This is going to execute a script before and after the "real" cd.
Here is what I have so far:
alias cd="echo before; cd $1; echo after"
This executes the echo before and echo after command however it always changes directory ~
How would I fix this?
I also tried cd(){ echo before; cd $1; echo after; } however it repetedly echos "before".
I also tried cd(){ echo before; cd $1; echo after; } however it repetedly echos "before".
because it calls recursively the cd defined by you. To fix, use the builtin keyword like:
cd(){ pwd; builtin cd "$#"; pwd; }
Ps: anyway, IMHO isn't the best idea redefining the shell builtins.
Just to add to #jm666's answer:
To override a non-builtin with a function, use command. For example:
ls() { command ls -l; }
which is the equivalent of alias ls='ls -l'.
command works with builtins as well. So, your cd could also be written as:
cd() { echo before; command cd "$1"; echo after; }
To bypass a function or an alias and run the original command or builtin, you can put a \ at the beginning:
\ls # bypasses the function and executes /bin/ls directly
or use command itself:
command ls

How do I access the command line argument variable when making an alias in Terminal?

I'm making a few aliases to speed up my development processes.
Right now I'm trying to make a cdls which is quite obviously a cd {arbitrary-file} && ls {arbitrary-file}
I was under the impression that alias cdls='cd $# && ls $# would work, but it looks like I was mistaken about $# carrying the argument (file path) since this sends me back to my $HOME directory every time.
Aliases don't process arguments. Use a function instead:
cdls () {
cd "$1"
ls
}
Try instead in .bash_profile
function cdls () { ls "$#" && eval cd "\"\$$#\"";}
I don't remember where I got that from but I have something similar to it working.

customizing cd command

Generally I keep directory specific settings in .bashrc and whenever I change directory execute the command source .bashrc to make those settings effective.
Now I was thinking of manipulating cd command in ~/.bashrc, so whenever I cd to new directory and if any .bashrc exists there, it will be loaded automatically.
Similar to this cd $1; source .bashrc ( I have verified that $1 is valid path), but problem is cd is shell builting, so it's a recursive loop ( cd always points to modifed cd ). We do not have elf file of cd ( which generally we have of other commands viz scp or others). So how can I achieve this ?
Also if shopt -s cdspell is supported then also I need to have cd spelled path in argument of $1.
You want the "builtin" command;
builtin shell-builtin [arguments]
Execute the specified shell builtin,
passing it arguments, and return its exit status. This is useful when
defining a function whose name is the same as a shell builtin,
retaining the functionality of the builtin within the function. The cd
builtin is commonly redefined this way. The return status is false if
shell-builtin is not a shell builtin command.
From: http://linux.die.net/man/1/bash
So, you could have something like (untested, don't have a bash handy either);
function cd() {
builtin cd $1 \
&& test -e .bashrc \
&& source .bashrc
}
You might check out direnv. https://github.com/zimbatm/direnv
RVM does this:
$ type cd
cd is a function
cd ()
{
if builtin cd "$#"; then
[[ -n "${rvm_current_rvmrc:-}" && "$*" == "." ]] && rvm_current_rvmrc="" || true;
__rvm_do_with_env_before;
__rvm_project_rvmrc;
__rvm_after_cd;
__rvm_do_with_env_after;
return 0;
else
return $?;
fi
}
And yes, this works on my machine. Essentially, as #RoryHunter said, use builtin and run some code if it succeeds, or return the exit code if it fails.
You could try this:
function cdd(){ cd $1; if [ -e ./.bashrc ] ; then source ./.bashrc; fi; }
alias cd = 'cdd'
?
Didn't tested this much, however.

How to change current working directory inside command_not_found_handle

I'm trying to write a not found handle in Bash that does the following:
If $1 exists and it's a directory, cd into it.
If $1 exists inside a user defined directory $DEV_DIR, `cd into it.
If the previous conditions don't apply, fail.
Right now I have something like this:
export DEV_DIR=/Users/federico/programacion/
function command_not_found_handle () {
if [ -d $1 ]; then # the dir exists in '.'
cd $1
else
to=$DEV_DIR$1
if [ -d $to ]; then
cd $to
echo `pwd`
else
echo "${1}: command not found"
fi
fi
}
And although it seems to be working (the echo pwd command prints the expected dir), the directory in the actual shell does not change.
I was under the impression that since this is a function inside my .bashrc the shell wouldn't fork and I could do the cd but apparently that's not working. Any tips on how to solve this would be appreciated.
I think what's going on is that the shell fork()s after setting up any redirections but before looking for commands, so command_not_found_handle can't affect the interactive shell process.
What you seem to want to do may partly possible using the autocd feature:
shopt -s autocd
From man bash:
autocd - If set, a command name that is the name of a directory
is executed as if it were the argument to the cd com‐
mand. This option is only used by interactive shells.
Otherwise, just create a function that you invoke by name that performs the actions you are trying to use command_not_found_handle for.
It won't change directies if you run this program as a script in your main shell because it creates a sub-shell when it executes. If you source the script in your current shell then it will have the desired effect.
~/wbailey> source command_not_found.sh
That said, I think the following would achieve the same result:
wesbailey#feynman:~/code_katas> cd xxx 2> /dev/null || cd ..; pwd
/Users/wesbailey
just replace the ".." with your env var defined directory and create an alias in your .bashrc file.
I've had the very same wish and the solution that I've been using for a while was opening a new tab in gnome terminal by issuing the command gnome-terminal --tab --working-directory="$FOLDER" from inside the command_not_found handle.
But today I've come up with a solution which is not tied to a specific terminal application, but has exactly the intended behaviour.
The solution uses the PROMPT_COMMAND, which is run before each prompt. The PROMPT_COMMAND is bound to a function responsible for checking for a file related to current shell, and cd'ing into the directory specified in that file.
Then, the command_not_found_handle fills in the file when a change in directory is desired. My original command_not_found_handle also checkout a git branch if the current directory is a git repository and the name matches an existing branch. But to keep focus on answering the current question, I've stripped that part of code.
The command_not_found_handle uses find for searching for the directory matching the given name and goes only 2 levels deep in the directory tree, starting from a configured list.
The code to be added to bash_rc follows:
PROMPT_COMMAND=current_shell_cd
CD_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/bash-cd/$$.cd"
current_shell_cd() {
if [ -r "$CD_FILE" ]; then
local CD_TARGET="$( cat "$CD_FILE" )"
[ ! -z "$CD_TARGET" ] && cd "$CD_TARGET" 2>/dev/null
rm "$CD_FILE"
fi
}
command_not_found_handle () {
local COMMAND="$1";
# List folders which are going to be checked
local BASE_FOLDER_LIST=(
"$HOME/Desenvolvimento"
"/var/www/html"
"$HOME/.local/opt/"
)
local FOLDER=$(
find "${BASE_FOLDER_LIST[#]}" \
-maxdepth 2 -type d \
-iname "$COMMAND" -print -quit )
if [ ! -z "$FOLDER" -a -d "$FOLDER" ]
then
mkdir -p "$( dirname "$CD_FILE" )"
echo "$FOLDER" > "$CD_FILE"
else
printf "%s: command not found\n" "$1" 1>&2
return 127
fi
}

Resources