Shell.Application - write new Values? - powershell

I have a directory with MP3 files. I can read the properties with this script. How I can write properties (Album, Genre, etc.)?
$com = (New-Object -ComObject Shell.Application).NameSpace('C:\Users\Peter\Music')
for( $index = 0; ((-not $bitrateAttribute) -or (-not $albumAttribute)); ++$index ) {
$name = $com.GetDetailsOf($com.Items,$index)
if ($name -eq 'Album') {$albumAttribute = $index}
if ($name -eq 'Bit rate') {$bitrateAttribute = $index}
}
$com.Items() | ForEach-Object {
New-Object -TypeName PSCustomObject -Property #{
Name = $_.Name
Album = $com.GetDetailsOf($_,$albumAttribute)
BitRate = $com.GetDetailsOf($_,$bitrateAttribute)
} | Select-Object -Property Name,Album,BitRate
}
Or is there a better way to write ID3 tags to MP3 files?

Related

Powershell for retrieve local USB printer details and insert into WMI

I am trying to get USB printer model and serial from the Win32_PnPEntity, then insert these info into a custom WMI namespace, so that I can use the Hardware inventory function in SCCM to collect the info for reporting.
I noticed that the location storing these info varies from one manufacturer to another.
such as the FujiXerox stores in "USB Printing Support" while HP and Brother stores in "USB Composite Device"
what weird is after I somehow got the below code working on my computer, when I try it on other computers, it only returns the first character, such as:
Results on my computer (two USB printers connected)
Model
Serial
HEWLETT-PACKARDHP_LASERJET_400_M401D
VNH3G0XXXX
FUJI_XEROXDOCUPRINT_P355_DB
YWG-50XXXX
Results on other computers (eg. a HP printer with serial no. starting with V)
Model
Serial
H
V
I am a system administrator managing SCCM, occasionally using PowerShell to help on my tasks, I just completely stuck at the moment as I didn't find any hint that will trim the results.
Thanks
Andrew
$ModelInfo = #()
$SerialInfo = #()
$FullInfo = #{}
$Final=#()
$USBPrinterModels = Get-WmiObject Win32_PnPEntity | Where-Object {$_.DeviceID -Match "USBPRINT"}|select DeviceID
$USBPrinterSerials2 = Get-WmiObject Win32_PnPEntity | Where-Object {$_.Description -Match "USB 列印支援" -or $_.Description -Match "USB Printing Support"}|select DeviceID
$USBPrinterSerials = Get-WmiObject Win32_PnPEntity | Where-Object {$_.Description -Match "USB Composite Device"}|select DeviceID
Foreach ($USBPrinterModel in $USBPrinterModels)
{
$ModelFull = $USBPrinterModel.DeviceID
$Model = #{}
$Model.model += ($ModelFull.Split("\"))[1]
$ModelInfo += $Model
}
Foreach ($USBPrinterSerial in $USBPrinterSerials)
{
$SerialFull = $USBPrinterSerial.DeviceID
$Serial = #{}
$Serial.serial += $SerialFull.Split("\")[2]
If($Serial.serial -notmatch "&")
{
$SerialInfo += $Serial
}
}
Foreach ($USBPrinterSerial2 in $USBPrinterSerials2)
{
$SerialFull2 = $USBPrinterSerial2.DeviceID
$Serial2 = #{}
$Serial2.serial += $SerialFull2.Split("\")[2]
If($Serial2.serial -notmatch "&")
{
$SerialInfo += $Serial2
}
}
$MaxLength = [Math]::Max($ModelInfo.Length, $SerialInfo.Length)
for ($loop_index = 0; $loop_index -lt $MaxLength; $loop_index++)
{
$Final += new-object psobject -Property #{
Model=$ModelInfo.model[$loop_index]
Serial=$SerialInfo.serial[$loop_index]
}
# $Final+=$ModelInfo[$loop_index]
# $Final+=$SerialInfo[$loop_index]
}
$Class = Get-WmiObject Win32_USBPrinterDetails -ErrorAction SilentlyContinue
If ($Class) {Remove-WmiObject -Class Win32_USBPrinterDetails}
$WMIClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);
$WMIClass["__CLASS"] = "Win32_USBPrinterDetails";
$WMIClass.Qualifiers.Add("Static", $true)
$WMIClass.Properties.Add("Model", [System.Management.CimType]::String, $false)
$WMIClass.Properties["Model"].Qualifiers.Add("read", $true)
$WMIClass.Properties.Add("Serial", [System.Management.CimType]::String, $false)
$WMIClass.Properties["Serial"].Qualifiers.Add("key", $true)
$WMIClass.Properties["Serial"].Qualifiers.Add("read", $true)
$WMIClass.Put()
ForEach ($FInfo in $Final) {
[void](Set-WmiInstance -Path \\.\root\cimv2:Win32_USBPrinterDetails -Arguments #{Model=$FInfo.model; Serial=$FInfo.serial})
}
I am so stupid,
When there is only ONE USB printer, the $ModelInfo and $SerialInfo become "String" type and caused the [loop_index] return the first character of the string instead of the first entry of the array.
I added a gettype check on the variables (surely not perfect).
$ModelInfo = #()
$SerialInfo = #()
$FullInfo = #{}
$Final=#()
$USBPrinterModels = Get-WmiObject Win32_PnPEntity | Where-Object {$_.DeviceID -Match "USBPRINT"}|select DeviceID
$USBPrinterSerials2 = Get-WmiObject Win32_PnPEntity | Where-Object {$_.Description -Match "USB 列印支援" -or $_.Description -Match "USB Printing Support"}|select DeviceID
$USBPrinterSerials = Get-WmiObject Win32_PnPEntity | Where-Object {$_.Description -Match "USB Composite Device"}|select DeviceID
Foreach ($USBPrinterModel in $USBPrinterModels)
{
$ModelFull = $USBPrinterModel.DeviceID
$Model = #{}
$Model.model += ($ModelFull.Split("\"))[1]
$ModelInfo += $Model
}
Foreach ($USBPrinterSerial in $USBPrinterSerials)
{
$SerialFull = $USBPrinterSerial.DeviceID
$Serial = #{}
$Serial.serial += $SerialFull.Split("\")[2]
If($Serial.serial -notmatch "&")
{
$SerialInfo += $Serial
}
}
Foreach ($USBPrinterSerial2 in $USBPrinterSerials2)
{
$SerialFull2 = $USBPrinterSerial2.DeviceID
$Serial2 = #{}
$Serial2.serial += $SerialFull2.Split("\")[2]
If($Serial2.serial -notmatch "&")
{
$SerialInfo += $Serial2
}
}
If ($ModelInfo.model.GetType().name -eq "String") {
$Final += new-object psobject -Property #{
Model=$ModelInfo.model
Serial=$SerialInfo.serial
}
}
ElseIf ($ModelInfo.model.GetType().name -ne "String"){
$MaxLength = [Math]::Max($ModelInfo.Length, $SerialInfo.Length)
for ($loop_index = 0; $loop_index -lt $MaxLength; $loop_index++)
{
$Final += new-object psobject -Property #{
Model=$ModelInfo.model[$loop_index]
Serial=$SerialInfo.serial[$loop_index]
}
}
}
$Class = Get-WmiObject Win32_USBPrinterDetails -ErrorAction SilentlyContinue
If ($Class) {Remove-WmiObject -Class Win32_USBPrinterDetails}
$WMIClass = New-Object System.Management.ManagementClass("root\cimv2", [String]::Empty, $null);
$WMIClass["__CLASS"] = "Win32_USBPrinterDetails";
$WMIClass.Qualifiers.Add("Static", $true)
$WMIClass.Properties.Add("Model", [System.Management.CimType]::String, $false)
$WMIClass.Properties["Model"].Qualifiers.Add("read", $true)
$WMIClass.Properties.Add("Serial", [System.Management.CimType]::String, $false)
$WMIClass.Properties["Serial"].Qualifiers.Add("key", $true)
$WMIClass.Properties["Serial"].Qualifiers.Add("read", $true)
$WMIClass.Put()
ForEach ($FInfo in $Final) {
[void](Set-WmiInstance -Path \\.\root\cimv2:Win32_USBPrinterDetails -Arguments #{Model=$FInfo.model; Serial=$FInfo.serial})
}
$final|FT

powershell sort-object and keeps window open

I have following script to check the installed software on local and remote machines.
Usually it works fine but i have two problems. It only works when i open it in ISE. If i open it in a normal powershell, the script close immediately. Even a pause or read-host command won't work.
For example here is my script for a local machine. Hope you guys can help me.
Function Get-InstalledSoftware {
Param(
[Alias('Computer','ComputerName','HostName')]
[Parameter(
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$true,
Position=1
)]
[string]$Name = $env:COMPUTERNAME
)
Begin{
$lmKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall","SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
$lmReg = [Microsoft.Win32.RegistryHive]::LocalMachine
$cuKeys = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
$cuReg = [Microsoft.Win32.RegistryHive]::CurrentUser
}
Process{
if (!(Test-Connection -ComputerName $Name -count 1 -quiet)) {
Write-Error -Message "Unable to contact $Name. Please verify its network connectivity and try again." -Category ObjectNotFound -TargetObject $Computer
Break
}
$masterKeys = #()
$remoteCURegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($cuReg,$computer)
$remoteLMRegKey = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($lmReg,$computer)
foreach ($key in $lmKeys) {
$regKey = $remoteLMRegKey.OpenSubkey($key)
foreach ($subName in $regKey.GetSubkeyNames()) {
foreach($sub in $regKey.OpenSubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
foreach ($key in $cuKeys) {
$regKey = $remoteCURegKey.OpenSubkey($key)
if ($regKey -ne $null) {
foreach ($subName in $regKey.getsubkeynames()) {
foreach ($sub in $regKey.opensubkey($subName)) {
$masterKeys += (New-Object PSObject -Property #{
"ComputerName" = $Name
"Name" = $sub.getvalue("displayname")
"SystemComponent" = $sub.getvalue("systemcomponent")
"ParentKeyName" = $sub.getvalue("parentkeyname")
"Version" = $sub.getvalue("DisplayVersion")
"UninstallCommand" = $sub.getvalue("UninstallString")
"InstallDate" = $sub.getvalue("InstallDate")
"RegPath" = $sub.ToString()
})
}
}
}
}
$woFilter = {$null -ne $_.name -AND $_.SystemComponent -ne "1" -AND $null -eq $_.ParentKeyName}
$props = 'Name','Version','ComputerName','Installdate','UninstallCommand','RegPath'
$masterKeys = ($masterKeys | Where-Object $woFilter | Select-Object $props | Sort-Object Name)
$masterKeys
}
End{}
}
Get-InstalledSoftware | select-object name | sort-object
Send your output somewhere, not just to the host window. I suspect the windows closes or your pause command is kicking in before its retrieved the results thus they aren't sent to the host window.
The following works fine for me run as a script:
Get-InstalledSoftware | Select-Object name | Sort-Object | Out-Gridview

Loop Confusion Comparing Objects

I am trying to create my own service comparison script. I see some online but want to do this myself. I've only gotten so far. I keep getting confused.
The desired output is with the following format. It doesn't even have to show what's different. I just want to see what the previous state was compared to the current state. I did a compare-object and it didn't give me the format I desired. I then thought maybe I should just do two nested loops and create a new object with the states I want in it. It didn't work out correctly, it returns an array. So then I thought, maybe a for loop in the foreach loop... I keep confusing myself and it's so close.
You have to provide a csv with some services to compare to to make this work as part of it's paramaters.
Usage
Inspect-ServiceSnapshot -SnapshotPath "C:\YourPath"
Desired output
Name CurrentState PreviousState
app1 Running Stopped
Code So Far
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidatePattern("C:")]
[string]
$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]
$TimeToWait
)
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
} else {
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
}
}
This should do it because it is actually checking the value for the old service to be the same as the one Get-Service provides at a certain time.
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Leaf})]
[string]$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]$TimeToWait = 0
)
if($TimeToWait) { Start-Sleep -Seconds $TimeToWait }
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (Get-Service)) {
# make sure we are dealing with the SAME service
$oldItem = $old | Where-Object { $_.Name -eq $entry.Name }
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = if ($oldItem) { $oldItem.status } else { 'Unknown' }
}
$list += $object
}
$list
}
This answer is assuming there are no new services added on a regular basis.
You almost got it! You can forgo the nested loops, get rid of the else block (it is redundant), and use an index loop. When you're using nested loops like that, you are iterating through all of the array elements in the $old array every time you iterate through one of the (get-service) objects. This can cause issues when your arrays include thousands of objects.
You can easily get to what you want by using a for loop.
e.g.
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
}
$list = #();
$old = Import-Csv -Path $SnapshotPath | Sort-Object Name;
$new = Get-Service | Sort-Object Name;
for ($i -eq 0; $i -lt $old.length -or $i -lt $new.length; $i++) {
$object = New-Object -TypeName psobject -Property #{
Name = $new[$i].Name
CurrentStatus = $new[$i].status
DisplayName = $new[$i].displayname
PreviousStatus = $old[$i].status
}
$list += $object;
}
$list;
A lot of your code is redundant and you can just do this all in one go. Since you're not technically comparing any objects, you can just populate the fields as they go.

