Why is the upstream command called SO many times? - powershell

Currently, I'm writing a PowerShell module which automatically configures aliases for all git commands, inspired by git-sh.
Then I wrote functions below.
The Enable-GitAliases function is the entry point to configure aliases automatically.
it collects git's subcommands by Get-GitCommands, which parses git --help -a to get all git's subcommands.
Then it defines the wrapper functions for the collected git commands.
My question is: why is git --help -a called so many times (possibly infinitely) when invoking Enable-GitAliases, which causing significant slow down?
After writing the code, I found Enable-GitAliases takes too much time (I've never seen it finishes).
According to the Task Manager, the git --help -a command is launched and exits repeatedly.
I expected the git --help -a command is called only once.
Actually, Get-GitCommands | % { echo $_ } calls git --help -a only once.
What is the difference, and what is best way to fix?
function Get-GitCommands {
-Split (git --help -a | select-string -pattern '^ [-a-zA-Z0-9.]+\s*')
}
function Enable-GitAliases($avoidConflicts = $true) {
Get-GitCommands | % {
$aliasName = $_
if (-not ($avoidConflicts -and (Get-Command $aliasName 2> $null) -ne $null)) {
Enable-GitAliases $aliasName
}
}
}
function Enable-GitAlias($commandName) {
$wrapper = #'
function global:{0} {{
git {0} $args
}}
'# -f $commandName
Invoke-Expression $wrapper
}

You call Enable-GitAliases recursively, but is this intended?
Is your intention this?
function Enable-GitAliases($avoidConflicts = $true) {
Get-GitCommands | % {
$aliasName = $_
if (-not ($avoidConflicts -and (Get-Command $aliasName 2> $null) -ne $null)) {
# Enable-GitAliases -> Enable-GitAlias
Enable-GitAlias $aliasName
}
}
}

Related

How to simultaneously capture external command output and print it to the terminal

Can I pipe back from:
$OUTPUT = $(flutter build ios --release --no-codesign | tail -1)
I would like to get both the last line from the build AND show progress, something like
$OUTPUT = $(flutter build ios --release --no-codesign | out | tail -1)
where the hypothetical out utility would also send the output to the terminal.
Do you know how?
Note:
On Unix-like platforms, with external-program output, js2010's elegant tee /dev/tty solution is the simplest.
The solutions below, which also work on Windows, may be of interest for processing external-program output line by line in PowerShell.
A general solution that also works with the complex objects that PowerShell-native commands can output, requires different approaches:
In PowerShell (Core) 7+, use the following:
# PS v7+ only. Works on both Windows and Unix
... | Tee-Object ($IsWindows ? 'CON' : '/dev/tty')
In Windows PowerShell, where Tee-Object unfortunately doesn't support targeting CON, a proxy function that utilizes Out-Host is required - see this answer.
A PowerShell solution (given that the code in your question is PowerShell[1]):
I'm not sure how flutter reports its progress, but the following may work:
If everything goes to stdout:
$OUTPUT = flutter build ios --release --no-codesign | % {
Write-Host $_ # print to host (console)
$_ # send through pipeline
} | select -Last 1
Note: % is the built-in alias for ForEach-Object, and select the one for Select-Object.
If progress messages go to stderr:
$OUTPUT = flutter build ios --release --no-codesign 2>&1 | % {
Write-Host $_.ToString() # print to host (console)
if ($_ -is [string]) { $_ } # send only stdout through pipeline
} | select -Last 1
[1] As evidenced by the $ sigil in the variable name in the LHS of an assignment and the spaces around =
($OUTPUT = ), neither of which would work as intended in bash / POSIX-like shells.
I assume you mean bash because to my knowledge there is no tail in powershell.
Here's how you can see a command's output while still capturing it into a variable.
#!/bin/bash
# redirect the file descriptor 3 to 1 (stdout)
exec 3>&1
longRunningCmd="flutter build ios --release --no-codesign"
# use tee to copy the command's output to file descriptor 3 (stdout) while
# capturing 1 (stdout) into a variable
output=$(eval "$longRunningCmd" | tee >(cat - >&3) )
# last line of output
lastline=$(printf "%s" "$output" | tail -n 1)
echo "$lastline"
I use write-progress in the pipeline.
In order to keep readable pipeline, I wrote a function
function Write-PipedProgress{
<#
.SYNOPSIS
Insert this function in a pipeline to display progress bar to user
.EXAMPLE
$Result = (Get-250Items |
Write-PipedProgress -PropertyName Name -Activity "Audit services" -ExpectedCount 250 |
Process-ItemFurther)
>
[cmdletBinding()]
param(
[parameter(Mandatory=$true,ValueFromPipeline=$true)]
$Data,
[string]$PropertyName=$null,
[string]$Activity,
[int]$ExpectedCount=100
)
begin {
Write-Verbose "Starting $($MyInvocation.MyCommand)"
$ItemCounter = 0
}
process {
Write-Verbose "Start processing of $($MyInvocation.MyCommand)($Data)"
try {
$ItemCounter++
# (3) mitigate unexpected additional input volume"
if ($ItemCounter -lt $ExpectedCount) {
$StatusProperty = if ($propertyName) { $Data.$PropertyName } > > else { ""}
$StatusMessage = "Processing $ItemCounter th $StatusProperty"
$statusPercent = 100 * $ItemCounter / $ExpectedCount
Write-Progress -Activity $Activity -Status $StatusMessage -> > PercentComplete $statusPercent
} else {
Write-Progress -Activity $Activity -Status "taking longer than expected" -PercentComplete 99
}
# return input data to next element in pipe
$Data
} catch {
throw
}
finally {
Write-Verbose "Complete processing of $Data in > $($MyInvocation.MyCommand)"
}
}
end {
Write-Progress -Activity $Activity -Completed
Write-Verbose "Complete $($MyInvocation.MyCommand) - processed $ItemCounter items"
}
}
Hope this helps ;-)
I believe this would work, at least in osx or linux powershell (or even Windows Subsystem for Linux) that have these commands available. I tested it with "ls" instead of "flutter". Is there actually an "out" command?
$OUTPUT = bash -c 'flutter build ios --release --no-codesign | tee /dev/tty | tail -1'
Or, assuming tee isn't aliased to tee-object. Actually, tee-object would work too.
$OUTPUT = flutter build ios --release --no-codesign | tee /dev/tty | tail -1
It would work with the $( ) too, but you don't need it. In powershell, it's used to combine multiple pipelines.

