Partial string match and folder creation using PowerShell - string

I have a directory with thousands of files, and I would like to create new directories based on part of the file name, and then sort the partial matching files into those directories.
The files all start with the same prefix, "123-", and then have numbers random and ranging in length, but I do not want whatever is after the numbers. I tried regular expressions, but I am having issues with matching the varying number lengths after the "123-" and skipping everything after the middle number.
Example files:
directory\123-4441Zebra.pdf
directory\123-4441Tango.pdf
directory\123-768987Beta.pdf
directory\123-768987Tango.pdf
directory\123-34263XYP.pdf
Example Result:
directory\123-4441\123-4441Zebra.pdf
directory\123-4441\123-4441Tango.pdf
directory\123-768987\123-768987Beta.pdf
directory\123-768987\123-768987Tango.pdf
directory\123-34263\123-34263XYP.pdf

Try this:
$yourdir="C:\temp\root"
gci $yourdir -File -Filter 123-*.pdf | %{$newdir=$yourdir + "\" + ($_.Name -replace '(123-\d+).*', '$1'); New-Item -Path $newdir -Force -ItemType Directory; Move-Item -Path $_.FullName -Dest $newdir -Force }

PS C:\> '123-4441Zebra.pdf' -replace '(123-\d+).*', '$1'
123-4441
e.g.
gci "c:\place\" | mv -Dest { "c:\out\$($_.Name -replace '(123-\d+).*', '$1')\" } -Force -WhatIf
Auto-generated PS help links from my codeblock (if available):
gci is an alias for Get-ChildItem (in module Microsoft.PowerShell.Management)
mv is an alias for Move-Item (in module Microsoft.PowerShell.Management)

Related

How to modify the file creation time of multiple files in single script

I'm stumped on this. I am making my way through a file share migration to SharePoint. There have been errors stating the "The item created time or modified time is not supported". No worries as I found a script to edit this in PowerShell:
cd "Directory"
Get-ChildItem -force | Select-Object Mode, Name, CreationTime, LastAccessTime, LastWriteTime | ft
$modifyfiles = Get-ChildItem -force | Where-Object {! $\_.PSIsContainer}
foreach($object in $modifyfiles)
{
$object.CreationTime=("1/3/2023 12:00:00")
$object.LastAccessTime=("1/3/2023 12:01:00")
$object.LastWritetime=("1/3/2023 12:02:00")
}
My question is how do I run this so I don't have to cd to each new directory every time. I have quite a few files in different folders that all need editing. I have the list of paths I need changed and I was hoping there would be a way to "pass" those paths in or somehow run this script in a loop.
Assuming your list of folders looks like this and can be placed in a seperate text file:
C:\folderpath\folder1
C:\folderpath\folder2
C:\folderpath\folder3
Then you could just do something like this:
get-content -Path "C:\folderpath\FileContainingFolderPaths.txt" | ForEach-Object {
$folderpath = $_
Get-ChildItem -Path $folderpath -force | Select-Object Mode, Name, CreationTime, LastAccessTime, LastWriteTime | ft
$modifyfiles = Get-ChildItem -Path $folderpath -force | Where-Object {! $\_.PSIsContainer}
foreach($object in $modifyfiles)
{
$object.CreationTime=("1/3/2023 12:00:00")
$object.LastAccessTime=("1/3/2023 12:01:00")
$object.LastWritetime=("1/3/2023 12:02:00")
}
}
Also I'm gonna have to question you on the $\_.PSIsContainer, is that a mistake? I would think it should be $_.PSIsContainer instead?

Referring to Files as variables in multiple ForEach loops [PowerShell]

Im am making some progress on my script that automatically updates links in excel files without opening them. I have successfully made the function work on a single file with inputs of file name and text to replace. Now I am trying to scale this so that it does the same actions for all files in the script directory.
Here is how the script goes with comments on steps:
# This part will be responsible from fetching the week number to replace from the script directory name, currently I am testing with pre-determined number
# $wk = Get-Item -Path $PWD | Select-Object -Property BaseName
# $wknn = "$wk".Substring(13,2) -as [int]
$wknn = 41
$wkold = $wknn-1
$wkprev = $wknn-2
$DefaultFiles = Get-ChildItem | Where-Object {$_.Name -like "*.xls*"}
ForEach($File in $DefaultFiles)
{
# Build ZIP file name
$zipFile = $_ -ireplace [regex]::Escape(".xlsb"), ".zip"
# Create temporary folder
$parent = [System.IO.Path]::GetTempPath();
[string] $guid = [System.Guid]::NewGuid();
$tempFolder = Join-Path $parent $guid;
New-Item -ItemType Directory -Path $tempFolder;
# Rename file to ZIP
Rename-Item -Path $_ -NewName $zipFile
# Not using Expand-Archive because it changes the ZIP format
C:\7z\7za.exe x "$zipFile" -o"$tempFolder"
# Replace old text with new text. First replace wk-1 to wk and then wk-2 to wk-1
$fileNames = Get-ChildItem -Path $tempFolder -Recurse -Include *.rels
foreach ($file in $fileNames)
{
(Get-Content -ErrorAction SilentlyContinue $file.FullName) |
Foreach-Object { $_ -replace $wkold, $wknn } |
Foreach-Object { $_ -replace $wkprev, $wkold } |
Set-Content $file.FullName
}
# Changing working folder because 7Zip option -w doesn't work
Set-Location -Path $tempfolder
# Update archive with new files. Not using Compress-Archive because it changes the ZIP format
C:\7z\7za.exe u -r "$zipFile" *.*
# Rename file back to XLSB
Rename-Item -Path $zipFile -NewName $_
#Move the final .xlsb file back to the script root
move-Item -Path $_ -destination $PSScriptRoot
#Set location to script root to start over
Set-Location -Path $PSScriptRoot
}
}
I am running into problems with the forEach loop. I am unsure on how do I refer to the file name within the first loop at the Build Zip File Name step. And how do I refer to the output file when i Want to move it to the script root afterwards. Also I suspect that stacking of forEach loops may be not as simple and require extra steps within the code, but due to me just starting out in C I dont have the experience and could not find a simple answer to my problem.
I would really appreciate some assistance with the syntax in my code :)
I would create a temporary folder outside the main loop and set the working directory to that folder. Then remove the folder and reset the working directory when all is done.
Also, there is no need to rename the finished zip file first and then move it back to its original location, because you can do that with the Move-Item cmdlet at the same time.
Try:
$wknn = 41
$wkold = $wknn - 1
$wkprev = $wknn - 2
$7zipExe = 'C:\7z\7za.exe' # path to 7za.exe
$sourcePath = $PSScriptRoot # path to where the Excel files are
# Create temporary folder
$tempFolder = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ((New-Guid).Guid)
$null = New-Item -ItemType Directory -Path $tempFolder -Force
# retrieve a collection of Excel files (FullNames only).
# If you ONLY want .xlsb files, set the Filter to '*.xlsb'
$excelFiles = (Get-ChildItem -Path $sourcePath -Filter '*.xls*' -File).FullName
# Changing working folder because 7Zip option -w doesn't work
Set-Location -Path $tempfolder
foreach($File in $excelFiles) {
# Build ZIP file name
$zipFile = [System.IO.Path]::ChangeExtension($File, '.zip')
# Rename file to ZIP
Rename-Item -Path $File -NewName $zipFile
# Not using Expand-Archive because it changes the ZIP format
& $7zipExe x "$zipFile" -o"$tempFolder" | Out-Null
# Replace old text with new text. First replace wk-1 to wk and then wk-2 to wk-1
Get-ChildItem -Path $tempFolder -Recurse -Filter '*.rels' -File | ForEach-Object {
(Get-Content -Path $_.FullName -Raw) -replace $wkold, $wknn -replace $wkprev, $wkold |
Set-Content $_.FullName
}
# Update archive with new files. Not using Compress-Archive because it changes the ZIP format
& $7zipExe u -r "$zipFile" *.* | Out-Null
# Rename and Move the updated file back to the original location (overwrite)
Move-Item -Path $zipFile -Destination $File -Force
# remove all files from the temporary folder to start fresh
Remove-Item -Path "$tempfolder\*" -Recurse -Force
}
# Set location back to script root
Set-Location -Path $PSScriptRoot
# remove the temporary folder
Remove-Item -Path $tempfolder -Recurse -Force

