Handling strings (path to files) with wildcard in Powershell - powershell

I'm writing a PS script that looks for specific files in different directory.
My code looks like this
# $Path is provided by the user, it's a path like
# $Path = "C:\Some Directory\Project [ABC]\Files\"
# There's a check to ensure path ends with a backslash
$PDFFiles = Get-Item $($Path + "*.pdf")
for ($counter=0; $counter -lt $PDFFiles.Length; $counter++) {
# Do stuff
}
The issue is that $Path may have character considered as wildcard (eg [ or ] in my example), but I can't use -LitteralPath because I need the *.pdf wildcard to be interpreted.
How to properly handle strings to tell PShell that this part is litteral, and this one is a wildcard?

Use Get-ChildItem instead of Get-Item.
Pass the path of the folder to -LiteralPath and then use the -Filter parameter for the wildcard file name filter:
$PDFFiles = Get-ChildItem -LiteralPath $Path -Filter "*.pdf"
-LiteralPath will not attempt to expand wildcard sequences in the path
An alternative approach is to escape the $Path value:
$escapedPath = [WildcardPattern]::Escape($Path)
Get-Item -Path (Join-Path $escapedPath *.pdf)

Related

Powershell dropping characters while creating folder names

I am having a strange problem in Powershell (Version 2021.8.0) while creating folders and naming them. I start with a number of individual ebook files in a folder that I set using Set-Location. I use the file name minus the extension to create a new folder with the same name as the e-book file. The code works fine the majority of the time with various file extensions I have stored in an array beginning of the code.
What's happening is that the code creates the proper folder name the majority of the time and moves the source file into the folder after it's created.
The problem is, if the last letter of the source file name, on files with the extension ".epub" end in an "e", then the "e" is missing from the end of the created folder name. I thought that I saw it also drop "r" and "p" but I have been unable to replicate that error recently.
Below is my code. It is set up to run against file extensions for e-books and audiobooks. Please ignore the error messages that are being generated when files of a specific type don't exist in the working folder. I am just using the array for testing and it will be filled automatically later by reading the folder contents.
This Code Creates a Folder for Each File and moves the file into that Folder:
Clear-Host
$SourceFileFolder = 'N:\- Books\- - BMS\- Books Needing Folders'
Set-Location $SourceFileFolder
$MyArray = ( "*.azw3", "*.cbz", "*.doc", "*.docx", "*.djvu", "*.epub", "*.mobi", "*.mp3", "*.pdf", "*.txt" )
Foreach ($FileExtension in $MyArray) {
Get-ChildItem -Include $FileExtension -Name -Recurse | Sort-Object | ForEach-Object { $SourceFileName = $_
$NewDirectoryName = $SourceFileName.TrimEnd($FileExtension)
New-Item -Name $NewDirectoryName -ItemType "directory"
$OriginalFileName = Join-Path -Path $SourceFileFolder -ChildPath $SourceFileName
$DestinationFilename = Join-Path -Path $NewDirectoryName -ChildPath $SourceFileName
$DestinationFilename = Join-Path -Path $SourceFileFolder -ChildPath $DestinationFilename
Move-Item $OriginalFileName -Destination $DestinationFilename
}
}
Thanks for any help you can give. Driving me nuts and I am pretty sure it's something that I am doing wrong, like always.
String.TrimEnd()
Removes all the trailing occurrences of a set of characters specified in an array from the current string.
TrimEnd method will remove all characters that matches in the character array you provided. It does not look for whether or not .epub is at the end of the string, but rather it trims out any of the characters in the argument supplied from the end of the string. In your case, all dots,e,p,u,b will be removed from the end until no more of these characters are within the string. Now, you will eventually (and you do) remove more than what you intended for.
I'd suggest using EndsWith to match your extensions and performing a substring selection instead, as below. If you deal only with single extension (eg: not with .tar.gz or other double extensions type), you can also use the .net [System.IO.Path]::GetFileNameWithoutExtension($MyFileName) method.
$MyFileName = "Teste.epub"
$FileExt = '.epub'
# Wrong approach
$output = $MyFileName.TrimEnd($FileExt)
write-host $output -ForegroundColor Yellow
#Output returns Test
# Proper method
if ($MyFileName.EndsWith($FileExt)) {
$output = $MyFileName.Substring(0,$MyFileName.Length - $FileExt.Length)
Write-Host $output -ForegroundColor Cyan
}
# Returns Tested
#Alternative method. Won't work if you want to trim out double extensions (eg. tar.gz)
if ($MyFileName.EndsWith($FileExt)) {
$Output = [System.IO.Path]::GetFileNameWithoutExtension($MyFileName)
Write-Host $output -ForegroundColor Cyan
}
You're making this too hard on yourself. Use the .BaseName to get the filename without extension.
Your code simplified:
$SourceFileFolder = 'N:\- Books\- - BMS\- Books Needing Folders'
$MyArray = "*.azw3", "*.cbz", "*.doc", "*.docx", "*.djvu", "*.epub", "*.mobi", "*.mp3", "*.pdf", "*.txt"
(Get-ChildItem -Path $SourceFileFolder -Include $MyArray -File -Recurse) | Sort-Object Name | ForEach-Object {
# BaseName is the filename without extension
$NewDirectory = Join-Path -Path $SourceFileFolder -ChildPath $_.BaseName
$null = New-Item -Path $NewDirectory -ItemType Directory -Force
$_ | Move-Item -Destination $NewDirectory
}

