See metadata variable names on files (Windows 10) - powershell

I am having a hard time defining this issue, but basically what I would like to know is "what are the symbolic variable names connected to files' metadata (preferably on a Windows installation)".
For example, taking a .mp3 file, checking its properties yields a Title, Bit Rate, Folder Path etc. descriptions. What I want to know is the name of the fields seen by programs (i.e. Title->title, Bit Rate->bit_rate etc.) if it makes any sense, as I've been trying to index some files and I'd like to gather as much info on them as possible.

I'm not convinced that there is such a thing as "symbolic names" for the metadata, especially not in relation to PowerShell. I suspect that Windows maintains support for a certain number of popular formats, and offers functionality through Explorer to view and sometimes edit them. I haven't found a source to prove this theory, but research implicitly supports it: there's several dozen search results about how to retrieve a file's metadata in PowerShell, and they all seem to suggest roughly the same approach (for example this blog post): using a Shell object to gather the information.
Since you tagged this PowerShell, here's my take on boiling it down to the essentials:
$path = 'C:\temp\file.txt' # pick a path
$parent = Split-Path -Parent $path # get the directory
$shell = New-Object -ComObject Shell.Application # get ourselves a shell
$folder = $shell.NameSpace($parent) # get a "folder namespace"
$file = $Folder.Items() | where { $_.Path -eq $path } # get the file itself from the folder
$count = 0 # zero our iterator
$object = New-Object PSObject # make a fresh object to hold our output
While ($folder.GetDetailsOf($folder.Items, $count) -ne "") { # iterate over the available metadata tags for the folder, and for each one get the value from the file
$object | Add-Member -Force NoteProperty ($folder.GetDetailsOf($folder.Items, $count)) ($folder.GetDetailsOf($file, $count))
$count += 1
}
Write-Output $object
Note that the attributes available for a given file are obviously not all of the attributes that could possibly be supported for any file, and additionally are not necessarily "symbolic names". I suspect that the process of querying the shell object causes it to examine the files in a folder and extract metadata that Windows recognizes--it might even do this based on the view type selected for the folder (Photos, Music, Documents, etc.).
As for writing the information, this might be possible through the same shell object, but I haven't explored that option. It's likely dependent on the specific format: for mp3 you probably want a library for viewing/editing mp3-specific metadata.

Related

Choose which CSV to import when running a PowerShell script

