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'
Related
Hey I'm writing a wrapper script for git that applies one git command to all its submodules e.g.: supergit commit -m "change message" commits to all submodules.
The script essentially does:
function git_foreach () {
git submodule foreach "git \"$#\" || : "
}
git_foreach "$#"
The problem is when the supergit call contains an argument with spaces (like in the commit message above) the space separated calls are interpreted as multiple arguments.
I read in this answer that the way to do it is to use "$#" but that doesn't work within a string.
Is there a way to expand $# to keep the quotes so that my function works as expected?
EDIT:
What I want is to pass the arguments to git submodule foreach, with supergit commit -m "commit message" I want to run:
git submodule foreach "git commit -m \"commit message\""
If Your /bin/sh Is Provided By Bash
printf %q will generate a version of your data correctly escaped to be parsed by a shell.
printf '%q ' "$#" generates a string containing an individually shell-escaped word for each argument in your array, with spaces after each word.
Thus:
git_foreach() {
local cmd_q
printf -v cmd_q '%q ' "$#"
git submodule foreach "git $cmd_q ||:"
}
...or, with bash 5.0 or newer (which adds a ${var#Q} expansion), we can make this one line:
git_foreach() { git submodule foreach "git ${##Q} ||:"; }
With current implementations of printf %q, git_foreach commit -m "commit message" invokes git submodule foreach 'git commit -m commit\ message' or a semantic equivalent; the escaping isn't identical to how you would write it by hand, but the effect of the command is exactly the same.
If You Need Broader Compatibility
Unfortunately, both printf %q and the newer ${variable#Q} expansion are able to generate strings that use bash-only syntax (particularly if your string contains newlines, tabs, or similar). If you don't control which shell git starts to run the foreach commands, then we need to generate a string that's escaped for consumption by any POSIX-compliant shell.
Bash doesn't have a feature for doing that... but Python does!
posix_escape() {
python3 -c 'import sys, shlex; print(" ".join([shlex.quote(s) for s in sys.argv[1:]]))' "$#"
}
git_foreach() {
local cmd_q
cmd_q=$(posix_escape "$#")
git submodule foreach "git $cmd_q ||:"
}
I'm trying to create a simple bash alias to commit with my branch name in MacOs. For instance, if my branch if CS-12 I'd usually commit as follows:
git commit /file/location/myfile -m "CS-12 my message goes in here"
So I'm trying to create an alias which will receive only the file name and the message, ie:
gcm /file/location/myfile "my message goes in here"
I've got the following but it's not working:
alias gcm="echo git commit $1 -m \"$(current_branch) - $2\""
where current_branch is the function:
function current_branch() {
ref=$(git symbolic-ref HEAD 2> /dev/null) || \
ref=$(git rev-parse --short HEAD 2> /dev/null) || return
echo ${ref#refs/heads/}
}
which does work.
The output of running my alias:
gcm src/pages/register/Register.js "aasdasd asdasd"
is giving me back:
git commit -m master - src/pages/register/Register.js aasdasd asdasd
any idea what I'm doing wrong? Bash is not my area of expertise.
Thanks
The escaped quotes are 'stripped' by alias, so you need to escape them once more:
alias x="echo \\\"foo\\\""
x
"foo"
aliases do not take parameters. Just write a function:
gcm() { git commit "$1" -m "$(current_branch) - $2"; }
Note that there's really no need for aliases, and you shouldn't use them. Since at least 1996, the bash man page has stated: "For almost every purpose, aliases are superseded by shell functions."
Error message: "fatal: your current branch 'master' does not have any commits yet"
After Making a file with this code executable
#!/usr/bin/env bash
cd "$(dirname "$(readlink -f "$BASH_SOURCE")")/.."
{
cat <<- 'EOH'
EOH
echo
git log --format='%aN <%aE>' | LC_ALL=C.UTF-8 sort -uf
} > AUTHORS
The problem is that you didn't add anything, and possibly didn't even have a change to commit, so no commit was done. If you really want that first commit without any changes, you can do this:
git commit --allow-empty -m "first commit"
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 have following bash function in my ~/.bashrc
function gitlab {
MSG='first commit'
CMD="git commit -m '${MSG}'"
echo $CMD
$CMD
}
Here is the result
$ gitlab
git commit -m 'first commit'
error: pathspec 'commit'' did not match any file(s) known to git.
What's the fix?
BASH FAQ entry #50: "I'm trying to put a command in a variable, but the complex cases always fail!"
Definitely read BashFAQ/050 that Ignacio linked to.
You could try this, though:
function gitlab {
local PS4='Running: '
local msg='first commit'
bash -xc "git commit -m '$msg'"
}
I suppose you should use \" instead of ' so it should be something like:
CMD="git commit -m \"${MSG}\""
Try putting your commit message in double quotes, as single and double quotes mean different things to bash.
function gitlab {
MSG="first commit"
CMD=`git commit -m \"${MSG}\"`
echo $CMD
$CMD
}