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.

Examples

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

Code

function Select-File {
    param(
        [Parameter(Position=0,Mandatory=$true, ValueFromPipeline=$true,  
            ValueFromPipelineByPropertyName=$true)]
        [Alias("PsPath")]
        [string]$LiteralPath,
        [Parameter(Position=1,Mandatory=$false)][string]$filter,
        [Parameter()][switch]$recurse,
        [Parameter()][switch]$hideDir,
        [Parameter(Mandatory=$false)][string]$sortProperty='Name'
    )
    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")
            return
        }
        $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')
        $form.Controls.Add($panel)
            
        $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
                [void]$panel.Items.Add($cb)
            }
    
        $ok = new-object System.Windows.Forms.Button 
        $ok.add_Click({  
            $form.close()
        })  
        $ok.BackColor = [System.Drawing.Color]::Green
        $ok.Text      = "Ok"
        $ok.Dock      = [Windows.Forms.DockStyle]::Bottom
        $form.Controls.Add($ok)
    
        $Form.Add_Shown({$form.Activate()})   
        [void]$form.showdialog()  
        
        $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..

Download

Meta: 2010-04-02, Pepa

Note, that there is a new version of the script.

From time to time I need to select some files for operations like copying, reading content etc. I know that the files have some extension and their file name contains some word, but I'm not sure if my filters get all the files I need, because the rules what file should be selected might be quite complex.
And besides that I sometimes have some set of files that I need to select visually (~ look at each file and decide) and then process. Then it is very hard time for me.

Command like the following takes my time to create and can be quite error prone, because you know that there might be some old files that don't contain holiday nor summer in the name:

Get-ChildItem c:\temp -filter *.jpg -rec | 
   ? { $_.Name -match 'holiday|summer' -and $_.LastWriteTime -lt '2008-01-01' } 

That's why it would be much better to have a possibility to select files from a list by hand.

Solution

After you run Select-File a form appears with one button. After you select the files (there are checked), just press the button and checked items will be sent to the pipeline. Dead simple.

It can be used similarly as Get-ChildItem, but it is much more simple and it works only on file system.

Navigation

  • You can use keys to move up and down.
  • When holding Shift, more items are selected (but not checked).
  • To check the items press space.

Examples

Basic usage

[1]: Select-File M:\install\ | 
	? { $_.Length -lt 1kb } | 
	Copy-Item -Destination c:\temp\install

You can filter the items and search recursively:

[2]: Select-File M:\install\ -rec -filter *.exe | 
	copy-item -Destination C:\temp\install

You can sort the items:

[3]: Select-File M:\install\ -rec -sort LastWriteTime | 
	copy-item -Destination C:\temp\install

I think the parameters are quite self-explanatory so I won't explain them here.

Code

function Select-File {
    param(
        [Parameter(Position=0,Mandatory=$true)][string]$dir,
        [Parameter(Position=1,Mandatory=$false)][string]$filter,
        [Parameter()][switch]$recurse,
        [Parameter()][switch]$hideDir,
        [Parameter(Mandatory=$false)][string]$sortProperty='Name'
    )
    begin {
        Add-Type -Assembly System.Windows.Forms
        $global:SelectedItems = @()
        
        $items = @{}
        $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')
        $form.Controls.Add($panel)
    
        $p = @{LiteralPath=$dir}
        if ($filter) { $p['filter'] = $filter }
        if ($recurse) { $p['recurse'] = $true }
        
        $dir = (Resolve-Path $dir).Path
        $dirE = [regex]::escape($dir)
        Get-ChildItem @p | 
            ? {!$_.PsIsContainer} | 
            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
                [void]$panel.Items.Add($cb)
            }
    
        $ok = new-object System.Windows.Forms.Button 
        $ok.add_Click({  
            $global:SelectedItems = @($panel.Items | ? { $_.Checked })
            $form.close()
        })  
        $ok.BackColor = [System.Drawing.Color]::Green
        $ok.Text      = "Ok"
        $ok.Dock      = [Windows.Forms.DockStyle]::Bottom
        $form.Controls.Add($ok)
    
        $Form.Add_Shown({$form.Activate()})   
        [void]$form.showdialog()  
    }
    process {
        $SelectedItems | % { 
            $key = $_.SubItems[5].Text
            Write-Debug "Key: $key, Value: $($items[$key])"
            Write-Output $items[$key]
        }
    }
}

This is just first attempt how to solve my frustration. So, it is far from perfect solution. Just take it as inspiration for your further improvements ;)

Download

Meta: 2010-04-01, Pepa

Some time ago I needed to list content of the folder recursively, which gave me list of files and directories. I didn't know whether to use classic .NET objects such as System.IO.Directory or to use Get-ChildItem.

Experiment

Today I will presents a quick comparison of the three approaches. The task is simple – list the content of given directory recursively:

  1. 1. we will try to use PowerShell cmdlets.
  2. 2. we will replace calling of the cmdlets with calling .NET methods.
  3. 3. we will dynamically create (=compile and load) a type in .NET used only for this listing.

Code

1. Core Cmdlets

$content = Get-ChildItem g:\ -rec | select -expand fullname

2. Call .NET methods

$content = @()
function listDir($dir){
    [System.IO.Directory]::GetFiles($dir) | % { $content += $_ }
    $subdirs = [System.IO.Directory]::GetDirectories($dir)
    $subdirs | % { $content += $_ }
    $subdirs | % { listDir $_  }
}
listDir 'g:\'

