powershellmethodscopy

Copying multiple items with Powershell and shell.application.copyHere


I'm using the typical method for copying items in powershell with the native Windows copy dialog.

$trnsfr = New-Object -ComObject "Shell.Application"
$target_folder = $trnsfr.NameSpace($trgdir) 
$target_folder.CopyHere($srcdir)

Everything works great, but I was wondering if there was a way I could provide a list of items to copy rather than a single path?


Solution

  • TL;DR: You can create a custom FolderItems collection, but Windows seems to require that all items are in the same source folder.


    The Folder.CopyHere method does accept a FolderItems object, which is a collection of items to copy to the target folder. Using the Folder.Items method you can get a FolderItems collection containing all files in a folder. E.g.

    $shell = New-Object -ComObject "Shell.Application"
    $folder = $shell.NameSpace($folderpath) 
    $folderItems = $folder.Items()
    

    As the usual namespaces (e.g. filesystem) actually return an object of type FolderItems3 (which extends FolderItems with more functionality), you can use its Filter method to get a collection of for example only the PDF files in the folder:

    $folderItems.Filter(-1,'*.pdf')
    

    Note that this actually sets the filter on the $folderItems object, you don't get a new filtered object.


    If such filtering is not enough and you really need to supply your own list of items to copy, you can actually provide your own implementation of the FolderItem interface:

    $csharp_impl = @"
    
    using System;
    using System.Linq;
    using System.Collections;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Runtime.CompilerServices;
    
    namespace Shell32Helper{
    
        //this interface actually has methods, but we only use it to ensure that an object of the right type is returned
        [ComImport, Guid("FAC32C80-CBE4-11CE-8350-444553540000")]
        public partial interface FolderItem{}
    
        //mostly generated this way when adding COM Reference to shell32.dll in Visual Studio
        [ComImport]
        [Guid("744129E0-CBE5-11CE-8350-444553540000")]
        [TypeLibType(4160)]
        public interface FolderItems : IEnumerable
        {
            //[DispId(1610743808)]
            int Count
            {
                [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
                [DispId(1610743808)]
                get;
            }
    
            //[DispId(1610743809)]
            object Application
            {
                [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
                [DispId(1610743809)]
                [return: MarshalAs(UnmanagedType.IDispatch)]
                get;
            }
    
            //[DispId(1610743810)]
            object Parent
            {
                [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
                [DispId(1610743810)]
                [return: MarshalAs(UnmanagedType.IDispatch)]
                get;
            }
    
            
            //[DispId(1610743811)]
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            [return: MarshalAs(UnmanagedType.Interface)]
            FolderItem Item([Optional][In][MarshalAs(UnmanagedType.Struct)] object index);
    
            //[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalType = "System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler, CustomMarshalers, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
            //changed to MarshalTypeRef because the PublicKeyToken might be different on different versions...
            [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
            [DispId(-4)]
            [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler))]
            new IEnumerator GetEnumerator();
    
        }
    
        public class FolderItemsFromEnumerable : FolderItems
        {
            public FolderItemsFromEnumerable(IEnumerable actualItems, object baseFolderItems)
            {
                this.actualItems = actualItems.Cast<FolderItem>().ToList();
                this.baseFolderItems = (FolderItems)baseFolderItems;
            }
    
            private List<FolderItem> actualItems;
            private FolderItems baseFolderItems;
    
            public int Count { get { return actualItems.Count; } }
            public object Application { get { return baseFolderItems.Application; } }
            public object Parent { get { return baseFolderItems.Parent; } }
            public FolderItem Item(object index) { return actualItems[(int)index]; }
            public IEnumerator GetEnumerator() { return actualItems.GetEnumerator(); }
        }
    }
    "@
    Add-Type -AssemblyName CustomMarshalers
    Add-Type -TypeDefinition $csharp_impl -ReferencedAssemblies @([System.Runtime.InteropServices.CustomMarshalers.EnumeratorToEnumVariantMarshaler].Assembly.Location)
    
    

    With that you can then create a FolderItems instance from any collection of FolderItem instances.

    For example by filtering a FolderItems collection within powershell by other criteria:

    #https://learn.microsoft.com/en-us/windows/win32/properties/props-system-datemodified
    $files = $folderItems|?{[DateTime]::new(2024,01,01) -lt [DateTime]$_.ExtendedProperty("System.DateModified")}
    
    
    $folderItems_FromList = [Shell32Helper.FolderItemsFromEnumerable]::new(
        [System.__ComObject[]]@($files),
        $folderItems #reference FolderItems object to provide Application and Parent property
    )
    

    Or alternatively from a list of paths by using ParseName on the virtual Desktop folder (ssfDESKTOP = 0):

    $files = @('C:\Windows\dxdiag.txt','C:\Windows\regedit.exe')
    
    $folderItems_FromList = [Shell32Helper.FolderItemsFromEnumerable]::new(
        [System.__ComObject[]]@($files|%{$shell.NameSpace(0).ParseName($_)}),
        $folderItems #reference FolderItems object to provide Application and Parent property
    )
    

    The FolderItems instance can then be used with Folder.CopyHere:

    $target_folder = $shell.NameSpace($trgdir)
    $target_folder.CopyHere($folderItems_FromList)
    

    Sadly for reasons I don't know this still only seems to work if all items to copy are in the same source folder.