How to recursively search all files in a directory and sub-directories using PowerShell?

I'm not understanding where the recursion is occurring nor how it's used in the below tree function (which is meant to emulate some of the linux tree command results).
From the tree function, how are files (or file names and their path) passed to, here, a SearchString function?
for context, here's a REPL session demonstrating the end-goal on a single file: getting the PSPath property for a file, and using that property for a simple regex.
Session transcript:
posh> $dir = "/home/nicholas/Calibre Library/Microsoft Office User/549 (1476)"
posh> $files = Get-ChildItem -Path $dir –File
posh> $files.Length
3
posh> $files[0].Extension
.txt
posh> $files[0].PSPath
Microsoft.PowerShell.Core\FileSystem::/home/nicholas/Calibre Library/Microsoft Office User/549 (1476)/549 - Microsoft Office User.txt
posh> $pattern = '(?=.*?foo)(?=.*?bar)'
posh> $string = Get-Content $files[0]
posh> $string | Select-String $pattern
This file doesn't have any "foo" and "bar" matches. The goal is to search the entire Calibre library using PowerShell as above.
large output from a tree of the Calibre library trimmed to a single result:
Directory: /home/nicholas/Calibre Library/Microsoft Office User/548 (1474)
Mode LastWriteTime Length Name
---- ------------- ------ ----
----- 2/20/2021 3:22 AM 159883 548 - Microsoft Office User.txt
----- 2/20/2021 2:13 AM 351719 cover.jpg
----- 2/20/2021 2:39 AM 1126 metadata.opf
posh> ./worker.ps1
How is the above file and path passed to the SearchString function?
the goal being to iterate through the entire library and search all plain-text file. (Assumption being that plain-text files have a ".txt" extension.)
library code:
function SearchFile($dir,$file)
{
$path = [string]::Concat($dir,"/",$file)
$pattern='(?=.*?foo)(?=.*?bar)'
$string = Get-Content $path
$result = $string | Select-String $pattern
$result
}
function tree($dir)
{
"$dir"
$tree = Get-ChildItem -Recurse
$tree = Get-ChildItem -Path $dir -Recurse
# get any files and invoke SearchFile here ?
$tree
}
worker code:
. /home/nicholas/powershell/functions/library.ps1
$dir = "/home/nicholas/Calibre Library"
tree $dir
The execution of the SearchFile function should be triggered when a ".txt" file is found. That logic is missing. But the larger missing piece is how to invoke SearchFile from the tree function so that every file gets searched.
How is that done? Leaving aside the file-type or file extension. Not seeing where the recursion occurs.
You are really overcomplicating things. You can do this very easily by using Get-ChildItem to find your txt files recursively in $dir path and then piping these FileInfo objects directly to Select-String cmdlet which accepts pipeline input and will grab the PSPath from the FileInfo object being passed to it and do its thing. Select-String will do this for every object that Get-ChildItem sends to it which are FileInfo objects for all txt files found recursively in your $dir path.
$dir = '/home/nicholas/Calibre Library/Microsoft Office User/549 (1476)'
Get-ChildItem -Recurse -Path $dir -Filter *.txt |
Select-String -Pattern '(?=.*?foo)(?=.*?bar)'
Get-ChildItem already does the recursion for you when you specify the -Recurse argument. For your code it doesn't make any difference. You get a linear list of all file informations that you can process using ForEach-Object in the same way as if you didn't specify -Recurse.
The SearchFile function should be executed when a ".txt" file is found.
Use the -Filter parameter to specify *.txt. Also when you want to get files only, always pass -File. This allows the filesystem provider to already skip directories, which is faster and also more correct (in theory there could be directories named e. g. foo.txt which would let SearchFile run into an error).
function tree($dir)
{
"$dir"
Get-ChildItem -Path $dir -Recurse -File -Filter *.txt | ForEach-Object {
SearchFile -dir $_.Directory.PSPath -file $_.Name
}
}
I don't know why your function SearchFile has separate parameters for directory and file name. The Get-ChildItem already outputs the full path in $_.PSPath. It doesn't make much sense to split the path apart and join it together again in SearchFile. I suggest you replace them by a single Path parameter.

