How to output jagged noteproperty values using Export-csv powershell - powershell

I wrote a function that parses out the folder names for files and stores them as note properties for each individual folder encountered, so directory(n) = direcory1, directoryn+1= directory2 etc... So for each file the directory(s) will be various lengths depending on where the file is in the directory structure.
The problem that I am facing is how to output the jagged directory results in column format using Export-csv combined with other static property values, since the noteproperty lengths will vary from file to file (Jagged) I am struggling to figure out the logic to try and output in csv format the directory's in column format.
The output should have headers like the following:
Example File1
Directory 1, Directory 2, Directory 3, Other properties
Directory Value 1, Directory Value 2, Directory value 3
File2
Directory 1, Directory 2, Directory 3, Directory 4
Directory value 1, Directory value 2, Directory 3, Directory 4
function Get-Folder ($Files)
{
foreach ($file in $Files)
{
$TotalDirLvl = ($file.FullName.Split('\').count)-1
$x =0
While($x -lt $TotalDirLvl){
$file|Add-Member -NotePropertyName Directory$x -NotepropertyValue
$file.FullName.Split('\')[$x]
$x++
}
}
Return $Files
}

You need to know how many directories will be in the tree that you export so that you can create the proper number of properties on your object that you Export-CSV or your csv file will not have properties "to the right" of first row. IE file 2 in your example would have Directory 1..3, but not 4. The way I did this is by looping over the files twice. The first time gets the max depth you will traverse, and the second time constructs a psobject and adds it to a array to be written to an csv file at the end.
For files that have fewer path segments than your maximum path segment, you need to specify an empty or null value for the segments that are not filled. Also, if you want to include other properties you should probably do it to the left of this directory tree. If you do not want a property for a given file, you still need to pass a null/blank value into your object's property or else.
The script below creates a csv file like:
from a directory structure that you explain in your post.
$files = Get-ChildItem -Path "$env:temp\SO" -Recurse | where { ! $_.PSIsContainer }
$outObjs = #()
$maxDepth = 0
foreach ($file in $files) {
$TotalDirLvl = ($file.FullName.Split('\').count)-1
if ($TotalDirLvl -gt $maxDepth){
$maxDepth = $TotalDirLvl
}
}
foreach($file in $files){
$outObj = New-Object PSObject
$fileDepth = ($file.FullName.Split('\').count)-1
$outObj | Add-Member -NotePropertyName DirectoryDepth -NotepropertyValue $fileDepth
$x = 0
#Add other properties for each file here to the left of your directory tree
While($x -le $maxDepth)
{
if ($x -gt $fileDepth){
$value = ''
}
else{
$value = $file.FullName.Split('\')[$x]
}
$outObj | Add-Member -NotePropertyName "Directory$x" -NotepropertyValue $value
$x++
}
$outOBjs += $outObj
}
$outOBjs | Export-Csv -Path "$env:temp\SO\test.csv" -NoTypeInformation -Force

Related

Export CSV: file structure with folders as columns

My question is quite similar to one posted here: Export CSV. Folder, subfolder and file into separate column
I have a file and folder structure containing possibly up to 10 folders deep and I want to run PowerShell to create a hash table that writes each file into a row, with each of the folders as a separate column, and then the filename at a dedicated column.
I start off with
gci -path C:\test -file -recurse|export-csv C:\temp\out.csv -notypeinformation
But this produces the standard table with some of the info I need but the directory is of course presented as one long string.
I'd like to get an output where each folder and its subfolder that houses the file to be presented as a column.
C:\Test\Folder1\Folder2\Folder3\file.txt
to be presented as
Name
Parent1
Parent2
Parent3
Parent4
Parent5
Parent6
Filename
file.txt
Folder1
Folder2
Folder3
file.txt
image1.png
Folder1
image.1png
Doc1.docx
Folder1
Folder2
Folder3
Folder4
Folder5
Folder6
Doc1.docx
table3.csv
Folder1
Folder2
table3.csv
As you can see there are some files which have just one folder whereas others could stored in several folders deep.
I need to keep this consistent, as I want to use Power Automate and the File system connector to read the file paths using the Excel table and then parse and create the file into SharePoint using the parent/folder levels as metadata/column in the document library.
I took zett42's code from the linked question and modified it.
$allItems = Get-ChildItem C:\Test -File -Recurse | ForEach-Object {
# Split on directory separator (typically '\' for Windows and '/' for Unix-like OS)
$FullNameSplit = $_.FullName.Split( [IO.Path]::DirectorySeparatorChar )
# Create an object that contains the splitted path and the path depth.
# This is implicit output that PowerShell captures and adds to $allItems.
[PSCustomObject] #{
FullNameSplit = $FullNameSplit
PathDepth = $FullNameSplit.Count
Filename = $_.Name
}
}
# Determine highest column index from maximum depth of all paths.
# Minus one, because we'll skip root path component.
$maxColumnIndex = ( $allItems | Measure-Object -Maximum PathDepth ).Maximum - 1
$allRows = foreach( $item in $allItems ) {
# Create an ordered hashtable
$row = [ordered]#{}
# Add all path components to hashtable. Make sure all rows have same number of columns.
foreach( $i in 1..$maxColumnIndex ) {
$row[ "Filename" ] = $item.Filename
$row[ "Column$i" ] = if( $i -lt $item.FullNameSplit.Count ) { $item.FullNameSplit[ $i ] } else { $null }
}
# Convert hashtable to object suitable for output to CSV.
# This is implicit output that PowerShell captures and adds to $allRows.
[PSCustomObject] $row
}
I can get the filename to show as a separate column but I don't want the script to add the filename at the last column.
PowerShell allrows output screenshot
Thanks
I've answered my own question.
Modified zett42's script, and included a few variables around splitting around just the Name of from GetChild-Item as opposed to the FullName and then of course the fixed column with just the filename in the hash table.
$allItems = Get-ChildItem C:\Test -File -Recurse | ForEach-Object {
# Split on directory separator (typically '\' for Windows and '/' for Unix-like OS)
# $FullNameSplit = $_.FullName.Split( [IO.Path]::DirectorySeparatorChar )
$FullNameSplit = split-path -Path $_.FullName
$DirNameSplit = $FullNameSplit.Split( [IO.Path]::DirectorySeparatorChar )
# Create an object that contains the splitted path and the path depth.
# This is implicit output that PowerShell captures and adds to $allItems.
[PSCustomObject] #{
#FullNameSplit = $FullNameSplit
#PathDepth = $FullNameSplit.Count
DirNameSplit = $DirNameSplit
PathDepth = $DirNameSplit.Count
Filename = $_.Name
}
}
# Determine highest column index from maximum depth of all paths.
# Minus one, because we'll skip root path component.
$maxColumnIndex = ( $allItems | Measure-Object -Maximum PathDepth ).Maximum - 1
$allRows = foreach( $item in $allItems ) {
# Create an ordered hashtable
$row = [ordered]#{}
# Add all path components to hashtable. Make sure all rows have same number of columns.
foreach( $i in 1..$maxColumnIndex ) {
$row[ "Filename" ] = $item.Filename
#$row[ "Column$i" ] = if( $i -lt $item.FullNameSplit.Count ) { $item.FullNameSplit[ $i ] } else { $null }
$row[ "Parent$i" ] = if( $i -lt $item.DirNameSplit.Count ) { $item.DirNameSplit[ $i ] } else { $null }
# $row[ "Column$i" ] = $item.DirNameSplit[$i]
}
# Convert hashtable to object suitable for output to CSV.
# This is implicit output that PowerShell captures and adds to $allRows.
[PSCustomObject] $row
}

Trouble linking commands together

So i'm pretty new to powershell and I'm trying to list all contents of a directory(on my vm) while stating if each is a reg file or directory along with it's path/size.
the code I have is:
#!/bin/bash
cd c:\
foreach ($item in get-childitem -Path c:\) {
Write-Host $item
}
########
if(Test-Path $item){
Write-Host "Regular File" $item
}
else {
Write-Host "Directory" $item
}
I can get all of the contents to print, but when I try to state whether file/directory, only one .txt file says "Regular File" next to it. I've been at it for hours on end and get figure it out. Also, my output doesn't state "directory" next to directories...
Here is an example of how you can enumerate the files and folders on your C Drive one level deep with their current size (if it's a folder, look for the files inside and get a sum of it's Length). Regarding trying to "state whether file / directory", you don't need to apply any logic to it, FileInfo and DirectoryInfo have an Attributes property which gives you this information already.
Get-ChildItem -Path C:\ | & {
process {
$object = [ordered]#{
Attributes = $_.Attributes
Path = $_.Name # change to $_.FullName for the Path
Length = $_.Length / 1mb
}
if($_ -is [IO.DirectoryInfo]) {
foreach($file in $_.EnumerateFiles()) {
$object['Length'] += $file.Length / 1mb
}
}
$object['Length'] = [math]::Round($object['Length'], 2).ToString() + ' Mb'
[pscustomobject] $object
}
}
If you want something more complex, i.e. seeing the hierarchy of a directory, like tree does, with the corresponding sizes you can check out this module.

Export CSV. Folder, subfolder and file into separate column

I created a script that lists all the folders, subfolders and files and export them to csv:
$path = "C:\tools"
Get-ChildItem $path -Recurse |select fullname | export-csv -Path "C:\temp\output.csv" -NoTypeInformation
But I would like that each folder, subfolder and file in pfad is written into separate column in csv.
Something like this:
c:\tools\test\1.jpg
Column1
Column2
Column3
tools
test
1.jpg
I will be grateful for any help.
Thank you.
You can split the Fullname property using the Split() method. The tricky part is that you need to know the maximum path depth in advance, as the CSV format requires that all rows have the same number of columns (even if some columns are empty).
# Process directory $path recursively
$allItems = Get-ChildItem $path -Recurse | ForEach-Object {
# Split on directory separator (typically '\' for Windows and '/' for Unix-like OS)
$FullNameSplit = $_.FullName.Split( [IO.Path]::DirectorySeparatorChar )
# Create an object that contains the splitted path and the path depth.
# This is implicit output that PowerShell captures and adds to $allItems.
[PSCustomObject] #{
FullNameSplit = $FullNameSplit
PathDepth = $FullNameSplit.Count
}
}
# Determine highest column index from maximum depth of all paths.
# Minus one, because we'll skip root path component.
$maxColumnIndex = ( $allItems | Measure-Object -Maximum PathDepth ).Maximum - 1
$allRows = foreach( $item in $allItems ) {
# Create an ordered hashtable
$row = [ordered]#{}
# Add all path components to hashtable. Make sure all rows have same number of columns.
foreach( $i in 1..$maxColumnIndex ) {
$row[ "Column$i" ] = if( $i -lt $item.FullNameSplit.Count ) { $item.FullNameSplit[ $i ] } else { $null }
}
# Convert hashtable to object suitable for output to CSV.
# This is implicit output that PowerShell captures and adds to $allRows.
[PSCustomObject] $row
}
# Finally output to CSV file
$allRows | Export-Csv -Path "C:\temp\output.csv" -NoTypeInformation
Notes:
The syntax Select-Object #{ Name= ..., Expression = ... } creates a calculated property.
$allRows = foreach captures and assigns all output of the foreach loop to variable $allRows, which will be an array if the loop outputs more than one object. This works with most other control statements as well, e. g. if and switch.
Within the loop I could have created a [PSCustomObject] directly (and used Add-Member to add properties to it) instead of first creating a hashtable and then converting to [PSCustomObject]. The choosen way should be faster as no additional overhead for calling cmdlets is required.
While a file with rows containing a variable number of items is not actually a CSV file, you can roll your own and Microsoft Excel can read it.
=== Get-DirCsv.ps1
Get-Childitem -File |
ForEach-Object {
$NameParts = $_.FullName -split '\\'
$QuotedParts = [System.Collections.ArrayList]::new()
foreach ($NamePart in $NameParts) {
$QuotedParts.Add('"' + $NamePart + '"') | Out-Null
}
Write-Output $($QuotedParts -join ',')
}
Use this to capture the output to a file with:
.\Get-DirCsv.ps1 | Out-File -FilePath '.\dir.csv' -Encoding ascii

Extract year from string row in file text and move torrent to relative folder reordered by year

Question is tricky because is an evolution of my previously question.
To move torrent in folders I use this powershell script
$ToFolder = "$env:USERPROFILE\Desktop\to"
$FromFolder = "$env:USERPROFILE\Desktop\From"
#Create the sample folder on your desktop
#This line can be commented out if your ToFolder exists
New-Item $ToFolder -ItemType directory -Force
GCI -Path $FromFolder *.torrent | % {
if ($_.Name -match "(19|20)\d{2}") {
#Check to see if year folder already exists at the destination
#If not then create a folder based on this year
if (!(Test-Path "$ToFolder\$($Matches[0])")) {
New-Item -Path "$ToFolder\$($Matches[0])" -ItemType directory
}
#Transfer the matching file to its new folder
#Can be changed to Move-Item if happy with the results
Move-Item -Path $_.FullName -Destination "$ToFolder\$($Matches[0])" -Force
}
}
but in my NEW situation I must extract year from file text .txt
Example list of file .torrent inside a folder
Caccia Spietata.torrent
Caccia Zero terrore del Pacifico.torrent
Caccia.A.Ottobre.Rosso.torrent
Cacciatore Bianco Cuore Nero.torrent
Cacciatore di Ex.torrent
Cacciatori Di Zombie.torrent
Example of string list in file text
Caccia grossa a casa di Topolino (2006)
Caccia selvaggia [HD] (1981)
Caccia spietata (2006)
Cacciatori Di Zombie (2005)
What script must do ?
A. extract year from string in file text (every string is on a single row because file text is a list)
N.B script should compare between torrent files names and strings in file text list.
Caccia spietata (2006)
Extract year is possibile only for equal text or very very similar text like
Caccia Spietata.torrent
Caccia spietata (2006)
If I have
caccia.spietata.torrent
caccia SPiETata (2006)
this is for me very similar strings.
B. Make folder
2006
C. Move torrent
Caccia Spietata.torrent
into folder 2006
I want this solution because I have many .torrent file name without year so I must reorder them correctly by year.
Thanks for any help.
The first hurdle is parsing dates and names out of the string file. You then add them to a hash of movie name strings.
$movies = #()
(get-content C:\Path\Test4.txt) | foreach($_){
$properties = #{
date = $_.substring($_.IndexOf("(")+1,4)
name = $_.substring(0,$_.IndexOf("("))
}
$movies += New-Object PSObject -Property $properties
}
$movies
Once you have the movie names and dates separate, you loop through each movie and create a folder if it does not exist.
foreach($movie in $movies){
$movie.date
$datePath = "C:\Path\$($movie.date)"
if(-not(test-path $datePath)) {
new-item $datePath -ItemType "directory"
}
After that, you can split the name into key words based on whitespace.
$words = $movie.name -split '\s'
$words
Below is as far as I've gotten during a break of mine. The next step is a bit complicated seeming, as you have to then match the torrent files to the object in the hash based on keywords. It will be hard to construct such a filter without access to the raw data. My first thought would be to match based on fileName.torrent -like "*word*", but it looks like there are a ton of duplicate words. The next option is to match on multiple words, or maybe only use words that are not common (exclude "caccia", articles, etc). Either way, that should move you a bit closer to your goal. Maybe someone else can help finish, or I can revisit it during another break.
$movies = #()
(get-content C:\Path\Test4.txt) | foreach($_){
$properties = #{
date = $_.substring($_.IndexOf("(")+1,4)
name = $_.substring(0,$_.IndexOf("("))
}
$movies += New-Object PSObject -Property $properties
}
$movies
foreach($movie in $movies){
$movie.date
$datePath = "C:\Path\$($movie.date)"
if(-not(test-path $datePath)) {
new-item $datePath -ItemType "directory"
}
$words = $movie.name -split '\s'
$words
#this is as far as I got
}
UPDATE
I've added a bit that we talked about in comments. Most of the changes are at the bottom of the script.
$movies = #()
(get-content $Path\Test4.txt) | foreach($_){
$properties = #{
date = $_.substring($_.IndexOf("(")+1,4)
name = $_.substring(0,$_.IndexOf("("))
}
write-host $date
write-host $name
$movies += New-Object PSObject -Property $properties
}
#no significant changes were made above this point
$torrentFiles = dir $torrentPath
foreach($movie in $movies){
$datePath = "$Path\$($movie.date)"
if(-not(test-path $datePath)) {
new-item $datePath -ItemType "directory"
}
$words = ($movie.name -split '\s') | ?{ $_.Length -gt 1}
#this is as far as I got last time; most of the changes are below, though I did change
#just a bit above
#this sets a number of words which needs to match. Currently, it has to match
#on all words. If you wanted, you set it to a static number (2)
# or do something like $words.count -1. There is a commented-out example of
#such a solution.
$significant = $words.Count
#if($words.Count -eq 1){$significant = 1}
#else{$significant = ($words.Count - 1)
# here you loop through the torrentfiles, finding files whose base names have a
#significant number of matching words with the string
foreach($torrentFile in $torrentFiles){
$matchingWords = 0
foreach($word in $words){
if($torrentFile.BaseName -match $word){
$matchingWords += 1
}
}
if($matchingWords -ge $significant){
$_ | Move-Item -Destination $datePath
}
}
}

Retrieve Custom Object From Hashtable

I've written a PowerShell function to create a custom object and stores it into a hashtable. The issue I'm facing is retrieving that object. I need to retrieve that object because it contains an array, I need to loop through that array and write it to a text file.
function removeItem {
<#Madatory Parameters for function, it takes the path to the files/folders
to clean up and path to the hashtable.#>
Param([Parameter(Mandatory=$True)]
[string]$path,
[string]$writetoText,
[hashtable] $hashWrite=#{}
)
<#Begin if statement: Test if Path Exists#>
if (Test-Path ($path)) {
<#Begin if statement: Check if file is Directory#>
if ((Get-Item $path) -is [System.IO.DirectoryInfo]) {
$pathObj = [pscustomobject]#{
pathName = $path
Wipe = (Get-ChildItem -Path $path -Recurse)
Count = (Get-ChildItem -Path $path -Recurse | Measure-Object).Count
}
# Write-Output $pathObj.Wipe
#Add Data to Hashtable
$hashWrite.Add($pathObj.pathName,$pathObj)
foreach ($h in $hashWrite.GetEnumerator()) {
Write-Host "$($h.Name): $($h.Value)"
}
<#
[string[]]$view = $pathObj.Wipe
for ($i=0; $i -le $view.Count; $i++){
Write-Output $view[$i]
}
#>
$pathObj.pathName = $pathObj.pathName + "*"
}<#End if statement:Check if file is Directory #>
}
}
My function takes 3 arguments, a path, the text file path, and a hashtable. Now, I create a custom object and store the path, files/folders contained in that path, and the count. Now, my issue is, I want to retrieve that custom object from my hashtable so that I can loop though the Wipe variable, because it's an array, and write it to the text file. If I print the hashtable to the screen it see the Wipe variable as System.Object[].
How do I retrieve my custom object from the hash table so I can loop through the Wipe Variable?
Possible Solution:
$pathObj = [pscustomobject]#{
pathName = $path
Wipe = (Get-ChildItem -Path $path -Recurse)
Count = (Get-ChildItem -Path $path -Recurse | Measure-Object).Count
}
#Add Data to Hashtable
$hashWrite.Add($pathObj.pathName,$pathObj)
foreach ($h in $hashWrite.GetEnumerator()) {
$read= $h.Value
[string[]]$view = $read.Wipe
for ($i=0; $i -le $view.Count; $i++) {
Write-Output $view[$i]
}
}
Is this the ideal way of doing it?
There are uses for GetEnumerator(), but in your case you're better off just looping over the keys of the hashtable:
$hashWrite.Keys | % {
$hashWrite[$_].Wipe
} | select -Expand FullName