'Set-Clipboard only remembering the last value when called in rapid succession

I am trying to create a powershell snippet that will copy the first column of a multi-line piped input to clipboard.

The intended usage is: kubectl get pods | copyfirst.
This should allow me to have all pod names in the clipboard, and use Win+V to select the individual pod name that I need.

What I have so far is:

function copyfirst {
    [CmdletBinding()]Param([Parameter(ValueFromPipeline)]$Param)
    process {
        $Param.Split(" ")[0] | Set-Clipboard
    }
}

The problem is - this only copies the last entry to clipboard, while all the others are ignored.

If I change Set-Clipboard to some other command - it works as intended. For example echo outputs all pod names, not just the last one.



Solution 1:[1]

I think mklement0's answer was the right one to begin with and I personally was not aware of this Win + V clipboard functionality. So, you were right, as it seems it can't capture the history when done in rapid succession.

By adding Start-Sleep it works fine:

function copyfirst {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)]
        [object[]] $Param
    )

    process {
        $Param.Split(' ')[0] | Set-Clipboard
        Start-Sleep -Milliseconds 250
    }
}

@'
string1 string4
string2 string5
string3 string6
'@ -split '\r?\n' | copyfirst

It should capture string1, string2 and string3.

Tweak the sleep timer until it's not too slow and it can capture everything.


After some testing, seems like Start-Sleep can be reduced to -Milliseconds 250, lower than that would produce inconsistent results.

Solution 2:[2]

Note:

  • This answer shows how to copy the first whitespace-separated field across all input lines as a single clipboard entry.

  • As it turns out, the OP's intent was to copy each such field separately to the clipboard, to create a succession of entries that can be recalled via the Windows 10 clipboard-history function (WinKey+V), for which Santiago Squarzon's helpful answer provides a solution.


To copy data from all input objects, you must collect it in the process block and copy the collection to the clipboard in the end block, after all input objects have been processed:

function copyfirst {
    [CmdletBinding()]Param([Parameter(ValueFromPipeline)]$Param)
    begin {
        # Initialize the collection (list) that will collect data.
        $coll = [System.Collections.Generic.List[object]]::new()
    }
    process {
        # Add to the collection.
        $coll.Add($Param.Split(" ")[0])
    }
    end {
        # Copy the collection to the clipboard.
        # More efficient alternative:
        #    Set-Clipboard $coll
        $coll | Set-Clipboard
    }
}

The process block is called for each input object, and Set-Clipboard always replaces the previous content on the clipboard, which explains why only the last data item was placed there.

Note that stdout output from external programs is passed line by line through the pipeline.


Note that in your case there is a simpler alternative using a simple (non-advanced) function and the automatic $input variable:

function copyfirst {
  $input | ForEach-Object { $_.Split(' ')[0] } | Set-Clipboard
}

The potential downside of this approach is the loss of the streaming aspect of the pipeline (one-by-one processing), because a simple function's body is like an implicit end block, in which PowerShell provides the collected-up-front input objects via $input.

However, in this case, that doesn't matter, because you need to collect all data anyway.

You could speed up the operation a bit by using the .ForEach() array method instead of the ForEach-Object cmdlet, though I doubt that it matters in this case.

Solution 3:[3]

What I've found is that the clipboard would be overwritten when the set-clipboard is used in the same process. There is a -append but that appends to the last clipboard value rather than adding to the clipboard list.

What I ended up doing was building a command and executing it for each item in the array under a new process (which is slow!) as below:

function copyfirst {
    [CmdletBinding()]Param([Parameter(ValueFromPipeline)]$Param)
    
    foreach($line in $Param.split("`n")){
        $podName = $line.split(" ")[0]
        if ($podName -ne "NAME"){
            $scriptBlock = "Write-Output `"$podName`"  | Set-Clipboard; Start-sleep -milliseconds 1"
            Powershell $scriptBlock
        }
    }
}


"NAME READY STATUS RESTARTS AGE 
redis-fd794cd65-k9mhp 1/1 Running 0 3h15m
website-restrictor-c6f5bbd56-fxb59 1/1 Running 0 3h15" | copyfirst

This results in individual clipboard entries as seen when using Windows Key + V

Note the Start-sleep -milliseconds 1 this is because of some weird behavior when executing the set-clipboard cmdlet very quickly, its noted here - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/set-clipboard?view=powershell-7.1#notes

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3