Move files that contain a string to a subfolder with the same name as the original (PowerShell)

I'm using PowerShell and it is two days that I'm struggling on this issue.
In the directory C:\dir_1 I have many subfolders (sub_1, sub_2, ..., sub_n). Each of them contains several text files. For each subfolder i=1,2,...,n, I want to move the text files that contain the string "My-String" to the directory C:\dir_2\sub_i.
For example, if the file X in the path C:\dir1\sub_5 contains the string "My-String", I want to move it to the location C:\dir_2\sub_5. The destination folder is already existing.
I tried several modifications of the following code, but it does not work:
Get-ChildItem "C:\dir_1" | Where-Object {$_.PSIsContainer -eq $True} | Foreach-Object {Get-ChildItem "C:\dir_1\$_" | Select-String -pattern "My-String" | group path | select name | %{Move-Item $_.name "C:\dir_2\$_"}}
So, basically, what I tried to do is: foreach subfolder in dir_1, take the files that contain the string and move them to the subfolder in dir_2 with the same name. I tried several small modifications of that code, but I cannot get around my mistakes. The main error is "move-item: The given path format is not supported"... any help?
I feel like I could do better but this is my first approach
$dir1 = "C:\temp\data\folder1"
$dir2 = "C:\temp\data\folder2"
$results = Get-ChildItem $dir1 -recurse | Select-String -Pattern "asdf"
$results | ForEach-Object{
$parentFolder = ($_.Path -split "\\")[-2]
Move-Item -Path $_.Path -Destination ([io.path]::combine($dir2,$parentFolder))
}
Select-String can take file paths for its pipeline input. We feed it all the files that are under $dir1 using -recurse to get all of its children in sub folders. $results would contain an array of match objects. One of the properties is the path of the matched file.
With all of those $results we then go though each and extract the parent folder from the path. Then combine that folder with the path $dir2 in order to move it to it destination.
There are several assumptions that we are taking here. Some we could account for if need be. I will mention the one I know could be an issue first.
Your folders should not have any other subfolders under "sub_1, sub_2, ..., sub_n" else they will attempt to move incorrectly. This can be addressed with a little more string manipulation. In an attempt to make the code terse using -Recurse created this caveat.
Here is a one liner that does what you want too:
Get-ChildItem "C:\dir_1" | Where-Object {$_.PSIsContainer -eq $True} | ForEach-Object {$SubDirName = $_.Name;ForEach ($File in $(Get-ChildItem $_.FullName)){If ($File.Name -like "*My-String*"){Move-Item $File.FullName "C:\dir_2\$SubDirName"}}}
And if you'd like to see it broken out like Matt's answer:
$ParentDir = Get-ChildItem "C:\dir_1" | Where-Object {$_.PSIsContainer -eq $True}
ForEach ($SubDir in $ParentDir){
$SubDirName = $SubDir.Name
ForEach ($File in $(Get-ChildItem $SubDir.FullName)){
If ($File.Name -like "*My-String*"){
Move-Item $File.FullName "C:\dir_2\$SubDirName"
}
}
}

