4

I don’t know any PowerShell, but I do know how to search, copy and paste. I think the following command will run "git gc" for all sub-directories that are git repos.

dir -recurse -include .git | %{ git -C "$_.FullName\.." gc }

Any issues?

I am using the "git -C" functionality to set the directory to run in. How can I instead execute a command in a specific directory?

Petter
  • 37,121
  • 7
  • 47
  • 62

3 Answers3

3
% { Set-Location $_.FullName; git gc }

or

% { Start-Process git -WorkingDirectory $_.FullName -ArgumentList gc -NoNewWindow -Wait}

the first variant will change your current working directory, so be aware of that.

4c74356b41
  • 69,186
  • 6
  • 100
  • 141
  • Thanks! For the record, I am going to use Set-Location "$_.FullName\.." since I want the directory above .git. – Petter Nov 27 '16 at 19:28
  • @mklement0 Actually the string I posted in the original question seems to work as-is. I have tested it with git status. – Petter Nov 29 '16 at 16:14
  • @Petter: Based on how PS string interpolation works, I don't think that's possible: `Set-Location "$_.FullName\.."` has no effect whatsoever (see my updated answer for an explanation), so you may have tested from a directory that itself happened to be a repo, which would have made `git status` _appear_ to work. (Sorry for the repost, but I wanted to fix the broken formatting). – mklement0 Dec 01 '16 at 16:44
  • @4c74356b41: Definitely better, thanks for updating; let me summarize (and I suggest we leave it at that): Your first command has the side effect of also changing the current session's working dir. While your 2nd command, based on `Start-Process -WorkingDirectory`, commendably avoids that - just like the OP's own `git -C` approach - `Start-Process` is generally the wrong tool to use for synchronous invocation of console programs such as `git` (my answer explains why). – mklement0 Dec 03 '16 at 18:26
  • why is it wrong with `-NoNewWindow -Wait`? @mklement0 – 4c74356b41 Dec 04 '16 at 10:37
  • @4c74356b41: While it works in this case, it is (a) much slower than direct invocation (`git gc`), and (b) the command invoked cannot accept pipeline input, and cannot send its output to PS's success stream. In other words: unless you're willing to take a big performance hit and your command neither accepts pipeline input nor produces it, `Start-Process -NoNewWindow -Wait` is the wrong tool for synchronous invocation of console applications. – mklement0 Dec 05 '16 at 21:22
1

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:

  • explicitly run a command in a new console window

    • Example: Start-Process cmd -WorkingDir $HOME asynchronously opens a new cmd.exe console ("Command Prompt") in the current user's home directory.
  • launch a GUI app with a specific working directory

    • Example: Start-Process notepad.exe -WorkingDir $HOME asynchronously launches Notepad with the current user's home directory set as the working directory

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:

  • asynchronously launching a no-UI (non-interactive) background process

    • Example: Start-Job { Set-Location $HOME; $PWD }; note how the working directory must be set explicitly within the script block { ... } defining the background command to run;
      Receive-Job must be called subsequently in order to obtain the background command's output.
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • I don’t know. The string I posted in the question seems to work. – Petter Nov 29 '16 at 16:15
  • @Petter: Based on how PS string interpolation works, I don't think that's possible - please see my update, which explains in detail why `"$_.FullName\.."` doesn't work the way you expect, and how a quirk in how PS resolves `\..` in paths may have misled you to believe that things are working (in short: your `Set-Location` calls were effectively ignored, and all `git` calls were made in the _current_ dir.) – mklement0 Nov 29 '16 at 18:09
1
dir -recurse -directory -force -filter .git | % { 
pushd "$($_.FullName)\.."
write-output '___'
$_.FullName
git count-objects -vH
git submodule foreach --recursive git count-objects -vH
git  gc 
git submodule foreach --recursive git gc 
git count-objects -vH
git submodule foreach --recursive git count-objects -vH
write-output  '___'
popd
}


A bit more detailed and handles submodules too.

vbjay
  • 107
  • 10
  • Doesn't run under cmd shell on first line - '{' is not recognized as an internal or external command, operable program or batch file. Also doesn't run under powershell - dir -recurse -directory -force -filter .git | { '{' is not recognized as an internal or external command, operable program or batch file. – lonstar Jun 30 '23 at 02:27
  • But the bare command - dir -recurse -directory -force -filter .git - does run and returns the set of .git folders. – lonstar Jun 30 '23 at 02:28
  • This almost works, with a little more tweaking it might be good recursion script - - as a one-liner (dir -recurse -directory -force -filter .git | % { pushd "$($_.FullName)\.." $_.FullName\.. git count-objects -vH }) it runs but fails on the subdirs with "Push-Location: A positional parameter cannot be found that accepts argument " and path to the subfolder's .git directory. – lonstar Jun 30 '23 at 02:36
  • powrershell not shell script – vbjay Aug 09 '23 at 18:33