PowerShell remove-item succeeds but throws exception - powershell

I'm running the following command in a directory that is the root of a Mercurial repository. I want to delete any files and folders beginning with ".hg":
gci -rec -filter ".hg*" | remove-item -recurse -force
The strange thing, is that it does actually work, but still produces the following exception:
Get-ChildItem : Could not find a part of the path 'C:\temp\WSCopyTest\MyCompany.Services.DaftPunk\.hg\'.
At line:1 char:4
+ gci <<<< -rec -filter ".hg*" | remove-item -recurse -force
+ CategoryInfo : ReadError: (C:\temp\WSCopyT...es.DaftPunk\.hg:String) [Get-ChildItem], DirectoryNotFoundException
+ FullyQualifiedErrorId : DirIOError,Microsoft.PowerShell.Commands.GetChildItemCommand
Because the exception is thrown by Get-ChildItem, I suspect my understanding of the pipelining in PowerShell is flawed. Almost like Get-ChildItem finds an item, passes it to the next cmdlet in the pipeline, then looks for the next one? Is that how it works?
The following was supposed to be a script that would replicate the problem, but, on the same machine, it works flawlessly:
$repoRoot = "C:\Temp\HgDeleteTest\MyRepo"
remove-item $repoRoot -recurse -force
md $repoRoot
pushd $repoRoot
hg.exe init
add-content ShouldBeLeftBehind.txt "This is some text in a file that should be left behind once the various .hg files/directories are removed."
add-content .hgignore syntax:glob
add-content .hgignore *.dll
add-content .hgignore *.exe
hg.exe commit --addremove --message "Building test repository with one commit."
gci -rec -filter ".hg*" | remove-item -recurse -force
popd

Could it be that you're removing a folder starting with .hg, which in turn contains a file starting with .hg, but that file no longer exists?

I expect Antony is correct - using the -recurse on the gci will enumerate both files and directories that match ".hg*". The directory will be returned first. Then remove-item deletes the directory first, and the -force switch deletes all the files in it. Then it tries to delete the files that were in that directory that match ".hg*" (that were there when the gci ran) but they're gone now. This should stop the errors:
gci ".hg*" -recurse | select -expand fullname | sort length -desc | remove-item -force
Sorting the full names in descending order of length insures that no matched parent directory is deleted before all the matched files in that directory are deleted.

The following will restrict its deletions to files only, and exclude folders.
gci -rec -filter ".hg*" | Where {$_.psIsContainer -eq $false} | remove-item -recurse -force
Then run it again, by deleting the folders:
gci -rec -filter ".hg*" | Where {$_.psIsContainer -eq $true} | remove-item -recurse -force

