Whenever I do git status
, I want to reference the files that were shown easily... whether it be so I can open it in vim, git add it, delete it, git checkout (to revert), etc. So after a bit of looking around, I couldn't find any existing script for this so I made my own. It works, but the code is not pretty, and being a beginner bash scripter (if that's even a noun), I feel like I can do so much better with this.
Here is the script:
#usage gits: sets env variables $m<n> $d<n> $u<n> $a (modified, deleted, untracked, added)
function gits {
git status
gitsall=$(git status -s)
m=$(echo "$gitsall" | grep "^ M")
d=$(echo "$gitsall" | grep "^ D")
u=$(echo "$gitsall" | grep "^??")
a=$(echo "$gitsall" | grep "^A ")
s=$(echo "$gitsall" | grep "^M ")
#i=0
#while [[ -n "$(echo {m,d,u}$(($i)))" ]]; do
# i=$(($i+1))
# unset {m,d,u}$(($i))
# done
count=1
while read -r tmpfilename; do
tmpfilename=${tmpfilename:2}
set m$(($count))="$(pwd)/$tmpfilename"
count=$(($count+1))
done <<< "$m"
count=1
while read -r tmpfilename; do
tmpfilename=${tmpfilename:2}
set d$(($count))="$(pwd)/$tmpfilename"
count=$(($count+1))
done <<< "$d"
count=1
while read -r tmpfilename; do
tmpfilename=${tmpfilename:3}
set u$(($count))="$(pwd)/$tmpfilename"
count=$(($count+1))
done <<< "$u"
count=1
while read -r tmpfilename; do
tmpfilename=${tmpfilename:3}
set a$(($count))="$(pwd)/$tmpfilename"
count=$(($count+1))
done <<< "$a"
count=1
while read -r tmpfilename; do
tmpfilename=${tmpfilename:3}
set s$(($count))="$(pwd)/$tmpfilename"
count=$(($count+1))
done <<< "$s"
unset m d u a s tmpfilename gitsall
}
Basically, I call git status -s
, save the output as a variable, then grep
it to get lists of filenames of modified, deleted, untracked, and added.
I then parse each line individually and set $
to the respective file in absolute paths.
Now, much of the parsing is the same. Things that vary are env var prefix, substring location (2 or 3 so far), and file list var name. I feel like this can somehow be cleaned up nicely.
Note: I do know this is not going to cover everything. Namely, I only so far implemented it for " M" " D" "??" "A ", but I know others exist like "M ", "MM", etc. which I eventually plan to address. This script is still a WIP for me but I wanted to clean it up before I add all the change types.
I did look around google thinking someone has made something like this because I thought it'll be something people would find convenient. For those who have different workflows that work around my situation, it'll be nice if I can hear them too.
Example usage:
gits
Output of git status and set the appropriate vars
vim $m1
Opens first modified, untracked file in the list from git status
2 Answers 2
Nice try
That's a fantastic idea.
It's very annoying to run git status
and then very often type some next command and copy paste selected files from the output.
I long wanted to have something like this but never made time to actually do it.
You gave me the final push, so thanks.
Setting variables dynamically
I don't think your script actually works :-)
At least on my computer, in Bash 3 or 4,
it's not possible to create variables dynamically with set
,
for example:
m1=
c=1
set m$c=x
echo $m1
This outputs nothing, because set m$c=x
does not actually perform m1=x
.
After some research, I found that this works with declare
:
m1=
c=1
declare m$c=x
echo $m1
This outputs x
alright. Unfortunately, according to help declare
:
[...] When used in a function, makes NAMEs local, as with the `local' command.
That is, the variables created dynamically this way inside the function will not be visible outside, so this is not usable for this purpose.
One obvious option is eval
, but not a good one, because eval
is evil.
A well-crafted filename in the working tree could wreak havoc.
Even if this is unlikely, I would rather not resort to such option.
Finally, there's a trick with printf -v
that will work well:
m1=
c=1
printf -v m$c x
echo $m1
This outputs x
, and even when used in a function, m1
will be visible outside.
The only downside is that printf
is not POSIX compliant,
but that's probably not a big problem in practice.
Unnecessary repeated processing of git status -s
This is really quite wasteful:
m=$(echo "$gitsall" | grep "^ M") d=$(echo "$gitsall" | grep "^ D") u=$(echo "$gitsall" | grep "^??") a=$(echo "$gitsall" | grep "^A ") s=$(echo "$gitsall" | grep "^M ")
It would be better to run git status -s
just once,
loop over the output,
use a case
statement to decide the type of the change,
and set the m
, d
, u
, a
, s
variables accordingly.
When you go this way, you can also directly create the variables with a count, so that you don't need all those repetitive while
loops for each status type.
Arithmetic context
When within $((...))
, you don't need to write $
to access the values of variables, for example instead of:
count=$(($count+1))
You can write:
count=$((count+1))
An even shorter equivalent:
((count++))
Current working directory
Instead of $(pwd)
it's better to use the $PWD
variable.
Local variables
Instead of using the unset
command at the end of the function to clear the variables used, it's much better to use local variables, for example:
local m d u a s
Not only this will make sure you don't clear the variables,
it also makes your use correct.
The current use is not correct,
because if some of these variables had value before calling the function,
the values get reset.
When using local
, the original values of those variables remain intact in the calling environment.
Alternative implementation
Putting the above together, the function could be written simpler as:
gits() {
git status
mc=1 dc=1 uc=1 ac=1 sc=1
local line status path name
while read -r line; do
[ "$line" ] || continue
status=${line:0:2}
path=${line:3}
case "$status" in
" M") name=m$((mc++)) ;;
" D") name=d$((dc++)) ;;
"??") name=u$((uc++)) ;;
"A ") name=a$((ac++)) ;;
"M ") name=s$((sc++)) ;;
*) echo unsupported status on line: $line
esac
printf -v $name "$path"
done <<< "$(git status -s)"
}
-
1\$\begingroup\$ wow great! It looks so much cleaner now. Also, regarding the set part, it worked on the computer I was writing this on but didn't work for another. Changing it to export also worked, although I didn't want that because it should really be local. One thing though: this does not handle the case when git status -s is empty (ie: no change), which can be common. I know it doesn't really matter because you can ignore the message, but for completion, I added: if [ "${line}" = "" ] then continue fi at the beginning of the loop \$\endgroup\$C. Sano– C. Sano2017年08月21日 15:09:29 +00:00Commented Aug 21, 2017 at 15:09
-
1\$\begingroup\$ @C.Sano you're right, the empty output needed to be handled. Thanks, and I improved your edit a bit. I'm not sure what you mean by "export" there... \$\endgroup\$janos– janos2017年08月21日 15:32:11 +00:00Commented Aug 21, 2017 at 15:32
-
1\$\begingroup\$ you can replace set with export and it should work in general (referring to when you mentioned set didn't work on yours). \$\endgroup\$C. Sano– C. Sano2017年08月21日 15:59:27 +00:00Commented Aug 21, 2017 at 15:59
-
1\$\begingroup\$ @C.Sano cool, didn't know that about
export
\$\endgroup\$janos– janos2017年08月21日 17:27:02 +00:00Commented Aug 21, 2017 at 17:27
I'll limit myself to points not addressed by janos' critique.
Firstly, when parsing lines using while-read
loops, you should ask yourself if you want bash to strip whitespace in the line. In this case, given that we may have lines like M file
, the answer is no, and IFS
should be set to an empty string. I have edited janos' answer to reflect this critique, as it was a showstopping bug as written.
The other concern I have is the use of dynamically-named variables. While your hack can be made to work, it is pretty magic and will make it harder for the next person reading to understand.
Moreover, I'm not sure what's to be gained by trying to adhere to POSIX -- you're already firmly in Bash territory due to your use of substring indexing, and would be going further there by using printf -v
and local
as janos suggests (thanks Shellcheck for flagging these!).
Accepting that, we gain access to Bash's data structures, specifically arrays:
gits() {
git status
m=() d=() u=() a=() s=()
local line status path name
while IFS= read -r line; do
[ "$line" ] || continue
status=${line:0:2}
path=${line:3}
case "$status" in
" M") m+=("$path") ;;
" D") d+=("$path") ;;
"??") u+=("$path") ;;
"A ") a+=("$path") ;;
"M ") s+=("$path") ;;
*) echo unsupported status on line: "$line"
esac
done < <(git status -s)
}
Now, we can access the first unstaged modified file as "${m[0]}"
, no dynamic variables needed (though, given the likely unpredictability of these indeces, I'm guessing you'll probably be using "${m[@]}"
instead -- it expands to every entry of m
, so can be used eg to populate the list of files to open in vim)
However, I appreciate the reminder of the power of dynamic variable names. Kudos on figuring that one out!