Yesterday I published a little PowerShell function that allows you to select files visually. It's a wrapper around Get-ChildItem. Check the post.

Bernd Kriszio suggested that the first argument should be accepted from pipeline too. So, here is the second version.


The examples from previous post work as expected. And there is a little bit more to show you.

For each directory in m:\temp\blog a form is shown. Selected files are sent to the pipeline

[1]: gci m:\temp\blog | 
	? { $_.PsIsContainer } | 
	Select-File -rec | copy-item -Destination c:\temp\

For each directory (m:\temp\blog and m:\backup) form is shown. Again, the selected files are sent to the pipeline.

[2]: 'm:\temp\blog','m:\backup' | select-file


function Select-File {
        [Parameter(Position=0,Mandatory=$true, ValueFromPipeline=$true,  
    begin {
        Add-Type -Assembly System.Windows.Forms
        $global:SelectedItems = @()
    process {
        $p = @{LiteralPath=$LiteralPath}
        if ($filter) { $p['filter'] = $filter }
        if ($recurse) { $p['recurse'] = $true }
        $files = @(Get-ChildItem @p | ? {!$_.PsIsContainer})
        if ($files.Length -eq 0) {
            #[Void][System.Windows.Forms.MessageBox]::Show("There is no item to select in $LiteralPath")
        $form = new-object Windows.Forms.Form  
        $form.Text = "Pick the files"
        $form.Size = new-object Drawing.Size @(500,600)  

        $panel = new-object System.Windows.Forms.ListView
        $panel.Dock = 'Fill'
        $panel.CheckBoxes = $true
        $panel.View = 'Details'
        $panel.FullRowSelect = $true
        [void]$panel.Columns.Add('', -1, 'Left')
        [void]$panel.Columns.Add('Dir', $(if($hideDir){0}else{-1}), 'Left')
        [void]$panel.Columns.Add('Name', -1, 'Left')
        [void]$panel.Columns.Add('Size', -1, 'Right')
        [void]$panel.Columns.Add('Modified', -1, 'Left')
        [void]$panel.Columns.Add('FullPath', 0, 'Left')
        $dir = (Resolve-Path $LiteralPath).Path
        $dirE = [regex]::escape($dir)
        $items = @{}
        $files | 
            Sort $sortProperty |
            % { 
                $items[$_.FullName] = $_
                $cb = New-Object Windows.Forms.ListViewItem
                $cb.Checked = $false
                [void]$cb.SubItems.Add(($_.FullName -replace "($dirE).*",'$1'))    #directory
                [void]$cb.SubItems.Add(($_.FullName -replace "$dirE\\?(.*)",'$1')) #file
                [void]$cb.SubItems.Add($_.Length)                                  #length
                [void]$cb.SubItems.Add($_.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) #modified
                [void]$cb.SubItems.Add($_.FullName)                                #helper full path
        $ok = new-object System.Windows.Forms.Button 
        $ok.BackColor = [System.Drawing.Color]::Green
        $ok.Text      = "Ok"
        $ok.Dock      = [Windows.Forms.DockStyle]::Bottom
        $panel.Items | 
            ? { $_.Checked } | 
            % { 
                $key = $_.SubItems[5].Text
                Write-Output $items[$key]

Now, when I think about the usage – originally I needed to select files only. However, why not to be able to select directories as well? Is it worthy spending my time? Maybe..


Meta: 2010-04-02, Pepa

Yesterday I needed to go through bunch of *.csproj files and to change reference from 'Spring.Data.NHibernate20.dll' to 'Spring.Data.NHibernate21.dll'. So I created an almost-oneliner very quickly and ran it.

gci c:\dev\Ginger\ *.csproj -rec | 
  % { 
    ($_|gc) -replace 'Spring.Data.NHibernate20.dll','Spring.Data.NHibernate21.dll' | sc $_.fullname 

That's why PowerShell is my choice #1 when it comes to some processing. There is no better option! :) That's my message to all others who hesitate: just use it, it will pay off. Definitely.

My second look

After a while I looked at it once again. In the Foreach-Object body there is a nested pipeline. I read the file, replace something and pipe it to Set-Content. What attracted me was the usage of $_.FullName.
Until today I considered $_ variable as the current pipeline item. So when I pipe replaced text to sc there shouldn't be any FullName property, because it is not [FileInfo] (from gc), but [string] (from replacement), right?. So the script should not work (?).

I though that the scopes work like this (same scopes where $_ has the same value are in same color):

  get-sth | % {  $_ | do-sth3 | do-sth4 $_ | % { $_ | do-sth5 $_ | do-sh6 { $_...} } }

But my oneliner worked so I had to be wrong, obviously. After Vadim Podans and Paul Chavez answered my question I created a test script. Everything started to make sense.

Solution – How $_ is assgined

Here is the test script:

filter Pi { 
  write-host $add Pipeline: $pip -fore Green; 
  write-host $add Parameter: $par -fore Blue; 
'test' | 
  % {
    write-host first $_
    'test2' |
      Pi -par $_ |             # filter 1
      % {
        write-host second $_
        'test3'| Pi -par $_    # filter 2

Just run the script and you will see what it produces:

first test
Pipeline: test2
Parameter: test
second test2
Pipeline: test3
Parameter: test2

It is pretty simple:

  • If you use $_ in a script block that is executed in a cmdlet of the pipeline (e.g. scriptblock in Foreach-Object) then the most nested piped object is accessed.
  • If you use $_ as parameter of a cmdlet, it is value from outer pipeline. It wouldn't make sense if it was a value from current pipeline. Why? Because in my script sc $_.fullname the $_ would change with every row from replacement.
    Look at first call of Pi -par $_ from my test example. Value of $_ inside the filter is 'test2', but value passed as parameter -par is 'test' – value from the same scope as write-host first $_ is.

So the scope looks better like this:

  get-sth | % {  $_ | do-sth3 { $_...} | do-sth4 $_ | % { $_ | do-sth5 $_ | do-sh6 { $_...} } }

And this is only the beginning...

Meta: 2010-01-17, Pepa