Powershell to return last existing folder in path - powershell

I need some help because test-path only returns $true or $false.
I need to return the last existing folder from $path.
$path = \\test.pl\power\shell\test\company
But the last one that exists is the \shell folder.
So how to get a return value like this:
$existingpath = \\test.pl\power\shell
$notexisting = \test\company

An attempt of solving this using the pipeline:
$path = '\\test.pl\power\shell\test\company'
# Create an array that consists of the full path and all parents
$pathAndParents = for( $p = $path; $p; $p = Split-Path $p ) { $p }
# Find the first existing path
$existingPath = $pathAndParents | Where-Object { Test-Path $_ } | Select-Object -First 1
# Extract the non-existing part of the path
$nonExistingPath = $path.SubString( $existingPath.Length + 1 )
The for loop creates a temporay variable $p so the original path will not be destroyed. At each iteration it outputs variable $p, which is automatically added to the array, due to PowerShell's implicit output behaviour. Then it sets $p to the parent path of $p (Split-Path with a single unnamed argument returns the parent path). The loop exits when there is no parent anymore.
The Where-Object | Select-Object line may seem inefficent, but because of Select-Object argument -First 1 it actually tests only the necessary number of paths. When the first existing path is found, the pipeline will be exited (like a break statement in a loop).
The above was the accepted answer. The following solution has been added later. It is more efficient, because it calls Split-Path only as many times as necessary and doesn't need Where-Object and Select-Object.
$path = '\\test.pl\power\shell\test\company'
# Find the first existing path
for( $existingPath = $path;
$existingPath -and -not (Test-Path $existingPath);
$existingPath = Split-Path $existingPath ) {}
# Extract the non-existing part of the path
$nonExistingPath = $path.SubString( $existingPath.Length + 1 )

You can check each folder path one by one, using Split-Path, Test-Path and a while loop:
$path = '.\test.pl\power\shell\test\company'
# Check folder at current path
while(-not(Test-Path $path)){
# Move on to the parent folder
$path = $path |Split-Path
}
# $path will now be `'.\test.pl\power\shell'`
$path

Further to the first answer, it is preferable to resolve and test the root of the passed path before processing:
$path = '\\test.pl\power\shell\test\company'
$resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
$root = Split-Path $resolvedPath -Qualifier
if (Test-Path $root -PathType Container) {
while (-not (Test-Path $resolvedPath -PathType Container)) {
$resolvedPath = Split-Path $resolvedPath
}
} else {
$resolvedPath = ''
}
$resolvedPath
Or using .NET methods:
$root = [System.IO.Path]::GetPathRoot($resolvedPath)
if ([System.IO.Directory]::Exists($root)) {
while (-not [System.IO.Directory]::Exists($resolvedPath)) {
$resolvedPath = [System.IO.Path]::GetDirectoryName($resolvedPath)
}
} else {
$resolvedPath = ''
}
$resolvedPath

Related

Variable for "Media created"? (Powershell)

