Remove-item isn't removing non-empty folders [duplicate] - powershell

When using the Remove-Item command, even utilizing the -r and -Force parameters, sometimes the following error message is returned:
Remove-Item : Cannot remove item C:\Test Folder\Test Folder\Target: The directory is not empty.
Particularly, this happens when the directory to be removed is opened in Windows Explorer.
Now, while it is possible to avoid this simply by having Windows Explorer closed or not browsing that location, I work my scripts in a multi-user environment where people sometimes just forget to close Windows Explorer windows, I am interested in a solution for deleting entire folders and directories even if they are opened in Windows Explorer.
Is there an option more powerful than -Force that I can set to achieve this?
To reliably reproduce this, create the folder C:\Test Folder\Origin and populate it with some files and subfolders (important), then take the following script or one like it and execute it once. Now open one of the subfolders of C:\Test Folder\Target (in my case, I used C:\Test Folder\Target\Another Subfolder containing A third file.txt), and try running the script again. You will now get the error. If you run the script a third time, you will not get the error again (depending on circumstances that I have yet to determine, though, the error sometimes occurs the second time and then never again, and at other times it occurs every second time).
$SourcePath = "C:\Test Folder\Origin"
$TargetPath = "C:\Test Folder\Target"
if (Test-Path $TargetPath) {
Remove-Item -r $TargetPath -Force
}
New-Item -ItemType directory -Path $TargetPath
Copy-Item $SourcePath -Destination $TargetPath -Force -Recurse -Container

