One of the many Sisyphean tasks my wife performs is keeping the all the metadata tags of our digital photographs up to date. She meticulously keeps tabs on the tags she’s used but with the sheer number of photos, new photos being added and the children starting to take photos too it’s a pretty time-consuming task.
So; what she needs is a list of all the tags used on each picture file (including ones missing with tags) in an easily sortable format.
Being the caring husband that I am I started spending some time solving that problem with PowerShell.
Of course, she might have been happier with me doing all the ironing for a week so she can focus fully on sorting the pictures out but this is a) more efficient and b) a solution that she can re-use (teach a woman to (photograph) fish etc) and so is a better use of our time.
At least, that’s my excuse and I’m sticking to it.The full script is here. It’s based on the metadata listing function I wrote here with some changes I’ll go into below.
The requirements are;
- The script must output to an easily usable file format like CSV.
- The individual tags on each file must be in a searchable / sortable format; ideally with a separate column for each.
- Files with no or missing tags must be shown.
The first set of changes I made was to take the original function and convert it to a PowerShell module. There will be two usable functions when I’m finished updating it and I may well add more in the future so it makes sense to do it now.
That’s not too difficult to do; make sure the file has a .psm1 extension and that all the functions you want available are exported;
Export-ModuleMember -Function "Get-MetaData" Export-ModuleMember -Function "Export-MetaTagListCSV"
The new function to export to the CSV is as follows;
Function Export-MetaTagListCSV { [CmdletBinding()] param ( [parameter(Mandatory=$True,ValueFromPipeline=$True)] [string[]]$Path, [ValidateScript({Test-Path -PathType Leaf -LiteralPath $_ -IsValid})] [string]$CSVFile=$PSScriptRoot+"\MetadataList.csv" ) $AllTags=@() #Get and sort all the Tags in the passed files. Keep track off all the Tag names; they'll be used later #to select the properties $Data=Get-MetaData $Path | Select-Object -Property FullName, Tags | ForEach {[string[]]$_.Tags = @(($_.Tags.Split(";")).Trim() | Sort-Object);$_} | %{Write-Verbose "Processing $($_.FullName)";$Output=New-Object -typename PSObject; $Output | Add-Member -MemberType NoteProperty -Name "FullName" -Value $_.FullName; ForEach ($Tag in $_.Tags){if ($Tag -ne ""){$Output | Add-Member -MemberType NoteProperty -Name $Tag -Value $True;$AllTags+=$Tag}};$Output} [string[]]$AllProperties=@("FullName")+($AllTags | Select-Object -Unique) $Data | Select-Object -Property $AllProperties | Export-Csv -NoTypeInformation -Path $CSVFile -Force -Verbose }
The only parameter that’s really mandatory is the path of the file or files to be examined. If a path to CSV export file is passed it has to be valid (Validate Script) otherwise if it’s omitted a default export file path is created.
The next bit is a large one-liner that’s the meat of the function so I’ll break it down below. The result of the entire command is stored in $Data for later.
$Data=Get-MetaData $Path | Select-Object -Property FullName, Tags |
This gets all the metadata information in the path and just selects the FullName of the file and the Tags property and passes it onto the next pipeline element.
Each tag in the Tags property is in the format;
[Tag Name]; [Space]
For example
Chell; Portal; GlaDos
ForEach {[string[]]$_.Tags = @(($_.Tags.Split(";")).Trim() | Sort-Object);$_}
For each of the file objects split the tags on the “;” character into an array (trimming the spaces as we go). These are then sorted alphabetically and added back to the object it’s passed down the pipeline.
%{Write-Verbose "Processing $($_.FullName)";$Output=New-Object -typename PSObject;$Output | Add-Member -MemberType NoteProperty -Name "FullName" -Value $_.FullName
This just writes the file name to Verbose (so we can see what the script is doing) and creates a new PowerShell object to hold the new data. It then adds the file name as the first property on that object.
ForEach ($Tag in $_.Tags){if ($Tag -ne ""){$Output | Add-Member -MemberType NoteProperty -Name $Tag -Value $True;$AllTags+=$Tag}};$Output}
This adds a new property to the object for each tag in the list and copies the tag name to a list for later use ($AllTags). It only does either of these if the tag is not blank.
[string[]]$AllProperties=@("FullName")+($AllTags | Select-Object -Unique)
Here I generate a new array of properties we want to export to the file. This made of the FullName of the file and all the tags we’ve found when processing the files (with duplicates removed).
I’m building a list of all the properties due to an interesting wrinkle of Export-CSV; it gets the names of the columns from the first object it’s passed. So if the first object doesn’t have ALL tags on it (likely) then those missing tags will never appear.
To get round this you perform Select-Object with an explicit set of properties; those properties are on every output object even if they’re not on the original object (they’re added as empty properties).
This means that Export-CSV always gets objects with the same properties on them and even the empty properties (missing tags) are included;
$Data | Select-Object -Property $AllProperties | Export-Csv -NoTypeInformation -Path $CSVFile -Force -Verbose