What is the variable for "Media created", using Powershell?
I'm trying to change many .mov files at one time, in a folder, from "IMG-3523" to their respective creation dates.
I was using the following code to do so, using LastWriteTime:
Get-ChildItem | Rename-Item -NewName {$_.LastWriteTime.ToString("yyyy-MM-dd hh.mm.ss ddd") + ($_.Extension)}
However for some reason there are many duplicate entries for that variable, and I run into an error stating so.
Is there any way I can edit this code to use the "Media created" variable? It's the only date without duplicates.
(I've tried editing the code to include microseconds and $_.CreationTime, however there are many duplicates with that variable too (actually all have the same CreationTime - the timestamp of when I copied the files over from an external disk).
For all files within a directory:
$fldPath = "D:\FolderName";
$flExt = ".mov";
$attrName = "media created"
(Get-ChildItem -Path "$fldPath\*" -Include "*$flExt").FullName | % {
$path = $_
$shell = New-Object -COMObject Shell.Application;
$folder = Split-Path $path;
$file = Split-Path $path -Leaf;
$shellfolder = $shell.Namespace($folder);
$shellfile = $shellfolder.ParseName($file);
$a = 0..500 | % { Process { $x = '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_); If ( $x.split("=")[1].Trim() ) { $x } } };
[int]$num = $a | % { Process { If ($_ -like "*$attrName*") { $_.Split("=")[0].trim() } } };
$mCreated = $shellfolder.GetDetailsOf($shellfile, $num);
$mCreated;
};
For one specific File, you can use:
$flPath = "D:\FolderName\FileName.mov";
$attrName = "media created"
$path = $flPath;
$shell = New-Object -COMObject Shell.Application;
$folder = Split-Path $path;
$file = Split-Path $path -Leaf;
$shellfolder = $shell.Namespace($folder);
$shellfile = $shellfolder.ParseName($file);
$a = 0..500 | % { Process { $x = '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_); If ( $x.split("=")[1].Trim() ) { $x } } };
[int]$num = $a | % { Process { If ($_ -like "*$attrName*") { $_.Split("=")[0].trim() } } };
$mCreated = $shellfolder.GetDetailsOf($shellfile, $num);
$mCreated;
This was answered in SuperUser by vomit-it-chunky-mess-style on Getting a Media Created via PS
Using the Shell.Application object you can get this value from the file, but unfortunately, the property names are all Localized, so if you are looking for a property named Media created on a non-english machine, you will not find it..
For .MOV files, the index of this property in numbered 4.
You can use below function to get that value for all the files in the path and then use that to rename them:
function Get-MetaData {
[CmdletBinding()]
[OutputType([Psobject[]])]
Param (
# Path can be the path to a folder or the full path and filename of a single file
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[string]$Path,
# Filter is unused if Path is pointing to a single file
[Alias('Pattern')]
[string]$Filter = '*.*',
[Alias('Indices')]
[int[]]$Properties = 1..500,
# Recurse is unused if Path is pointing to a single file
[switch]$Recurse,
[switch]$IncludeEmptyProperties
)
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if (!$item) { Write-Error "$Path could not be found."; return }
if (!$item.PSIsContainer) {
# it's a file
$files = #($item)
$Path = $item.DirectoryName
}
else {
# it's a folder
$files = Get-ChildItem -Path $Path -Filter $Filter -File -Recurse:$Recurse
}
$shell = New-Object -ComObject "Shell.Application"
$objDir = $shell.NameSpace($Path)
foreach($file in $files) {
$objFile = $objDir.ParseName($file.Name)
$mediaFile = $objDir.Items()
foreach($index in $Properties) {
$name = $objDir.GetDetailsOf($mediaFile, $index)
if (![string]::IsNullOrWhiteSpace($name)) {
# to be on the safe side, remove any control character (ASCII < 32) that may be in there
$value = $objDir.GetDetailsOf($objFile, $index) -replace '[\x00-\x1F]+'
if (![string]::IsNullOrWhiteSpace($value) -or $IncludeEmptyProperties) {
[PsCustomObject]#{
Path = $file.FullName
Index = $index
Property = $name
Value = $value
}
}
}
}
}
# clean-up Com objects
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objFile)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objDir)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
# property index 3 --> Last Modified
# property index 4 --> Date Created # we'll use this one
# property index 5 --> Last Accessed
$SourceFolder = 'X:\Somewhere'
Get-MetaData -Path $SourceFolder -Filter '*.mov' -Properties 4 |
ForEach-Object {
# HH for 24-hour clock; hh for 12-hour clock
$dateCreated = '{0:yyyy-MM-dd HH.mm.ss ddd}' -f ([datetime]$_.Value)
$directory = [System.IO.Path]::GetDirectoryName($_.Path)
$extension = [System.IO.Path]::GetExtension($_.Path)
# test if the suggested new name is already in use, append a sequence
# number to the basename until we have a unique name
$newName = '{0}{1}' -f $dateCreated, $extension
$index = 1
while (Test-Path -Path (Join-Path -Path $directory -ChildPath $newName) -PathType Leaf) {
$newName = '{0}({1}){2}' -f $dateCreated, $index++, $extension
}
Rename-Item -Path $_.Path -NewName $newName -WhatIf
}
The -WhatIf is a safety measure. It will make the code only show in the console what would happen. No file is actually renamed.
If you are convinced that output is correct, then remove the -WhatIf switch and run the code again
As "Media created" property of a .mov file might also be multiple, or undefined in many cases, use it may cause also file name duplication, even error.
I'd propose another approach, we first check existence of both "Media created" and "Date taken", if nonexistent, we keep using $_.LastWriteTime as basic file name, adding a number to each file name if duplicated, like this:
#
# fnc001: get file media created, date taken or LastWriteTime:
#
function entGetMediaCreatedOrLastWriteTime($objFile) {
$idxMediaCreated = 208
$objShell = New-Object -COMObject Shell.Application
$objShellFolder = $objShell.NameSpace($objFile.DirectoryName)
$iState = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objShell)
$objShellFile = $objShellFolder.ParseName($objFile.Name)
$mediaCreated = $objShellFolder.GetDetailsOf($objShellFile, $idxMediaCreated)
#
# if media created is empty, we check if we have Date taken:
#
if($mediaCreated -eq "") {
#
# canon cameras set Date taken for photos:
#
$idxDateTaken = 12
$dateTaken = $objShellFolder.GetDetailsOf($objShellFile, $idxDateTaken)
#
# return LastWriteTime if neither media created, nor Date taken:
#
if($dateTaken -eq "") {
return $objFile.LastWriteTime
}
#
# otherwise return Date taken, removing non-ascii before:
#
else
{
return [DateTime]($dateTaken -replace '\P{IsBasicLatin}')
}
}
#
# otherwise return valid media created, removing non-ascii before:
#
else {
return [DateTime]($mediaCreated -replace '\P{IsBasicLatin}')
}
}
#
# fnc001: increment filename if it already exists:
#
function entIncrementIfExistent($filename) {
$fnew = $filename
$ext = Split-Path $filename -Extension
#
# define prefix before file number:
#
$prefix = "-"
$i = 0
#
# save file base length:
#
$lngbase = $fnew.length - $ext.length
#
# here $filename is like
# 2023-02-05 08.33.00 Sun.mov,
#
# if it exists, we try to use first available of:
#
# 2023-02-05 08.33.00 Sun-1.mov
# 2023-02-05 08.33.00 Sun-2.mov
# ...
# 2023-02-05 08.33.00 Sun-9.mov
# ...
# 2023-02-05 08.33.00 Sun-10.mov
# 2023-02-05 08.33.00 Sun-11.mov
# ...
#
while (Test-Path $fnew)
{
$i++
$fnew = $fnew.Substring(0, $lngbase) + $prefix + $i + $ext
}
return $fnew
}
Get-ChildItem *.mov | Rename-Item -NewName {
$xDateTime = entGetMediaCreatedOrLastWriteTime $_
$fnew = $xDateTime.ToString("yyyy-MM-dd hh.mm.ss ddd") + $_.Extension
entIncrementIfExistent $fnew
}
I've tried this code, it gets something like this:

Powershell - Skip folder if it doesn't exist

I would like to run a PowerShell script by right-clicking a .PS1 file -> Run with PowerShell. The issue is that the $srcRoot includes three different parent directories, which one, some, or none may exist of 'C:\parentfolder5.5\web\','C:\parentfolder7.0\web\', and/or 'C:\parentfolder8.0\web\'. However, running the script directly in a PowerShell terminal seems to work for the parent folders that do exist even though there are errors that pop up for the parent folders that do not exist.
The goal would be to continue running the script based on the parent directories that do exist, which currently it looks like the .PS1 file stops after looking at the first item in the $srcRoot list. Below is the code being worked on:
$filterLists = '*overview*', '*summary*', '*home*', '*floor*', '*flr*', '*level*', '*lvl*', '*roof*', '*basement*', '*first*', '*second*', '*third*', '*fourth*'
$srcRoot = 'C:\parentfolder5.5\web\','C:\parentfolder7.0\web\','C:\parentfolder8.0\web\'
$dstRoot = $MyInvocation.MyCommand.Path
$params = #{
Path = LiteralPath = $srcRoot |Where-Object { Test-Path -LiteralPath $_ -PathType Container }
Filter = 'views'
Recurse = $true
Directory = $true
}
# All folders under `$srcRoot` with name 'views'
$viewsFolders = Get-ChildItem #params #this line is where the issue
seems to start when Right-clicking -> Run with PowerShell
$params.LiteralPath = $viewsFolders.FullName
$params.Filter = 'graphics'
# All folders under `$viewsFolders` with name 'graphics'
$graphicsFolders = Get-ChildItem #params
$params.Remove('Directory')
$params.LiteralPath = $graphicsFolders.FullName
$params.File = $true # Only search for Files
$params.Force = $true
$params.Remove('Filter')
# All files under `$graphicsFolders`
foreach($file in Get-ChildItem #params)
{
# Where the file name contains one of these filters
foreach($filter in $filterLists)
{
if($file.Name -like $filter)
{
#$file
Copy-Item -Path $($file.FullName) -Destination $dstRoot
# if you want to stop at the first finding
# add `break` here
}
}
}
Help on this would be greatly appreciated!
You can use Where-Object to filter the list of paths. Use Test-Path to test whether each exists and is a directory path:
$params = #{
LiteralPath = $srcRoot |Where-Object { Test-Path -LiteralPath $_ -PathType Container }
Filter = 'views'
Recurse = $true
Directory = $true
}
# this will only attempt directory paths that actually exist now
$viewsFolders = Get-ChildItem #params
Note: Use of LiteralPath (instead of Path) above is intentional - using Path will cause PowerShell to attempt to expand wildcards like ?, * or [abc], whereas -LiteralPath only take exact file/folder names.

How can I find out the exact location where the recursive operation is working?

My problem is, that the string for replacement needs to change according to the folder depth the designated file is located and I don't have a clue how to get that info. I need to work with relative addresses.
I want the script to be run from 2 folder levels above the folder where all the files are that need correcting. So I've set the $path in line 1. That folder suppose to be 'depth 0'. In here, the replacement string needs to be in it's native form -> stylesheet.css.
For files in the folders one level below 'depth 0' the string for replacement needs to be prefixed with ../ once -> ../stylesheet.css.
For files in the folders two level below 'depth 0' the string for replacement needs to be prefixed with ../ twice -> ../../stylesheet.css.
...and so on...
I'm stuck here:
$depth = $file.getDepth($path) #> totally clueless here
I need $depth to contain the number of folders under the root $path.
How can I get this? Here's the rest of my code:
$thisLocation = Get-Location
$path = Join-Path -path $thisLocation -childpath "\Files\depth0"
$match = "findThisInFiles"
$fragment = "stylesheet.css" #> string to be prefixed n times
$prefix = "../" #> prefix n times according to folder depth starting at $path (depth 0 -> don't prefix)
$replace = "" #> this will replace $match in files
$depth = 0
$htmlFiles = Get-ChildItem $path -Filter index*.html -recurse
foreach ($file in $htmlFiles)
{
$depth = $file.getDepth($path) #> totally clueless here
$replace = ""
for ($i=0; $i -lt $depth; $i++){
$replace = $replace + $prefix
}
$replace = $replace + $fragment
(Get-Content $file.PSPath) |
Foreach-Object { $_ -replace $match, $replace } |
Set-Content $file.PSPath
}
Here's a function I've written that uses Split-Path recursively to determine the depth of a path:
Function Get-PathDepth ($Path) {
$Depth = 0
While ($Path) {
Try {
$Parent = $Path | Split-Path -Parent
}
Catch {}
if ($Parent) {
$Depth++
$Path = $Parent
}
else {
Break
}
}
Return $Depth
}
Example usage:
$MyPath = 'C:\Some\Example\Path'
Get-PathDepth -Path $MyPath
Returns 3.
Unfortunately, I had to wrap Split-Path in a Try..Catch because if you pass it the root path then it throws an error. This is unfortunate because it means genuine errors won't cause an exception to occur but can't see a way around this at the moment.
The advantage of working using Split-Path is that you should get a consistent count regardless of whether a trailing \ is used or not.
Here is a way to get the depth in the folder structure for all files in a location. Hope this helps get you in the right direction
New-Item -Path "C:\Logs\Once\Test.txt" -Force
New-Item -Path "C:\Logs\Twice\Folder_In_Twice\Test.txt" -Force
$Files = Get-ChildItem -Path "C:\Logs\" -Recurse -Include *.* | Select-Object FullName
foreach ($File in $Files) {
[System.Collections.ArrayList]$Split_File = $File.FullName -split "\\"
Write-Output ($File.FullName + " -- Depth is " + $Split_File.Count)
}
Output is this just for illustration
C:\Logs\Once\Test.txt -- Depth is 4
C:\Logs\Twice\Folder_In_Twice\Test.txt -- Depth is 5

powershell wrong results for file length

I have a file with length of 309247 bytes as this image:
If I run this powershell script:
(Get-Item 'C:\TestFile.082').length
309247
But if run the same script but a $file variable it returns this:
$fpath = Get-Item $file.FullName
$fpath.length
309800
# or
(Get-Item $file.FullName).length
309800
# or
$file.length
309800
NOTE $file is a variable into a loop for all files contained in a directory
Why if I'm referencing the same file the results are different?
How can I do to get the real value (309247)?
This is my Full Code
$filesToFtp = Get-Item $pathToFtp -Exclude "*.PreARM"
foreach($file in $filesToFtp)
{
$fpath = Get-Item $file.FullName
$fpath.Length
$FileNode = $oXMLDocumentFiles.CreateElement("element","curtmpremotelxml_files","")
$nodoXML = "<versionnum>"+$versplain+"</versionnum>"+
"<filename>"+$file.Name.ToUpper()+"</filename>"+
"<filesize>"+$file.length+".00</filesize>"+
$FileNode.InnerXml = $nodoXML
$oXMLDocumentFiles.DocumentElement.AppendChild($FileNode)
}
It looks like you aren't using the cmdlet you think you are:
foreach( $file in (Get-Item 'c:\')) { ($file.FullName).Length }
is a condensed version of your variable to fill $fPath, but what it returns is a System.IO.FileSystemInfo object, of which there is a member called FullPath, a string.
What I assume you want it
foreach( $file in (Get-ChildItem 'c:\')) { ($file.FullName).Length }
which would return an Array of filepaths of the child items (or files within the folder)
This would also let you clean up a few things
$currentFilePath = $file.FullName
$fLength = $currentFilePath.Length

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