Is there a concise, general, idiomatic bash construction that would force a statement to error when a subshell it invokes errors? For example,
cd $(git rev-parse --show-toplevel)
will invariably return 0 even if the git command errors, which makes it difficult to script something like
cd $(git rev-parse --show-toplevel) && echo 'Success!'
Of course you can just do the following, but I was wondering if there was a better way:
DIR=$(git rev-parse --show-toplevel) && cd $DIR && echo 'Success!'
It's not quite a general solution, but in that example you could do:
cd $(git rev-parse --show-toplevel || echo -#) && echo 'Success!'
This solution works by turns the output into something that the command won't accept if the command in the substitution fails.
Related
I've been trying to create an alias of a long Git command. The command is of this format:
git clone "https://MyUserName#MyDomain.com/a/PathToRepo/RepoName" && (cd "RepoName" && mkdir -p .git/hooks && curl -Lo `git rev-parse --git-dir`/hooks/commit-msg https://MyUserName#MyDomain.com/tools/hooks/commit-msg; chmod +x `git rev-parse --git-dir`/hooks/commit-msg)
I am fine with either an alias or a bash function that can help me accomplish this.
Don't use aliases. They are in every aspect inferior to functions.
Writing this as a function also avoids the quoting errors you probably were bumping into when trying to create an alias (though it's not impossible to solve those, too; but if that's what you want, probably ask a new question with your actual attempt).
The following only has very small changes compared to your original (and to the extent that the original worked in the first place, it would have worked just as well without any changes at all).
func () {
git clone "https://MyUserName#MyDomain.com/a/PathToRepo/RepoName" &&
(
cd "RepoName" &&
mkdir -p .git/hooks &&
curl -Lo "$(git rev-parse --git-dir)/hooks/commit-msg" "https://MyUserName#MyDomain.com/tools/hooks/commit-msg" &&
chmod +x "$(git rev-parse --git-dir)/hooks/commit-msg"
)
}
The switch from legacy `command substitution` syntax to modern $(command substitution) syntax is mainly for aesthetic reasons. The addition of double quotes is crucial for handling file names with spaces or other shell metacharacters in them. Adding && instead of ; before chmod +x seemed to make sense for consistency.
Personally, I would calling git rev-parse --git-dir twice, and just create a variable with the name of the directory:
func () {
git clone "https://MyUserName#MyDomain.com/a/PathToRepo/RepoName" &&
local hookdir=RepoName/$(git -C "RepoName" rev-parse --git-dir)/hooks &&
mkdir -p "$hookdir" &&
curl -Lo "$hookdir/commit-msg" "https://MyUserName#MyDomain.com/tools/hooks/commit-msg" &&
chmod +x "$hookdir/commit-msg"
}
If you want to make the repository name and/or the URL configurable parameters, I would suggest to make the repo name the first parameter and the base URL the second, but this obviously depends on your use case.
func () {
git clone "${2-https://MyUserName#MyDomain.com/a/PathToRepo}/$1" &&
local hookdir="$1"/$(git -C "$1" rev-parse --git-dir)/hooks &&
mkdir -p "$hookdir" &&
curl -Lo "$hookdir/commit-msg" "https://MyUserName#MyDomain.com/tools/hooks/commit-msg" &&
chmod +x "$hookdir/commit-msg"
}
The syntax ${2-default} falls back to default if $2 is unset.
I'm trying to create a script to do this:
git add "file"
git commit -m "Comment"
My idea is to run:
gac "file" "Comment"
I know I can do something similar but for all files, with:
echo 'alias gac="/path/to/gitaddcommit.sh"' >> ~/.bash_profile
And the .sh would be:
!/bin/bash
git add .
echo “Enter commit message: “
git commit -am “$commitMessage”
Well you need two things :
A bin folder where you can put every sh script you want to use everywhere.
More knowledge about shell scripting and how you can get argv (in your ex: 'file' 'Comment')
So first go to your /home/<username> then mkdir bin && cd bin && pwd
then copy the pwd and add it into your PATH env variable inside your .bashrc
path example: PATH='/bin/:/sbin/:/home//bin
Then source ~/.bashrc you can now use every sh script inside you bin folder everywhere.
Cool so first problem done !
you don't have to do echo alias gac="/path/to/gitaddcommit.sh"' >> ~/.bash_profile anymore.
Now second problem here a post that can help you post
And let me show you for your example :
cd ~/bin && vi gac.sh
Now the script :
#!/bin/sh
if [ "$#" -ne 2 ]; then
echo "Usage: ./gac FILENAME COMMIT_MESSAGE" >&2
exit 1
fi
git add "$1"
git commit -am "$2"
First we check the number or arg then git add and commit.
Simple and fast maybe checking if arg one is a file might be a good idea too.
PS: i'm going to re write my post ahah
Here's what I have in my .bashrc:
ga ()
{
if test "$1" != "-f" && git rev-parse HEAD > /dev/null 2>&1 && ! git diff-index --quiet HEAD; then
echo 'Repo is dirty. -f to force' 1>&2;
return 1;
fi;
git add "$#";
list=$(git diff --name-only --cached | tr \\n \ );
git commit -m "Add $list"
}
The commit message is autogenerated, but you could easily modify it to prompt the user or take it from somewhere else.
I have this for loop
for repository in ./*/; do
echo $repository && cd $repository && git checkout -b prod && cd - >/dev/null;
done
But if branch prod already exists it prints a message and exit the loop.
How can ignore this error and just go to the next directory ?
Thanks
So the problem is that git checkout -b prod returns failure to the shell if the branch already exists. Since it's connected to the next command (cd -) with the conditional operator &&, that next command only runs if git succeeds. So when git fails, the cd doesn't run, and your shell is left in the wrong directory to continue its loop.
In general, when you want your code to continue even if a command fails, separate the commands with ; or newlines instead of &&.
But a better solution in this case is to just do the cd in a subshell so that it doesn't affect the outer loop's working directory and you don't have to cd - at all:
for repository in ./*/; do
echo "$repository" && (
cd "$repository" && git checkout -b prod
)
done
That will work fine even if the branch creation fails. It will still print out the error message; if you don't want to see those, add the redirect:
for repository in ./*/; do
echo "$repository" && (
cd "$repository" && git checkout -b prod
) 2>/dev/null
done
I've also quoted the expansion of $repository in the commands, which you should almost always do in shell scripts. With the unquoted version, you would get an error if any of the repo directory names had spaces in them, for instance.
Also, that "no side effects in a subshell" thing is great for doing part of your work in a different directory, but it applies more widely. If you had a more complicated loop that set any shell variables or anything while it was in the subdir, those would also be lost. Just something to keep in mind.
Like this
home=$PWD
for repository in "$home"/*/; do
basename "$repository" # to 'echo' $repository
cd "$repository" && git checkout -b prod
done
Better use pushd and popd and additionally it is saver to use find:
while read -r repository; do
pushd "${repository}"
if git checkout -b prod; then
echo "git checkout success"
else
echo "git chechout error"
fi
popd
done < <( find . -mindepth 1 -maxdepth 1 -type d -print )
I'm experimenting a little bit with a shell script, which should run git commands for multiple repositories on the same level. This project structure might be a bad idea, but this is another story.
Everything works fine until I've run into this problem:
DETAIL="test test" && command="commit -m '${DETAIL}'" && echo $(git ${command})
# -> error: pathspec 'test'' did not match any file(s) known to git.
I've also tried other opportunities like
DETAIL="test test" && command="commit -m ${DETAIL}" && echo $(git ${command})
DETAIL="test test" && command="commit -m $DETAIL" && echo $(git ${command})
All give the same result (see above). I've also scanned these docs about string expansion, but I don't have the problem, that the variables/strings might be null or undefined. The last echo is not the problem, you can also store the result of $(git status) in a variable and echo this one (my way in the script).
I know, there are similar questions, but I did not found a similar scenario yet, since I'm just dealing with simple and non-null strings, but with (too?) many quotes.
Interesting variant:
DETAIL="test test" && command="commit -m '${DETAIL}'" && echo $("git ${command}")
# -> git commit -m 'test test': command not found # WHAT?
Also interesting, just:
command="commit -m 'test'" && echo $(git ${command})
works fine.
Use bash arrays with proper quoting...
DETAIL="test test" && command=(commit -m "$DETAIL") && git "${command[#]}"
To your code:
echo "$(command)" is the same as command (ok, trailing empty newlines are removed)
"command blabla" does not execute file command with the first argument blabla. It will execute a filename named exactly with space command blabla.
Inside $("git ${command}") you want to execute a filename named git commit -m 'test test' (exactly, this is the whole filename name, with spaces, after ${command} is expanded). As on you system there is no file named git commit -m 'test test' bash returns command not found.
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'