I get a CSV every week that our finance team puts in a shared drive. I have a script for that CSV that I run once I get it.
The first command of the script is of course Import-Csv.
The problem is, the finance team insists on naming the file differently each time plus they don't always put it in the same location within the drive.
As a result, I have to first hunt for the file, put it into the directory that the script points to and then rename the file.
I've tried talking to the team about putting it in the same location and making sure the filename is the same but they only follow the instructions for a couple of weeks before just doing whatever.
Ideally, I'd like for it so that when I run the script, there would be a popup that would ask me to pick a CSV (Similar to how it looks when you do "Save As" on an Office Document).
Anyway for this to be done within PowerShell?
You can access .Net classes and interface with the forms library to instantiate and take input from the standard FileOpen dialog. Something like below:
Using Namespace System.Windows.Forms
$FileBrowser = [OpenFileDialog]::new()
$FileBrowser.InitialDirectory = 'c:\temp'
$FileBrowser.Filter = 'Comma Separated Values (*.csv) | *.csv'
[Void]$FileBrowser.ShowDialog()
$CsvFile = $FileBrowser.FileName
Then use $CsvFile int he Import-Csv command.
You can change the .InitialDirectory property to make navigating a little more convenient.
Use the .Filter property to limit the file open display to CSV files, to make things that much more convenient.
Also, use the [Void] class to prevent the status return (usually 'OK' or 'Cancel') from echoing to the screen.
Note: A simple Google search will turn up many examples. I refined some of the work from here. That will also document some of the other properties if you want to explore etc.
If you are willing to settle for a selection box that doesn't look as nice as the Save As dialog, you can use Out-Gridview. Something along these lines might help.
$filenames =
#(Get-ChildItem -Path C:\temp -Recurse -Filter *.csv |
Sort-Object LastWriteTime -Descending |
Out-GridView -Title 'Choose a file' -PassThru)
$csvfile = $filenames[0].FullName
Import-Csv $csvfile | More
The -Path specifies a directory that contains all the locations where your csv file might be delivered. The sort is just to put the recently written files at the top of the grid. This supposedly makes selection easier. The #() wrapper merely makes sure the result stored in $filenames is an array.
You would do something else with the results of Import-Csv.
Steven's response certainly satisfies your original question, but an alternative would be to let PowerShell do the work. If you know the drive, and you know the name of the file this week, you can pass the name to your script and let it search the drive filtering on the specific csv file you need. Make it recursive, and open the only file that matches. Sorry, didn't have time yesterday to include code. Here's a function that returns the full file path when provided with a top level search path and a filename with possible wildcards.
function gfp { $result=gci $args[0] -recurse -include $args[1]; return ($result.DirectoryName + "\" + $result.Name) }
Example: gfp "d:\rootfolder" "thisweeksfilename.csv"

Get localized file (resource) names, as shown in Windows Explorer

I am looking for a PowerShell function like Get-LocalizedName($FilePath), returning the localized name of a file or its filename if it is not localized. I know that the localized names are stored in the LocalizedFileNames section of the respective desktop.ini files, but usually as resource file pointers rather than clear names.
Example: For the Administrative Tools folder and the locale de-DE, I want the clear name Windows-Verwaltungsprogramme instead of #%SystemRoot%\system32\shell32.dll,-21762.
I was not able to find such a function, and also was not successful in analyzing the attributes of Get-ChildItem or google a regarding solution.
Is there any such function that I could use from PowerShell (v7)?
The following solution works for folders only. See this answer for a solution that works for files, localized via LocalizedFileNames section of Desktop.ini.
This can be done using the Shell.Application COM object:
$shell = New-Object -ComObject Shell.Application
# Get full path to the admin tools folder
$adminToolsPath = [Environment]::GetFolderPath('AdminTools')
# Get the shell folder corresponding to this path
if( $folder = $shell.NameSpace( $adminToolsPath ) ) {
$folder.Title # Output localized title
}
# Alternative:
if( $folder = $shell.NameSpace( [Environment+SpecialFolder]::AdminTools ) ) {
$folder.Title # Output localized title
}
[Environment]::GetFolderPath() gives us the filesystem path of a system folder.
The Shell.Namespace() function returns a Folder object corresponding to this path which can be queried for its localized name.
The alternative shows how you can get the localized name more directly, by passing an enumeration value of [Environment+SpecialFolder] to the Shell.Namespace() function.
When passing a path to the Shell.Namespace() method, it works for any folder customized via "desktop.ini", even if it's not a system folder.
I finally found a solution for Get-LocalizedName, digging into 20 years old VBS code using GetDetailsOf:
function Get-LocalizedName {
Param([Parameter(Mandatory=$True)][string]$FilePath)
$ChildObj = Get-ChildItem $FilePath
$FldrName = $ChildObj.DirectoryName
$FileName = $ChildObj.Name
$Shell = New-Object -ComObject Shell.Application
$Folder = $Shell.Namespace($FldrName)
$File = $Folder.ParseName($FileName)
return $($Folder.GetDetailsOf($File,0))
}

How do I copy a list of files and rename them in a PowerShell Loop

We are copying a long list of files from their different directories into a single location (same server). Once there, I need to rename them.
I was able to move the files until I found out that there are duplicates in the list of file names to move (and rename). It would not allow me to copy the file multiple times into the same destination.
Here is the list of file names after the move:
"10.csv",
"11.csv",
"12.csv",
"13.csv",
"14.csv",
"15.csv",
"16.csv",
"17.csv",
"18.csv",
"19.csv",
"20.csv",
"Invoices_Export(16) - Copy.csv" (this one's name should be "Zebra.csv")
I wrote a couple of foreach loops, but it is not working exactly correctly.
The script moves the files just fine. It is the rename that is not working the way I want. The first file does not rename; the other files rename. However, they leave the moved file in place too.
This script requires a csv that has 3 columns:
Path of the file, including the file name (eg. c:\temp\smefile.txt)
Destination of the file, including the file name (eg. c:\temp\smefile.txt)
New name of the file. Just the name and extention.
# Variables
$Path = (import-csv C:\temp\Test-CSV.csv).Path
$Dest = (import-csv C:\temp\Test-CSV.csv).Destination
$NN = (import-csv C:\temp\Test-CSV.csv).NewName
#Script
foreach ($D in $Dest) {
$i -eq 0
Foreach ($P in $Path) {
Copy-Item $P -destination C:\Temp\TestDestination -force
}
rename-item -path "$D" -newname $NN[$i] -force
$i += 1
}
There were no error per se, just not the outcome that I expected.
Welcome to Stack Overflow!
There are a couple ways to approach the duplicate names situation:
Check if the file exists already in the destination with Test-Path. If it does, start a while loop that appends a number to the end of the name and check if that exists. Increment the number you append after each check with Test-Path. Keep looping until Test-Path comes back $false and then break out of the loop.
Write an error message and skip that row in the CSV.
I'm going to show a refactored version of your script with approach #2 above:
$csv = Import-Csv 'C:\temp\Test-CSV.csv'
foreach ($row in $csv)
{
$fullDestinationPath = Join-Path -Path $row.Destination -ChildPath $row.NewName
if (Test-Path $fullDestinationPath)
{
Write-Error ("The path '$fullDestinationPath' already exists. " +
"Skipping row for $($row.Path).")
continue
}
# You may also want to check if $row.Path exists before attempting to copy it
Copy-Item -Path $row.Path -Destination $fullDestinationPath
}
Now that your question is answered, here are some thoughts for improving your code:
Avoid using acronyms and abbreviations in identifiers (variable names, function names, etc.) when possible. Remember that code is written for humans and someone else has to be able to understand your code; make everything as obvious as possible. Someone else will have to read your code eventually, even if it's Future-You™!
Don't Repeat Yourself (called the "DRY" principle). As Lee_daily mentioned in the comments, you don't need to import the CSV file three times. Import it once into a variable and then use the variable to access the properties.
Try to be consistent. PowerShell is case-insensitive, but you should pick a style and stick to it (i.e. ForEach or foreach, Rename-Item or rename-item, etc.). I would recommend PascalCase as PowerShell cmdlets are all in PascalCase.
Wrap literal paths in single quotes (or double quotes if you need string interpolation). Paths can have spaces in them and without quotes, PowerShell interprets a space as you are passing another argument.
$i -eq 0 is not an assignment statement, it is a boolean expression. When you run $i -eq 0, PowerShell will return $true or $false because you are asking it if the value stored in $i is 0. To assign the value 0 to $i, you need to write it like this: $i = 0.
There's nothing wrong with $i += 1, but it could be shortened to $i++, if you want to.
When you can, try to check for common issues that may come up with your code. Always think about what can go wrong. "If I copy a file, what can go wrong? Does the source file or folder exist? Is the name pulled from the CSV a valid path name or does it contain characters that are invalid in a path (like :)?" This is called defensive programming and it will save you so so many headaches. As with anything in life, be careful not to go overboard. Only check for likely scenarios; rare edge-cases should just raise errors.
Write some decent logs so you can see what happened at runtime. PowerShell provides a pair of great cmdlets called Start-Transcript and Stop-Transcript. These cmdlets log all the output that was sent to the PowerShell console window, in addition to some system information like the version of PowerShell installed on the machine. Very handy!

getfolderpath & Program Data

I have been successfully using [environment]::getfolderpath("ProgramFiles") to get the path to program Files, but now I have a need to also access Program Data, and it looks from this enumeration like ProgramData is not available with this method. Is that true, or am I missing something here?
$env: to access environmental variables
$env:ProgramData
The quickest way is to use $env:ProgramData as BenH already pointed out in your question comments.
Using the .net Specialfolder, you would have needed to use the CommonApplicationData
Instead of using a string though such as your initial example:
[Environment]::GetFolderPath('CommonApplicationData')
I'd suggest using the enumeration as you will get the possible enumeration values directly into the intellisense while developping.
[Environment]::GetFolderPath([System.Environment+SpecialFolder]::CommonApplicationData)
Finally, because you knew the path you were looking for but not the corresponding variable, you could have listed them all neatly using something like:
$SpecialFolders = New-Object -TypeName psobject
[Environment+SpecialFolder]::GetNames([Environment+SpecialFolder]) | sort |
foreach {Add-Member -InputObject $SpecialFolders -Type NoteProperty -Name
($_) -Value ([Environment]::GetFolderPath($_)) }
$SpecialFolders | fl
Using that snippet, you could have determined that c:\programdata was a special folder path belonging to CommonApplicationData.
The enumeration can still be handy if a specified folder is not in the $env scope (example: My documents special folder).

Create variable from CSV

I want to make variables from a particular column in a CSV.
CSV will have the following headers:
FolderName,FolderManager,RoleGroup,ManagerEmail
Under FolderName will be a list of rows with respective folder names such as: Accounts,HR,Projects, etc... (each of these names is a separate row in the FolderName column)
So I would like to create a list of variables to call on in a later stage. They would be something like the following:
$Accounts,
$HR,
$Projects,
I have done a few different scripts based on searching here and google, but unable to produce the desired results. I am hoping someone can lead me in the right direction here to create this script.
Versions of this question ("dynamic variables" or "variable variables" or "create variables at runtime") come up a lot, and in almost all cases they are not the right answer.
This is often asked by people who don't know a better way to approach their problem, but there is a better way: collections. Arrays, lists, hashtables, etc.
Here's the problem: You want to read a username and print it out. You can't write Hello Alice because you don't know what their name is to put in your code. That's why variables exist:
$name = Read-Host "Enter your name"
Write-Host "Hello $name"
Great, you can write $name in your source code, something which never changes. And it references their name, which does change. But that's OK.
But you're stuck - how can you have two people's names, if all you have is $name? How can you make many variables like $name2, $name3? How can you make $Alice, $Bob?
And you can...
New-Variable -Name (Read-Host "Enter your name") -Value (Read-Host "Enter your name again")
Write-Host "Hello
wait
What do you put there to write their name? You're straight back to the original problem that variables were meant to solve. You had a fixed thing to put in your source code, which allowed you to work with a changing value.
and now you have a varying thing that you can't use in your source code because you don't know what it is again.
It's worthless.
And the fix is that one variable with a fixed name can reference multiple values in a collection.
Arrays (Get-Help about_Arrays):
$names = #()
do {
$name = Read-Host "Enter your name"
if ($name -ne '')
{
$names += $name
}
} while ($name -ne '')
# $names is now a list, as many items long as it needs to be. And you still
# work with it by one name.
foreach ($name in $names)
{
Write-Host "Hello $name"
}
# or
$names.Count
or
$names | foreach { $_ }
And more collections, like
Hashtables (Get-Help about_Hash_Tables): key -> value pairs. Let's pair each file in a folder with its size:
$FileSizes = #{} # empty hashtable. (aka Dictionary)
Get-ChildItem *.txt | ForEach {
$FileSizes[$_.BaseName] = $_.Length
}
# It doesn't matter how many files there are, the code is just one block
# $FileSizes now looks like
#{
'readme' = 1024;
'test' = 20;
'WarAndPeace' = 1048576;
}
# You can list them with
$FileSizes.Keys
and
foreach ($file in $FileSizes.Keys)
{
$size = $FileSizes[$file]
Write-Host "$file has size $size"
}
No need for a dynamic variable for each file, or each filename. One fixed name, a variable which works for any number of values. All you need to do is "add however many there are" and "process however many there are" without explicitly caring how many there are.
And you never need to ask "now I've created variable names for all my things ... how do I find them?" because you find these values in the collection you put them in. By listing all of them, by searching from the start until you find one, by filtering them, by using -match and -in and -contains.
And yes, New-Variable and Get-Variable have their uses, and if you know about collections and want to use them, maybe you do have a use for them.
But I submit that a lot of people on StackOverflow ask this question solely because they don't yet know about collections.
Dynamic variables in Powershell
Incrementing a Dynamic Variable in Powershell
Dynamic variable and value assignment in powershell
Dynamically use variable in PowerShell
How to create and populate an array in Powershell based on a dynamic variable?
And many more, in Python too:
https://stackoverflow.com/a/5036775/478656
How can you dynamically create variables via a while loop?
Basically you want to create folders based on the values you are getting from CSV File.
(FileName has headers such as FolderName,
FolderManager,
RoleGroup,
ManagerEmail)
$File=Import-csv "FileName"
$Path="C:\Sample"
foreach ($item in $File){
$FolderName=$item.FolderName
$NewPath=$Path+"\$FolderName"
if(!(Test-Path $NewPath))
{
New-Item $NewPath -ItemType Directory
}
}
Hope this HElps.
In PowerShell, you can import a CSV file and get back custom objects. Below code snippet shows how to import a CSV to generate objects from it and then dot reference the properties on each object in a pipeline to create the new variables (your specific use case here).
PS>cat .\dummy.csv
"foldername","FolderManager","RoleGroup"
"Accounts","UserA","ManagerA"
"HR","UserB","ManagerB"
PS>$objectsFromCSV = Import-CSV -Path .\dummy.csv
PS>$objectsFromCSV | Foreach-Object -Process {New-Variable -Name $PSItem.FolderName }
PS>Get-Variable -name Accounts
Name Value
---- -----
Accounts
PS>Get-Variable -name HR
Name Value
---- -----
HR
`