1

I'm trying to copy files using copy-item. Specifically, I want to copy files with a particular extension that are within a folder or its subfolders to another location, and to retain the subfolder hierarchy. I've tried using -filter and -include to specify the file extension, but no files are copied.

My source and destination paths are stored in variables $packageSourcePath and $objPath. When called, $packageSourcePath will be like the following ".\src\projects\Project1\PackageFiles" and $objPath will be like the following ".\bld\Project1\obj".

The command I've tried using is this:

Copy-Item -Path $packageSourcePath\* -Filter *.resw -Destination $objPath -Recurse

I've also tried variations, such as leaving off * from the path, or using -Include instead of -Filter. Nothing works. If I leave out the -Filter argument, then files copy, but all of the files are copied. I only want files with the particular extension.

JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
Peter Constable
  • 2,707
  • 10
  • 23
  • Have you verified the source by running: Get-ChildItem -path $packageSourcePath\* -Filter *.resw ? Is that getting you the sources you expect? I am guessing not, and that's likely where your problem is. – Adil Hindistan Dec 29 '17 at 18:54

2 Answers2

0

This should do it (obviously this could be done in 1 line; I've assigned values to the variables just to help make it readable / self-explanatory):

[string]$filter = '*.resw'
[string]$source = Join-Path -Path $packageSourcePath -ChildPath '*'
[string]$target = $objPath
$source | Convert-Path | Copy-Item -Filter $filter -Recurse -Destination $target -Container #-Force

Notes:

  • We append the asterisk to the source path to ensure that we copy the contents of the source folder to the destination, without copying the source's root folder into the destination (i.e. say we're copying c:\temp\from to c:\temp\to, we don't want c:\temp\to\from (unless it's a copy of c:\temp\from\from)).
  • We use the Join-Path cmdlet to append this asterisk to ensure the appropriate slashes are inserted into the path.
  • We do a Convert-Path on the source to resolve the asterisk to the child folder/file names... for some reason copy-item doesn't handle these asterisks well. NB: Convert-Path will potentially return an array of paths; i.e. if there's more than one file/subfolder directly under the source folder. Get-Item or Resolve-Path could equally be used for this; I prefer Convert-Path since it returns a simple string array, rather than a more complex type; but there's no strong argument for using any one over the others.
  • We pipe these source paths to the Copy-Item command so it can be applied to each path returned by Convert-Path.
  • We include -Recurse to say we're interested in anything in the subfolders of the copied path.
  • We include the -Container parameter to say that we want to preserve any folder structure when copying. Strictly this is not needed, as this switch is defaulted to true (i.e. rather we should specify if we don't want this behaviour: -Container:$false; but I like to be clear that I deliberately want to preserve the directory structure, as opposed to leaving the assumption that I may not have thought of this. There's a better explanation of this here: https://stackoverflow.com/a/21798660/361842.
  • You could optionally include -Force; this would mean that should an item of the same name already exist in the target we overwrite it instead of getting an error.

Related documentation:


Update 2018-01-03

Per comments, this solution should ensure that only those items you want get copied, and pre-existing directories shouldn't cause issues.

[string]$filter = '*.resw'
[string]$source = $packageSourcePath 
[string]$target = $objPath
#copy all files in subfolders of the source
$source | Get-ChildItem -Directory | Copy-Item -Filter $filter -Recurse -Destination $target -Container -Force
#copy all files in root of the source
$source | Get-ChildItem -File -Filter $filter | Copy-Item -Destination $target -Container -Force

This solution uses 2 steps; there's probably a better option, but due to the peculiarities / bug in this cmdlet the above's a reliable option.

Community
  • 1
  • 1
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178
  • 1
    Convert-Path provided the key difference. I did get "already exists" errors for subfolders, and so adding -Force was needed to avoid that. (I don't understand why Copy-Item doesn't have -Directory and -File switch parameters like Get-ChildItem. I suspect being able to specify -File would avoid the "already exists" errors without using -Force, and -Directory would provide an equivalent to xcopy /T.) – Peter Constable Dec 30 '17 at 16:05
  • Nice suggestion; you may want to pass that to the relevant team via UserVoice: https://windowsserver.uservoice.com/forums/301869-powershell?query=copy-item. Once shared if you provide a link to your suggestion here anyone hitting this post will be aware of the suggestion so can head over to upvote. – JohnLBevan Dec 31 '17 at 10:37
  • This still isn't doing quite what I want: The -filter argument in Copy-Item doesn't seem to be working on input piped from Convert-Path. So, the files I want are getting copied, but so are a bunch of files that I don't want. – Peter Constable Jan 02 '18 at 23:51
  • What filter are you using / is there any pattern to those files which are copied vs those which aren't? – JohnLBevan Jan 03 '18 at 10:52
  • (also, which version of PowerShell (`$PSVersionTable.PSVersion.ToString()`)) – JohnLBevan Jan 03 '18 at 10:59
  • The filter is as above, *.resw. The extra files that get included are all the files directly within the root of the source folder. E.g., given this: …\packagefiles\foo.txt …\packagefiles\en\resources.resw I would want the target result to be …\obj\en\resources.resw But what I end up with is this: …\obj\foo.txt …\obj\en\resources.resw – Peter Constable Jan 03 '18 at 16:16
  • Ah ok; so the filter's not applied if the item being piped in is a file rather than a directory; hadn't thought of that... new solution coming soon... – JohnLBevan Jan 03 '18 at 17:13
  • 1
    @JohnLBevan The relevant bug has been filed [here](https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/32782822-copy-item). – Scrambo Jun 05 '18 at 18:40
0

I've given up on Copy-Item. JohnLBevan's answer didn't actually do what I want since all files in the source root get copied, even though they don't match the filter. I tried piping Convert-Path | Select-String | Copy-Item but still got all files in the source root being copied.

A contact in a different context provided a couple of suggestions:

1)

Get-ChildItem -Force -Recurse -ErrorAction Ignore -Path $packageSourcePath -Filter *.resw | % {
$src = $_.FullName
$dst = Join-Path $objPath $src.SubString($packageSourcePath.Length)
echo "copy ""$src"" ""$dst"""
}

I think this is a bit harder to follow, hence less maintainable for the next person (likely another PS-neophyte like me) a year from now. ("Why is the -ErrorAction parameter needed here? What's the behaviour of the Substring() method, and why can't I find that using Get-Help?")

This suggestion is a bit clearer, after re-familiarizing with attrib and checking the effect of the xcopy switches:

2)

cd $packageSourcePath
attrib -a /s
attrib +a *.resw /s
xcopy /eidlm $packageSourcePath $objPath

But if we're going to use xcopy, we don't need to call attrib:

xcopy $packageSourcePath*.resw $objPath /s /i > $null

The only problem with this for my scenario is that xcopy emits an error if no matching files are found. My script is being used for a VSTS build task, and the xcopy errors cause the build task to fail. (For that reason, I'm guess that suggestion 2 also wouldn't work for me.)

So, I've opted for this:

   # In PS version 5.1, nothing gets copied using Copy-Item $packageSourcePath\* -Filter *.resw ...  
   # so resorting to using xcopy, which mostly works. The one issue is that xcopy will output an  
   # error if no matching file is found, so using GCI first to test for a matching file.  
   if ($(Get-ChildItem $packageSourcePath\*.resw -Recurse).count -gt 0) {
       xcopy $packageSourcePath\*.resw $objPath /s /i > $null
   }

The condition using GCI is added to check there are matching files before calling xcopy, thereby avoiding any errors.

I'm still amazed that Copy-Item -Filter -Recurse didn't work.

Peter Constable
  • 2,707
  • 10
  • 23