Renaming many folders in PowerShell

I have over 1000+ files that have to be renamed.
The first set folder and/or files are grouped by location, so the first four characters are the same for each file; there are four-five different locations. I need to delete the first few characters of the folder's name.
Example:
Old File: ABC_Doe, Jane
New File: Doe, Jane
any suggestions as to the quickest way to carry this out?
I've tried all of the following:
1st Attempt
$a = Get-ChildItem C:\example
$b = Where-Object {$_.name -like “*ABC_*”}
$cmdlet_name = “Rename-Item”
$d = (cmdlet_name $a $b)
invoke-expression $d
2nd Attempt
$e = Get-ChildItem C:\example
$f = $e.TrimStart (“ABC_”)
3rd Attempt
Rename-Item -{$_.name -like “*ASD*”, “”}
Try this, get all child items (files only), remove abc_ by replacing them (with nothing) and rename each file. To rename files in sub-directories add the -Recurse switch to the Get-ChildItem command:
Get-ChildItem c:\example -Filter ABC_* | Where-Object {!$_.PSIsContainer} | Rename-Item -NewName { ($_.BaseName -replace '^ABC_') + $_.Extension }
UPDATE
Actually, this should work as well and is much shorter (no need to append the file extension cause renaming is performed on the file name).
Get-ChildItem c:\example -Filter ABC_* | Where-Object {!$_.PSIsContainer} | Rename-Item -NewName { $_.Name -replace '^ABC_' }
get-childItem ABC_* | rename-item -newname { $_.name -replace 'ABC_','' }
Source: get-help rename-item -full

Resources