run command inside of ${ curly braces - bash

I want to alias cd so that it takes me to the root of my current git project, and if that can't be found it takes me to my normal home dir.
I am trying to set HOME to either the git root or, if that can't be found, my normal home variable.
alias cd='HOME="${$(git rev-parse --show-toplevel):-~}" cd'
It doesn't work though.

You can't run a command inside ${}, except in the fallback clause for when a value is not set (in POSIX sh or bash; might be feasible in zsh, which allows all manner of oddball syntax).
Regardless, far fewer contortions are needed if using a function:
# yes, you can call this cd, if you *really* want to.
cdr() {
if (( $# )); then
command cd "$#"
else
local home
home=$(git rev-parse --show-toplevel 2>/dev/null) || home=$HOME
command cd "$home"
fi
}
Note:
Using a function lets us test our argument list, use branching logic, have local variables, &c.
command cd is used to call through to the real cd implementation rather than recursing.

Of course, it is possible to execute commands inside parameter expansions.
Well, only on the failure side, that is:
$ unset var
$ echo ${var:-"$(echo "hello world!")"}
So, you may get the git command executed if you use the failure side.
Assuming that var is empty:
unset var
var=${var:-"$(git rev-parse --show-toplevel 2>/dev/null)"}"
But that would be simpler with:
var="$(git rev-parse --show-toplevel 2>/dev/null)"
And, if var is still empty after that, use:
HOME=${var:-~} builtin cd
That yields:
var="$(git rev-parse --show-toplevel 2>/dev/null)"; HOME=${var:-~} builtin cd
Which may be used in an alias as :
alias cdr='var="$(git …)"; HOME=${var:-~} builtin cd'

Related

zshrc doesn't recognize custom bash functions

I recently moved from bash to zsh, and like most of people I had my custom bash aliases/functions to ease git and env sourcing operations. In particular there are 2 of them which doesn't work properly when run on zsh but work completely fine on bash.
export REPO_ROOT=/home/pablo/repos/my_repo
alias croot='cd $REPO_ROOT'
alias subroot='cd $REPO_ROOT/subrepo/subrepo_1/'
repcheckout(){
git checkout "$1"
if [ $(pwd) == $REPO_ROOT ]; then
subroot
else
croot
fi
git checkout "$1"
if [ $(pwd) == $REPO_ROOT ]; then
subroot
else
croot
fi
}
The idea is that I have a set of main_repo-submodule branches and when I checkout the main repo, I want to checkout the submodule in the corresponding branch, instead of doing:
$ git submodule update --init --recursive subrepo/subrepo_1
which checkouts the proper commit in the submodule but doesn't update that I switched to a certain local branch.
For the previous func, the error dropped by zsh when running
$ repcheckout my_cool_branch
is
M subrepo/subrepo_1/
Switched to branch 'my_cool_branch'
repcheckout:2: = not found
Later I have a setup.sh file that I source which goes as follows:
add2path() {
if ! echo ${!1} | egrep "(^|:)$2(:|\$)" > /dev/null ; then
declare -g $1="${!1}:$2"
export "$1"
fi
}
# GENERATED BINARY A
export BIN_A_HOME="$REPO_ROOT/bin_a"
add2path PATH "$BIN_A_HOME/bin"
Same with some generated python modules that are added to PYTHONPATH using the same add2path
Which drops the error:
add2path:1: bad substitution
Both functions use bashisms that aren't valid in zsh.
In repcheckout, the problem is using the == operator in a [ ] test -- the standard operator is =, but bash allows == as a synonym; zsh doesn't. I'd also recommend double-quoting both strings to avoid problems with weird characters in the path (and maybe using "$PWD" instead of $(pwd)):
if [ "$PWD" = "$REPO_ROOT" ]; then
In add2path, the problem is the indirect variable reference ${!1} in both the echo and declare commands. zsh also allows indirect variable references, but its syntax is completely different: ${(P)1}. You could probably make a cross-compatible version with eval, but that tends to cause weird bugs if you don't use it exactly right; I'd just rewrite the function as needed for zsh.
EDIT: If you want to use the same code under both bash and zsh, eval is probably better than trying to detect which shell you're in and using conditional code based on that. Here's a quick stab at writing a cross-shell compatible version:
if ! eval "echo \"\$$1\"" | egrep "(^|:)$2(:|\$)" > /dev/null ; then
eval "declare -g $1=\"\$$1:\$2\""
export "$1"
...
The quoting is ugly, but it should work ok as long as $1 contains a valid identifier; if it doesn't, the usual eval problems may rear their ugly heads.

Is there a git or bash way to pull multple git repos other than moving to each directory?

I wrote this (brute force) script bring multiple repos up to date.
for app in $(/bin/ls -d $#)
do
cd $app
pwd
git pull
cd ..;
done
Is there a simpler way I can do this please?
You could at least simplify the script:
Don't parse ls
Use for x instead of for x in "$#"
Use a subshell to avoid cd-ing out
And to avoid bugs:
Quote your args
Use -- on cd to avoid arguments being interpreted as options
Skip the loop if cd fails
for app; do
(
cd -- "$app" || continue
pwd
git pull
)
done
BTW Shellchek is a great resource for debugging shell scripts. I got most of these tips there.

Echo directory after change?

(Since I couldn't find an explicit answer to this anywhere and I find it useful, I though I would add it to SO. Better alternatives welcome.)
In Bash, how do you alias cd to echo the new working directory after a change? Like this:
$ pwd
/some/directory
$ cd newness
/some/directory/newness
$
Something simple like alias cd='cd "$#" && pwd' doesn't work. For some reason, Bash responds as though you used cd - and returns to $OLDPWD and you get caught in a loop. I don't understand this behavior.
Apparently, you need to do this through a function:
function mycd() {
cd "$#" && pwd
}
alias cd=mycd
However, if you use cd - you wind up printing the directory twice, so this is more robust:
function mycd() {
if [ "$1" == "-" ]; then
cd "$#"
else
cd "$#" && pwd
fi
}
alias cd=mycd
I may be missing some edge cases, like cd -P - and cd -L -, though I don't know if those even make sense.
(See Adrian Frühwirth's answer below for the reason why a simple alias doesn't work and why I feel dumb now.)
The alias doesn't work because aliases don't support arguments, so you cannot chain commands that require parameters. Because of this cd "$#" doesn't make sense in the context of an alias and whatever argument you supply to an alias just gets appended:
$ alias foo='echo "[$#]" && echo'
$ foo bar
[]
bar
Obviously, that's not what you want and exactly why you need to resort to a function.

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.

Is there a Bash shortcut for traversing similar directory structures?

The KornShell (ksh) used to have a very useful option to cd for traversing similar directory structures; e.g., given the following directories:
/home/sweet/dev/projects/trunk/projecta/app/models
/home/andy/dev/projects/trunk/projecta/app/models
Then if you were in the /home/sweet... directory then you could change to the equivalent directory in andy's structure by typing
cd sweet andy
So if ksh saw 2 arguments then it would scan the current directory path for the first value, replace it with the second and cd there. Is anyone aware of similar functionality built into Bash? Or if not, a hack to make Bash work in the same way?
Other solutions offered so far suffer from one or more of the following problems:
Archaic forms of tests - as pointed out by Michał Górny
Incomplete protection from directory names containing white space
Failure to handle directory structures which have the same name used more than once or with substrings that match: /canis/lupus/lupus/ or /nicknames/Robert/Rob/
This version handles all the issues listed above.
cd ()
{
local pwd="${PWD}/"; # we need a slash at the end so we can check for it, too
if [[ "$1" == "-e" ]]
then
shift
# start from the end
[[ "$2" ]] && builtin cd "${pwd%/$1/*}/${2:-$1}/${pwd##*/$1/}" || builtin cd "$#"
else
# start from the beginning
[[ "$2" ]] && builtin cd "${pwd/\/$1\///$2/}" || builtin cd "$#"
fi
}
Issuing any of the other versions, which I'll call cdX, from a directory such as this one:
/canis/lupus/lupus/specimen $ cdX lupus familiaris
bash: cd: /canis/familiaris/lupus/specimen: No such file or directory
fails if the second instance of "lupus" is the one intended. In order to accommodate this, you can use the "-e" option to start from the end of the directory structure.
/canis/lupus/lupus/specimen $ cd -e lupus familiaris
/canis/lupus/familiaris/specimen $
Or issuing one of them from this one:
/nicknames/Robert/Rob $ cdX Rob Bob
bash: cd: /nicknames/Bobert/Rob: No such file or directory
would substitute part of a string unintentionally. My function handles this by including the slashes in the match.
/nicknames/Robert/Rob $ cd Rob Bob
/nicknames/Robert/Bob $
You can also designate a directory unambiguously like this:
/fish/fish/fins $ cd fish/fins robot/fins
/fish/robot/fins $
By the way, I used the control operators && and || in my function instead of if...then...else...fi just for the sake of variety.
cd "${PWD/sweet/andy}"
No, but...
Michał Górny's substitution expression works nicely. To redefine the built-in cd command, do this:
cd () {
if [ "x$2" != x ]; then
builtin cd ${PWD/$1/$2}
else
builtin cd "$#"
fi
}

Resources