When using the rm command to delete files in Powershell, they are permanently deleted.
Instead of this, I would like to have the deleted item go to the recycle bin, like what happens when files are deleted through the UI.
How can you do this in PowerShell?
2017 answer: use the Recycle module
Install-Module -Name Recycle
Then run:
Remove-ItemSafely file
I like to make an alias called trash for this.
If you don't want to always see the confirmation prompt, use the following:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('d:\foo.txt','OnlyErrorDialogs','SendToRecycleBin')
(solution courtesy of Shay Levy)
It works in PowerShell pretty much the same way as Chris Ballance's solution in JScript:
$shell = new-object -comobject "Shell.Application"
$folder = $shell.Namespace("<path to file>")
$item = $folder.ParseName("<name of file>")
$item.InvokeVerb("delete")
Here is a shorter version that reduces a bit of work
$path = "<path to file>"
$shell = new-object -comobject "Shell.Application"
$item = $shell.Namespace(0).ParseName("$path")
$item.InvokeVerb("delete")
Here's an improved function that supports directories as well as files as input:
Add-Type -AssemblyName Microsoft.VisualBasic
function Remove-Item-ToRecycleBin($Path) {
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if ($item -eq $null)
{
Write-Error("'{0}' not found" -f $Path)
}
else
{
$fullpath=$item.FullName
Write-Verbose ("Moving '{0}' to the Recycle Bin" -f $fullpath)
if (Test-Path -Path $fullpath -PathType Container)
{
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else
{
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
}
}
Remove file to RecycleBin:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile('e:\test\test.txt','OnlyErrorDialogs','SendToRecycleBin')
Remove folder to RecycleBin:
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::Deletedirectory('e:\test\testfolder','OnlyErrorDialogs','SendToRecycleBin')
Here's slight mod to sba923s' great answer.
I've changed a few things like the parameter passing and added a -WhatIf to test the deletion for the file or directory.
function Remove-ItemToRecycleBin {
Param
(
[Parameter(Mandatory = $true, HelpMessage = 'Directory path of file path for deletion.')]
[String]$LiteralPath,
[Parameter(Mandatory = $false, HelpMessage = 'Switch for allowing the user to test the deletion first.')]
[Switch]$WhatIf
)
Add-Type -AssemblyName Microsoft.VisualBasic
$item = Get-Item -LiteralPath $LiteralPath -ErrorAction SilentlyContinue
if ($item -eq $null) {
Write-Error("'{0}' not found" -f $LiteralPath)
}
else {
$fullpath = $item.FullName
if (Test-Path -LiteralPath $fullpath -PathType Container) {
if (!$WhatIf) {
Write-Verbose ("Moving '{0}' folder to the Recycle Bin" -f $fullpath)
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else {
Write-Host "Testing deletion of folder: $fullpath"
}
}
else {
if (!$WhatIf) {
Write-Verbose ("Moving '{0}' file to the Recycle Bin" -f $fullpath)
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
else {
Write-Host "Testing deletion of file: $fullpath"
}
}
}
}
$tempFile = [Environment]::GetFolderPath("Desktop") + "\deletion test.txt"
"stuff" | Out-File -FilePath $tempFile
$fileToDelete = $tempFile
Start-Sleep -Seconds 2 # Just here for you to see the file getting created before deletion.
# Tests the deletion of the folder or directory.
Remove-ItemToRecycleBin -WhatIf -LiteralPath $fileToDelete
# PS> Testing deletion of file: C:\Users\username\Desktop\deletion test.txt
# Actually deletes the file or directory.
# Remove-ItemToRecycleBin -LiteralPath $fileToDelete
Here is a complete solution that can be added to your user profile to make 'rm' send files to the Recycle Bin. In my limited testing, it handles relative paths better than the previous solutions.
Add-Type -AssemblyName Microsoft.VisualBasic
function Remove-Item-toRecycle($item) {
Get-Item -Path $item | %{ $fullpath = $_.FullName}
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fullpath,'OnlyErrorDialogs','SendToRecycleBin')
}
Set-Alias rm Remove-Item-toRecycle -Option AllScope
Related
I am looking to find a way to compact and repair all the Access databases in a certain directory using Powershell via a script.
The VBA codes below work, but need one for Powershell:
Find all Access databases, and Compact and Repair
I am new to Powershell so will be grateful for the assistance.
Thanks
You may try this.
Add-Type -AssemblyName Microsoft.Office.Interop.Access
$rootfolder = 'c:\some\folder'
$createlog = $true # change to false if no log desired
$access = New-Object -ComObject access.application
$access.Visible = $false
$access.AutomationSecurity = 1
Get-ChildItem -Path $rootfolder -File -Filter *.accdb -Recurse -PipelineVariable file | ForEach-Object {
$newname = Join-Path $file.Directory ("{0}_compacted{1}" -f $file.BaseName,$file.Extension)
$message = #"
Current file: {0}
Output file: {1}
"# -f $file.FullName,$newname
Write-Host $message -ForegroundColor Cyan
$access.CompactRepair($file.fullname,$newname,$createlog)
}
$access.Quit()
This will output each compacted database as the name of the original file with _compacted appended to the name (before the extension.) I have tested this in every way except actually compacting databases.
Edit
Regarding your comment, a few minor changes should achieve the desired result. Keep in mind that this will put all new files in the same folder. This may not be an issue for your case but if there are duplicate file names you will have problems.
$rootfolder = 'c:\some\folder'
$destination = 'c:\some\other\folder'
$todaysdate = get-date -format '_dd_MM_yyyy'
Add-Type -AssemblyName Microsoft.Office.Interop.Access
$createlog = $true # change to false if no log desired
$access = New-Object -ComObject access.application
$access.Visible = $false
$access.AutomationSecurity = 1
Get-ChildItem -Path $rootfolder -File -Filter *.accdb -Recurse -PipelineVariable file | ForEach-Object {
$newname = Join-Path $destination ("{0}$todaysdate{1}" -f $file.BaseName,$file.Extension)
$message = #"
Current file: {0}
Output file: {1}
"# -f $file.FullName,$newname
Write-Host $message -ForegroundColor Cyan
$access.CompactRepair($file.fullname,$newname,$createlog)
}
$access.Quit()
So I'm new to PowerShell, and I'm trying to get this function to work.
I have 2 ValidateSet arrays with 3 parameters. These parameters are supposed to change the file path and copy them over from one server to another. For some reason, I keep getting the command prompt for the parameters instead of them passing through. I'm guessing it's an issue with the ForEach-Object, but I'm at a loss. It IS, however, working for the $ArchivePath. I'm new, so please be gentle... TIA
param(
[Parameter(Mandatory = $true)]
[ValidateSet("One", "Two", "Three")]
[string[]]$Channel
,[Parameter(Mandatory = $true)]
[Alias('Phase')]
[ValidateSet("Devl", "Test", "Prod")]
[string[]]$Phase
,[Parameter(Mandatory = $false)]
[string]$FilenameFilter = '.csv'
,[Parameter(Mandatory = $false)]
[switch]$CreateTrigger
)
function ExitWithCode { param($exitcode) $host.SetShouldExit($exitcode); exit $exitcode }
$exitcode = 0
try {
# Get a list of files on the host server.
#
$files = Get-ChildItem -File -Path "\\ServerName\d\Extract\$Phase\FileTransfer\$Channel\Outbound"
# Destination directory.
#
$LocalPath = "\\ServerName\d\Extract\$Phase\FileTransfer\$Channel\Outbound" #for testing
# Set up folder name for Archive server. Formated as YYYYMMDDTHHMMSS YYYYMMDD --> Var_Date, 'T' --> Var_Constant & HHMMSS --> Var_Time
#
$Var_Date = get-date -UFormat "%Y-%m-%d"
$Var_Constant = 'T'
$Var_Time = get-date -UFormat "%H-%M-%S"
$Var_Fulldate = $Var_Date + $Var_Constant + $Var_Time
$ArchivePath = $env:USERPROFILE + "\Desktop\$Channel\$Var_Fulldate" #For testing
New-Item -Type Directory -Path $ArchivePath
if (-not (Test-Path -Path $ArchivePath -ErrorAction SilentlyContinue)) { $ArchivePath = $Env:TEMP }
#Look for files in Outbound directory and remove
Get-ChildItem -File -Path $LocalPath | ForEach-Object { Copy-Item $_.FullName } #Using copy instead of remove for test
$FileCount = 0
Write-Output Try2 #for testing
pause #for testing
foreach ($file in $files) {
if ((-not $file.IsDirectory) -and ($File.FullName -match $FilenameFilter)) {
$localfilename = $LocalPath + $file.Name
if (Test-Path $localfilename) { Copy-Item $localfilename }
try {
Copy-Item -Path $(Join-Path -Path $LocalPath -ChildPath $file.Name) -Destination $ArchivePath
#Remove files from outbound since they've been archived
#
#Remove-Item -Path $file.FullName
"Retrieved file $file"
$FileCount++
}
catch {
Write-Output Try13 #for testing
$exitcode = 13
"failed to retrieve $file"
}
finally {
$error.Clear()
}
}
}
}
catch {
Write-Output Try3
$exitcode = 14
}
finally {
Write-Output Try4
$error.Clear()
}
if ($CreateTrigger -and ($exitcode -eq 0) -and ($FileCount -gt 0)) {
New-Item -Path "$LocalPath\Trigger_File.trg" -ItemType File | Out-Null
}
#ExitWithCode $exitcode # take out for testing
The output:
PS C:\Users\me> \\Server\blah\USERS\me\My Documents\Folder\Get-FileName_t4.ps1
cmdlet Get-FileName_t4.ps1 at command pipeline position 1
Supply values for the following parameters:
Channel[0]: Three
Channel[1]:
Phase[0]: Devl
Phase[1]:
Directory: C:\Users\me\Desktop\Three
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 11/22/2019 12:17 PM 2019-11-22T12-17-23
Try2
Press Enter to continue...:
Retrieved file File1_20191122080912.csv
Retrieved file File2_20191122080922.csv
Retrieved file File3_20191122080925.csv
Retrieved file File4_20191122080932.csv
Retrieved file File5_20191122080933.csv
Retrieved file File6_20191122080933.csv
Try4
You are getting prompted because you're not passing in those parameters but Mandatory=$true is set on the arguments you are getting prompted for. Since your session is interactive, it asks you to input the correct values. If you don't want to get prompted, provide the mandatory arguments:
"\\Server\blah\USERS\me\My Documents\Folder\Get-FileName_t4.ps1" -Channel Three -Phase Dev1
A couple of other things I noticed:
You don't need to provide Mandatory=$false, as Mandatory is $false by default
Setting an alias of Phase for the -Phase argument is also redundant
I am trying to write something like a CSharp watch task in PowerShell. So, what I want to happen is when a CSharp file changes in a certain directory, it tries to find the csproj file and initiates a build.
Here's what I have currently
function Watch-CSharp-Files() {
set-location "D:\path\to\csharp\files\"
$originalPath = Get-Location
Write-host "Welcome to The Watcher. It keeps track of changing files in this solution directory (including subdirectories) and triggers a build when something changes..."
$existingEvents = get-eventsubscriber
foreach ($item in $existingEvents) {
Unregister-event -SubscriptionId $item.SubscriptionId
write-host "Unsubscribed existing event:" $item.Action.Name
}
$folder = get-location
$filter = '*.*'
$watcher = New-Object IO.FileSystemWatcher $folder, $filter -Property #{IncludeSubdirectories = $true;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}
Register-ObjectEvent $watcher Changed -SourceIdentifier FileChanged -Action {
$path = $Event.SourceEventArgs.FullPath
if ($path -match "(\.cs~|.cs$)") {
write-host "file changed: $path"
Invoke-Expression -Command "Find-And-Build-Project '$path'"
}
}
}
function Find-And-Build-Project([string]$path) {
write-host "BUILD PROJECT REPORTING FOR DUTY"
$pathParts = "$path".Split("\\")
$end = $pathParts.Count - 2 # skip the file name to the parent directory
$testPath = $pathParts[0..$end] -join "\"
write-host "testing path $testPath"
$csproj = Get-ChildItem -path $testPath *.csproj
For ($i = 0; $i -le 10; $i++) {
$newEnd = $end - $i
$newPath = $pathParts[0..$newEnd] -join "\"
$csproj = Get-ChildItem -path $newPath *.csproj
write-host "$i. trying: $newPath, csproj: $csproj"
if ($csproj) {
write-host "found on $i, at $newPath, $csproj"
break
}
}
write-host "Ready: $newPath\$csproj"
$msbuild = "C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe"
write-host "trying to MSBUILD"
& $msbuild ("$newPath\$csproj", "/target:Build", "/p:configuration=debug", "/verbosity:n")
}
Watch-CSharp-Files
What I have found is that within the function Find-And-Build-Project, the & $msbuild doesn't get invoked. But, I don't understand why.
Any ideas?
Ok, this helped me: https://stackoverflow.com/a/37724701/1326235.
This script targets the saved .cs file (or .cs~tmp[0-9].cs as Visual Studio seems to create), then searches back up the directory tree to find a .csproj file and builds it.
I published the module to the PowerShell Gallery, it's called CSharp-Watch
I've encountered a problem with my script, whenever I do a trashbin deletion and the map location has a folder within a folder that has an file that has to be deleted, it will not work.
The problem is that I need to add a recurse and -erroraction silently continue. But I dont know how I can do that since the methods for moving files to trash bin are quite hard in my opinion.
Could you guys help me out?
My code:
## Top of the script
param(
[Parameter(Mandatory=$true)]
[ValidateRange(0,99999)]
[int]$minutes,
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_})]
[string]$maplocation,
[Parameter(Mandatory=$true)]
[ValidateSet("Direct","TrashBin")]
[string]$consequence
)
## Variables
$maxAge = (Get-Date).AddMinutes(-$minutes)
$files = Get-ChildItem $maplocation -Recurse
$time = get-date
$fortrashbin = $maplocation + '\' + $file
##
foreach ($file in $files)
{
if ($file.lastwritetime -lt $maxage)
{
switch ($consequence)
{
"direct"
{
write-verbose "File Found $file" -verbose
remove-item $file.fullname -recurse -erroraction silentlycontinue
write-verbose "Deleting $file" -verbose
}
"trashbin" {
write-verbose "File Found $file" -verbose
write-verbose "moving $file to trashbin" -verbose
Add-Type -AssemblyName Microsoft.VisualBasic
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile($fortrashbin,'OnlyErrorDialogs','SendToRecycleBin')
}
}
}
}
I think you don't have to iterate over each file if you delete a folder. Instead, you can use the DeleteDirectory method:
if (Test-Path -Path $maplocation -PathType Container)
{
# delete directory
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(
$maplocation,'OnlyErrorDialogs','SendToRecycleBin')
}
else
{
# delete folder
[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(
$maplocation,'OnlyErrorDialogs','SendToRecycleBin')
}
Please also consider to replace your $consequence parameter with a [switch] like:
param
(
[Parameter(Mandatory=$true)]
[ValidateRange(0,99999)]
[int]$minutes,
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_})]
[string]$maplocation,
[Parameter(Mandatory=$false)]
[switch]$Direct
)
Now you can invoke your script without any parameters to delete the location to trashbin or with the -Direct switch to permanently remove it.
As part of my continuous integration build I am creating an SQL script. This SQL script has to be checked back in to TFS after it is generated. I'm using the TFS Powertools in Powershell.
The code I used on my machine was:
Add-TfsPendingChange -Add -Item $filename | New-TfsChangeSet
This worked fine on my dev box because the folder I was in is mapped to a TFS workspace. When I move it to my build server it no longer works because TeamCity doens't map it's checkouts to a workspace it just pulls the files down.
How do I check files into a specific folder in TFS without being in a mapped workspace? Is that even possible?
I worked on something to do this for our continuous delivery project using GO. I got it working using a combination of PowerShell and the .NET assemblies provider with Team Explorer. I could not get it working purely in PowerShell (although there may be a way!)
The following script will check-in whatever is contained in the material path which is supplied as a parameter into the specified server path (also a parameter). You can also specify credentials to use and a url for the TFS server.
This code requires either Visual Studio or the TFS Team Explorer client to be installed. You need to provide the directory location of the assemblies to the script in the AssemblyPath parameter. If these assemblies are not found then the script will error and show which ones are missing.
NOTE: The code has not been checked for a while so there may be typos etc. If you have any problems let me know and I will try to help.
[CmdletBinding(PositionalBinding=$false)]
Param(
[Parameter(Mandatory)] [string] $ServerUrl,
[Parameter(Mandatory)] [string] $ServerPath,
[Parameter(Mandatory=$False)] [string] $Domain = "",
[Parameter(Mandatory=$False)] [string] $Username = "",
[Parameter(Mandatory=$False)] [System.Security.SecureString] $Password,
[Parameter(Mandatory)] [string] [ValidateScript({($_ -eq $null) -or (Test-Path -Path $_ -PathType Container)})] $MaterialPath,
[Parameter(Mandatory)] [string] [ValidateScript({ Test-Path -Path $_ -PathType Container})] $AssemblyPath
)
<#
.SYNOPSIS
Responsible for checking in files into Source Control
.DESCRIPTION
#>
$clientDllName = "Microsoft.TeamFoundation.Client.dll"
$commonDllName = "Microsoft.TeamFoundation.Common.dll"
$versionControlClientDllName = "Microsoft.TeamFoundation.VersionControl.Client.dll"
$versionControlClientCommonDllName = "Microsoft.TeamFoundation.VersionControl.Common.dll"
#Create global variables to hold the value of Debug and Verbose action preferences which can then be used for all module function calls and passed into the remote session.
$verboseParameter = $PSCmdlet.MyInvocation.BoundParameters["Verbose"]
if ($verboseParameter -ne $null)
{
$Global:Verbose = [bool]$verboseParameter.IsPresent
}
else
{
$Global:Verbose = $false
}
$debugParameter = $PSCmdlet.MyInvocation.BoundParameters["Debug"]
if ($debugParameter -ne $null)
{
$Global:Debug = [bool]$debugParameter.IsPresent
}
else
{
$Global:Debug = $false
}
$scriptName = $(Split-Path -Leaf $PSCommandPath)
#Ensure any errors cause failure
$ErrorActionPreference = "Stop"
Write-Host "Running script ""$scriptName"" as user ""$env:USERDOMAIN\$env:USERNAME"""
#Check assembly path is a valid directory
If (Test-Path -Path $AssemblyPath -PathType Container)
{
Write-Host "Loading required assemblies from assembly path ""$AssemblyPath"""
$clientDllPath = Join-Path -Path $AssemblyPath -ChildPath $clientDllName
$commonDllPath = Join-Path -Path $AssemblyPath -ChildPath $commonDllName
$versionControlClientDllPath = Join-Path -Path $AssemblyPath -ChildPath $versionControlClientDllName
$versionControlClientCommonDllPath = Join-Path -Path $AssemblyPath -ChildPath $versionControlClientCommonDllName
If (!Test-Path -Path $clientDllPath -PathType Leaf)
{
Throw "Required assembly ""$clientDllName"" not found at path ""$clientDllPath"""
}
If (!Test-Path -Path $commonDllPath -PathType Leaf)
{
Throw "Required assembly ""$commonDllName"" not found at path ""$commonDllPath"""
}
If (!Test-Path -Path $versionControlClientDllPath -PathType Leaf)
{
Throw "Required assembly ""$versionControlClientDllName"" not found at path ""$versionControlClientDllPath"""
}
If (!Test-Path -Path $versionControlClientCommonDllPath -PathType Leaf)
{
Throw "Required assembly ""$versionControlClientCommonDllName"" not found at path ""$versionControlClientCommonDllPath"""
}
#Load the Assemblies
[Reflection.Assembly]::LoadFrom($clientDllPath) | Out-Null
[Reflection.Assembly]::LoadFrom($commonDllPath)| Out-Null
[Reflection.Assembly]::LoadFrom($versionControlClientDllPath) | Out-Null
[Reflection.Assembly]::LoadFrom($versionControlClientCommonDllPath) | Out-Null
#If the credentials have been specified then create a credential object otherwise we will use the default ones
If ($Username -and $Password)
{
$creds = New-Object System.Net.NetworkCredential($Username,$Password,$Domain)
Write-Host "Created credential object for user ""$($creds.UserName)"" in domain ""$($creds.Domain)"""
$tfsProjectCollection = New-Object Microsoft.TeamFoundation.Client.TFSTeamProjectCollection($ServerUrl, $creds)
}
else
{
Write-Host "Using default credentials for user ""$Env:Username"""
$tfsProjectCollection = New-Object Microsoft.TeamFoundation.Client.TFSTeamProjectCollection($ServerUrl)
}
$versionControlType = [Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer]
$versionControlServer = $tfsProjectCollection.GetService($versionControlType)
Write-Host "Version control server authenticated user: $($versionControlServer.AuthenticatedUser)"
#Create a local path in the temp directory to hold the workspace
$LocalPath = Join-Path -Path $env:TEMP -ChildPath $([System.Guid]::NewGuid().ToString())
$null = New-Item -Path $LocalPath -ItemType Directory
#Create a "workspace" and map a local folder to a TFS location
$workspaceName = "PowerShell Workspace_{0}" -f [System.Guid]::NewGuid().ToString()
$workspace = $versionControlServer.CreateWorkspace($workspaceName, $versionControlServer.AuthenticatedUser)
$workingfolder = New-Object Microsoft.TeamFoundation.VersionControl.Client.WorkingFolder($ServerPath,$LocalPath)
$result = $workspace.CreateMapping($workingFolder)
$result = $workspace.Get() #Get the latest version into the workspace
Write-Host "Copying files from materials path ""$MaterialPath"" to temporary workspace path ""$LocalPath"""
robocopy $MaterialPath $LocalPath /s | Out-Null
$checkInComments = "Files automatically checked in by PowerShell script ""$scriptName"""
#Submit file as a Pending Change and submit the change
$result = $workspace.PendAdd($LocalPath,$true)
$pendingChanges = $workspace.GetPendingChanges()
Write-Host "Getting pending changes"
#Only try to check in if there are changes
If ($pendingChanges -ne $null)
{
If ($pendingChanges.Count -gt 0)
{
$changeSetId = $workspace.CheckIn($pendingChanges,$checkInComments)
Write-Host "Successfully checked in ""$($pendingChanges.Count)"" changes using changeset id ""$changeSetId"""
}
else
{
Write-Host "No changes to check-in"
}
}
else
{
Write-Host "No changes to check-in"
}
Write-Host "Deleting workspace and temporary folders"
$result = $workspace.Delete()
$null = Remove-Item -Path $LocalPath -Recurse -Force
}
else
{
Write-Error "The path to required assemblies ""$AssemblyPath"" cannot be found"
}