I ended up separating the search from the removal. I believe my issue was to do with the way piping works by default (i.e. one item at a time) but I've not been able to build a test to check it.
In any case, the following works fine:
$hgObjects = gci -rec | ?{ $_.name -like ".hg*" -and $_.name -ne ".hgignore" }
remove-item $hgObjects -force -recurse
Using this style, the remove-item cmdlet gets an array of items found and works through them.
The .hg folder in my original example didn't have anything in it called .hg*, so I don't see what was going on. It felt more like it was trying to delete the folder twice, which I find very strange. I wonder if it's actually a PowerShell manifestation of this standard .NET behaviour:
An enumerator remains valid as long as the collection remains
unchanged. If changes are made to the collection, such as adding,
modifying or deleting elements, the enumerator can be invalidated and
the next call to MoveNext or Reset can throw an
InvalidOperationException. If the collection is modified between
MoveNext and Current, Current returns the element that it is set to,
even if the enumerator is already invalidated.
(Taken from http://msdn.microsoft.com/en-us/library/system.collections.ienumerable.getenumerator(v=vs.71).aspx).

Related

How do you do a selective copy from a directory tree using PowerShell?

I am new to PowerShell but have experience with Bash scripting and Python, but need to use PowerShell for a specific project. I thought my use-case was pretty easy, but am really having a hard time getting it to work.
I have a directory structure like this:
-- Data
-- ProjectA
-- Exception
- exception1.txt
-- Custom
- file1.txt
- file2.txt
-- ProjectB
-- Exception
- exception2.txt
-- Custom
- file3.txt
- file4.txt
What I am trying to do is copy this structure, but copy the 'Exception' directories and their contents to a different path to the rest of the files. So I want to end up with something like this:
-- NewFolder
-- ProjectA
-- Custom
- file1.txt
- file2.txt
-- ProjectB
-- Custom
- file3.txt
- file4.txt
-- ExceptionFolder
-- Exception
- exception1.txt
- exception2.txt
Notice that the contents of the exception folders are all merged into a single folder.
I have tried with doing a Copy-Item and then --exclude '*Exception*' but that only seems to work if the 'Exception' folder is at the top level. So I then tried with Get-ChildItem and pipe that into a where ($_fullname -notin '*Exception*') and pipe that into further steps to copy at each level, but that doesn't seem to work either. I've tried a few other things as well but none give me the results I'm looking for.
Not sure if it makes a difference, but I'm using PowerShell 7.1.1 on a Mac.
Any guidance on this one would be much appreciated!
Convoluted, yes, works? yes.
$source = '.\Data'
$destination = '.\New Folder'
$exceptionFolder = '.\ExceptionFolder'
$exceptionRegex = 'Exception|foobar'
$directories = Get-ChildItem $source -Recurse -Directory
$redirect = $directories | ? {$_.Name -match $exceptionRegex}
switch -Regex ($directories){
{$_.Name -match $exceptionRegex} {
# Create directory based on the regex match.
$ExceptionDestination = [io.directoryinfo]("{0}\{1}" -f $ExceptionFolder, $_.Name)
if (-not(Test-Path -Path $ExceptionDestination)){
$null = md $ExceptionDestination
}
# Copy top level files from the matched directory name into the $ExceptionDestination directory.
Copy-Item -Path ("{0}\*.*" -f $_.FullName) -Destination $ExceptionDestination -Verbose
}
}
# Skips copying of the contents of directories matching $exceptionRegex.
Copy-Item -Path $source -Destination $destination -Exclude ("*{0}*" -f $redirect).Name -Recurse -Verbose
# Actually remove directories.
Get-ChildItem $destination -Recurse -Directory | ? {$_.Name -match $exceptionRegex} | Remove-Item -Recurse -Force -Verbose
You could try Robocopy ! You have an exclude parameter.
https://pureinfotech.com/exclude-files-folders-robocopy-windows-10/
I may be wrong but, this might be what youre asking for?
#add -Recurse to copy subfolders
Get-ChildItem -Path C:\Path1 -Exclude "Exception" | Copy-Item -Destination C:\Path2
Or,
#add -Recurse to copy subfolders
Copy-Item -Path C:\path1\* -Exclude 'Exception' -Destination C:\path2
First copy all items, excluding files named like "Exception*" (there seems to be no way to exclude based on directory name, apart from a Get-ChildItem pipeline):
Copy-Item 'Data\*' -Destination 'NewFolder' -Exclude 'Exception*' -Recurse
Unfortunately this creates empty "Exception" folders in the destination, so we have to delete them:
Remove-Item 'NewFolder\*' -Include 'Exception' -Recurse
Here is an example for a Get-ChildItem pipeline in case you need a more general solution. You could change the Where-Object condition to filter on full path, e. g. $_.Fullname -notlike '*\Exception\*'.

How do I remove a Folder in Powershell?

we got a small script that creates folders named by the daily date. I got a script that deletes folders which are older than 30 days.
dir "\\nas\Backup_old\*" -ErrorAction SilentlyContinue |
Where { ((Get-Date) - $_.LastWriteTime).days -gt 30} |
Get-ChildItem -Recurse | Remove-Item -Recurse -Force
Principally it works fine. The Subfolders with contend will be deleted.
But the main folder is still existing and the LastWriteTime is canged to the runtime of the script. The folder is empty. Someone have a idea to solve this problem?
You probably just need to remove the second instance of Get-ChildItem (noting that dir is just an alias for Get-ChildItem), as that is causing it to remove the children of each of the directories returned by the first:
Get-ChildItem "\\nas\Backup_old\*" -ErrorAction SilentlyContinue |
Where-Object { ((Get-Date) - $_.LastWriteTime).days -gt 30} |
Remove-Item -Recurse -Force -WhatIf
Have a look at the WhatIf output and if it looks like it will now remove what you expect, remove -WhatIf.

Using Remove-Item cmdlet but excluding sub-directory

I want to remove the following files from the source, however in the source there is a sub-directory that contains files with similar names. When I run the following command it is deleting files in the sub-directory with similar file name. Is there a way to just delete the files from the source and not the sub-directory?
Example: test_1_file, test_2_file, test_3_file exists in each directory, TestFolder and TestFolder/sub
$source = testfolder
remove-item -Path $source -filter test_*_file -recurse -force
It's usually easiest to pipe the output of Get-ChildItem cmdlet into Remove-Item. You then can use the better filtering of Get-ChildItem as I think -Recurse in Remove-Item has some issues. You can even use Where-Object to further filter before passing to Remove-Item
$source = testfolder
Get-ChildItem -Path $source -Filter test_*_file -Recurse |
Where-Object {$_.Fullname -notlike "$source\sub\*"} |
Remove-Item -Force
If the files to delete:
are all located directly in $source
and no other files / directories must be deleted:
Remove-Item -Path $source/test_*_file -Force
No need for -Recurse (as #Bill_Stewart notes).
Note: For conceptual clarity I've appended the wildcard pattern (test_*_file) directly to the $source path.
Using a wildcard expression separately with -Filter is generally faster (probably won't matter here), but it has its quirks and pitfalls.

How to keep a specific folder and delete rest of the files using powershell

I am trying delete all files within a folder but there is 1 folder called pictures which I would like to keep but don't know how to do that. I am using the following script , it deletes everything in a folder
if ($message -eq 'y')
{
get-childitem "C:\test" -recurse | % {
remove-item $_.FullName -recurse
}
}
One solution is to use something like:
Get-ChildItem -Path "c:\test" -Recurse | Where-Object { $_.FullName -cnotmatch "\\Pictures($|\\)" -and (Get-ChildItem $_.FullName -Include "Pictures" -Recurse).Length -eq 0 } | Remove-Item -Recurse -ErrorAction SilentlyContinue;
I suspect there must be a way more elegant way to do this. Here's what this does: it enumerates all files in the C:\test folder recursively (Get-ChildItem), then it removes all items from the result list using Where-Object where the path contains the directory to be excluded (specified using regex syntax) or when the item in question has child items that contains the file or directory to be excluded. The resulting list is fed to Remove-Item for removal. The -ErrorAction SilentlyContinue switch is applied to prevent errors being logged with recursive removal.
Get-ChildItem $PSScriptRoot -Force| Where-Object {$_.Name -ne "Pictures"} | Remove-Item -Recurse
I just tried this, and it worked for me. If you want to change what is deleted just change the "Pictures". This uses $PSScriptRoot for the path, which is the execution path of the Powershell script. You can rename that to be the path of where you want to delete.

XCOPY deployment script - how to include certain files?

I need to copy only certain parts of a folder using Powershell, specifically this list:
$files = #("MyProgram.exe",
"MyProgram.exe.config",
"MyProgram.pdb",
".\XmlConfig\*.xml")
In human readable form: 3 specific MyProgram.* files under root of target folder and all XML files under XmlConfig folder which itself is under root of source path (..\bin\Release\ in my case). XmlConfig folder must be created in destination, if it does not exist.
What I have tried:
(1) I tried the following, but it did not work, i.e. no folder or files were created at the destination path:
Copy-Item -Recurse -Path "..\bin\Release\" -Destination ".\Test\" -Include $files
(2) When -Include is removed, whole folder structure is successfully created, including subfolders and files:
Copy-Item -Recurse -Path "..\bin\Release\" -Destination ".\Test\"
It must be something wrong with my understanding of how -Include filter works:
(3) I tested an assumption that -Include needs an array of wildcards, but this did not work either:
$files = #("*MyProgram.exe*",
"*MyProgram.exe.config*",
"*MyProgram.pdb*",
"*.\XmlConfig\*.xml*")
Please advise on how to properly do Copy-Item in my case.
UPDATE (based on below answers):
I am looking for a generic implementation that takes an array of strings. It opens the possibility to put all necessary files/paths in one place, for easy editing, so that a non-Powershell knowledgeable person can understand and modify it as required. So in the end it would be single script to perform XCOPY deployments for any project, with input file being the only variable part. For above example, the input would look like this (saved as input.txt and passed as an argument to the main script):
MyProgram.exe
MyProgram.exe.config
MyProgram.pdb
.\XmlConfig\*.xml
I would prefer wildcards approach, since not many people know regex.
i don't know what is wrong with filter but you can still do
$files | % { copy-item ..\bin\release\$_ -Destination .\test}
if you want to preserve directoty structure you'll have to weak this a little, like :
$sourcedir="c:\temp\test"
$f=#("existing.txt","hf.csv";"..\dir2\*.txt")
$f |%{
$source=ls (join-Path $sourcedir $_) |select -expand directoryname
if ("$source" -like "$sourcedir*"){
$destination=$source.Substring($sourcedir.Length)+".\"
}
else{
$destination=$_
}
copy-item $sourcedir\$_ -Destination $destination -WhatIf
}
AFAICT -Include works only with file names or directory names and not combinations i.e. paths. You can try something like this:
$files = 'MyProgram\.exe|MyProgram\.exe\.config|MyProgram\.pdb|XmlConfig\\.*?\.xml'
Get-ChildItem ..\bin\release -r | Where {!$_.PSIsContainer -and ($_.FullName -match $files)} |
Copy-Item -Dest .\test
With wildcards you could do it this way:
$files = #('*MyProgram.exe','*MyProgram.exe.config','*MyProgram.pdb','*\XmkConfig\*.xml')
Get-ChildItem ..\bin\release -r |
Foreach {$fn=$_.Fullname;$_} |
Where {!$_.PSIsContainer -and ($files | Where {$fn -like $_})} |
Copy-Item -Dest .\test