Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 215
akilin Avatar asked Dec 14 '22 07:12

akilin


2 Answers

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.

like image 99
Santiago Squarzon Avatar answered Jan 14 '23 07:01

Santiago Squarzon


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.

like image 40
mklement0 Avatar answered Jan 14 '23 08:01

mklement0