Powershell assert pipeline returns not an array

Background:
I'm trying to filter a specific GIT branch from my local branches. Therefore I'm using git branch --all.
Powershell specific question:
I'm performing pipeline filtering via Where-Object and want to ensure that only one object and not an array is returned from the pipeline.
I've e.g. :
$branch = Invoke-Expression "git branch --all" | % { $_.Trim('*').Trim() | ? { $_ -match "MySpecificBranchRegex" }
If I mess up my specific filter regex $branch might be an array and not a string.
Is there an elegant way to ensure only one string is returned. Possible solutions I don't like:
Call Select-Object -First 1 at the end of the pipeline
Perform if '$arr.GetType().BaseType.Name -eq "Array"`
Thx.
Well Select-Object -First 1 is the elegant solution imho, but you could turn it around by forcing it to always return an array:
$branches = #(Invoke-Expression "git branch --all" | % { $_.Trim('*').Trim() | ? { $_ -match "MySpecificBranchRegex" })
if ($branches.Count -ne 1)
{
throw "Something went wrong..."
}
$result = git branch --all | where { $_ -match 'MySpecificBranchRegex' }
$result.count
I wouldn't be above using findstr (even with quotes) instead of where. /i is case insensitive.
$result = git branch --all | findstr /i MySpecificBranchRegex

Windows version of Linux's 'which'? [duplicate]

How do I ask PowerShell where something is?
For instance, "which notepad" and it returns the directory where the notepad.exe is run from according to the current paths.
The very first alias I made once I started customizing my profile in PowerShell was 'which'.
New-Alias which get-command
To add this to your profile, type this:
"`nNew-Alias which get-command" | add-content $profile
The `n at the start of the last line is to ensure it will start as a new line.
Here is an actual *nix equivalent, i.e. it gives *nix-style output.
Get-Command <your command> | Select-Object -ExpandProperty Definition
Just replace with whatever you're looking for.
PS C:\> Get-Command notepad.exe | Select-Object -ExpandProperty Definition
C:\Windows\system32\notepad.exe
When you add it to your profile, you will want to use a function rather than an alias because you can't use aliases with pipes:
function which($name)
{
Get-Command $name | Select-Object -ExpandProperty Definition
}
Now, when you reload your profile you can do this:
PS C:\> which notepad
C:\Windows\system32\notepad.exe
I usually just type:
gcm notepad
or
gcm note*
gcm is the default alias for Get-Command.
On my system, gcm note* outputs:
[27] » gcm note*
CommandType Name Definition
----------- ---- ----------
Application notepad.exe C:\WINDOWS\notepad.exe
Application notepad.exe C:\WINDOWS\system32\notepad.exe
Application Notepad2.exe C:\Utils\Notepad2.exe
Application Notepad2.ini C:\Utils\Notepad2.ini
You get the directory and the command that matches what you're looking for.
Try this example:
(Get-Command notepad.exe).Path
My proposition for the Which function:
function which($cmd) { get-command $cmd | % { $_.Path } }
PS C:\> which devcon
C:\local\code\bin\devcon.exe
A quick-and-dirty match to Unix which is
New-Alias which where.exe
But it returns multiple lines if they exist so then it becomes
function which {where.exe command | select -first 1}
I like Get-Command | Format-List, or shorter, using aliases for the two and only for powershell.exe:
gcm powershell | fl
You can find aliases like this:
alias -definition Format-List
Tab completion works with gcm.
To have tab list all options at once:
set-psreadlineoption -editmode emacs
This seems to do what you want (I found it on http://huddledmasses.org/powershell-find-path/):
Function Find-Path($Path, [switch]$All = $false, [Microsoft.PowerShell.Commands.TestPathType]$type = "Any")
## You could comment out the function stuff and use it as a script instead, with this line:
#param($Path, [switch]$All = $false, [Microsoft.PowerShell.Commands.TestPathType]$type = "Any")
if($(Test-Path $Path -Type $type)) {
return $path
} else {
[string[]]$paths = #($pwd);
$paths += "$pwd;$env:path".split(";")
$paths = Join-Path $paths $(Split-Path $Path -leaf) | ? { Test-Path $_ -Type $type }
if($paths.Length -gt 0) {
if($All) {
return $paths;
} else {
return $paths[0]
}
}
}
throw "Couldn't find a matching path of type $type"
}
Set-Alias find Find-Path
Check this PowerShell Which.
The code provided there suggests this:
($Env:Path).Split(";") | Get-ChildItem -filter notepad.exe
Try the where command on Windows 2003 or later (or Windows 2000/XP if you've installed a Resource Kit).
BTW, this received more answers in other questions:
Is there an equivalent of 'which' on Windows?
PowerShell equivalent to Unix which command?
If you want a comamnd that both accepts input from pipeline or as paramater, you should try this:
function which($name) {
if ($name) { $input = $name }
Get-Command $input | Select-Object -ExpandProperty Path
}
copy-paste the command to your profile (notepad $profile).
Examples:
❯ echo clang.exe | which
C:\Program Files\LLVM\bin\clang.exe
❯ which clang.exe
C:\Program Files\LLVM\bin\clang.exe
I have this which advanced function in my PowerShell profile:
function which {
<#
.SYNOPSIS
Identifies the source of a PowerShell command.
.DESCRIPTION
Identifies the source of a PowerShell command. External commands (Applications) are identified by the path to the executable
(which must be in the system PATH); cmdlets and functions are identified as such and the name of the module they are defined in
provided; aliases are expanded and the source of the alias definition is returned.
.INPUTS
No inputs; you cannot pipe data to this function.
.OUTPUTS
.PARAMETER Name
The name of the command to be identified.
.EXAMPLE
PS C:\Users\Smith\Documents> which Get-Command
Get-Command: Cmdlet in module Microsoft.PowerShell.Core
(Identifies type and source of command)
.EXAMPLE
PS C:\Users\Smith\Documents> which notepad
C:\WINDOWS\SYSTEM32\notepad.exe
(Indicates the full path of the executable)
#>
param(
[String]$name
)
$cmd = Get-Command $name
$redirect = $null
switch ($cmd.CommandType) {
"Alias" { "{0}: Alias for ({1})" -f $cmd.Name, (. { which $cmd.Definition } ) }
"Application" { $cmd.Source }
"Cmdlet" { "{0}: {1} {2}" -f $cmd.Name, $cmd.CommandType, (. { if ($cmd.Source.Length) { "in module {0}" -f $cmd.Source} else { "from unspecified source" } } ) }
"Function" { "{0}: {1} {2}" -f $cmd.Name, $cmd.CommandType, (. { if ($cmd.Source.Length) { "in module {0}" -f $cmd.Source} else { "from unspecified source" } } ) }
"Workflow" { "{0}: {1} {2}" -f $cmd.Name, $cmd.CommandType, (. { if ($cmd.Source.Length) { "in module {0}" -f $cmd.Source} else { "from unspecified source" } } ) }
"ExternalScript" { $cmd.Source }
default { $cmd }
}
}
Use:
function Which([string] $cmd) {
$path = (($Env:Path).Split(";") | Select -uniq | Where { $_.Length } | Where { Test-Path $_ } | Get-ChildItem -filter $cmd).FullName
if ($path) { $path.ToString() }
}
# Check if Chocolatey is installed
if (Which('cinst.bat')) {
Write-Host "yes"
} else {
Write-Host "no"
}
Or this version, calling the original where command.
This version also works better, because it is not limited to bat files:
function which([string] $cmd) {
$where = iex $(Join-Path $env:SystemRoot "System32\where.exe $cmd 2>&1")
$first = $($where -split '[\r\n]')
if ($first.getType().BaseType.Name -eq 'Array') {
$first = $first[0]
}
if (Test-Path $first) {
$first
}
}
# Check if Curl is installed
if (which('curl')) {
echo 'yes'
} else {
echo 'no'
}
You can install the which command from https://goprogram.co.uk/software/commands, along with all of the other UNIX commands.
If you have scoop you can install a direct clone of which:
scoop install which
which notepad
There also always the option of using which. there are actually three ways to access which from Windows powershell, the first (not necessarily the best) wsl -e which command (this requires installation of windows subsystem for Linux and a running distro). B. gnuwin32 which is a port of several gnu binaries in .exe format as standle alone bundled lanunchers option three, install msys2 (cross compiler platform) if you go where it installed in /usr/bin you'll find many many gnu utils that are more up-to-date. most of them work as stand alone exe and can be copied from the bin folder to your home drive somewhere amd added to your PATH.
There also always the option of using which. there are actually three ways to access which from Windows powershell
The first, (though not the best) is wsl(windows subsystem for linux)
wsl -e which command
This requires installation of windows subsystem for Linux and a running distro.
Next is gnuwin32 which is a port of several gnu binaries in .exe format as standle alone bundled lanunchers
Third, install msys2 (cross compiler platform) if you go where it installed in /usr/bin you'll find many many gnu utils that are more up-to-date. most of them work as stand alone exe and can be copied from the bin folder to your home drive somewhere amd added to your PATH.

Executing command doesn't result in script exception

We have a powershell script that deploys a database script. However, if the database script fails, the output doesn't throw exceptions to the powershell script.
Below is an example of the .ps1 file:
function Publish-DatabaseProject
{
sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
}
function Add-Timestamp {
process {
if ($_.GetType() -eq [string]) {
"[$(Get-Date -Format o)] $_"
} else {
$_
}
}
}
function Write-LogFile {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)] [string] $Path,
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] [object[]] $InputObject
)
begin {
$root = Split-Path -Path $Path -Parent
if ($root -and !(Test-Path -Path $root)) { New-Item -Path $root -Type Directory
| Out-Null }
}
process {
$InputObject |
Add-Timestamp |
Tee-Object -File $Path -Append
}
}
Publish-DatabaseProject -ErrorVariable DeployError 2>&1 |
Write-LogFile -Path "C:\output.log"
if ($DeployError -and $DeployError.Count -ne 0)
{
Write-Output "Failed"
} else
{
Write-Output "Succeeded"
}
The query in question is executing against a non-existent table. The text output shows:
[2015-12-11T14:42:45.1973944+00:00] Msg 4902, Level 16, State 1, Server ABDN-DEV-PC1, Line 1
[2015-12-11T14:42:45.2053944+00:00] Cannot find the object "xx" because it does not exist or you do not have permission
s.
Succeeded
I am expecting the last line to read: Failed.
If you run the sqlcmd line on its own, and follow it up with $LastExitCode, it does spit out a non-zero exit code.
> sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
Msg 4902, Level 16, State 1, Server ABDN-DEV-PC1, Line 1
Cannot find the object "xx" because it does not exist or you do not have permissions.
> $LastExitCode
1
For various reasons, we cannot use Invoke-SqlCmd, and need to stick with SQLCMD.exe.
How can we make exceptions from within SQLCMD bubble out correctly to the calling script?
Your -ErrorVariable DeployError statement would only get triggered if the Publish-DatabaseProject cmdlet itself fails to execute. As that function is mostly a wrapper around sqlcmd.exe, there isn't any intelligence to bubble up this error. We can wrap it though by using the $LastExitCode automatic variable.
function Publish-DatabaseProject
{
sqlcmd -S . -b -v DatabaseName=Integration -q "alter table xx add test Varchar(10)"
if ($LastExitCode -ne 0)
{
Write-error $LastExitCode
throw $LastExitCode
}
}
Now, PowerShell will catch this error from the .exe, and you can use -ErrorVariable.
Update
So, since you want to keep running and not abandon ship when enountering an error, we need to wrap your Publish-DataBaseProject function with a try{}catch{} block, to catch the error we're generating, without stopping execution.
try {Publish-DatabaseProject -ErrorAction STOP -ErrorVariable DeployError 2>&1 |
Write-LogFile -Path "C:\output.log" }
catch{
Write-Output "Current job $($_), failed"
Write-Output "Exception $($Error.ExceptionID)"
}
Now we're properly generating an exception from a CMD, bubbling it up through our Function, and handling it as if it were a normal function, all using Native PowerShell. I believe this is what you were seeking to do. If not, post a gist of the whole script and I'll help you in a more targeted fashion.

Call powersehell with parameters

I have this in a powershell
if (Test-Path env:\names)
{
[string[]] $names= (dir env:\names).Value.Split(",") | % { $_.Trim() }
} else {
[string[]] $names= "peter","mikael","Anders","William"
}
Write-Host -n "names: " ; [string]$names
If I would like to call it from command prompt is that possible?
I have tried this:
powershell -ExecutionPolicy RemoteSigned -File MainScript.ps1 -PARAM "Peter,Mikael"
I do understand that wont work since I am looking for a env:names but how can I make it work? What I am after is that I would like to send in a parameter with names and that should be caught in something like the code above.
You have a complete mess here. First env:names means value of environment variable names, there shouldn't be \.
Then, if you want you script to accept parameters - just add param(string[] $names) as first line of your script
If you want to run your script with parameters - just start powershell and type: full_path_to_script Peter,Mikael
Like that?
param($name)
if (Test-Path env:\names)
{
$names = $env:names.Split(",") | % { $_.Trim() }
}
else
{
$names = $name.Split(",") | % { $_.Trim() }
}
Write-Host -n "names: $names"