Match string with specific numbers from array

I want to create a script that searches through a directory for specific ".txt" files with the Get-ChildItem cmdlet and after that it copies the ".txt" to a location I want. The hard part for me is to extract specific .txt files string from the array. So basically I need help matching specific files names in the array. Here is an example of the array I'm getting back with the following cmdlet:
$arrayObject = (Get-ChildItem -recurse | Where-Object { $_.Name -eq "*.txt"}).Name
The arrayobject variable is something like this:
$arrayobject = "test.2.5.0.txt", "test.1.0.0.txt", "test.1.0.1.txt",
"test.0.1.0.txt", "test.0.1.1.txt", "test.txt"
I want to match my array so it returns the following:
test.2.5.0.txt, test.1.0.0.txt, test.1.0.1.txt
Can someone help me with Regex to match the above file names from the $arrayObject?
As you already add the -Recurse parameter to Get-ChildItem, you can also use the -Include parameter like this:
$findThese = "test.2.5.0.txt", "test.1.0.0.txt", "test.1.0.1.txt"
$filesFound = (Get-ChildItem -Path 'YOUR ROOTPATH HERE' -Recurse -File -Include $findThese).Name
P.S. without the -Recurse parameter you need to add \* to the end of the rootfolder path to be able to use -Include
Maybe something like:
$FileList = Get-ChildItem -path C:\TEMP -Include *.txt -Recurse
$TxtFiles = 'test1.txt', 'test3.txt', 'test9.txt'
Foreach ($txt in $TxtFiles) {
if ($FileList.name -contains $txt) {Write-Host File: $Txt is present}
}
A general rule: Filter as left as possible! Less objects to be processed, less resources to be used, faster to be processed!
Hope it helps!
Please try to clarify what the regex should match.
I created a regex which matches out of the given filenames only the files you wanted to retrieve:
"*.[1-9].[0-9].[0-9].txt"
You can tryout the small check I wrote.
ForEach($file in $arrayobject){
if($file -LIKE "*.[1-9].[0-9].[0-9].txt"){
Write-Host $file
}}
I think the "-LIKE" operator would be better to check if a string matches a regex.
Let me know if this helps.
Sorry for the late reply. Just got back in the office today. My question has been misinterpreted but that's my fault. I wasn't clear what I really want to do.
What I want to do is search through a directory and retrieve/extract in my case the (major)version of a filename. So in my case file "test.2.5.0.txt" would be version 2.5.0. After that I will get the MajorVersion and that's 2. Then in an If statement I would check if it's greater or equal to 1 and then copy it to a specific destination. To add some context to it. It's nupkg files and not txt. But I figured it out. This is code:
$sourceShare = "\\server1name\Share\txtfilesFolder"
destinationShare = "\\server2name\Share\txtfilesFolder"
Get-ChildItem -Path $sourceShare `
-Recurse `
-Include "*.txt" `
-Exclude #("*.nuspec", "*.sha512") `
| Foreach-Object {
$fileName = [System.IO.Path]::GetFileName($_)
[Int]$majorVersion = (([regex]::Match($fileName,"(\d+(.\d+){1,})" )).Value).Split(".")[0]
if ($majorVersion -ge 1)
{
Copy-Item -Path $_.FullName `
-Destination $destinationShare `
-Force
}
}
If you have anymore advice. Let me know. I would be great to extract the major version without using the .Split method
Grtz

How Get-Childitem and only include subfolders and files?

