Split property in array into many properties - powershell

I use get-childitem to get files from directory structure.
Get-ChildItem $path -Recurse -include *.jpg,*.png | select-object Directory, BaseName, Extension
I get an array of objects with properties Directory, BaseName, Extension.
Like:
Directory BaseName Extension
C:\dir1\dir2\dir3\dir4 file txt
I wan't to break directory structure into multiple properties inside same array - each subdirectory level it's own property.
The end result properties should be (I can remove c:\ earlier in script):
dir1 dir2 dir3 dir4 Basename Extension
dir1 dir2 dir3 dir4 file txt
I used to export that to csv and import it back with delimiter to another array and than rebuilding the original array, but I think there must be an easier way!

Here is a possible approach:
$path = 'C:\test'
$maxDepth = 0
Set-Location $path # Set base path for Resolve-Path -Relative
# Get all files and splits their directory paths
$tempResult = Get-ChildItem $path -Recurse -include *.jpg,*.png | ForEach-Object {
# Make directory path relative to $path
$relPath = Resolve-Path $_.Directory -Relative
# Create an array of directory path components, skipping the first '.' directory
$dirNames = $relPath -replace '^\.\\' -split '\\|/'
# Remember maximum directory depth
$maxDepth = [Math]::Max( $dirNames.Count, $maxDepth )
# Implicit output that PowerShell adds to $tempResult
[PSCustomObject]#{
dirNames = $dirNames
fileInfo = $_ | Select-Object BaseName, Extension
}
}
# Process $tempResult to add directory properties and file name properties
$finalResult = $tempResult.ForEach{
$outProps = [ordered] #{}
# Add directory properties
for( $i = 0; $i -lt $maxDepth; ++$i ) {
$outProps[ "Dir$i" ] = if( $i -lt $_.dirNames.Count ) { $_.dirNames[ $i ] } else { $null }
}
# Add all fileInfo properties
$_.fileInfo.PSObject.Properties.ForEach{ $outProps[ $_.Name ] = $_.Value }
# Implicit output that PowerShell adds to $finalResult
[PSCustomObject] $outProps
}
$finalResult # Output to console
This is done in two passes to ensure all output elements have the same number of directory properties:
Iterate over all files and split their directory paths. Determine maximum directory depth (number of directory properties).
Iterate over the intermediate result to create the desired objects consisting of directory properties and file name properties. This is done by first adding the properties to an ordered hashtable and then converting that hashtable to a PSCustomObject. This is easier and more efficient than using Add-Member.
Test input:
C:\test
\subdir1
file1.png
file2.jpg
\subdir2
\subdir3
file3.jpg
Output:
Dir0 Dir1 BaseName Extension
---- ---- -------- ---------
subdir1 file1 .png
subdir1 file2 .jpg
subdir2 subdir3 file3 .jpg

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
}

Retrieving file information of specific date using powershell