Getting unassigned phone numbers for Skype for Business in powershell

Has anyone ever done something like this in powershell? I'm automating new user creation and have to set up their skype for business accounts. Here's what I have so far:
<# set up skype for business #>
Enable-CsUser -Identity $FULLNAME `
-RegistrarPool REDACTED
-SipAddress $SIP
Set-CsUser $FULLNAME `
-EnterpriseVoiceEnabled $true `
-ExchangeArchivingPolicy Uninitialized `
-LineURI $PHONENUMBER
# only for driver manager/managers
Grant-CsConferencingPolicy $FULLNAME `
-PolicyName $ConferencingPolicy
Grant-CsExternalAccessPolicy -identity $FULLNAME `
-PolicyName 'Allow external access'
All I have left is the to assign the LineURI, and I can't seem to find anything about how to do that. I did, however, find a script that does this, but it doesn't seem like it's specific to my needs. My question is this: how can I query Skype for Business in powershell to grab the first unassigned number and assign it as the LineURI? I have been using New-CSUnassignedNumber but cannot seem to make any headway.
SfB doesn't keep a record of your number ranges, so it doesn't know if you assigning 1299 is the last in a range, or if the range stretches into the 1300's
Get-CsUser | ? LineUri | Select LineUri | Sort
this will give you a sorted list of all assigned uri's in your organization, so you'll be able to pick out ones which may be assignable, regardless you can use a number of methods to find what you're looking for, but you may be interested in this for the longer-term approach, it's a script which takes in your number ranges, and outputs a list of the used and available ones in a nice Csv.
#region Input
$NumRangeKeyTable = #{
#Make sure numbers are in the same format as they are in Skype,
#Do not include any ';ext' or 'tel:' etc. formatting!
#Put single numbers in the format:
# "+35313456789" = ""
#Put number ranges in the format:
# "+35313456700" = "+35313456799"
"+35313456789" = ""
"+35313456700" = "+35313456799"
}
#Save Location, set to $null to be prompted for location.
$FileName = $null
#endregion
#region Code
#region Helper Functions
Function Get-CsAssignedURIs {
$AllNumbers = #()
$Users = Get-CsUser
$Users | ? {$_.LineURI -ne ""} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "User" }}
$Users | ? {$_.PrivateLine -ne ""} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.PrivateLine ; Type = "PrivateLine" }}
Get-CsRgsWorkflow | Where-Object {$_.LineURI -ne ""} | Select Name,LineURI | %{$AllNumbers += New-Object PSObject -Property #{Name = $_.Name ; SipAddress = $_.PrimaryUri ; Number = $_.LineURI ; Type = "Workflow" }}
Get-CsCommonAreaPhone -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "CommonArea" }}
Get-CsAnalogDevice -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "AnalogDevice" }}
Get-CsExUmContact -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "ExUmContact" }}
Get-CsDialInConferencingAccessNumber -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.PrimaryUri ; Number = $_.LineURI ; Type = "DialInAccess" }}
Get-CsTrustedApplicationEndpoint -Filter {LineURI -ne $null} | %{ $AllNumbers += New-Object PSObject -Property #{Name = $_.DisplayName ; SipAddress = $_.SipAddress ; Number = $_.LineURI ; Type = "ApplicationEndpoint" }}
Return $AllNumbers
}
function Get-UniqueExt {
Param(
[string]$Uri1,
[string]$Uri2
)
$Reg = "^([0-9+])+$"
if ([string]::IsNullOrEmpty($uri1) -and [string]::IsNullOrEmpty($Uri2)) { return "Two blank strings provided" }
if ($Uri1 -eq $Uri2) { return $Uri1 }
if ([string]::IsNullOrEmpty($uri1)) { return $Uri2 }
if ([string]::IsNullOrEmpty($uri2)) { return $Uri1 }
if ($Uri1.Length -ne $Uri2.Length) { return "Strings cannot be different lengths" }
if (($Uri1 -notmatch $Reg) -or ($Uri2 -notmatch $Reg)) { return "Strings must be in the format '0123..' or '+123..'" }
($Uri1.Length-1)..0 | % {
if ($Uri1[$_] -ne $Uri2[$_]) { $Diff = $_ }
}
$Start = $Uri1.Substring(0,$Diff)
$Sub1 = $Uri2.Substring($Diff)
$Sub2 = $Uri1.Substring($Diff)
if ($Sub1 -lt $Sub2) {
$Min = $Sub1 ; $Max = $Sub2
} else {
$Min = $Sub2 ; $Max = $Sub1
}
$FormatStr = "" ; 1..$Min.Length | % { $FormatStr += "0"}
$Min..$Max | % { "$($Start)$($_.ToString($FormatStr))" }
}
function Save-ToFile {
Param(
[Parameter(ValueFromPipeline=$True)]
$Item = $null,
[switch]$ReturnName,
$ExtFilter = "*",
$WinTitle = "Select File",
$FileTypeDisplay = $null
)
If ($FileTypeDisplay -eq $null) {
If ($ExtFilter -eq "*") {
$ExtName = "All"
} Else {
$ExtName = (Get-Culture).TextInfo.ToTitleCase($ExtFilter)
}} Else {
$ExtName = (Get-Culture).TextInfo.ToTitleCase($FileTypeDisplay) }
[System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null
$FolderDialog = New-Object System.Windows.Forms.SaveFileDialog
$FolderDialog.Filter = "$($ExtName) files (*.$($ExtFilter.ToLowerInvariant()))| *.$($ExtFilter.ToLowerInvariant())"
$FolderDialog.Title = $WinTitle
$Result = $FolderDialog.ShowDialog()
If ($Result -eq "OK"){
$Item | Out-File $FolderDialog.FileName -Append
If ($ReturnName) { return $FolderDialog.FileName }}
Else {
Write-Error "No file selected" }
}
#endregion
Function Main {
Param ( [Hashtable]$NumRanges )
#region Process Data
$AllNums = $NumRanges.Keys | % {
Get-UniqueExt -Uri1 $_ -Uri2 $NumRanges[$_]
}
$S4BNums = Get-CsAssignedURIs
$S4BNums | % { $_.Number = ($_.Number.Split(';')[0] -ireplace "tel:","") }
$KT = #{}
$S4BNums | % {
$KT[$_.Number] = $_
}
$FullRecord = $AllNums | Sort | % {
$Number = $_
$Type = ""
$Name = ""
if ($KT[$_] -ne $null){
$UseDetails = $KT[$_]
$Name = $UseDetails.Name
$Type = $UseDetails.Type
}
[PSCustomObject]#{
Number = $Number
Name = $Name
Type = $Type
}
}
#endregion
return $FullRecord
}
$Results = Main $NumRangeKeyTable
#region Output-Data
if ($FileName -eq $null) {
$FileName = (Save-ToFile -Item "" -ReturnName -ExtFilter "Csv")
}
if ($FileName -ne $null) {
$Results | Export-Csv -Path $FileName -NoTypeInformation
} else { $Results | Out-GridView }
#endregion
#endregion