Update: Starting with (at least [1]) Windows 10 version 20H2 (I don't know that Windows Server version and build that corresponds to; run winver.exe to check your version and build), the DeleteFile Windows API function now exhibits synchronous behavior on supported file-systems, including NTFS, which implicitly solves the problems with PowerShell's Remove-Item and .NET's System.IO.File.Delete / System.IO.Directory.Delete (but, curiously, not with cmd.exe's rd /s).
This is ultimately only a timing issue: the last handle to a subdirectory may not be closed yet at the time an attempt is made to the delete the parent directory - and this is a fundamental problem, not restricted to having File Explorer windows open:
Incredibly, the Windows file and directory removal API is asynchronous: that is, by the time the function call returns, it is not guaranteed that removal has completed yet.
Regrettably, Remove-Item fails to account for that - and neither do cmd.exe's rd /s and .NET's [System.IO.Directory]::Delete() - see this answer for details.
This results in intermittent, unpredictable failures.
The workaround comes courtesy of in this YouTube video (starts at 7:35), a PowerShell implementation of which is below:
Synchronous directory-removal function Remove-FileSystemItem:
Important:
The synchronous custom implementation is only required on Windows, because the file-removal system calls on Unix-like platforms are synchronous to begin with. Therefore, the function simply defers to Remove-Item on Unix-like platforms. On Windows, the custom implementation:
requires that the parent directory of a directory being removed be writable for the synchronous custom implementation to work.
is also applied when deleting directories on any network drives.
What will NOT prevent reliable removal:
File Explorer, at least on Windows 10, does not lock directories it displays, so it won't prevent removal.
PowerShell doesn't lock directories either, so having another PowerShell window whose current location is the target directory or one of its subdirectories won't prevent removal (by contrast, cmd.exe does lock - see below).
Files opened with FILE_SHARE_DELETE / [System.IO.FileShare]::Delete (which is rare) in the target directory's subtree also won't prevent removal, though they do live on under a temporary name in the parent directory until the last handle to them is closed.
What WILL prevent removal:
If there's a permissions problem (if ACLs prevent removal), removal is aborted.
If an indefinitely locked file or directory is encountered, removal is aborted. Notably, that includes:
cmd.exe (Command Prompt), unlike PowerShell, does lock the directory that is its current directory, so if you have a cmd.exe window open whose current directory is the target directory or one of its subdirectories, removal will fail.
If an application keeps a file open in the target directory's subtree that was not opened with file-sharing mode FILE_SHARE_DELETE / [System.IO.FileShare]::Delete (using this mode is rare), removal will fail. Note that this only applies to applications that keep files open while working with their content. (e.g., Microsoft Office applications), whereas text editors such as Notepad and Visual Studio Code, by contrast, do not keep they've loaded open.
Hidden files and files with the read-only attribute:
These are quietly removed; in other words: this function invariably behaves like Remove-Item -Force.
Note, however, that in order to target hidden files / directories as input, you must specify them as literal paths, because they won't be found via a wildcard expression.
The reliable custom implementation on Windows comes at the cost of decreased performance.
function Remove-FileSystemItem {
<#
.SYNOPSIS
Removes files or directories reliably and synchronously.
.DESCRIPTION
Removes files and directories, ensuring reliable and synchronous
behavior across all supported platforms.
The syntax is a subset of what Remove-Item supports; notably,
-Include / -Exclude and -Force are NOT supported; -Force is implied.
As with Remove-Item, passing -Recurse is required to avoid a prompt when
deleting a non-empty directory.
IMPORTANT:
* On Unix platforms, this function is merely a wrapper for Remove-Item,
where the latter works reliably and synchronously, but on Windows a
custom implementation must be used to ensure reliable and synchronous
behavior. See https://github.com/PowerShell/PowerShell/issues/8211
* On Windows:
* The *parent directory* of a directory being removed must be
*writable* for the synchronous custom implementation to work.
* The custom implementation is also applied when deleting
directories on *network drives*.
* If an indefinitely *locked* file or directory is encountered, removal is aborted.
By contrast, files opened with FILE_SHARE_DELETE /
[System.IO.FileShare]::Delete on Windows do NOT prevent removal,
though they do live on under a temporary name in the parent directory
until the last handle to them is closed.
* Hidden files and files with the read-only attribute:
* These are *quietly removed*; in other words: this function invariably
behaves like `Remove-Item -Force`.
* Note, however, that in order to target hidden files / directories
as *input*, you must specify them as a *literal* path, because they
won't be found via a wildcard expression.
* The reliable custom implementation on Windows comes at the cost of
decreased performance.
.EXAMPLE
Remove-FileSystemItem C:\tmp -Recurse
Synchronously removes directory C:\tmp and all its content.
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium', DefaultParameterSetName='Path', PositionalBinding=$false)]
param(
[Parameter(ParameterSetName='Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]] $Path
,
[Parameter(ParameterSetName='Literalpath', ValueFromPipelineByPropertyName)]
[Alias('PSPath')]
[string[]] $LiteralPath
,
[switch] $Recurse
)
begin {
# !! Workaround for https://github.com/PowerShell/PowerShell/issues/1759
if ($ErrorActionPreference -eq [System.Management.Automation.ActionPreference]::Ignore) { $ErrorActionPreference = 'Ignore'}
$targetPath = ''
$yesToAll = $noToAll = $false
function trimTrailingPathSep([string] $itemPath) {
if ($itemPath[-1] -in '\', '/') {
# Trim the trailing separator, unless the path is a root path such as '/' or 'c:\'
if ($itemPath.Length -gt 1 -and $itemPath -notmatch '^[^:\\/]+:.$') {
$itemPath = $itemPath.Substring(0, $itemPath.Length - 1)
}
}
$itemPath
}
function getTempPathOnSameVolume([string] $itemPath, [string] $tempDir) {
if (-not $tempDir) { $tempDir = [IO.Path]::GetDirectoryName($itemPath) }
[IO.Path]::Combine($tempDir, [IO.Path]::GetRandomFileName())
}
function syncRemoveFile([string] $filePath, [string] $tempDir) {
# Clear the ReadOnly attribute, if present.
if (($attribs = [IO.File]::GetAttributes($filePath)) -band [System.IO.FileAttributes]::ReadOnly) {
[IO.File]::SetAttributes($filePath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
}
$tempPath = getTempPathOnSameVolume $filePath $tempDir
[IO.File]::Move($filePath, $tempPath)
[IO.File]::Delete($tempPath)
}
function syncRemoveDir([string] $dirPath, [switch] $recursing) {
if (-not $recursing) { $dirPathParent = [IO.Path]::GetDirectoryName($dirPath) }
# Clear the ReadOnly attribute, if present.
# Note: [IO.File]::*Attributes() is also used for *directories*; [IO.Directory] doesn't have attribute-related methods.
if (($attribs = [IO.File]::GetAttributes($dirPath)) -band [System.IO.FileAttributes]::ReadOnly) {
[IO.File]::SetAttributes($dirPath, $attribs -band -bnot [System.IO.FileAttributes]::ReadOnly)
}
# Remove all children synchronously.
$isFirstChild = $true
foreach ($item in [IO.directory]::EnumerateFileSystemEntries($dirPath)) {
if (-not $recursing -and -not $Recurse -and $isFirstChild) { # If -Recurse wasn't specified, prompt for nonempty dirs.
$isFirstChild = $false
# Note: If -Confirm was also passed, this prompt is displayed *in addition*, after the standard $PSCmdlet.ShouldProcess() prompt.
# While Remove-Item also prompts twice in this scenario, it shows the has-children prompt *first*.
if (-not $PSCmdlet.ShouldContinue("The item at '$dirPath' has children and the -Recurse switch was not specified. If you continue, all children will be removed with the item. Are you sure you want to continue?", 'Confirm', ([ref] $yesToAll), ([ref] $noToAll))) { return }
}
$itemPath = [IO.Path]::Combine($dirPath, $item)
([ref] $targetPath).Value = $itemPath
if ([IO.Directory]::Exists($itemPath)) {
syncremoveDir $itemPath -recursing
} else {
syncremoveFile $itemPath $dirPathParent
}
}
# Finally, remove the directory itself synchronously.
([ref] $targetPath).Value = $dirPath
$tempPath = getTempPathOnSameVolume $dirPath $dirPathParent
[IO.Directory]::Move($dirPath, $tempPath)
[IO.Directory]::Delete($tempPath)
}
}
process {
$isLiteral = $PSCmdlet.ParameterSetName -eq 'LiteralPath'
if ($env:OS -ne 'Windows_NT') { # Unix: simply pass through to Remove-Item, which on Unix works reliably and synchronously
Remove-Item #PSBoundParameters
} else { # Windows: use synchronous custom implementation
foreach ($rawPath in ($Path, $LiteralPath)[$isLiteral]) {
# Resolve the paths to full, filesystem-native paths.
try {
# !! Convert-Path does find hidden items via *literal* paths, but not via *wildcards* - and it has no -Force switch (yet)
# !! See https://github.com/PowerShell/PowerShell/issues/6501
$resolvedPaths = if ($isLiteral) { Convert-Path -ErrorAction Stop -LiteralPath $rawPath } else { Convert-Path -ErrorAction Stop -path $rawPath}
} catch {
Write-Error $_ # relay error, but in the name of this function
continue
}
try {
$isDir = $false
foreach ($resolvedPath in $resolvedPaths) {
# -WhatIf and -Confirm support.
if (-not $PSCmdlet.ShouldProcess($resolvedPath)) { continue }
if ($isDir = [IO.Directory]::Exists($resolvedPath)) { # dir.
# !! A trailing '\' or '/' causes directory removal to fail ("in use"), so we trim it first.
syncRemoveDir (trimTrailingPathSep $resolvedPath)
} elseif ([IO.File]::Exists($resolvedPath)) { # file
syncRemoveFile $resolvedPath
} else {
Throw "Not a file-system path or no longer extant: $resolvedPath"
}
}
} catch {
if ($isDir) {
$exc = $_.Exception
if ($exc.InnerException) { $exc = $exc.InnerException }
if ($targetPath -eq $resolvedPath) {
Write-Error "Removal of directory '$resolvedPath' failed: $exc"
} else {
Write-Error "Removal of directory '$resolvedPath' failed, because its content could not be (fully) removed: $targetPath`: $exc"
}
} else {
Write-Error $_ # relay error, but in the name of this function
}
continue
}
}
}
}
}
[1] I've personally verified that the issue is resolved in version 20H2, by running the tests in GitHub issue #27958 for hours without failure; this answer suggests that the problem was resolved as early as version 1909, starting with build 18363.657, but Dinh Tran finds that the issue is not resolved as of build 18363.1316 when removing large directory trees such as node_modules. I couldn't find any official information on the subject.

Related

How to load variables automatically in PowerShell for every session? [duplicate]

On Windows, not counting ISE or x86, there are four (4) profile scripts.
AllUsersAllHosts # C:\Program Files\PowerShell\6\profile.ps1
AllUsersCurrentHost # C:\Program Files\PowerShell\6\Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts # C:\Users\lit\Documents\PowerShell\profile.ps1
CurrentUserCurrentHost # C:\Users\lit\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
On Linux with pwsh 6.2.0 I can find only two locations.
CurrentUserAllHosts # ~/.config/powershell/Microsoft.PowerShell_profile.ps1
CurrentUserCurrentHost # ~/.config/powershell/profile.ps1
Are there any "AllUsers" profile scripts on Linux? If so, where are they?
tl;dr (also applies to Windows):
The conceptual about_Profiles help topic describes PowerShell's profiles (initialization files).
The automatic $PROFILE variable contains a string that is the path of the initialization file for the current user and the current PowerShell host environment (typically, the terminal a.k.a console).
Additional profile files are defined - along the dimensions of (a) all-users vs. current-user and (b) all host environments vs. the current one - which are exposed via properties that the $PROFILE string variable is decorated with, which makes them nontrivial to discover - see below.
None of the profile files exist by default, and in some case even their parent directories may not; the bottom section of this answer shows programmatic on-demand creation and updating of the $PROFILE file.
Olaf provided the crucial pointer in comment:
$PROFILE | select * # short for: $profile | Select-Object -Property *
shows all profile file locations, whether or not the individual profile files exist.
E.g., on my Ubuntu machine with PowerShell installed in /home/jdoe/.powershell, I get:
AllUsersAllHosts : /home/jdoe/.powershell/profile.ps1
AllUsersCurrentHost : /home/jdoe/.powershell/Microsoft.PowerShell_profile.ps1
CurrentUserAllHosts : /home/jdoe/.config/powershell/profile.ps1
CurrentUserCurrentHost : /home/jdoe/.config/powershell/Microsoft.PowerShell_profile.ps1
Length : 62
Note the presence of the [string] type's native Length property, which you could omit if you used $PROFILE | select *host* instead.
That you can get the profile locations that way is not obvious, given that $PROFILE is a string variable (type [string]).
PowerShell decorates that [string] instance with NoteProperty members reflecting all profile locations, which is why select (Select-Object) is able to extract them.
Outputting just $PROFILE - i.e. the string value - yields /home/jdoe/.config/powershell/Microsoft.PowerShell_profile.ps1, i.e. the same path as its CurrentUserCurrentHost property, i.e. the path of the user-specific profile file specific to the current PowerShell host environment (typically, the terminal aka console).[1]
You can verify the presence of these properties with reflection as follows, (which reveals their values too):
$PROFILE | Get-Member -Type NoteProperty
This means that you can also use regular property access and tab completion to retrieve individual profile locations; e.g.:
# Use tab-completion to find a specific profile location.
# Expands to .Length first, then cycles through the profile-location properties.
$profile.<tab>
# Open the all-users, all-hosts profiles for editing.
# Note: Only works if the file already exists.
# Also, you must typically run as admin to modify all-user profiles.
Invoke-Item $profile.AllUsersAllHosts
Convenience functions for getting profile locations and opening profiles for editing:
The code below defines:
Get-Profile enumerates profiles, showing their location and whether they exist on a given machine.
Edit-Profile opens profile(s) for editing (use -Force to create them on demand); note that modifying all-user profiles typically requires running as admin.
function Get-Profile {
<#
.SYNOPSIS
Gets the location of PowerShell profile files and shows whether they exist.
#>
[CmdletBinding(PositionalBinding=$false)]
param (
[Parameter(Position=0)]
[ValidateSet('AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost')]
[string[]] $Scope
)
if (-not $Scope) {
$Scope = 'AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost'
}
foreach ($thisScope in $Scope) {
[pscustomobject] #{
Scope = $thisScope
FilePath = $PROFILE.$thisScope
Exists = (Test-Path -PathType Leaf -LiteralPath $PROFILE.$thisScope)
}
}
}
function Edit-Profile {
<#
.SYNOPSIS
Opens PowerShell profile files for editing. Add -Force to create them on demand.
#>
[CmdletBinding(PositionalBinding=$false, DefaultParameterSetName='Select')]
param (
[Parameter(Position=0, ValueFromPipelineByPropertyName, ParameterSetName='Select')]
[ValidateSet('AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost')]
[string[]] $Scope = 'CurrentUserCurrentHost'
,
[Parameter(ParameterSetName='All')]
[switch] $All
,
[switch] $Force
)
begin {
$scopes = New-Object Collections.Generic.List[string]
if ($All) {
$scopes = 'AllUsersAllHosts', 'AllUsersCurrentHost', 'CurrentUserAllHosts', 'CurrentUserCurrentHost'
}
}
process {
if (-not $All) { $scopes.Add($Scope) }
}
end {
$filePaths = foreach ($sc in $scopes) { $PROFILE.$sc }
$extantFilePaths = foreach ($filePath in $filePaths) {
if (-not (Test-Path -LiteralPath $filePath)) {
if ($Force) {
if ((New-Item -Force -Type Directory -Path (Split-Path -LiteralPath $filePath)) -and (New-Item -Force -Type File -Path $filePath)) {
$filePath
}
} else {
Write-Verbose "Skipping nonexistent profile: $filePath"
}
} else {
$filePath
}
}
if ($extantFilePaths.Count) {
Write-Verbose "Opening for editing: $extantFilePaths"
Invoke-Item -LiteralPath $extantFilePaths
} else {
Write-Warning "The implied or specified profile file(s) do not exist yet. To force their creation, pass -Force."
}
}
}
[1] PowerShell considers the current-user, current-host profile the profile of interest, which is why $PROFILE's string value contains that value. Note that in order to decorate a [string] instance with note properties, Add-Member alone is not enough; you must use the following idiom: $decoratedString = $string | Add-Member -PassThru propName propValue - see the Add-Member help topic.

PowerShell -Confirm only once per function call

I'm obviously trying to do something wrong, so hoping someone can help since it seems easy and it is probably an easy mistake. Basically I am writing a little script to create new folders and I want to include -Confirm, but I want it to basically ask ONCE if the user wants to continue, instead of asking for each nested folder that is created.
Here's what I have so far
function Create-NewFolderStructure{
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt"
)
process {
$here = $LiteralPath
$directories = Get-Content -LiteralPath "$($here)\$($inputFile)"
If (-not (Test-Path -Path "$($here)\$($NewFolderName)")){
$directories | ForEach-Object {New-Item -ItemType "directory" -Path "$($here)\$($NewFolderName)" -Name $_}
}
Else {
Write-Host "A $($NewFolderName) folder already exists"
}
}
end {
}
}
The issue is that if I use -Confirm when calling my function, or directly at New-Item - I end up getting the confirmation prompt for every single 'child folder' that is being created. Even with the first prompt, saying "Yes to all" doesn't suppress future prompts
In order to consume the user's answer to the confirmation prompt, you have to actually ask them!
You can do so by calling $PSCmdlet.ShouldContinue():
function Create-NewFolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string]$NewFolderName,
[Parameter()]
$LiteralPath = (Get-Location),
[Parameter()]
$inputFile = "zChildDirectoryNames.txt",
[switch]$Force
)
process {
if(-not $Force){
$yesToAll = $false
$noToAll = $false
if($PSCmdlet.ShouldContinue("Do you want to go ahead and create new directory '${NewFolderName}'?", "Danger!", [ref]$yesToAll, [ref]$noToAll)){
if($yesToAll){
$Force = $true
}
}
else{
return
}
}
# Replace with actual directory creation logic here,
# pass `-Force` or `-Confirm:$false` to New-Item to suppress its own confirmation prompt
Write-Host "Creating new folder ${NewFolderName}" -ForegroundColor Red
}
}
Now, if the user presses [A] ("Yes To All"), we'll remember it by setting $Force to true, which will skip any subsequent calls to ShouldContinue():
PS ~> "folder 1","folder 2"|Create-NewFolderStructure
Danger!
Do you want to go ahead and create new directory 'folder 1'?
[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A
Creating new folder folder 1
Creating new folder folder 2
PS ~>
I'd strongly suggest reading through this deep dive to better understand the SupportsShouldProcess facility: Everything you wanted to know about ShouldProcess
Mathias R. Jessen's helpful answer provides an alternative to using the common -Confirm parameter, via a slightly different mechanism based on a custom -Force switch that inverts the logic of your own attempt (prompt is shown by default, can be skipped with -Force).
If you want to make your -Confirm-based approach work:
Call .ShouldProcess() yourself at the start of the process block, which presents the familiar confirmation prompt, ...
... and, if the method returns $true - indicating that the use confirmed - perform the desired operations after setting a local copy of the $ConfirmPreference preference variable to 'None', so as to prevent the cmdlets involved in your operations to each prompt as well.
function New-FolderStructure {
[CmdletBinding(SupportsShouldProcess)]
param (
[Parameter(Mandatory, ValueFromPipeline)]
[string] $NewFolderName,
[string] $LiteralPath = $PWD,
[string] $InputFile = "zChildDirectoryNames.txt"
)
# This block is called once, before pipeline processing begins.
begin {
$directories = Get-Content -LiteralPath "$LiteralPath\$InputFile"
}
# This block is called once if parameter $NewFolderName is bound
# by an *argument* and once for each input string if bound via the *pipeline*.
process {
# If -Confirm was passed, this will prompt for confirmation
# and return `$true` if the user confirms.
# Otherwise `$true` is automatically returned, which also happens
# if '[A] Yes to All' was chosen for a previous input string.
if ($PSCmdlet.ShouldProcess($NewFolderName)) {
# Prevent the cmdlets called below from *also* prompting for confirmation.
# Note: This creates a *local copy* of the $ConfirmPreference
# preference variable.
$ConfirmPreference = 'None'
If (-not (Test-Path -LiteralPath "$LiteralPath\$NewFolderName")) {
$directories | ForEach-Object { New-Item -ItemType Directory -Path "$LiteralPath\$NewFolderName" -Name $_ }
}
Else {
Write-Host "A $NewFolderName folder already exists"
}
}
}
}
Note:
I've changed your function's name from Create-NewFolderStructure to New-FolderStructure to comply with PowerShell's naming conventions, using the approved "verb" New and streamlined the code in a number of ways.
ValueFromPipeline was added as a property to the $NewFolderName parameter so as to support passing multiple values via the pipeline (e.g.
'foo', 'bar' | New-FolderStructure -Confirm)
Note that the process block is then called for each input string, and will also prompt for each, unless you respond with [A] Yes to All); PowerShell remembers this choice between process calls and makes .ShouldProcess() automatically return $true in subsequent calls; similarly, responding to [L] No to All automatically returns $false in subsequent calls.
The need to set $ConfirmPreference to 'None' stems from the fact that PowerShell automatically translates the caller's use of -Confirm into a function-local $ConfirmPreference variable with value 'Low', which makes all cmdlets that support -Confirm act as if -Confirm had been passed.
.ShouldProcess() does not pick up in-function changes to the value of this $ConfirmPreference copy, so it is OK to set it to 'None', without affecting the prompt logic of subsequent process invocations.

