Please note the improved techniques added at the end.

When I switched my Emacs configuration to straight.el, I needed to find all the immediate children of a particular directory that had uncommitted Git changes. I spent 10 minutes thinking about the best way to do this, which seemed smarter than spending a couple of minutes doing it manually. I considered trying to do it in Emacs with Dired, but I’ve been attempting to translate my CLI knowledge to PowerShell in recent months and thought I might be able to do it with the ever-useful fd. I even knew how to tackle the quoting issues I’d had in the past. The final invocation was:

PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '||' echo 'CHANGED: {}' ';'

That’s right: since cd is a builtin, not an executable, I had to use cmd to run it through fd. The output looked like this:

fountain-mode.el
"CHANGED: fountain-mode.git"
livescript-mode.el
"CHANGED: livescript-mode"
separedit.el
"CHANGED: separedit.el"
.gitignore
.travis.yml
Cask
README.rst
features/highlight.feature
features/imenu.feature
features/indent.feature
features/step-definitions/jsonconfig-mode-steps.el
features/support/env.el
jsonconfig-config.el
jsonconfig-spec.el
"CHANGED: jsonconfig-mode"
ace-jump-mode.el
"CHANGED: ace-jump-mode"
c++/LICENSE.txt
c++/ekam-provider/c++header
c++/ekam-provider/canonical
"CHANGED: capnproto"

Inelegant, but it worked. I didn’t know how to make git diff-index suppress its output entirely (that would be --quiet, as it turns out) and didn’t mind looking for the CHANGED lines. In retrospect, I could have filtered the result with the (also amazing) ripgrep command, like so:

PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '||' echo '!!!CHANGED!!!: {}' ';' | rg -F '!!!CHANGED!!!'

But it would have been even better to redirect the Git output, which I can confirm works:

PowerShellfd -t d --maxdepth 1 -x cmd /c cd '{}' '&&' git.exe diff-index --exit-code --name-only HEAD . '>' 'NUL' '||' echo 'CHANGED:' '{}' ';'

I’ll remember that for next time. I would like to do it without cmd, but git -C still has quoting issues:

> fd -t d --maxdepth 1 -x git -C "$(pwd)/{}" diff-index --exit-code --name-only HEAD . '>' NUL '||' echo 'CHANGED:' '{}' ';'

fatal: >: no such path in the working tree.
Use 'git <command> -- <path>...' to specify paths that do not exist locally.
fatal: >: no such path in the working tree.
Use 'git <command> -- <path>...' to specify paths that do not exist locally.
fatal: >: no such path in the working tree.
Use 'git <command> -- <path>...' to specify paths that do not exist locally.
fatal: >: no such path in the working tree.
Use 'git <command> -- <path>...' to specify paths that do not exist locally.

Funnily enough, all the changed directories revealed themselves to be false positives—mostly whitespace issues, for some reason, which I ought to have told Git to ignore—so I was able to safely proceed.

Addendum: The right way

I was wrong. The quoting is correct. The real problem is that whereas cmd is a shell and therefore treats > and || are specially, fd simply includes those characters as arguments, so Git is passed the literals > and ||. I don’t think it’s possible to sequence commands like I’m trying to do without some sort of shell. Using PowerShell works:

PowerShellfd -t d --maxdepth 1 -x pwsh -c '& {Set-Location {} && git diff-index --exit-code --name-only --quiet HEAD .; if ($LASTEXITCODE -ne 0) { echo {} }}' ';'

(&& and || don’t treat an exit code of 128 as false, for some reason, so I explicitly test $LASTEXITCODE.)

However, the best solution I’ve found dispenses with cmd and fd entirely:

PowerShellGet-ChildItem -Directory -Name | ForEach-Object { git -C $_ diff-index -b --exit-code --name-only --quiet HEAD .; if ($LASTEXITCODE -ne 0) { echo $_ } }

You can even omit -Name to get proper file objects if you need to use more than just the names in your pipeline.