I have a base folder as:
D:\St\Retail\AMS\AMS\FTP-FromClient\AMS
It contains various folders of dates:
2022-04-01
2022-04-02
...
...
2022-02-02
2021-05-05
2019-04-12
And each of these folders contains own files inside the folder. So, I want to retrieve all the filename inside the folder if it has 2022-04. So if the folder has '2022-04' as the base name ,I need to retreive all the file inside the folder like '2022-04-01','2022-04-02','2022-04-03'. The way I tried is:
cls
$folerPath = 'D:\St\Retail\AMS\AMS\FTP-FromClient\AMS'
$files = Get-ChildItem $folerPath
[System.Collections.ArrayList]$data = #()
foreach ($f in $files) {
$a = Get-ChildItem $f.FullName
foreach ($inner in $a) {
echo $inner.FullName
$outfile = $inner.FullName -match '*2022-04*'
$datepart = $inner.FullName.split('\')[-1]
if ($outfile) {
$data.add($datepart + '\' + $inner.Name.Trim())
}
}
}
My final $data may contains like this:
2022-04-01/abc.txt
2022-04-02/cde.pdf
2022-04-03/e.xls
You can do this by first collecting the directories you want to explore and then loop over these to get the files inside.
Using a calculated property you can output in whatever format you like:
$folderPath = 'D:\St\Retail\AMS\AMS\FTP-FromClient\AMS'
$data = Get-ChildItem -Path $folderPath -Filter '2022-04*' -Directory | ForEach-Object {
$dir = $_.Name
(Get-ChildItem -Path $_.FullName -File |
Select-Object #{Name = 'FolderFile'; Expression = {'{0}\{1}' -f $dir, $_.Name}}).FolderFile
}
After this, $data would be a string array with this content:
2022-04-01\abc.txt
2022-04-02\cde.pdf
2022-04-03\e.xls
By using wildcards for both directory and file name, you only need a single Get-ChildItem call:
$folderPath = 'D:\St\Retail\AMS\AMS\FTP-FromClient\AMS'
$folderDate = '2022-04'
[array] $data = Get-ChildItem "$folderPath/$folderDate*/*" -File | ForEach-Object{
# Join-Path's implicit output will be captured as an array in $data.
Join-Path $_.Directory.Name $_.Name
}
$data will be an array of file paths like this:
2022-04-01\abc.txt
2022-04-02\cde.pdf
2022-04-03\e.xls
Notes:
[array] $data makes sure that the variable always contains an array. Otherwise PowerShell would output a single string value when only a single file is found. This could cause problems, e. g. when you want to iterate over $data by index, you would iterate over the characters of the single string instead.
To make this answer platform-independent I'm using forward slashes in the Get-ChildItem call which work as path separators under both Windows and *nix platforms.
Join-Path is used to make sure the output paths use the expected default path separator (either / or \) of the platform.

Performing minus from parent to child files using powershell

I have three files in test1 folder:
I have three files in test2 folder:
Common between these two folders is file2.
I want to subtract common files between these two folders and get only unique files from test1 folder.
My expected output is:
file1.txt
file3.txt
I tried using:
cls
$parent='D:\test1'
$child='D:\test2'
$final = [System.Collections.ArrayList]#()
$parentarrays=Get-ChildItem $parent
$childarrays=Get-ChildItem $child
foreach ($p1 in $parentarrays) {
foreach ($p2 in $childarrays){
if($p1 -notcontains $p2){
$final.add($p1)
}
}
}
Write-Host $final
But I am getting output:
file1.txt.txt file1.txt.txt file1.txt.txt file2.txt.txt file2.txt.txt file2.txt.txt file3.txt.txt file3.txt.txt file3.txt.txt
Get a list of filenames in folder 2 and do the same for folder1. Use a Where-Object clause to filter out any filename that is also in the reference list:
$filesInFolder2 = (Get-ChildItem -Path 'D:\Test2' -File).Name
(Get-ChildItem -Path 'D:\Test1' -File).Name | Where-Object { $filesInFolder2 -notcontains $_ }
Output:
file1.txt
file3.txt

Move every folder to top level including content recursively in Powershell

how do I recursively move all directories to the top level including all their sub directories.
The files in the directories should be copied as well.
And if a directory already exists its contents should be merged and all the files should remain (maybe via renaming the files)
e.g.
dir1
----img1
----img2
----dir2
--------img1
--------img2
------------dir1
------------img1
------------img2
------------img3
dir4
----img1
----img2
----img3
becomes
dir1
----img1
----img1_1
----img2
----img2_2
----img3
dir2
----img1
----img2
dir4
----img1
----img2
----img3
My approach was something like that.
Get-ChildItem $SOURCE_PATH -Recurse |
Foreach-Object {
$IS_DIR = Test-Path -Path $_.FullName -PathType Container
if ($IS_DIR) {
Move-Item $_.FullName -dest ($DESTPATH + "/" + $_.Name)
}
}
thanks.
Instead of moving directories I would move individual files to have control over destination name of each file.
This code is untested, it is just intended to give you an idea. Tweak as needed.
# Use parameter -File to operate on files only
Get-ChildItem $SOURCE_PATH -File -Recurse | Foreach-Object {
# Get name of parent directory
$parentDirName = $_.Directory.Name
# Make full path of destination directory
$destSubDirPath = Join-Path $DESTPATH $parentDirName
# Create destination directory if not exists (parameter -Force).
# Assignment to $null to avoid unwanted output of New-Item
$null = New-Item $destSubDirPath -ItemType Directory -Force
# Make desired destination file path. First try original name.
$destFilePath = Join-Path $destSubDirPath $_.Name
# If desired name already exists, append a counter until we find an unused name.
$i = 1
while( Test-Path $destFilePath ) {
# Create a new name like "img1_1.png"
$destFilePath = Join-Path $destSubDirPath "$($_.BaseName)_$i.$($_.Extension)"
$i++
}
# Move and possibly rename file.
Move-Item $_ $destFilePath
}

Search and replace files and folders names with txt file support

I have many folders and inside these different files. Each folder and their children files have the same name and different extension, so in the ABC folder there are the ABC.png, ABC.prj, ABC.pgw files, in the DEF folder there are the DEF.png, DEF.prj, DEF.pgw files and so on.
With a script I have created a txt file with the list of png file names. Then I put in row 2 a new name for the name in row1, in row 4 a new name for the name in row 3, and so on.
Now I'm searching a powershell script that:
- scan all folder for the name in row 1 and replace it with name in row2
- scan all folder for the name in row 3 and replace it with name in row4 and so on
I have try with this below, but it doesn't work.
Have you some suggestions? Thank you
$0=0
$1=1
do {
$find=Get-Content C:\1\Srv\MapsName.txt | Select -Index $0
$repl=Get-Content C:\1\Srv\MapsName.txt | Select -Index $1
Get-ChildItem C:\1\newmaps -Recurse | Rename-Item -NewName { $_.name -replace $find, $repl} -verbose
$0=$0+2
$1=$1+2
}
until ($0 -eq "")
I believe there are several things wrong with your code and also the code Manuel gave you.
Although you have a list of old filenames and new filenames, you are not using that in the Get-ChildItem cmdlet, but instead try and replace all files it finds.
Using -replace uses a Regular Expression replace, that means the special character . inside the filename is regarded as Any Character, not simply a dot.
You are trying to find *.png files, but you do not add a -Filter with the Get-ChildItem cmdlet, so now it will return all filetypes.
Anyway, I have a different approach for you:
If your input file C:\1\Srv\MapsName.txt looks anything like this:
picture1.png
ABC_1.png
picture2.png
DEF_1.png
picture3.png
DEF_2.png
The following code will use that to build a lookup Hashtable so it can act on the files mentioned in the input file and leave all others unchanged.
$mapsFile = 'C:\1\Srv\2_MapsName.txt'
$searchPath = 'C:\1\NewMaps'
# Read the input file as an array of strings.
# Every even index contains the file name to search for.
# Every odd index number has the new name for that file.
$lines = Get-Content $mapsFile
# Create a hashtable to store the filename to find
# as Key, and the replacement name as Value
$lookup = #{}
for ($index = 0; $index -lt $lines.Count -1; $index += 2) {
$lookup[$lines[$index]] = $lines[$index + 1]
}
# Next, get a collection of FileInfo objects of *.png files
# If you need to get multiple extensions, remove the -Filter and add -Include '*.png','*.jpg' etc.
$files = Get-ChildItem -Path $searchPath -Filter '*.png' -File -Recurse
foreach ($file in $files) {
# If the file name can be found as Key in the $lookup Hashtable
$find = $file.Name
if ($lookup.ContainsKey($find)) {
# Rename the file with the replacement name in the Value of the lookup table
Write-Host "Renaming '$($file.FullName)' --> $($lookup[$find])"
$file | Rename-Item -NewName $lookup[$find]
}
}
Edit
If the input text file 'C:\1\Srv\MapsName.txt' does NOT contain filenames including their extension, change the final foreach loop into this:
foreach ($file in $files) {
# If the file name can be found as Key in the $lookup Hashtable
# Look for the file name without extension as it is not given in the 'MapsName.txt' file.
$find = [System.IO.Path]::GetFileNameWithoutExtension($file.Name)
if ($lookup.ContainsKey($find)) {
# Rename the file with the replacement name in the Value of the lookup table
# Make sure to add the file's extension if any.
$newName = $lookup[$find] + $file.Extension
Write-Host "Renaming '$($file.FullName)' --> '$newName'"
$file | Rename-Item -NewName $newName
}
}
Hope that helps
The problem in your snippet is that it never ends.
I tried it and it works but keeps looping forever.
I created a folder with the files a.txt, b.txt and c.txt.
And in the map.txt I have this content:
a.txt
a2.md
b.txt
b2.md
c.txt
c2.md
Running the following script I managed to rename every file to be as expected.
$0=0
$1=1
$find=Get-Content D:\map.txt | Select -Index $0
while($find) {
$find=Get-Content D:\map.txt | Select -Index $0
$repl=Get-Content D:\map.txt | Select -Index $1
if(!$find -Or !$repl) {
break;
}
Get-ChildItem D:\Files -Recurse | Rename-Item -NewName { $_.name -replace $find, $repl} -verbose
$0=$0+2
$1=$1+2
}