Using a Variable to Copy Files From One Location to Multiple Using Powershell

So i have a file that i want to transfer from one location to multiple computers on my network using powershell, this is what i have so far.
robocopy "\\PTFGW-061403573\c$\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
"\\$onlineComputers\c$\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" /mir /r:1 /o:0
when i run it it says the location is not found, and i can confirm that the variable holds all of the online computers within it. any ideas?
Zizzay,
Here's some code that should do what you want. It puts the copy-item command in a TRY/Catch construct to catch and report any errors in the copying process. I assumed that the last item on your source (Startup) is a folder, thus the need for the Recurse argument in the $CIArgs. If it is not a folder remove the Recurse line. I also broke up your paths to cut down on repetition and make things easier to change. Note: I tested this on a Peer-to-Peer LAN, should work on a Domain based LAN.
$onlineComputers = "Computer1","ComputerN"
$FSBase = "C$\ProgramData\Microsoft\Windows\Start Menu\Programs"
$OnlineComputers |
ForEach-Object {
Try {
$CIArgs =
#{Path = "\\PTFGW-061403573\$FSBase"
Destination = "\\$_.\$FSBase\Startup"
Force = $True
Recurse = $True
ErrorAction = 'Stop'}
Copy-Item #CIArgs
}
Catch {
Write-Host #"
Could not write to: $_.
"#
} #End Catch
} #End ForEach...
HTH
Try this
$onlineComputers = "Computer1","ComputerN"
$onlineComputers | Foreach {
& robocopy.exe --% "\\PTFGW-061403573\c$\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
"\\$($_)\c$\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup" /mir /r:1 /o:0
}
You need to do a foreach on each item of the collection and specify $onlineComputers as an array. And added the stop parsing (--%) symbol to run the command more correctly. That's optional, and for safety.

