How can I run a command in bash or any other bash-like shell only if another command outputs something?
I am working on an API that updates the cache storage of a tool and commits the updated cache to GitHub. What I want to do is, after rebuilding the cache, I want to check if git diff HEAD ./path/to/cache outputs something and if it does, then I want to run git add ./path/to/cache.
Unfortunately, this doesn't work:
git diff HEAD ./path/to/cache && git add ./path/to/cache
I think because the first command doesn't return false or exit and instead outputs an empty string or nothing at all. So, what's the correct way to achieve this in bash?
Use the --exit-code option, which causes git diff to exit with 0 if there are no diffs and 1 otherwise. Then you can ignore the output.
git diff --exit-code HEAD ./path/to/cache || git add ./path/to/cache
The answer posted by #chepner is definitely the best for my use case. But my question was originally for any use case. If you want to achieve the same behavior for any other commands, you can use the grep command to check if the first command outputs anything.
git diff HEAD ./path/to/cache | grep -q . && git add ./path/to/cache
[ -z $(git diff HEAD ./path/to/cache) ] || git add ./path/to/cache
# [ EXPRESSION ], -z the length of STRING is zero.
# || If no, execute subsequent shell
Related
This question already has answers here:
How do I set a variable to the output of a command in Bash?
(15 answers)
Closed 5 months ago.
This is probably a simple one for a bash scripter, which I am not.
I'm running a cron job that downloads some data, and then depending on that data, may or may not modify a second file. After the job, I want to git commit one or both files. For the conditional commit, I tried this in a .sh script:
# attempt to capture whether MyNotes.txt was changed
# by counting lines in git status output
mywc=(git status -s MyNotes.txt | wc -l)
echo $mywc found!
if [ $mywc = 1 ]; then
echo Add file for commit
else
echo Nothing to add
fi
I'm pretty much getting nowhere; this thing seems to fail on the first line with syntax error near unexpected token '|'. If I run git status -s MyNotes.txt | wc -l on the command line, I get the numeric output I expect.
What am I doing wrong and how can I make this work?
If there's a more elegant way to determine whether a file changed, feel free to share.
Also, for my edification, how could I get this to work without the interim mywc variable? I.e., if I wanted to just do the command within the if, something like this:
if [[ $(git status -s MyNotes.txt | wc -l) = 1 ]]; then
...
Thanks!
What am I doing wrong and how can I make this work?
put a dollar before parenthesis.
foo=$(command)
The thing you are using looks like a bash array
declare -a letters=(a b c d)
If there's a more elegant way to determine whether a file changed, feel free to share.
Consider this:
$ git diff -s --exit-code README.md || echo has changed
has changed
$ git checkout README.md
Updated 1 path from the index
$ git diff -s --exit-code README.md || echo has changed
The OR (||) runs if the first command exits with a non-zero code.
Same thing essentially:
$ false || echo false exits with 1
false exits with 1
$ true || echo will not trigger
An aspect of bash that people overlook is that [[, ]], [ and ] are separate commands. They have return codes too. With this knowledge, you can leverage the return codes with if and any other command.
$ if true; then echo yes; else echo no; fi
yes
$ if false; then echo yes; else echo no; fi
no
So for detecting changes in a tracked file:
$ if git diff -s --exit-code README.md; then echo same as in index; else echo changed; fi
same as in index
$ echo 123 >> README.md
$ if git diff -s --exit-code README.md; then echo same as in index; else echo changed; fi
changed
With all of that said...
Just add the file. You don't need to check anything. If it hasn't changed, nothing will happen.
$ echo foo >> myfile
$ git add myfile
$ git commit -m 'maybe changed' myfile
[master b561cc1] maybe changed
1 file changed, 1 insertion(+), 1 deletion(-)
$ git add myfile
$ git commit -m 'maybe changed' myfile
no changes added to commit (use "git add" and/or "git commit -a")
if you need to avoid a non-zero exit code (such as with set -e), just put a || true after the command that you want to ignore the exit status of:
$ cat foo.sh
#!/bin/basho
set -e
echo foo >> myfile
git add myfile
git commit -m 'maybe changed' myfile
git add myfile
git commit -m 'maybe changed' myfile > /dev/null || true
echo no error here. it\'s fine..
false
echo fill never reach this.
Try running that script and see what happens
I search for a way for checking if file changed.
git diff --exit-code -s <path>
Now the bash scripter knows that every command returns a status code which can be checked with $?. In case everything went smoothly, 0 is returned. In that case we get 0 if file is not changed.
Every bash scripter knows too that you can use that with && and || operators (because of lazy evaluation) to write such construct:
git diff --exit-code -s <path> && echo "should add file"
About your edification, what you wrote is perfectly fine!
As CryptoFool pointed out in a comment, I failed to include a $ in my variable assignment. Simple fix in the first line of my script:
mywc=$(git status -s MyNotes.txt | wc -l)
As matt pointed out in a subsequent comment, doing a git add on a file that hasn't changed has no effect. It won't stage the file for commit. So instead of doing conditional logic to determine whether to git add myfile.txt, I'll just blindly execute git add myfile.txt, which will either stage the file if there are changes, or do nothing if there are no changes. Therefore, my entire script can be replaced with one line:
git add MyNotes.txt
git cherry-pick --strategy=recursive -X theirs -n "$GitHub_SHA"
Hello, I'm specifying -s recursive -X theirs, why is it still necessary to git rm and git add?
Also how do i differentiate when to git rm or git add in my bash script?
git cherry-pick --strategy=recursive -X theirs -n "$GitHub_SHA" || merge_conflict=1
if [ ${merge_conflict:-0} -eq 1 ]
then
git add -all
fi
-X theirs and -X ours act on conflicting hunks. If a file has been removed by one side, no conflicting hunks are generated. At the same time, the context lines are not generated either. Without the hunks and context lines, the strategy option cannot pick lines.
To differentiate one conflicting type from another, you could try git status -s and parse the result in short format. For details on short format, see Short Format. For example, for a deleted by us file foo.txt, git status -s foo.txt prints DU foo.txt. For both modified, it's UU foo.txt.
I have a bash script containing the following code:
git -C "$1" push origin "$(git_current_branch)"
where "$1" is the git dir I am trying to push.
The problem is, I cannot get the correct "$(git_current_branch)" variable when I run the script from outside the git directory. I know I can just cd to "$1" before pushing, but I want a better solution (which makes my script cleaner).
Is there any way to do that? Thanks in advance!
Edit: I prefer to do this through git command option, if possible.
You can try that:
current_git_branch=$(git -C "$1" branch | sed '/^\*/!d;s/\* //')
explanation
call git in directory "$1" and print all branches
delete all branches without * at start of line
remove '* ' marker for current branch from line
i've made the following bash script to commit the parent repo after some change in submodule.
it's all about that the script want to cd .. to check the parent repo current branch but the problem is that the cd .. is not affecting the upcoming commands because i guess the subshell
i've tried to run
1- cd ../ && before each command
2- make alias but didn't succeed
3- run exec but the script didn't continued
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "post-commit".
commit_msg= git log -1 --pretty=%B
if [[ $(git branch | grep \* | cut -d ' ' -f2) == "int1177/next" ]]; then
cd ..
if [[ $(git branch | grep \* | cut -d ' ' -f2) == "B0/next" ]]; then
git add 6_Tests
git commit -m "bs esss"
echo "development branch B0/next has now new commit"
else
echo "development branch isn't B0/next"
fi
else
echo "current branch isn't int1177/next"
fi
Actually, this particular problem is not a bash issue, but rather a Git issue.
Why doesn't "cd" work in a shell script? is valid in general, and is a suitable answer to many other questions. But this particular post-commit hook is trying to chdir out of a submodule into its parent superproject, then make a commit within the parent superproject. That is possible. It may be a bad idea for other reasons—in general it's unwise to have Git commit hooks create commits, even in other repositories1—but in this particular case you're running into the fact that Git finds its directories through environment variables.
In particular, there's an environment variable GIT_DIR that tells Git: The .git directory containing the repository is at this path. When Git runs a Git hook, Git typically sets $GIT_DIR to . or .git. If $GIT_DIR is not set, Git will find the .git directory by means of a directory-tree search, but if $GIT_DIR is set, Git assumes that $GIT_DIR is set correctly.
The solution is to unset GIT_DIR:
unset GIT_DIR
cd ..
The rest of the sub-shell commands will run in the one-step-up directory, and now that $GIT_DIR is no longer set, Git will search the superproject's work-tree for the .git directory for the superproject.
As an aside, this:
$(git branch | grep \* | cut -d ' ' -f2)
is a clumsy way to get the name of the current branch. Use:
git rev-parse --abbrev-ref HEAD
instead, here. (The other option is git symbolic-ref --short HEAD but that fails noisily with a detached HEAD, while you probably want the quiet result to be just the word HEAD, which the rev-parse method will produce.)
1The main danger in this case is that the superproject repository is not necessarily in any shape to handle a commit right now. Edit: or, as discovered in this comment, is not even set up to be a superproject for that submodule, yet, much less to have a submodule-updating commit added.
I have a bare remote repository with source files in which I want to build only the changed files after it has been pushed to. I thought the best way to detect which files have been changed would be by putting the command
changed_files=$(git diff-tree --no-commit-id --name-only -r HEAD) into a post-receive hook.
However, the variable ends up empty as I have verified by echoing it into a file. If I put HEAD^ instead of HEAD, it does show the changed files of the second to most recent commit. However, it doesn't show the most recent changes when I put HEAD but just shows nothing.
Can anyone help me? Or is there a smarter approach to my problem altogether?
I would definitely prefer a lean approach like automatically triggering a build with a push over one that would have to e. g. periodically check for changes.
At the point the post-receive hook is executed, all the references have already been updated. Therefore, HEAD means the new head, not the old one.
This may not produce the results you want, since it assumes that there is one non-merge commit and you want to diff with its parent, while you may have pushed a merge or multiple commits.
What you probably want to do is take advantage of the standard input which provides the old and new values. Something like the following will print the changed files as output from the remote side when you push:
#!/bin/sh
while read old new ref
do
# Handle created or deleted branches.
echo $old | grep -qsE '^0+$' && old=$(git hash-object -t tree /dev/null)
echo $new | grep -qsE '^0+$' && new=$(git hash-object -t tree /dev/null)
git diff-tree --no-commit-id --name-only -r "$old" "$new"
done
OK, I've figured it out: I was getting a
remote: fatal: ambiguous argument 'HEAD': both revision and filename
error in the push command which I had not noticed. After changing
changed_files=$(git diff-tree --no-commit-id --name-only -r HEAD)
to
changed_files=$(git diff-tree --no-commit-id --name-only -r HEAD --)
everything is working fine. Apparently, this is caused by the hook being executed in the .git directory of the remote repository, and there is a file called HEAD in that directory, which makes referring to the HEAD revision as HEAD ambiguous.