3. Code compiled to assembly

Add-Type -TypeDefinition @"
  public class Lister
  {
    public static System.Collections.ArrayList ListDir(string dir)
    { 
      System.Collections.ArrayList ret = new System.Collections.ArrayList();
      ListDir(dir, ret);
      return ret;
    }
    public static void ListDir(string dir, System.Collections.ArrayList list)
    {
      foreach(string file in System.IO.Directory.GetFiles(dir))
        list.Add(file);
      string[] subdirs = System.IO.Directory.GetDirectories(dir);
      foreach(string d in subdirs)
        list.Add(d);
      foreach(string d in subdirs)
        ListDir(d, list);
    }
  }
"@

$content = [Lister]::ListDir('g:\')

Results of the measurements

The winner – fastest code – is of course the compiled code. It took only 3,2 seconds to return all the 15405 files!
What about silver medal? At the beginning I guessed that direct calling of .NET methods will be faster, but that's not true. Cmdlets won the silver medal. The time is 40,3 seconds. The difference is huge!
The slowest solution is the one that calls the .NET methods directly. It took 52,4 seconds!

So once again

  1. Compiled code – 3,2secs
  2. Cmdlets – 40,3secs
  3. .NET methods calls – 52,4

Conclusion

It is not surprising, that the compiled code is the fastest. However, I assumed, that direct call to .NET methods is faster that calling cmdlets. Wrong! Why? Because cmdlets are of course compiled as well, so they are fast. What makes them slower is the interaction with Powershell framework, providers, some type magic, etc.

So, trust the core commands – they are already optimized.

Python example for fun

I have started learning Python as well, so I tried to list the dirs in Python too. The code is as following:

content = []

def listDirectory(directory):
  for item in os.listdir(directory):
      path = os.path.join(directory, item)
      content.append(path)
      if os.path.isdir(path):
        listDirectory(path)
          
listDirectory(dir)

This solution was the second fastest from the four codes. It took 32,3 secs to get the list of files and dirs.
You can see, that it is pretty fast. However, it is far away from the compiled version (of course).

Meta: 2009-10-31, Pepa

Shortly after you started using PowerShell regularly you will probably feel that some kinds of tasks could be done automatically, after a certain period of time during your session. That means that you will go for task scheduling. However, there is a catch in case you would like to interac with the user. But let's go step by step.

Running PS command from command line

First we need to know how to run powershell tasks from windows command line. The path to PowerShell console is present in $env:path variable, so the system knows what application to launch. Ok, how do we specify the command to execute? Just type powershell -command "write-host test" and you will see the results.

C:\>powershell -command "write-host test" test C:\>
In case you don't want to execute your custom profile (located at $profile path), specify -NoProfile parameter as well.

After that we can schedule the test task:

C:\>schtasks /Create /SC MINUTE /TN schtest /TR "powershell -command 'get-date | add-content c:\temp\test.txt'"

Hide the console window

Everything is running as expected, however after several minutes you start being a little bit nervous because of the window that appears every minute. Wouldn't it be great to have the window hidden? After some searching I found a solution. Therefore we have to create a vbs file that will contain:
Dim objShell
Set objShell=CreateObject("WScript.Shell")
strExpression="get-date | add-content c:\temp\test.txt"
strCMD="powershell -sta -noProfile -NonInteractive  -nologo -command " & Chr(34) &_
"&{" & strExpression &"}" & Chr(34) 
objShell.Run strCMD,0
The magic is in the last row where number 0 says that the console window should be hidden. The script is then scheduled in this way:
C:\>schtasks /Create /SC MINUTE /TN schtest /TR "wscript c:\test.vbs"

And what happens if you use WinForms?

The previous method works for scripts that doesn't show any outputs. Just running in the background is enough. However when I needed to show a WinForm with some information, it didn't work. Nothing appeared. The only solution I was given in the discussions was to create a simple program in C# - application that creates PowerShell runspace and executes the script in the runspace. The code looks like the following:

using System; using System.Management.Automation.Runspaces; using System.Windows.Forms; namespace PowershellRunner { static class Program { [STAThread] static void Main(string[] args) { try { if (args == null || args.Length != 1) throw new ApplicationException("Empty argument list"); Runspace runspace = RunspaceFactory.CreateRunspace(); runspace.Open(); Pipeline pipeline = runspace.CreatePipeline(); pipeline.Commands.AddScript(args[0]); pipeline.Invoke(); } catch(Exception e) { MessageBox.Show(e.Message); } } } }
If you create a new project from scratch, select Windows application (not Console) and delete the default form files. To compile the code you will need to download PowerShell SDK and reference System.Management.Automation assembly.

Then after compiling the code above you can compare how the two approaches run:

C:\>powershell "Add-Type –a system.windows.forms; $form = new-object Windows.Forms.Form; [void]$form.showdialog()"
and
C:\>PSRun.exe "Add-Type –a system.windows.forms; $form = new-object Windows.Forms.Form; [void]$form.showdialog()"
You can see, that the PSRun doesn't show any command line window. So, we are done!

Last catch

There is only one disadvantage: I don't know why, but your custom profile (located at $profile) is not loaded in PSRun runspace. Even variable $profile is not defined. To bypass that you have to load the profile by handthe first:

C:\>PSRun.exe ". c:\users\....\..._profile.ps1; ..."

Meta: 2008-09-22, Pepa