Is there a way to use the equivalent of touch in PowerShell to update timestamps of a file?

I use the Unix command touch a lot to update timestamps of any new acquired/downloaded file in my system. Many times the timestamp of the existing file is old, so it can get lost in the system. Is there a way in MS Windows PowerShell to do so. I have both PowerShell 5 as well as PowerShell 6.
You can use
$file = Get-Item C:\Intel\Logs\IntelCpHDCPSvc.log
$file.LastWriteTime = (Get-Date)
Or CreationTime or LastAcccessTime, if you prefer.
As a function
Function UpdateFileLastWriteTimeToToday() {
Param ($FileName)
$file = Get-Item $FileName
Echo "Current last write time: " $file.LastWriteTime
$file.LastWriteTime = (Get-Date)
Echo "New last write time: " $file.LastWriteTime
}
#How to use
UpdateFileLastWriteTimeToToday -FileName C:\Intel\Logs\IntelGFX.Log
With the output of
Current last write time:
Tuesday, April 16, 2019 1:09:49 PM
New last write time:
Monday, November 4, 2019 9:59:55 AM
Mark's helpful answer shows how to update a single file's last-modified timestamp.
Below is the source code for function Touch-File, which implements most of the functionality that the Unix touch utility offers in a PowerShell-idiomatic fashion, including support for -WhatIf, verbose output, and a pass-thru option.
It works in both Windows PowerShell (version 3 or higher) and PowerShell Core.
You can put the function in your $PROFILE, for instance; call Get-Help Touch-File for help.
If you want to define an alias, I recommend against naming it touch, to prevent confusion with the native touch utility on Unix-like platforms;
Set-Alias tf Touch-File would work, for instance.
Examples:
# Sets the last-modified and last-accessed timestamps for all text files
# in the current directory to the current point in time.
Touch-File *.txt
# Creates files 'newfile1' and 'newfile2' and outputs information about them as
# System.IO.FileInfo instances.
# Note the need to use "," to pass multiple paths.
Touch-File newfile1, newfile2 -PassThru
# Updates the last-modified and last-accessed timestamps for all text files
# in the current directory to midnight (the start of) of today's date.
Touch-File *.txt -DateTime (Get-Date).Date
# Sets the last-modified and last-accessed timestamps of all text files
# in the current directory back by 1 hour.
Get-Item *.txt | Touch-File -Offset '-1:0'
# Sets the last-modified and last-accessed timestamps of all files in the
# current directory to the last-modified timestamp of the current directory.
Get-ChildItem -File | Touch-File -ReferencePath . '-0:1'
Touch-File source code:
Note:
The function below is also available as an MIT-licensed Gist, and only the latter will be maintained going forward. Assuming you have looked at the linked code to ensure that it is safe (which I can personally assure you of, but you should always check), you can install it directly as follows:
irm https://gist.github.com/mklement0/82ed8e73bb1d17c5ff7b57d958db2872/raw/Touch-File.ps1 | iex
Touch is not an approved verb in PowerShell, but it was chosen nonetheless,
because none of the approved verbs can adequately convey the core functionality of
this command.
function Touch-File {
<#
.SYNOPSIS
"Touches" files.
.DESCRIPTION
Similar to the Unix touch utility, this command updates the last-modified and
last-accessed timestamps of files or creates files on
demand.
The current point in time is used by default, but you can pass a
specific timestamp with -DateTime or use an existing file or directory's
last-modified timestamp with -ReferencePath.
Alternatively, the target files' current timestamps can be adjusted with
a time span passed to -Offset.
Symbolic links are invariably followed, which means that it is a file link's
*target* whose last-modified timestamp get updated.
Note:
* This means that a *link*'s timestamp is itself never updated.
* If a link's target doesn't exist, a non-terminating error occurs.
Use -WhatIf to preview the effects of a given command, and -Verbose to see
details.
Use -PassThru to pass the touched items through, i.e., to output updated
information about them.
Note that in order to pass multiple target files / patterns as arguments
you must *comma*-separate them, because they bind to a single, array-valued
parameter.
.PARAMETER Path
The paths of one or more target files, optionally expressed
as wildcard expressions, and optionally passed via the pipeline.
.PARAMETER LiteralPath
The literal paths of one or more target files, optionally
passed via the pipeline as output from Get-ChildItem or Get-Item.
.PARAMETER DateTime
The timestamp to assign as the last-modified (last-write) and last-access
values.
By default, the current point in time is used.
.PARAMETER ReferencePath
The literal path to an existing file or directory whose last-modified
timestamp should be applied to the target file(s).
.PARAMETER Offset
A time span to apply as an offset to the target files' current last-write
timestamp.
Since the intent is to adust the current timestamps of *existing* files,
non-existent paths are ignored; that is, -NoNew is implied.
Note that positive values adjust the timestamps forward (to a more recent date),
whereas negative values adjust backwards (to an earlier date.)
Examples of strings that can be used to specify time spans:
* '-1' adjust the current timestamp backward by 1 day
* '-2:0' sets it backward by 2 hours
Alternatively, use something like -(New-TimeSpan -Days 1)
.PARAMETER NoNew
Specifies that only existing files should have their timestamps updated.
By default, literal target paths that do not refer to existing items
result in files with these paths getting *created* on demand.
A warning is issued for any non-existing input paths.
.PARAMETER PassThru
Specifies that the "touched" items are passed through, i.e. produced as this
command's output, as System.IO.FileInfo instances.
.PARAMETER Force
When wildcard expressions are passed to -Path, hidden files are matched too.
.EXAMPLE
Touch-File *.txt
Sets the last-modified and last-accessed timestamps for all text files
in the current directory to the current point in time.
.EXAMPLE
Touch-File newfile1, newfile2 -PassThru
Creates files 'newfile1' and 'newfile2' and outputs information about them as
System.IO.FileInfo instances.
Note the need to use "," to pass multiple paths.
.EXAMPLE
Touch-File *.txt -DateTime (Get-Date).Date
Updates the last-modified and last-accessed timestamps for all text files
in the current directory to midnight (the start of) of today's date.
.EXAMPLE
Get-Item *.txt | Touch-File -Offset '-1:0'
Adjusts the last-modified and last-accessed timestamps of all text files
in the current directory back by 1 hour.
.EXAMPLE
Get-ChildItem -File | Touch-File -ReferencePath .
Sets the last-modified and last-accessed timestamps of all files in the
current directory to the last-modified timestamp of the current directory.
.NOTES
"Touch" is not an approved verb in PowerShell, but it was chosen nonetheless,
because none of the approved verbs can adequately convey the core functionality
of this command.
In PowerShell *Core*, implementing this command to support multiple target
paths *as individual arguments* (as in Unix touch) would be possible
(via ValueFromRemainingArguments), but such a solution would misbehave in
Windows PowerShell.
#>
# Supports both editions, but requires PSv3+
#requires -version 3
[CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess)]
param(
[Parameter(ParameterSetName = 'Path', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName = 'PathAndDateTime', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName = 'PathAndRefPath', Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string[]] $Path
,
[Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName = 'LiteralPathAndDateTime', Mandatory, ValueFromPipelineByPropertyName)]
[Parameter(ParameterSetName = 'LiteralPathAndRefPath', Mandatory, ValueFromPipelineByPropertyName)]
[Alias('PSPath', 'LP')]
[string[]] $LiteralPath
,
[Parameter(ParameterSetName = 'PathAndRefPath', Mandatory)]
[Parameter(ParameterSetName = 'LiteralPathAndRefPath', Mandatory)]
[string] $ReferencePath
,
[Parameter(ParameterSetName = 'PathAndDateTime', Mandatory)]
[Parameter(ParameterSetName = 'LiteralPathAndDateTime', Mandatory)]
[datetime] $DateTime
,
[Parameter(ParameterSetName = 'Path')]
[Parameter(ParameterSetName = 'LiteralPath')]
[timespan] $Offset
,
[switch] $NoNew
,
[switch] $PassThru
,
[switch] $Force
)
begin {
Set-StrictMode -Version 1
$ErrorActionPreference = 'Continue' # We want to pass non-terminating errors / .NET method-call exceptions through.
$haveRefPath = $PSBoundParameters.ContainsKey('ReferencePath')
$haveDateTime = $PSBoundParameters.ContainsKey('DateTime')
$haveOffset = $PSBoundParameters.ContainsKey('Offset')
if ($haveOffset) { $NoNew = $true } # -NoNew is implied.
# Initialize defaults (even though they may not be used).
# Defining them unconditionally prevents strict-mode violations in pseudo-ternary conditionals.
if (-not ($haveDateTime -or $haveRefPath)) { $DateTime = [datetime]::Now }
if (-not $haveOffset) { $Offset = 0 }
# If a reference item was given, obtain its timestamp now and abort if that fails.
if ($haveRefPath) {
try {
$DateTime = (Get-Item -ErrorAction Stop $ReferencePath).LastWriteTime
}
catch {
Throw "Failed to get the reference path's last-modified timestamp: $_"
}
}
$touchedCount = 0
}
process {
$wildcardsSupported = $PSCmdlet.ParameterSetName -notlike 'LiteralPath*'
# Try to retrieve existing files.
[array] $files, $dirs =
$(
if ($wildcardsSupported) {
Get-Item -Path $Path -ErrorAction SilentlyContinue -ErrorVariable err -Force:$Force
}
else {
Get-Item -LiteralPath $LiteralPath -ErrorAction SilentlyContinue -ErrorVariable err -Force:$Force
}
).Where( { -not $_.PSIsContainer }, 'Split')
# Ignore directories among the (globbed) input paths, but issue a warning.
if ($dirs) {
Write-Warning "Ignoring *directory* path(s): $dirs"
}
# -WhatIf / -Confirm support.
# Note: The prompt message is also printed with -Verbose
$targets = ($LiteralPath, ($Path, $files.FullName)[$files.Count -gt 0])[$wildcardsSupported] -replace '^Microsoft\.PowerShell\.Core\\FileSystem::' -replace ('^' + [regex]::Escape($PWD) + '[\\/]?')
if ($targets.Count -gt 1) { $targets = "`n" + ($targets -join "`n") + "`n" }
$newDateTimeDescr = if ($haveOffset -and -not ($haveDateTime -or $haveRefPath)) { "the last-modified timestamp offset by $Offset" } else { "$($DateTime + $Offset)" }
$actionDescr = ("Updating / creating with a last-modified timestamp of $newDateTimeDescr", "Updating the last-modified timestamp to $newDateTimeDescr")[$NoNew.IsPresent]
if (-not $PSCmdlet.ShouldProcess($targets, $actionDescr)) { return }
# Try to create the files that don't yet exist - unless opt-out -NoNew was specified.
if ($err) {
if ($NoNew) {
Write-Warning "Ignoring non-existing path(s): $($err.TargetObject)"
}
else {
$files += foreach ($file in $err.TargetObject) {
Write-Verbose "Creating: $file..."
New-Item -ItemType File -Path $file -ErrorAction SilentlyContinue -ErrorVariable e
# If an error occurred - such as the parent directory of a literal target path not existing - pass it through.
# The only acceptable error is if the file has just been created, between now and the time we ran Get-Item above.
if ($e -and -not (Test-Path -PathType Leaf -LiteralPath $file)) { $e | Write-Error }
}
}
}
# Update the target files' timestamps.
foreach ($file in $files) {
# Note: If $file is a symlink, *setting* timestamp properties invariably sets the *target*'s timestamps.
# *Getting* a symlink's timestame properties, by contrast, reports the *link*'s.
# This means:
# * In order to apply an offset to the existing timestamp, we must explicitly get the *target*'s timestamp
# * With -PassThru, unfortunately - given that we don't want to quietly switch to the *target* on output -
# this means that the passed-through instance will reflect the - unmodified - *link*'s properties.
$target =
if ($haveOffset -and $file.LinkType) {
# Note: If a link's target doesn't exist, a non-terminating error occurs, which we'll pass through.
# !! Due to inconsistent behavior of Get-Item as of PowerShell Core 7.2.0-preview.5, if a broken symlink
# !! is (a) specified literally and (b) alongside at least one other path (irrespective of whether -Path or -LiteralPath is used),
# !! it generates an *error* - even though passing that path *as the only one* or *by indirect inclusion via a pattern*
# !! does NOT (it lists the non-existent target in the Name column, but doesn't error).
# !! Thus, if (a) and (b) apply, the resulting error may have caused the non-existent target to be created above,
# !! assuming that its parent directory exists.
Get-Item -Force -LiteralPath $file.Target
} else {
$file
}
if ($target) {
# Set the last-modified and (always also) the last-access timestamps.
$target.LastWriteTime = $target.LastAccessTime = if ($haveOffset) { $target.LastWriteTime + $Offset } else { $DateTime }
}
if ($PassThru) { $file }
}
$touchedCount += $files.Count
}
end {
if (-not $WhatIfPreference -and $touchedCount -eq 0) {
Write-Warning "Nothing to touch."
}
}
}

