I wrote out a simple PowerShell script that backs up a directory to C:\ and then deletes any of the backup folders when its age = X days.
For some reason, when I use the Remove-Item cmdlet I'm getting a Remove-Item: Cannot find path 'C:\Windows\system32\ [Sub-Folder name]' because it does not exist error.
Below is the snippet:
$TargetFolder = "C:\Folder\"
$Folders = get-childitem -path $TargetFolder
foreach ($Folder in $Folders)
{
remove-item $Folder -recurse -force
}
Within the $TargetFolder = "C:\Folder\", there are a few sub-folders.
Examples: C:\Folder\SubfolderA, C:\Folder\SubfolderB, etc.
When I do a Write-Host for $Folder it lists SubFolderA, SubFolderB, etc, correctly so I'm not exactly sure why I'm getting a Cannot find path error.
It seems that you want to do this on the basis of the directory LastWriteTime, but you did not mention -Directory on Get-ChildItem.
[cmdletbinding()]
Param()
$TargetFolder = "C:\Users\lit\Documents"
$Folders = Get-ChildItem -Path $TargetFolder -Directory
$Days = 80
foreach ($Folder in $Folders) {
if ($Folder.LastWriteTime -lt (Get-Date).AddDays(-$Days)) {
Write-Verbose "Deleting directory $($Folder.FullName)"
Remove-Item -WhatIf "$($Folder.FullName)" -Recurse -Force
}
}
tl;dr
To ensure that Remove-Item correctly identifies a directory object as returned by Get-ChildItem (an instance of type [System.IO.DirectoryInfo]):
When passing the object as a parameter value (argument), in Windows PowerShell (no longer in PowerShell Core) you must use .FullName for the command to work reliably:
Remove-Item -LiteralPath $Folder.FullName ... # !! Note the need for .FullName
-LiteralPath is not strictly needed, but is the more robust choice, given that Remove-Item $Folder.FullName implicitly binds to the -Path parameter instead, which interprets its argument as a wildcard expression; often this will make no difference, but it can.
When using the pipeline, you can pass the objects as-is.
Get-ChildItem -Directory | Remove-Item ...
The surprising need to use .FullName is the result of a design quirk; the resulting behavior and its implications are discussed below; a fix has been proposed in this GitHub issue.
Liturgist's helpful answer and AJK's helfpul answer contain complementary pieces of the solution (Liturgist's answer has since been amended to provide a complete solution):
To limit what Get-ChildItem returns to directories (folders), you must use -Directory (PSv3+).
To unambiguously identify a filesystem object when passing it to Remove-Object as a parameter that is converted to a string, its full path must be specified.
Note: The situational filename-only stringification described below affects only Windows PowerShell; fortunately, the problem has been fixed in PowerShell Core, fortunately.
In the case at hand, given that $Folder contains a [System.IO.DirectoryInfo] object as returned by Get-ChildItem, $Folder.FullName is simplest (but constructing the path by prepending $TargetPath works too).
In Windows PowerShell, even though $Folder is a [System.IO.DirectoryInfo] object that does contain the full path information, when converted to a string it situationally may only expand to its mere directory name (last path component) - it depends on how the [System.IO.DirectoryInfo] and [System.IO.FileInfo]instances were obtained, namely:
If Get-ChildItem was called either without a path argument or via a path argument that is a directory, the output objects stringify to their mere file/directory name - see this answer for details.
Simple example: $d = (Get-ChildItem -Directory $HOME)[0]; "$d" yields Contacts, for instance, not C:\Users\jdoe\Contacts.
By contrast, if you pass such objects via the pipeline to Remove-Item, PowerShell does use the full path.
Important: If you do pass the target folder as a parameter and neglect to specify the full path while not in the same location as the target folder, the name may therefore interpreted as relative to the current location, and you'll either get a Cannot find path error - as you saw - or, even worse, you may end up deleting a different folder by the same name if one happens to be present in your current location (directory).
As stated, you can avoid the full-path problem by piping the folder objects returned by Get-ChildItem to Remove-Item, which also enables a more elegant, single-pipeline solution (PSv3+):
$TargetFolder = "C:\Folder"
$Days = 5
Get-ChildItem -Directory $TargetFolder |
Where-Object LastWriteTime -lt (Get-Date).Date.AddDays(-$Days) |
Remove-Item -WhatIf -Force -Recurse
Remove the -WhatIf to perform actual removal.
Try executing remove-item on the full path, e.g.
$TargetFolder = "C:\Folder\"
$Folders = get-childitem -path $TargetFolder
foreach ($Folder in $Folders)
{
remove-item $TargetFolder$Folder -recurse -force
}
Related
Could you help me again with a powershell script?
I want to check if multiple folders exist, if they exist then delete the complete folder.
Also provide information if the folder has been deleted or information if the folder does not exist.
I now use the script below for multiple files. (thanks to good help)
I want to use the same script for 1 or more folders.
For example, delete folder c:\test1\ and c:test2
Folders may be deleted, even if they still contain files.
$paths = "c:\test\1.txt", "c:\test\2.txt", "c:\test\3.txt"
foreach($filePath in $paths)
{
if (Test-Path $filePath) {
Remove-Item $filePath -verbose
} else {
Write-Host "Path doesn't exits"
}
}
I'm not super handy with powershell, hope you can help me with this again.
Thanks
Tom
To remove a directory (folder) that has content, you must use the -Recurse switch with Remove-Item - otherwise, an interactive confirmation prompt is presented.
A given path existing doesn't necessarily mean that it is a directory - it may be a file. To specifically test if a given path is a directory / file, use -PathType Container / -PathType Leaf with Test-Path.
While only strictly necessary when paths happen to contain [ characters, the robust way to pass literal paths is via the -LiteralPath parameter that file-processing cmdlets support - by contrast, the first positional argument typically binds to the -Path parameter (e.g., Test-Path foo is the same as Test-Path -Path foo), which interprets its argument(s) as wildcard expressions.
Applied to your use case (note that no attempt is made to distinguish files from directories):
# Input directory paths.
$paths = 'c:\test1', 'c:\test2', 'c:\test3'
foreach ($path in $paths) {
if (Test-Path -LiteralPath $path) {
Remove-Item -LiteralPath $path -Verbose -Recurse -WhatIf
} else {
"Path doesn't exist: $path"
}
}
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Another, more efficient option is to use Get-Item to get objects representing the file-system items, if they exist, and pipe them to Remove-Item:
$paths = 'c:\test1', 'c:\test2', 'c:\test3'
Get-Item -LiteralPath $paths -ErrorAction SilentlyContinue -ErrorVariable errs |
Remove-Item -Recurse -Verbose -WhatIf
if ($errs) {
"The following path(s) do not exist: $($errs.TargetObject)"
}
Note the use of -ErrorAction SilentlyContinue to silence errors resulting from nonexistent paths, and -ErrorVariable errs in order to collect these errors in self-chosen variable $errs.
The .TargetObject property of the [System.Management.Automation.ErrorRecord] instances collected in $errs then contains the path that triggered the error, resolved to a full path.
Function Zip
{
Param
(
[string]$zipFile
,
[string[]]$toBeZipped
)
$CurDir = Get-Location
Set-Location "C:\Program Files\7-Zip"
.\7z.exe A -tzip $zipFile $toBeZipped | Out-Null
Set-Location $CurDir
}
$Now = Get-Date
$Days = "60"
$TargetFolder = "C:\users\Admin\Downloads\*.*"
$LastWrite = $Now.AddDays(-$Days)
$Files = Get-Childitem $TargetFolder -Recurse | Where {$_.LastWriteTime -le "$LastWrite"}
$Files
Zip C:\Users\Admin\Desktop\TEST.zip $Files
I am testing out this script I found online. My problem is that instead of zipping the files in the target folder, it is copying and zipping the contents of the 7-zip program file folder. What could cause this? Thanks in advance
Pass the files as full paths to the Zip function, using their .FullName property (PSv3+ syntax):
Zip C:\Users\Admin\Desktop\TEST.zip $Files.FullName
The problem is that, in Windows PowerShell, the [System.IO.FileInfo] instances returned by Get-ChildItem situationally[1] stringify to their file names only, which is what happened in your case, so your Zip function then interpreted the $toBeZipped values as relative to the current location, which is C:\Program Files\7-Zip at that point.
That said, it's better not to use Set-Location in your function altogether, so that in the event that you do want to pass actual relative paths, they are correctly interpreted as relative to the current location:
Function Zip {
Param
(
[Parameter(Mandatory)] # make sure a value is passed
[string]$zipFile
,
[Parameter(Mandatory)] # make sure a value is passed
[string[]]$toBeZipped
)
# Don't change the location, use & to invoke 7z by its full path.
$null = & "C:\Program Files\7-Zip\7z.exe" A -tzip $zipFile $toBeZipped
# You may want to add error handling here.
}
[1] When Get-ChildItem output stringifies to file names only:
Note:
If Get-ChildItem output is to be passed to other file-processing cmdlets, say Rename-Item, the problem can be bypassed by providing input to them via the pipeline, which implicitly binds to the target cmdlet's -LiteralPath parameter by full path - see this answer for more information.
The related Get-Item cmdlet output always stringifies to the full path, fortunately.
In PowerShell (Core) v6.1+, Get-ChildItem too always stringifies to the full path, fortunately.
The following therefore only applies to Get-ChildItem in Windows PowerShell:
The problem is twofold:
Even PowerShell's built-in cmdlets bind file / directory arguments (parameter values - as opposed to input via the pipeline) not as objects, but as strings (changing this behavior is being discussed in GitHub issue #6057).
Therefore, for robust argument-passing, you need to ensure that your Get-ChildItem output consistently stringifies to full paths, which Get-ChildItem does not guarantee - and it's easy to forget when name-only stringification occurs or even that you need to pay attention to it at all.
Always passing the .FullName property values instead is the simplest workaround or, for reliable operation with any PowerShell provider, not just the filesystem, .PSPath.
[System.IO.FileInfo] and [System.IO.DirectoryInfo] instances output by a Get-ChildItem command stringify to their file names only, if and only if:
If one or more literal directory paths are passed to -LiteralPath or -Path (possibly as the 1st positional argument) or no path at all is passed (target the current location); that is, if the contents of directories are enumerated.
and does not also use the -Include / -Exclude parameters (whether -Filter is used makes no difference).
By contrast, whether or not the following are also present makes no difference:
-Filter (optionally as the 2nd positional argument, but note that specifying a wildcard expression such as *.txt as the 1st (and possibly only) positional argument binds to the -Path parameter)
-Recurse (by itself, but note that it is often combined with -Include / -Exclude)
Example commands:
# NAME-ONLY stringification:
Get-ChildItem | % ToString # no target path
Get-ChildItem . | % ToString # path is literal dir.
Get-ChildItem . *.txt | % ToString # path is literal dir., combined with -Filter
# FULL PATH stringification:
Get-ChildItem foo* | % ToString # non-literal path (wildcard)
Get-ChildItem -Recurse -Include *.txt | % ToString # use of -Include
Get-ChildItem file.txt | % ToString # *file* path
If you (temporarily) disable the |Out-Null you'll see what error msg pass along.
$Files contains objects not just an array of file names.
By default powershell tries to stringify this using the Name property which doesn't contain the path - so 7zip can't find the files as you also change the path to the 7zip folder (and -recurse collecting $files)
So change the line
$Files = Get-Childitem $TargetFolder -Recurse | Where {$_.LastWriteTime -le "$LastWrite"}
and append
| Select-Object -ExpandProperty FullName
A slightly reformatted verson ofyour source:
Function Zip{
Param (
[string]$zipFile,
[string[]]$toBeZipped
)
& "C:\Program Files\7-Zip\7z.exe" A -tzip $zipFile $toBeZipped | Out-Null
}
$Days = "60"
$LastWrite = (Get-Date).Date.AddDays(-$Days)
$TargetFolder = "$($ENV:USERPROFILE)\Downloads\*"
$Files = Get-Childitem $TargetFolder -Recurse |
Where {$_.LastWriteTime -le $LastWrite} |
Select-Object -ExpandProperty FullName
$Files
Zip "$($ENV:USERPROFILE)\Desktop\TEST.zip" $Files
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.
I'm using the following command to copy a directory tree from one folder to another.
Copy-Item $SOURCE $DEST -Filter {PSIsContainer} -Recurse -Force -Verbose
The verbose option is correctly showing each folder that is copied. However, I would like to tell the Verbose option to only shows the first level of the subfolders that are copied. Hence the subfolders/subfolders/... etc wouldn't appear.
Is it possible?
Instead of using the -Verbose option, you could use the -PassThru option to process the successfully processed items via the pipeline. In the following example, I am assuming that $DEST is the existing directory in which the newly copied directory will appear. (You cannot call Get-Item on non-existant objects.)
$SOURCE = Get-Item "foo"
$DEST = Get-Item "bar"
Copy-Item $SOURCE $DEST -Filter {PSIsContainer} -Recurse -Force -PassThru | Where-Object {
# Get the parent object. The required member is different between
# files and directories, which makes this a bit more complex than it
# might have been.
if ($_.GetType().Name -eq "DirectoryInfo") {
$directory = $_.Parent
} else {
$directory = $_.Directory
}
# Select objects as required, in this case only allow through
# objects where the second level parent is the pre-existing target
# directory.
$directory.Parent.FullName -eq $DEST.FullName
}
Count the number of backslashes in the path and add logic to select first level only perhaps. Something like this perhaps?
$Dirs=get-childitem $Source -Recurse | ?{$_.PSIsContainer}
Foreach ($Dir in $Dirs){
$Level=([regex]::Match($Dir.FullName,"'b")).count
if ($Level -eq 1){Copy-Item $Dir $DEST -Force -Verbose}
else{Copy-Item $Dir $DEST -Force}}
*Edited to include looping and logic per requirements
I would suggest using robocopy instead of copy-item. Its /LEV:n switch sounds like it's exactly what you're looking for. Example (you'll need to test & tweak to meet your requirements):
robocopy $source $dest /LEV:2
robocopy has approximately 7 gazillion options you can specify to get some very useful and interesting behavior out of it.
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