move-item doesn't work in loop - loops

ls *.gif | Foreach { $newname = $_.Name -replace '\[','' -replace '\]',''
write-host $_.Name $newname
move-Item -Path $_.Name -Destination $newname; }
ls *.gif
So while trying to help someone rename files with [], I found out move-item doesn't work in a loop. It seems to work just fine outside the loop.
Ideas?

Update: Based on the comment below, I want to clarify this: The special characters in the file names require you to use -LiteralPath parameter. -Path cannot handle those characters. Outside a loop, -Path works since you are escapting the special characters using `. This isn't possible when walking through a collection.
In a loop, you need to use -LiteralPath parameter instead of -Path.
-LiteralPath <string[]>
Specifies the path to the current location of the items. Unlike Path, the value of
LiteralPath is used exactly as it is typed. **No characters are interpreted as
wildcards. If the path includes escape characters, enclose it in single quotation
marks.** Single quotation marks tell Windows PowerShell not to interpret any
characters as escape sequences.
SO, this will be:
GCI -Recurse *.txt | % { Move-Item -LiteralPath $_.FullName -Destination "SomenewName" }

If you use the pipeline binding feature of PowerShell, you can make this much simpler and eliminate the need for the explicit Foreach-Object e.g.:
ls *.gif | Move-Item -Destination {$_ -replace '\[|\]',''} -WhatIf
This works because the LiteralPath parameter is set up to bind ByPropertyName. However you may wonder, where does it get a property by the name of "LiteralPath" from on the output of Get-ChildItem (alias ls). Well it doesn't find that property name, however the LiteralPath parameter has an alias of PSPath defined which does exist on each object output by Get-ChildItem. That's how it binds to the LiteralPath paramter. The other speed tip here is that because the Destination parameter is also pipeline bound (ByPropertyName), you can use a scriptblock to provide the value. And inside that scriptblock you have access to the pipeline object.
Inside the scriptblock, this uses the -replace operator to come up with the new name based on the original full name. While I could have used $_.FullName or even $_.Name in this case (assuming you want to essentially rename the files within the same dir), I use just $_. Since -replace is a string operator, it will coerce $_ to a string before using it. You can see what this would be by executing:
ls *.gif | Foreach {"$_"}
Which is the full path in this case but you have to be careful because you don't always get the full path e.g.:
ls | Foreach {"$_"}
displays just the filename. In your examples (rename to same dir) this doesn't matter but in other cases it does. It is probably a good practice just to be explicit and use $_.Name or $_.FullName in a script but when hacking this stuff out at the console, I tend to use just $_. The saying: it's a sharp stick, don't poke your eye out applies here. :-)

You can find the "official" informations about the role of "[" in Path strings on this Microsoft article.
Or look in google for : Windows PowerShell Tip of the Week : "Taking Things (Like File Paths) Literally".
The only tip wich was not clear for me is that Rename-Item does not support LiteralPath, and that we can use Move-Item to rename files or directories.
JP

This worked for me (atleast in my situation.
Move-Item -literalpath $_.FullName -Destination ( ( ( (Join-Path -Path (Get-Location) -ChildPath $_.BaseName) -replace "\[","`[") -replace "\]","`]") )
Had hundreds of movies and it associated subtitles stored without folders. Decided to put each of the movies and subtitles in their own folders
Full Code
Get-ChildItem -File | %
{
if(Test-Path -LiteralPath ( Join-Path -Path (Get-Location) -ChildPath $_.BaseName ))
{
Move-Item -literalpath $_.FullName -Destination ( ( ( (Join-Path -Path (Get-Location) -ChildPath $_.BaseName) -replace "\[","`[") -replace "\]","`]") )
}
else
{
New-Item ( Join-Path -Path (Get-Location) -ChildPath $_.BaseName ) -ItemType Directory
Move-Item $_ -Destination ( Join-Path -Path (Get-Location) -ChildPath $_.BaseName )
}
}

Related

Powershell array failing because of "Copy-Item : Illegal characters in path."

I have a specific usecase where I need to identify if files from a list exist, and if so, copy them to a separate location with the relevant file structure kept. I need to keep my list of targets in the same script.
I believe my issue is something to do with the way the data inside isn't being parsed correctly due to ":" for drive letters, but I'm unsure of how to get round this issue.
As you can see from the code below, I attempted to fix the issue by ignoring the drive letter, and appending it during the Copy-Item, but it doesn't seem to work either. (e.g: C:\folder\file becomes \folder\file in the list.)
I created test directory to just help show the issue, of examples of files/folders that I want to grab (purely for testing, the real files are multiple locations/file types).
- test_dir_cmd
- folder
- folder1
* file2.db
* file3.json
* file2.txt
* file3.js
- folder2
* file.bak
* file.db
* file.txt
* temp.dat
This method works for folders and their contents, but not for specific files or wildcard.
"\USERS\$USER\AppData\Local\test_dir_cmd\folder\folder1",
"\USERS\$USER\AppData\Local\test_dir_cmd\folder\*.txt",
"\USERS\$USER\AppData\Local\test_dir_cmd\*\file.db",
"\USERS\$USER\AppData\Local\test_dir_cmd\temp.dat”
This is an example of how the list of files I'll need to get is presented and I'll need to work with.
Errors given:
Copy-Item : Illegal characters in path.
At F:\P2P.ps1:37 char:1
+ Copy-Item "C:$path" -Destination "$triage_location\$path" -Force -Rec ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Copy-Item], ArgumentException
+ FullyQualifiedErrorId : System.ArgumentException,Microsoft.PowerShell.Commands.CopyItem
Command
Full script used for context:
$triage_location = "C:\temp\output\Triage\c"
ForEach-Object { #Looping through C:\Users to find folders that begin with numbers only and add to an array called $users
$users = #(Get-ChildItem -Path 'C:\Users'| Where-Object { $_.Name -match '^c+' } | Select -ExpandProperty Name)
}
Write-Host "users = $users"
write-host ""
$path_array = foreach ($user in $users) { # Loop through contents of users array and add each user to known locations
#(
"\USERS\$USER\AppData\Local\test_dir_cmd\folder\folder1",
"\USERS\$USER\AppData\Local\test_dir_cmd\folder\*.txt",
"\USERS\$USER\AppData\Local\test_dir_cmd\*\file.db",
"\USERS\$USER\AppData\Local\test_dir_cmd\temp.dat”
)
}
Write-Host "path_array = $path_array"
write-host ""
foreach ($path in $path_array) {
$a = Test-Path -Path "C:$path" # Creating variable called 'a' and setting it to Test-path value which is either True/False
if ($a -eq "True") # Test if browser location paths exist or not. If a returns True/False...
{
Write-Host "C:$path exists"
if(!(Test-Path -Path "$triage_location"))
{
New-Item -ItemType Directory -Path $triage_location
}
Copy-Item "C:$path" -Destination "$triage_location\$path" -Force -Recurse
}
else
{Write-Host "C:$path doesn't exist"}
}
if(Test-Path -Path "C:\temp\output\Triage")
{
Write-Host ""
Write-Host "Creating relevant .ZIP"
Compress-Archive -Path 'C:\temp\output\Triage' -DestinationPath 'C:\temp\output\P2P.zip' -Force # put zip in documents
}
Any help and advice on how I can fix this would be greatly appreciated!
The issue is that you are not joining the paths well. You do this:
-Destination "$triage_location\$path"
At that point $triage_location is C:\temp\output\Triage\c and $path is something like \USERS\TMTech\AppData\Local\test_dir_cmd\folder\folder1. You just make the path with string expansion but since $path starts with a \ and you include that in your string, so your string comes out looking like this:
"C:\temp\output\Triage\c\\USERS\TMTech\AppData\Local\test_dir_cmd\folder\folder1"
Use Join-Path instead:
Copy-Item (Join-Path 'C:\' $path) -Destination (Join-Path $triage_location $path) -Force -Recurse

Replacing an images with different file names with one image

I am working on a project and need to replace many images:
I have one image 1.png which needs to replace about 40 different png files with different names so 2.png, 3.png etc. What is important is that when 1.png replaces 2.png, 3.png etc. the file name does not change. In other words, the content of 1.png should replace the content of 2.png and 3.png, but it should not replace the file name.
I am not sure if this is possible, however, if it is I would be very happy to hear how.
I think you could achieve this with powershell.
Something along these lines.
$files = Get-ChildItem -Path 'c:\source\folder' -Recurse
ForEach ($file In $files)
{
Write-Host 'Replacing:'$file.FullName
If ($file.Name -eq '2.png')
{
Copy-Item -Path 'c:\source\file\1.png' -Destination $file.FullName -Force
Rename-Item -Path $file.FullName -NewName "2.png"
}
}
Here is a link in case you haven't created a PS script before
https://www.windowscentral.com/how-create-and-run-your-first-powershell-script-file-windows-10

How to remove duplicate files from an arraylist

A bit different from the others. I'm retrieving an arraylist of files for processing (basically handling DLL registration on a local machine), and I need my script to properly handle multiple DLLs with the same name. The select -Unique doesn't work, since technically the files aren't duplicates - each has its own unique full path.
I need this script to retrieve all DLLs in a folder (as well as sub-folders), but only return the last instance of each named file. For example if I have files:
C:\Path\Update1\GRM.DLL
C:\Path\Update1\HTCP.DLL
C:\Path\Update2\GRM.DLL
C:\Path\Update3\GRM.DLL
The script should return the objects for Update3\GRM.DLL and Update1\HTCP.DLL.
[System.Collections.ArrayList]$dlls = #(Get-ChildItem -Path $PSScriptRoot -Recurse | Where-Object
{$_.Extension -eq ".dll" -and $_.FullName -notmatch 'dll_Old'})
Edit: Got it going with this, but it's selecting the first instance that shows up, and I need the last. In this example, that means it's snagging Update1/GRM.DLL instead of Update3/GRM.DLL
$dlls = #(Get-ChildItem -Path $PSScriptRoot -Recurse | Where-Object {$_.Extension -eq ".dll" -and $_.FullName -notmatch 'dll_Old'}) | Select-Object -Unique
Use a hashtable to keep track of the last file seen for a specific file name:
$files = #{}
Get-ChildItem -Path $PSScriptRoot -File -Recurse -Filter *.dll |Where-Object FullName -notmatch 'dll_Old' |ForEach-Object {
$files[$_.Name] = $_
}
$uniqueFiles = $files.Values
Mathias R. Jessen's helpful answer is probably the best (fastest) solution in this case, but here's an alternative based on the Group-Object cmdlet:
Get-ChildItem -LiteralPath $PSScriptRoot -Recurse -Filter *.dll |
Where-Object FullName -notmatch dll_Old |
Group-Object Name |
ForEach-Object { $_.Group[-1] }
Group-Object Name groups all matching files by their .Name property.
ForEach-Object { $_.Group[-1] } then extracts the last (-1) member from each resulting group.
Note that Group-Object will implicitly sort the groups by the grouping property, so the resulting list of file-info objects (System.IO.FileInfo, as output by Get-ChildItem) will be sorted by file name.

PowerShell How to convert Get-ChildItem output to a string array

Alright, been working on this for hours and researching like crazy, but still not getting something to work. I need a string[] object created from get-childitem to pass to the Copy-Item -exclude parameter.
The hurdle is that I need to do recursion and need to have relative paths, so this is what I came up with:
$((Get-ChildItem -Path $Dest -Recurse -File).FullName.TrimStart($Dest))
This results in a clean list of existing files in $dest that are presented with a relative path to $dest. The problem is, if I add this to the copy-item -exclude parameter it seems to ignore it. Further research online suggests that copy-item will ignore the -exclude parameter if it is not of type string[].
If I check the type returned by the above command, I get System.Object[]. I expect it to be System.String[] or just plain String[].
How do I convert the output of the above command to a string array?
The full command using copy-item, for clarity, is:
Copy-Item -Path (Join-Path $src "*") -Destination $dest -Recurse -Force -Exclude $((Get-ChildItem -Path $Dest -Recurse -File).FullName.TrimStart($Dest))
My end goal is to copy files recursively without overwriting existing files.
To get a string[] from the names of get-childitem cmdlet use the following
[string[]]$files = (Get-ChildItem).Name
This will do what it appears you say that you want? But, I think that may not be everything to your question.
$((Get-ChildItem -Path $Dest -Recurse -File).FullName.Replace("$Dest",'.'))
#mklement0 is right, -Exclude and -Include support file name patterns (i.e. "*.txt") and not an array of explicit paths.
This sounds like an awful lot like an XY Problem ;-)
If you simply want to copy files recursively without overwriting them, use Robocopy with the Mirror switch. e.g.:
robocopy C:/Source C:/Dest /mir
EDIT:
By default Copy-Item will always overwrite the files on copy, and there is no switches to get around this. I usually recommend Robocopy as it really simplifies things like this and is very "robust" and reliable.
If your requirements are for a "pure" PowerShell version, then you have to break the scrip out into two parts, 1. Get a list of all the files 2. Iterate through the filer and test to see if they are already in the destination before copying.
$SrcPath = "C:/Source"
$DestPath = "C:/Dest"
$SrcFiles = Get-ChildItem $SrcPath -Recurse
#Iterate through files testing:
$SrcFiles | ForEach-Object {
#Calculate Destination File/Folder name/path
$DestObj = $_.FullName.Replace($SrcPath, $DestPath)
if(Test-Path -LiteralPath $DestObj)
{
#File already Exists
Write-Host "File already Exists: $DestObj"
}
else
{
#File Does not exist - Copy
Write-Host "File Does not Exist Copy: $DestObj"
Copy-Item -Path $_ -Destination $DestObj
}
}

Move-Item causes "File Already Exists" error, despite folder having been deleted

I have some code which deletes a folder, then copies files from a temporary directory to where that folder had been.
Remove-Item -Path '.\index.html' -Force
Remove-Item -Path '.\generated' -Force -Recurse #folder containing generated files
#Start-Sleep -Seconds 10 #uncommenting this line fixes the issue
#$tempDir contains index.html and a sub folder, "generated", which contains additional files.
#i.e. we're replacing the content we just deleted with new versions.
Get-ChildItem -Path $tempDir | %{
Move-Item -Path $_.FullName -Destination $RelativePath -Force
}
I get an intermittent error, Move-Item : Cannot create a file when that file already exists. on the Move-Item line for the generated path.
I've been able to prevent this by adding a hacky Start-Sleep -Seconds 10 after the second Remove-Item statement; though that's not a great solution.
I assume the issue is that the Remove-Item statement completes / code moves on to the next line, before the OS has caught up with the actual file deletion; though that seems odd/worrying. NB: There are ~2,500 files in the generated folder (all between 1-100 KBs).
There are no other processes accessing the folders (i.e. I've even closed my explorer windows & tested with this directory being excluded from my AV).
I've considered other options:
using Copy-Item instead of Move-Item. I don't like this as it requires creating new files when they're not required (i.e. a copy is slower than a move)... It's faster than my current sleep hack; but still not ideal.
deleting the files & not the folder, then iterating through the subfolders & copying files to the new locations. This would work, but is a lot more code for something that should be simple; so I don't want to pursue that option.
Robocopy would do the trick; but I'd prefer a pure PowerShell solution. This is the option I'll eventually pick if there is no clean solution though.
Question
Has anyone seen this before?
Is it a bug, or have I missed something?
Is anyone aware of a fix / good workaround?
Update
Running the remove in a separate job (i.e. using the code below) did not resolve the issue.
Start-Job -ScriptBlock {
Remove-Item -Path '.\index.html' -Force
Remove-Item -Path '.\generated' -Force -Recurse #folder containing generated files
} | Wait-Job | Out-Null
#$tempDir contains index.html and a sub folder, "generated", which contains additional files.
#i.e. we're replacing the content we just deleted with new versions.
Get-ChildItem -Path $tempDir | %{
Move-Item -Path $_.FullName -Destination $RelativePath -Force
}
Update #2
Adding this works; i.e. rather than waiting a fixed time, we wait for the path to be removed / checking every second. If it's not removed after 30 seconds we assume it's not going to be; so carry on regardless (which will cause the move-item to throw an error which gets handled elsewhere).
# ... remove-item code ...
Start-Job -ScriptBlock {
param($Path)
while(Test-Path $Path){start-sleep -Seconds 1}
} -ArgumentList '.\generated' | Wait-Job -Timeout 30 | Out-Null
# ... move-item code ...
In the end I settled for this solution; not perfect, but it works.
Remove-Item -Path '.\index.html' -Force
Remove-Item -Path '.\generated' -Force -Recurse #folder containing generated files
#wait until the .\generated directory is full removed; or until ~30 seconds has elapsed
1..30 | %{
if (-not (Test-Path -Path '.\generated' -PathType Container)) {break;}
Start-Sleep -Seconds 1
}
Get-ChildItem -Path $tempDir | %{
Move-Item -Path $_.FullName -Destination $RelativePath -Force
}
This does the same as the job in update #2 of the question; only doesn't require the overhead of a job; just loops until the file's removed.
Here's the above logic wrapped as a reuable cmdlet:
function Wait-Item {
[CmdletBinding()]
param (
[Parameter(Mandatory, ValueFromPipeline, HelpMessage = 'The path of the item you wish to wait for')]
[string]$Path
,
[Parameter(HelpMessage = 'How many seconds to wait for the item before giving up')]
[ValidateRange(1,[int]::MaxValue)]
[int]$TimeoutSeconds = 30
,
[Parameter(HelpMessage = 'By default the function waits for an item to appear. Adding this switch causes us to wait for the item to be removed.')]
[switch]$Remove
)
process {
[bool]$timedOut = $true
1..$TimeoutSeconds | %{
if ((Test-Path -Path $Path) -ne ($Remove.IsPresent)){$timedOut=$false; return;}
Start-Sleep -Seconds 1
}
if($timedOut) {
Write-Error "Wait-Item timed out after $TimeoutSeconds waiting for item '$Path'"
}
}
}
#example usage:
Wait-Item -Path '.\generated' -TimeoutSeconds 30 -Remove

Resources