Improving the speed of Get-FileMetaData

I'm currently using the below script taken from scriptingguys.com (all credit to them, I just added the bottom 2 lines.) That takes a directory and pulls the file path and comments field from the meta data of the files. Currently the script take's a little over 1.5 minutes to fully run. Is there anyway to speed this up or use a different method to get this data?
I am using this script at the start of some software I have written and 1.5+ minutes is too long for the script to complete. Any thoughts/comments?
Function Get-FileMetaData
{
Param([string[]]$folder)
foreach($sFolder in $folder)
{
$a = 0
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($File in $objFolder.items())
{
$FileMetaData = New-Object PSOBJECT
for ($a ; $a -le 266; $a++)
{
if($objFolder.getDetailsOf($File, $a))
{
$hash += #{$($objFolder.getDetailsOf($objFolder.items, $a)) =
$($objFolder.getDetailsOf($File, $a)) }
$FileMetaData | Add-Member $hash
$hash.clear()
} #end if
} #end for
$a=0
$FileMetaData
} #end foreach $file
} #end foreach $sfolder
} #end Get-FileMetaData
$fileMetaData = Get-FileMetaData -folder "C:\Pics" | select 'Name', 'Path', 'Comments' | Sort-Object 'Name'
$fileMetaData | select 'Name', 'Path', 'Comments' | Export-CSV "C:\SCRIPTS\TestDirectory.txt" -encoding Utf8 -NoTypeInformation
Solved by wOxxOm, thanks for your help! Running the below and now working.
Function Get-FileMetaData(
[string[]]$folders,
[string[]]$properties
) {
$shellApp = New-Object -ComObject Shell.Application
$supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
$hash = if ($supportsOrdered) { [ordered]#{} } else { #{} }
# walk the folders and get the properties by index found above
$folders | ForEach {
$shellFolder = $shellApp.namespace($_)
# get all headers and find their indexes
$allProps = #{}
foreach ($index in 0..266) {
$allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
}
$shellFolder.items() | ForEach {
$file = $_
$hash.Clear()
foreach ($prop in $properties) {
if (($index = $allProps[$prop]) -ne $null) {
if ($value = $shellFolder.getDetailsOf($file, $index)) {
$hash[$prop] = $value
}
}
}
if ($supportsOrdered) {
[PSCustomObject]$hash
} else {
Select $properties -inputObject (
New-Object PSObject -Property $hash
)
}
}
}
}
Get-FileMetaData -folders 'C:\PICS' -properties Name, Path, Comments | Sort-Object Name |
select Name, Path, Comments | Export-Csv 'C:\Scripts\test.txt' -encoding UTF8 -NoTypeInformation
getDetailsOf is slow, and your code needlessly invokes it 267 times for each file when you only need it for 3 properties.
Collect the property names just once at the start of the function, don't do it on every file
Add-Member is slow. Don't invoke it on every property. Collect all found properties in a hashtable and pass it once to Add-Member or, since you create an empty object, directly to New-Object. To enforce the order of properties use Select-Object in PowerShell 2. Note, PowerShell 3.0 and newer support [ordered] and [PSCustomObject] typecast (see the code below).
Use pipelining instead of foreach statements so that the results appear immediately
Files are already sorted by name, at least on NTFS file system in Windows, so no need to sort.
Function Get-FileMetaData(
[string[]]$folders,
[string[]]$properties
) {
$shellApp = New-Object -ComObject Shell.Application
# get all headers and find their indexes
$shellFolder = $shellApp.namespace($folders[0])
$allProps = #{}
foreach ($index in 0..266) {
$allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
}
$supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
$hash = if ($supportsOrdered) { [ordered]#{} } else { #{} }
# walk the folders and get the properties by index found above
$folders | ForEach {
$shellFolder = $shellApp.namespace($_)
$shellFolder.items() | ForEach {
$file = $_
$hash.Clear()
foreach ($prop in $properties) {
if (($index = $allProps[$prop]) -ne $null) {
$hash[$prop] = $shellFolder.getDetailsOf($file, $index)
}
}
if ($supportsOrdered) {
[PSCustomObject]$hash
} else {
Select $properties -inputObject (
New-Object PSObject -Property $hash
)
}
}
}
}
Usage example 1:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments
Usage example 2:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments |
Export-Csv r:\results.csv -encoding UTF8 -NoTypeInformation
Usage example 3 gets all properties, which is slow:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2'