I have a script, and currently I do the following, which gets the full path to the files in the subdir:
$filenameOut = "out.html"
#get current working dir
$cwd = Get-ScriptDirectory
#get files to display in lists
$temp = Join-Path $cwd "Initial Forms"
$temp = Join-Path $temp "General Forms"
$InitialAppointmentGenArr = Get-ChildItem -Path $temp
So this will return a list where the first file in the array looks like this:
"//server/group/Creds/Documents/Initial Forms/General Forms/Background Check.pdf"
However, to have my generated web page work on our extranet, I can't give the full path to the file. I just need it to return:
"Initial Forms/General Forms/Background Check.pdf"
This will be a link I can use on the extranet. How do I get get-childitem to return just the sub-path?
My script is run from
//server/group/Creds/Documents
I can't find any examples similar to this. I'd like to avoid hard-coding the script location as well, in case it gets moved.
The easy way is to simply trim the unneeded path including trailing slash:
$filenameOut = "out.html"
#get current working dir
$cwd = Get-ScriptDirectory
#get files to display in lists
$temp = Join-Path $cwd "Initial Forms"
$temp = Join-Path $temp "General Forms"
$FullPath = Get-ChildItem -Path $temp
$InitialAppointmentGenArr = $FullPath | %{ $_.FullName.Replace($cwd + "\","")}
I suggest the following approch:
$relativeDirPath = Join-Path 'Initial Forms' 'General Forms'
Get-ChildItem -LiteralPath $PSScriptRoot/$relativeDirPath | ForEach-Object {
Join-Path $relativeDirPath $_.Name
}
Note that I've used $PSScriptRoot in lieu of $cwd, as it sound like the latter contains the directory in which your script is located, which automatic variable $PSScriptRoot directly reports.
Here's a generalized variation that also works with recursive use of Get-ChildItem:
$relativeDirPath = Join-Path 'Initial Forms' 'General Forms'
Get-ChildItem -LiteralPath $PSScriptRoot/$relativeDirPath | ForEach-Object {
$_.FullName.Substring($PSScriptRoot.Length + 1)
}
As an aside: In the cross-platform PowerShell (Core) 7+ edition, the underlying .NET Core framework's System.IO.Path type now has a .GetRelativePath() method, which is a convenient way to obtain a relative path from an absolute one, via a reference path:
# PowerShell (Core) 7+ only.
PS> [IO.Path]::GetRelativePath('/foo/bar', '/foo/bar/bam/baz.txt')
bam/baz.txt
Note:
Since .NET's working directory typically differs from PowerShell's, be sure to provide full input paths.
Also, be sure that the paths are file-system-native paths, not based on PowerShell-only drives.
Convert-Path can be used to determine full, file-system-native paths.

Powershell change extension

I have a problem with change extension of a file. I need to write a script which is replicating data, but data have two files. Filename is not a string, so we can't use normal -replace
I need to get from
filename.number.extension
this form
filename.number.otherextension
We try to use a split, but this command show us things like below
filename
number
otherextension
Thanks for any ideas,
[System.IO.Path]::ChangeExtension("test.old",".new")
You probably want something like the -replace operator:
'filename.number.extension' -replace 'extension$','otherextension'
The $ is regular expression syntax meaning end of line. This should ensure that the -replace does not match "extension" appearing elsewhere in the filename.
A simple Utility Function
<#
# Renames all files under the given path (recursively) whose extension matches $OldExtension.
# Changes the extension to $NewExtension
#>
function ChangeFileExtensions([string] $Path, [string] $OldExtension, [string] $NewExtension) {
Get-ChildItem -Path $Path -Filter "*.$OldExtension" -Recurse | ForEach-Object {
$Destination = Join-Path -Path $_.Directory.FullName -ChildPath $_.Name.Replace($OldExtension, $NewExtension)
Move-Item -Path $_.FullName -Destination $Destination -Force
}
}
Usage
ChangeFileExtensions -Path "c:\myfolder\mysubfolder" -OldExtension "extension" -NewExtension "otherextension"
But it can do more than just this. If you had the following files in the same folder as your script
example.sample.csv
example.txt
mysubfolder/
myfile.sample.csv
myfile.txt
this script would rename all the .sample.csv files to .txt files in the given folder and all subfolders and overwrite any existing files with those names.
# Replaces all .sample.csv files with .txt extensions in c:\myfolder and in c:\myfolder\mysubfolder
ChangeFileExtensions -Path "c:\myfolder" -OldExtension "sample.csv" -NewExtension "txt"
If you don't want it to be recursive (affecting subfolders) just change
"*.$OldExtension" -Recurse | ForEach-Object
to
"*.$OldExtension" | ForEach-Object
This could work:
Get-ChildItem 'C:\Users\Administrator\Downloads\text files\more text\*' *.txt | rename-item -newname { [io.path]::ChangeExtension($_.name, "doc") }
You can remove the last item with the the [0..-1] slice and add the new extension to that
(("filename.number.extension" -split "\.")[0..-1] -join '.') +".otherextension"

Powershell set Attributes sh when name starts with ~

I need to write a script that goes recursive through several hundred directories and if it finds a file beginning with "~", the hidden and system attribute should be set at that file.
So far I got this:
Get-ChildItem C:\test\~* - Recurse | foreach {$_.Attributes = 'Hidden, System'}
But it only seems to change the first file.
Use the -Filter parameter to find files beginning with ~.
Add the -File switch to exclude directories.
Remove the space between - and Recurse
This should work:
Get-ChildItem 'C:\test\' -Filter '~*' -Recurse -File | foreach {
$_.Attributes = 'Hidden, System'
}
Do something like dir c:\ -recurse | ? Name -match "~", that gets you all with the ~ in. Then set the attribute like you are.