Right string expansion in Bash (for shell script) - bash

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.

Related

Bash Script - Determine whether to add file to Git commit [duplicate]

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

How does eval stop the pathspec errors in this script?

I want to automate the many version control steps of Git. I was successful until I used git commit -S -m ${var} in my Bash script. This line gives me (pathspec errors x # of word) - 1... unless I use eval. How does eval make my script work?
I thought this article had the answer, but my issue involves a string, not an array.
Gif video of the broken vs. working Bash script
Broken code
brokenCommitCode () {
# Give it a multi-word, space-separated message
read -p 'Commit message (use quotes): ' commitMsg
commitMsg="'${commitMsg}'"
echo ${commitMsg}
git add -A &&
git commit -S -m ${commitMsg}
}
Working code
workingCommitCode () {
read -p 'Commit message (use quotes): ' commitMsg
commitMsg="'${commitMsg}'"
echo ${commitMsg}
git add -A &&
eval git commit -S -m ${commitMsg}
}
I expected the brokenCommitCode to commit properly with the message I enter on the prompt. The actual result is a pathspec error when it reaches git commit -S -m ${commitMsg}. How does eval make this work?
I'm using GNU bash, version 4.4.19(1)-release (x86_64-pc-msys) with git version 2.16.2.windows.1 on a Windows 8.1 PC.
Correct fix is
funname() {
read -p 'Commit message (use quotes): ' commitMsg
echo "${commitMsg}"
git add -A &&
git commit -S -m "${commitMsg}"
}
Why eval seems to fix:
single quotes where added to commitMsg variable (seems intent was to prevent message argument to be split on a whitespace)
looking what happens with the following message:
commitMsg="this is a message"
git commit -S -m ${commitMsg}
git commit -S -m this is a message
[error because "is" "a" "message" are taken as different additional arguments]
however it doesn't prevent because single quote is not re-interpreted but is like any other character in variable content
following with the example
git commit -S -m ${commitMsg}
git commit -S -m \'this is a message\'
[error "is" "a" "message'" are taken as different additional arguments]
with eval the single quotes are re-interpreted but also any other character which has a particular meaning in bash (;, &, ${..}, ..)
Suppose for example the following commit message which can inject arbitrary command.
commitMsg="message'; ls -l; echo 'done"
git commit -S -m 'message'; ls -l; echo 'done'

Why does my command fail even if I use `|| echo ""`

I run my bash script with -e
set -e
git push --delete sometag || echo "git delete tag failed"
echo "some other commands"
However git push returns a fatal error, and the rest of the script is not executed (it does execute echo "git delete tag failed")
I thought that was the purpose of using ||. Is a "fatal" error any different?
Note: I have seen other questions but they say that || is the solution

Create Mac terminal script with variable

I'd like to create a script that executes several lines of code, but also ask the user a question to use as a variable.
For example, this is what I execute in terminal:
git add -A && git commit -m "Release 0.0.1."
git tag '0.0.1'
git push --tags
pod trunk push NAME.podspec
I'd like to make 0.0.1 and NAME as variables that I ask the user with questions to start the script off:
What is the name of this pod?
What version?
Then I'd like to merge these variables into the script above. I'm confused about what "dialect" to use (sh, bash, csh, JavaScript?, etc) and that extension I should save it as so I just have to double-click it.
How can I do this?
This should do:
#!/bin/bash
read -e -p "What is the name of this pod?" name
read -e -p "What version?" ver
git add -A && git commit -m "Release $ver."
git tag "$ver"
git push --tags
pod trunk push "$name".podspec
Give this script a suitable name (script or script.sh etc..), then assign proper permission:
chmod +x path/to/the/script
then run it from terminal:
path/to/the/script
You can make your script take the name and version as arguments too. A method to do that combining the above is:
#!/bin/bash
name="$1";ver="$2"
[[ $name == "" ]] && read -e -p "What is the name of this pod?" name
[[ $ver == "" ]] && read -e -p "What version?" ver
...
This has the advantage of taking arguments while working like the first one. You can now call the script with arguments:
path/to/the/script podname ver
and it won't ask for name and ver, instead it will take podname as name and ver as version from the arguments passed.
If the second argument is not passed, it will ask for ver.
If none of the arguments is passed, it will ask for both of them, just like the first code sample.

Use forward slash in variable

I wrote a script to ease the syncing and building of Android source. I tried adding a function to cherrypick patches, but I can't get it to work properly. I know it's because of the forward slashes, but I don't know how to protect/escape them.
Part of the code is:
echo "Copy/paste the project folder, i.e. 'frameworks/base'"
read folder
echo ""
echo "Now paste the cherry-pick git link, i.e. 'git fetch <someproject> refs/changes/... && git cherry-pick FETCH_HEAD'"
read cherry
echo ""
Begin
clear
echo ""
export IFS="&&"
for x in $cherry
do
cd ${CM}/${folder}
CHERRY=$(trim "$x")
$CHERRY
done
Let's say that the 'cherry' variable is:
git fetch http://r.cyanogenmod.com/CyanogenMod/android_frameworks_base refs/changes/68/22968/2 && git cherry-pick FETCH_HEAD
I would get this error:
/home/tristan202/bin/build_cm.sh: line 159: git fetch http://r.cyanogenmod.com/CyanogenMod/android_frameworks_base refs/changes/91/23491/2: No such file or directory
/home/tristan202/bin/build_cm.sh: line 159: git cherry-pick FETCH_HEAD: command not found
I cannot figure out why it fails.
The 'trim' function it calls is a function that trims leading and trailing spaces. If I do echo "$CHERRY" within the for loop, the commands are printed correctly, but it still fails.
I will give your another example:
cmd='echo hello && echo world'
$cmd
The result is:
hello && echo world
bash parses the command $cmd as Simple Commands not Lists of Commands.
After Parameter Expansion, && is passed as argument to echo(1st word after Word Splitting).
The solution is pulling && out:
cmd1='echo hello'
cmd2='echo world'
$cmd1 && $cmd2
Once you put && in a variable it ceases to be interpreted as separating two commands:
$ A="echo a && echo b"
$ echo $A
echo a && echo b
$ echo c && ${A}
c
a && echo b
So you need to avoid putting && into a variable.
git was even telling you that the && was the problem in its error message.

Resources