A positional parameter cannot be found that accepts argument '\*'

I have a number of web services that I'm attempting to use PowerShell to install on a DEV server. I want to include, in the script, a section that copies all files and folders from a staging area to the respective folder on the DEV server.
In order to do this I've created two parallel string arrays - one containing the direct file paths of the staging folders and the other with the names of each folder on the DEV server (I use a couple other variables to store the rest of the directory).
I'd like to create a for loop and simply iterate over both arrays to complete the transfer of files/folders from staging to dev server. However when the loop runs the copy-item command gives this error each time
Copy-Item : A positional parameter cannot be found that accepts argument '*'.
If I decide not to use the loop, the error goes away and the files transfer properly. Only a couple days new to PowerShell so I'm not sure what's going on/haven't been able to find anything specific to this on the web.
Variables
#web services
$periscopeWebServices = "periscopeWebServices"
$periscopeRetrieveComments = "periscopeRetrieveComments"
$periscopeRetrieveGeneral = "periscopeRetrieveGeneral"
$periscopeRetrieveMasterSubs = "periscopeRetrieveMasterSubs"
$periscopeRetrieveProducers = "periscopeRetrieveProducers"
$periscopeRetrieveProfiles = "periscopeRetrieveProfiles"
$periscopeRetrieveRelationships = "periscopeRetrieveRelationships"
$periscopeSearch = "periscopeSearch"
$periscopeServicesArray = #($periscopeRetrieveComments, $periscopeRetrieveGeneral,
$periscopeRetrieveMasterSubs, $periscopeRetrieveProducers,
$periscopeRetrieveProfiles, $periscopeRetrieveRelationships,
$periscopeSearch)
#staging areas
$stagingComments = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveComments"
$stagingGeneral = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveGeneral"
$stagingMasterSubs = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveMasterSubs"
$stagingProducers = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveProducers"
$stagingProfiles = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveProfiles"
$stagingRelationships = "\\install\PeriscopeServices\periscopeWebServices\periscopeRetrieveRelationships"
$stagingSearch = "\\install\PeriscopeServices\periscopeWebServices\periscopeSearch"
$stagingArray = #($stagingComments, $stagingGeneral, $stagingMasterSubs, $stagingProducers
$stagingProfiles, $stagingRelationships, $stagingSearch)
#server variables
$domain = "InsideServices.dev.com"
$directory = "E:\webcontent"
$domainpath = "$directory\$domain"
Loop - throws error on 'copy-item'
#copy files
Write-Host "Copying Files" -ForegroundColor Yellow
For ($i=0; $i -lt $periscopeServicesArray.Length; $i++)
{
Write-Host "Copying $($periscopeServicesArray[$i])" -ForegroundColor Yellow
if(Test-Path -path "$domainpath\$periscopeWebServices\$($periscopeServicesArray[$i])")
{
copy-item -Path $($stagingArray[$i])\* -Destination $domainpath\$periscopeWebServices\$($periscopeServicesArray[$i]) -recurse -Force
}
}
Succesful copy-item command
copy-item -Path $stagingComments\* -Destination $domainpath\$periscopeWebServices\$periscopeRetrieveComments -recurse -Force
I think it's because you're using a sub-expression $() inside the loop, but not outside. Try these:
"$($stagingArray[$i])\*"
Surrounding it in quotes makes it evaluate the whole thing as a string.
$($stagingArray[$i]) + '\*'
This will concatenate the result of the sub-expression and the string containing the \*.