tl;dr:
Broken string interpolation:
dir -recurse -directory -force -filter .git | % { git -C "$($_.FullName)\.." gc }
That is, simply enclosing $_.FullName
in $(...)
inside the double-quoted string is sufficient to fix the problem (plus adding -Force
to dir
(Get-ChildItem
) to make sure it finds the hidden .git
subdirectories;
adding -directory
to limit matching to directories and using -filter
instead of -include
is not strictly needed, but makes for much better performance).
Understanding the need for $(...)
in double-quoted strings is important in general - see below.
For the task at hand, the above is a simple, robust, and efficient solution: targeting the desired directory is left to git
via its -C
option, without having to change the calling session's current directory.
Changing to a different directory before invoking a command:
4c74356b41's answer bypasses the problem by focusing on the 2nd part of the OP's question - how to change to a different location before invoking a command - while avoiding the broken string interpolation ("$_.FullName\.."
). Their (first) solution is effective, however, because git
recognizes a repository by its .git
subfolder too, so the \..
part to reference the parent dir. is not needed.
Still, their first command has the side effect of changing the session's current directory, and the Start-Process
cmdlet is best avoided for console applications such as git
.
See the bottom of this post for an explanation and a more robust idiom.
Broken String Interpolation
Expanding $_.FullName
inside "..."
(double-quoted strings) will not work as-is: since you're accessing a property of the object referenced by the $_
variable, you must enclose your expression in the subexpression operator, $(...)
, as follows:
"$($_.FullName)\.."
As an aside: you actually don't need string interpolation at all in your case, because git
also recognizes a repo by its .git
subfolder, so the \..
component to refer to the parent dir. is not needed; a direct reference to $_.FullName
is enough:
... | % { git -C $_.FullName gc }
How is "$_.FullName\.."
expanded (interpolated), and why is it broken?
Pipeline variable $_
is expanded by itself, by calling the value's .ToString()
method.
$_
is an instance of type [System.IO.DirectoryInfo]
in this case, and calling .ToString()
on it yields the mere name of the given directory, not its path (e.g., if $_
represents C:\Users\jdoe
, "$_"
expands to just joe
).
.FullName\..
is interpreted as a literal part of the string.
Let's say $_
represents directory C:\Users\jdoe
; "$_.FullName\.."
is therefore expanded to the following, which is clearly not the intent:
jdoe.FullName\..
In other words: Inside a double-quoted string, $_.FullName
is not recognized as a single expression, unless it is enclosed in $(...)
, the subexpression operator.
The corrected form of the string, "$($_.FullName)/.."
, then yields the expected result:
C:\Users\jdoe\..
Here are test commands you can run as-is that illustrate the difference:
dir -recurse $HOME | % { "$_.FullName\.." } | Select -First 2 # WRONG
dir -recurse $HOME | % { "$($_.FullName)\.." } | Select -First 2 # OK
For a full discussion of the rules governing PowerShell's string interpolation (expansion) see this answer of mine.
Additionally, a quirk in how PowerShell (Windows in general) processes filesystem paths may obscure the problem with the string interpolation in your case:
Appending \..
to a non-existing path component still results in a valid path - the two components effectively cancel each other out.
If we use the string-interpolation-gone-wrong example from above:
Set-Location jdoe.FullName\..
is an effective no-op, because PowerShell lexically concludes that you mean the current directory (given that jdoe.FullName\..
is a relative path), even though no subdirectory named jdoe.FullName
exists.
Thus, in the context of using something like this:
... | % { Set-Location "$_.FullName\.."; git gc }
the Set-Location
command has no effect, and all git
invocations run in the current directory.
Changing to a Different Directory in Every Iteration of a Pipeline
For synchronous invocation of console (command-line) utilities such as git
, the following technique is the best choice:
dir -recurse -directory -force -filter .git | % { pushd $_.FullName; git gc; popd }
Note how the git
call is sandwiched between pushd
(Push-Location
) and popd
(Pop-Location
) calls, which ensures that the calling session's current location is ultimately not changed.
Note: If a terminating error were to occur after the pushd
call, the current location would still change, but note that invoking external utilities never results in terminating errors (only PowerShell-native calls and .NET framework methods can generate terminating errors, which can be handled with try ... catch
or trap
statements).
Setting the Working Directory for a GUI App or New Console Window
Start-Process
with its -WorkingDirectory
parameter is the right tool in the following scenarios:
Note:
Start-Process
is asynchronous by default, which means that it launches the specified command, but doesn't wait for it to complete before returning to the PowerShell prompt (or moving on to the next command in a script).
Add -Wait
if you actually want to wait for the process to terminate (which typically happens when the window is closed).
It generally makes little sense to run console applications in the current console window with Start-Process -NoNewWindow -Wait
(without -Wait
, output would arrive asynchronously and thus unpredictably in the console):
The console application invoked won't be connected to the current session's input and output streams, so it won't receive input from the PowerShell pipeline, and its output won't be sent to PowerShell's success and error streams (which means that you cannot send the output through the pipeline or capture it with >
or >>
; you can, however, send a file as input via -RedirectStandardInput
, and, similarly, capture stdout and stderr output in files with -RedirectStandardOutput
and -RedirectStandardError
)
Additionally, direct invocation is not only syntactically simpler (compare git gc
to Start-Process gc -ArgumentList gc -NoNewWindow -Wait
), but noticeably faster.
The only scenarios in which it makes sense to use Start-Process
with a console application are: (a) starting it in a new console window (omit -NoNewWindow
), (b) running it as a different user (use the -Verb
and -Credential
parameters), or (c) running it with a pristine environment (use -UseNewEnvironment
).
Setting the Working Directory for a Background Command
Finally, Start-Job
is the right tool for: