Well I've struggled long enough with this one. I have a project to compare two folders, one on each of two servers. We are comparing files on the source server with those on the target server and will create a list of the files from the source that will need to be refreshed once an update is completed on the target server.
Here's my script (many thanks to http://quickanddirtyscripting.wordpress.com for the original) :
param ([string] $src,[string] $dst)
function get-DirHash()
{
begin
{
$ErrorActionPreference = "silentlycontinue"
}
process
{
dir -Recurse $_ | where { $_.PsIsContainer -eq $false -and ($_.Name -like "*.js" -or $_.Name -like "*.css"} | select Name,FullName,#{Name="SHA1 Hash"; Expression={get-hash $_.FullName -algorithm "sha1" }}
}
end
{
}
}
function get-hash
{
param([string] $file = $(throw 'a filename is required'),[string] $algorithm = 'sha256')
try
{
$fileStream = [system.io.file]::openread((resolve-path $file));
$hasher = [System.Security.Cryptography.HashAlgorithm]::create($algorithm);
$hash = $hasher.ComputeHash($fileStream);
$fileStream.Close();
}
catch
{
write-host $_
}
return $hash
}
Compare-Object $($src | get-DirHash) $($dst | get-DirHash) -property #("Name", "SHA1 Hash")
Now for some reason if I run this against local paths say c:\temp\test1 c:\temp\test2 it works fine, but when I run it using UNC paths between two servers I get
Exception calling "OpenRead" with "1" argument(s): "The given path's format is not supported."
Any help with this would be greatly appreciated. The end result should be a list of files, but for some reason it doesn't like the UNC path.
The script name is compare_js_css.ps1 and is called as such:
.\compare_js_css.ps1 c:\temp\test1 c:\temp\test2 <-- This works
.\compare_js_css.ps1 \\\\devserver1\c$\websites\site1\website \\\\devserver2\c$\websites\site1\website <-- Returns the aforementioned exception.
Why?
This gives the path you are after without the Microsoft.PowerShell.Core\FileSystem:::
(Resolve-Path $file).ProviderPath
No need to use a string replace.
OpenRead supports UNC paths. Resolve-Path returns you an object. Use (Resolve-Path MyFile.txt).Path.Replace('Microsoft.PowerShell.Core\FileSystem::', '') as the argument for OpenRead. The path returned from Resolve-Path when using UNC paths includes PowerShell's fully qualified schema which contains a header which is unsupported by the OpenRead method so it needs to be omitted.
Use the Convert-Path cmdlet, which will provide you with the path in the 'regular' UNC form. This will be required any time you use any shell commands, or need to pass an entire path to a .Net method etc...
See https://technet.microsoft.com/en-us/library/ee156816.aspx
Related
I was wondering if someone could help me understand why does System.IO.FileInfo behaves differently on Windows than on Linux when handling relative paths.
Example
On Linux
PS /home/user/Documents> ([System.IO.FileInfo]'./test.txt').FullName
/home/user/Documents/test.txt
On Windows
PS C:\Users\User\Documents> ([System.IO.FileInfo]'.\test.txt').FullName
C:\Users\User\test.txt
EDIT
To clarify on the above, there is no difference on how System.IO.FileInfo handles relative paths on Windows or Linux. The issue is related to [System.IO.Directory]::GetCurrentDirectory() not being updated by Push-Location or Set-Location.
A simple example:
PS /home/user> [System.IO.Directory]::GetCurrentDirectory()
/home/user
PS /home/user> cd ./Documents/
PS /home/user/Documents> [System.IO.Directory]::GetCurrentDirectory()
/home/user
And assuming this is a expected behavior, what would be an optimal way to approach our param(...) blocks on scripts and functions to accept both cases (absolute and relative). I used to type constraint the path parameter to System.IO.FileInfo but now I can see it is clearly wrong.
This is what I came across, but I'm wondering if there is a better way.I believe Split-Path -IsAbsolute will also bring problems if working with Network Paths, please correct me if I'm wrong.
param(
[ValidateScript({
if(Test-Path $_ -PathType Leaf) {
return $true
}
throw 'Invalid File Path'
})]
[string] $Path
)
if(-not (Split-Path $Path -IsAbsolute)) {
[string] $Path = Resolve-Path $Path
}
Feels a bit duplicate, but since you asked..
I'm sorry I don't know about Linux, but in Windows:
You can add a test first to see if the path is relative and if so, convert it to absolute like:
$Path = '.\test.txt'
if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
$Path = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
}
I added $Path -match '^\\[^\\]+' to also convert relative paths starting with a backslash like \ReadWays.ps1 meaning the path starts at the root directory. UNC paths that start with two backslashes are regarded as absolute.
Apparently (I really have no idea why..) the above does not work on Linux, because there, when using a UNC path, the part ![System.IO.Path]::IsPathRooted('\\server\folder') yields True.
It seems then you need to check the OS first and do the check differently on Linux.
$Path = '\\server\share'
if ($IsWindows) { # $IsWindows exists in version 7.x. Older versions do `$env:OS -match 'Windows'`
if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
$Path = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
}
}
else {
if ($Path -notlike '\\*\*') { # exclude UNC paths as they are not relative
if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
$Path = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
}
}
}
Another option:
As you wanted the result of a cast to [System.IO.FileInfo], you can instead use Get-Item, which will also return a [System.IO.FileInfo] object, but with resolved relative paths as expected. It will also incorporate some error detection (invalid characters or non-existent path etc.).
Example:
PS C:\Users\User\Documents> (Get-Item -LiteralPath '.\test.txt').FullName
C:\Users\User\Documents\test.txt
The easiest alternative would be to use Convert-Path to:
Handle UNC, Relative, Absolute and Rooted Paths.
Be compatible with Windows and Linux
Be efficient
Another neat option if we are using [cmdletbinding()] is to use $PSCmdlet.GetUnresolvedProviderPathFromPSPath(..) method:
function ResolvePath {
[cmdletbinding()]
param($path)
$PSCmdlet.GetUnresolvedProviderPathFromPSPath($path)
}
ResolvePath \\server01\test # => \\server01\test
ResolvePath C:\Users\user\Documents # => C:\Users\user\Documents
ResolvePath C:Documents # => C:\Documents
(ResolvePath .) -eq $PWD.Path # => True
(ResolvePath ~) -eq $HOME # => True
This question already has answers here:
How does PowerShell treat "." in paths?
(3 answers)
Closed 1 year ago.
Get-Content appears to use the current working directory location to resolve realative paths. However, the .Net System.Io.File Open() method does not. What is the PowerShell-centric way to resolve a relative path for .Net?
PS C:\src\t> type .\ReadWays.ps1
[CmdletBinding()]
param (
[Parameter(Mandatory=$true)]
[String]$Path
)
Write-Host "Path is $Path"
Get-Content -Path $Path | Out-Null
if ([System.IO.StreamReader]$sr = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open)) { $sr.Close() }
PS C:\src\t> .\ReadWays.ps1 -Path '.\t.txt'
Path is .\t.txt
MethodInvocationException: C:\src\t\ReadWays.ps1:8
Line |
8 | if ([System.IO.StreamReader]$sr = [System.IO.File]::Open($Path, [Syst …
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| Exception calling "Open" with "2" argument(s): "Could not find file 'C:\Program Files\PowerShell\7\t.txt'."
PS C:\src\t> $PSVersionTable.PSVersion.ToString()
7.2.0
You can add a test to see if the path is relative and if so, convert it to absolute like:
if (![System.IO.Path]::IsPathRooted($Path) -or $Path -match '^\\[^\\]+') {
$path = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($pwd, $Path))
}
I added $Path -match '^\\[^\\]+' to also convert relative paths starting with a backslash like \ReadWays.ps1 meaning the path starts at the root directory. UNC paths that start with two backslashes are regarded as absolute.
The following works fine for me and is compatible with Windows
and Linux. This is using Convert-Path to resolve the relative paths. I was previously using Resolve-Path which is incorrect, only the former resolves to file-system-native paths, thanks mklement0 for pointing it out
param(
[ValidateScript({
if(Test-Path $_ -PathType Leaf)
{
return $true
}
throw 'Invalid File Path'
})]
[string]$Path
)
if(-not $Path.StartsWith('\\'))
{
[string]$Path = Convert-Path $Path
}
$reader = [System.IO.StreamReader]::new(
[System.IO.File]::Open(
$Path, [System.IO.FileMode]::Open
)
)
$reader.BaseStream
$reader.Close()
Last Edit
The following should be able to handle:
UNC Paths
Work on Windows and Linux
Be efficient
Handle Relative Paths
Starting from the base that $Path is valid thanks to the ValidateScript attribute, we only need to determine if the path we are dealing with is UNC, Relative or Absolute.
UNC paths must always be fully qualified. They can include relative directory segments (. and ..), but these must be part of a fully qualified path. You can use relative paths only by mapping a UNC path to a drive letter.
We can assume a UNC path must always start with \\, so this condition should suffice to determine if $Path will be manipulated or not:
if(-not $Path.StartsWith('\\'))
Lastly, in the begin block, updating the environment's current directory each time our script or function runs with:
[Environment]::CurrentDirectory = $pwd.ProviderPath
By doing so, ([System.IO.FileInfo]$Path).FullName should give us the absolute path of our parameter, be it UNC, Relative or Absolute.
param(
[ValidateScript({
if(Test-Path $_ -PathType Leaf) {
return $true
}
throw 'Invalid File Path'
})] [string]$Path
)
begin
{
[Environment]::CurrentDirectory = $pwd.ProviderPath
}
process
{
if(-not $Path.StartsWith('\\'))
{
$Path = ([System.IO.FileInfo]$Path).FullName
}
try
{
$reader = [System.IO.StreamReader]::new(
[System.IO.File]::Open(
$Path, [System.IO.FileMode]::Open
)
)
$reader.BaseStream
}
catch
{
$_.Exception.Message
}
finally
{
$reader.Close()
$reader.Dispose()
}
}
This is a common question. Somehow .net and powershell don't agree on the current directory.
[System.IO.File]::Open("$pwd\$Path", [System.IO.FileMode]::Open)
1. Code Description alias how it is intended to work
User enters a path to a directory in PowerShell. Code checks if any folder within the declared directory contains no data at all. If so, the path of any empty folder will be shown on the prompt to the user and eventually removed from the system.
2. The Issue alias what I am struggling with
The code I just wrote doesn't count the depth of a folder hierarchy as I would expect (the column in the output table is blank). Besides that, the program works okay - I've still got to fix the issue where my code removes empty parent directories first and child directories later, which of course will cause an error in PowerShell; for instance, take
C:\Users\JohnMiller\Desktop\Homework
where Homework consists of Homework\Math\School Project and Homework\Computer Science\PowerShell Code. Note that all directories are supposed to be empty with the exception of PowerShell Code, the folder containing this script. (Side note: A folder is considered empty when no file dwells inside. At least that's what my code is based on for now.)
3. The Code
# Delete all empty (sub)folders in [$path]
[Console]::WriteLine("`n>> Start script for deleting all empty (sub)folders.")
$path = Read-Host -prompt ">> Specify a path"
if (test-path $path)
{
$allFolders = Get-ChildItem $path -recurse | Where {$_.PSisContainer -eq $True}
$allEmptyFolders = $allFolders | Where-Object {$_.GetFiles().Count -eq 0}
$allEmptyFolders | Select-Object FullName,#{Name = "FolderDepth"; Expression = {$_.DirectoryName.Split('\').Count}} | Sort-Object -descending FolderDepth,FullName
[Console]::WriteLine("`n>> Do you want do remove all these directories? Validate with [True] or [False].") #'#
$answer = Read-Host -prompt ">> Answer"
if ([System.Convert]::ToBoolean($answer) -eq $True)
{
$allEmptyFolders | Remove-Item -force -recurse
}
else
{
[Console]::WriteLine(">> Termination confirmed.`n")
exit
}
}
else
{
[Console]::WriteLine(">> ERROR: [$($path)] is an invalid directory. Program terminates.`n")
exit
}
The depth-count problem:
Your code references a .DirectoryName property in the calculated property passed to Select-Object, but the [System.IO.DirectoryInfo] instances output by Get-ChildItem have no such property. Use the .FullName property instead:
$allEmptyFolders |
Select-Object FullName,#{Name='FolderDepth'; Expression={$_.FullName.Split('\').Count}} |
Sort-Object -descending FolderDepth,FullName
Eliminating nested empty subfolders:
To recap your problem with a simple example:
If c:\foo is empty (no files) but has empty subdir. c:\foo\bar, your code outputs them both, and if you then delete c:\foo first, deleting c:\foo\bar next fails (because deleting c:\foo also removed c:\foo\bar).
If you eliminate all nested empty subdirs. up front, you not only declutter what you present to the user, but you can then safely iterative of the output and delete one by one.
With your approach you'd need a 2nd step to eliminate the nested empty dirs., but here's a depth-first recursive function that omits nested empty folders. To make it behave the same way as your code with respect to hidden files, pass -Force.
function Get-RecursivelyEmptyDirectories {
[cmdletbinding()]
param(
[string] $LiteralPath = '.',
[switch] $Force,
[switch] $DoNotValidatePath
)
$ErrorActionPreference = 'Stop'
if (-not $DoNotValidatePath) {
$dir = Get-Item -LiteralPath $LiteralPath
if (-not $dir.PSIsContainer) { Throw "Not a directory path: $LiteralPath" }
$LiteralPath = $dir.FullName
}
$haveFiles = [bool] (Get-ChildItem -LiteralPath $LiteralPath -File -Force:$Force | Select-Object -First 1)
$emptyChildDirCount = 0
$emptySubdirs = $null
if ($childDirs = Get-ChildItem -LiteralPath $LiteralPath -Directory -Force:$Force) {
$emptySubDirs = New-Object System.Collections.ArrayList
foreach($childDir in $childDirs) {
if ($childDir.LinkType -eq 'SymbolicLink') {
Write-Verbose "Ignoring symlink: $LiteralPath"
} else {
Write-Verbose "About to recurse on $($childDir.FullName)..."
try { # If .AddRange() fails due to exceeding the array list's capacity, we must fail too.
$emptySubDirs.AddRange(#(Get-RecursivelyEmptyDirectories -DoNotValidatePath -LiteralPath $childDir.FullName -Force:$Force))
} catch {
Throw
}
# If the last entry added is the child dir. at hand, that child dir.
# is by definition itself empty.
if ($emptySubDirs[-1] -eq $childDir.FullName) { ++$emptyChildDirCount }
}
} # foreach ($childDir ...
} # if ($childDirs = ...)
if (-not $haveFiles -and $emptyChildDirCount -eq $childDirs.Count) {
# There are no child files and all child dirs., if any, are themselves
# empty, so we only output the input path at hand, as the highest
# directory in this subtree that is empty (save for empty descendants).
$LiteralPath
} else {
# This directory is not itself empty, so output the (highest-level)
# descendants that are empty.
$emptySubDirs
}
}
Tips regarding your code:
Get-ChildItem -Directory is available in PSv3+, which is not only shorter but also more efficient than Get-ChildItem | .. Where { $_.PSisContainer -eq $True }.
Use Write-Host instead of [Console]::WriteLine
[System.Convert]::ToBoolean($answer) only works with the culture-invariant string literals 'True' and 'False' ([bool]::TrueString and [bool]::FalseString, although case variations and leading and trailing whitespace are allowed).
I am trying to write a PowerShell script that will loop through a directory in C:\ drive and parse the filenames with the file extension to another script to use.
Basically, the output of the directory listing should be accessible to be parsed to another script one by one. The script is a compiling script which expects an argument (parameter) to be parsed to it in order to compile the specific module (filename).
Code:
Clear-Host $Path = "C:\SandBox\"
Get-ChildItem $Path -recurse -force | ForEach { If ($_.extension -eq ".cob")
{
Write-Host $_.fullname
}
}
If ($_.extension -eq ".pco")
{
Write-Host $_.fullname }
}
You don't need to parse the output as text, that's deprecated.
Here's something that might work for you:
# getmyfiles.ps1
Param( [string])$Path = Get-Location )
dir $Path -Recurse -Force | where {
$_.Extension -in #('.cob', '.pco')
}
# this is another script that calls the above
. getmyfile.ps1 -Path c:\sandbox | foreach-object {
# $_ is a file object. I'm just printing its full path but u can do other stuff eith it
Write-host $_.Fullname
}
Clear-Host
$Path = "C:\Sandbox\"
$Items = Get-ChildItem $Path -recurse -Include "*.cob", "*.pco"
From your garbled code am guessing you want to return a list of files that have .cob and .pco file extensions. You could use the above code to gather those.
$File = $Items.name
$FullName = $items.fullname
Write-Host $Items.name
$File
$FullName
Adding the above lines will allow you to display them in various ways. You can pick the one that suites your needs then loop through them on a for-each.
As a rule its not a place for code to be writen for you, but you have tried to add some to the questions so I've taken a look. Sometimes you just want a nudge in the right direction.
I'm trying to process a list of files that may or may not be up to date and may or may not yet exist. In doing so, I need to resolve the full path of an item, even though the item may be specified with relative paths. However, Resolve-Path prints an error when used with a non-existant file.
For example, What's the simplest, cleanest way to resolve ".\newdir\newfile.txt" to "C:\Current\Working\Directory\newdir\newfile.txt" in Powershell?
Note that System.IO.Path's static method use with the process's working directory - which isn't the powershell current location.
You want:
c:\path\exists\> $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(".\nonexist\foo.txt")
returns:
c:\path\exists\nonexists\foo.txt
This has the advantage of working with PSPaths, not native filesystem paths. A PSPath may not map 1-1 to a filesystem path, for example if you mount a psdrive with a multi-letter drive name.
What's a pspath?
ps c:\> new-psdrive temp filesystem c:\temp
...
ps c:\> cd temp:
ps temp:\>
temp:\ is a drive-qualified pspath that maps to a win32 (native) path of c:\temp.
-Oisin
When Resolve-Path fails due to the file not existing, the fully resolved path is accessible from the thrown error object.
You can use a function like the following to fix Resolve-Path and make it work like you expect.
function Force-Resolve-Path {
<#
.SYNOPSIS
Calls Resolve-Path but works for files that don't exist.
.REMARKS
From http://devhawk.net/blog/2010/1/22/fixing-powershells-busted-resolve-path-cmdlet
#>
param (
[string] $FileName
)
$FileName = Resolve-Path $FileName -ErrorAction SilentlyContinue `
-ErrorVariable _frperror
if (-not($FileName)) {
$FileName = $_frperror[0].TargetObject
}
return $FileName
}
I think you're on the right path. Just use [Environment]::CurrentDirectory to set .NET's notion of the process's current dir e.g.:
[Environment]::CurrentDirectory = $pwd
[IO.Path]::GetFullPath(".\xyz")
Join-Path (Resolve-Path .) newdir\newfile.txt
This has the advantage of not having to set the CLR Environment's current directory:
[IO.Path]::Combine($pwd,"non\existing\path")
NOTE
This is not functionally equivalent to x0n's answer. System.IO.Path.Combine only combines string path segments. Its main utility is keeping the developer from having to worry about slashes. GetUnresolvedProviderPathFromPSPath will traverse the input path relative to the present working directory, according to the .'s and ..'s.
I've found that the following works well enough.
$workingDirectory = Convert-Path (Resolve-Path -path ".")
$newFile = "newDir\newFile.txt"
Do-Something-With "$workingDirectory\$newFile"
Convert-Path can be used to get the path as a string, although this is not always the case. See this entry on COnvert-Path for more details.
function Get-FullName()
{
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline = $True)] [object[]] $Path
)
Begin{
$Path = #($Path);
}
Process{
foreach($p in $Path)
{
if($p -eq $null -or $p -match '^\s*$'){$p = [IO.Path]::GetFullPath(".");}
elseif($p -is [System.IO.FileInfo]){$p = $p.FullName;}
else{$p = [IO.Path]::GetFullPath($p);}
$p;
}
}
}
I ended up with this code in my case. I needed to create a file later in the the script, so this code presumes you have write access to the target folder.
$File = ".\newdir\newfile.txt"
If (Test-Path $File) {
$Resolved = (Resolve-Path $File).Path
} else {
New-Item $File -ItemType File | Out-Null
$Resolved = (Resolve-Path $File).Path
Remove-Item $File
}
I also enclosed New-Item in try..catch block, but that goes out of this question.
I had a similar issue where I needed to find the folder 3 levels up from a folder that does not exist yet to determine the name for a new folder I wanted to create... It's complicated. Anyway, this is what I ended up doing:
($path -split "\\" | select -SkipLast 3) -join "\\"
You can just set the -errorAction to "SilentlyContinue" and use Resolve-Path
5 > (Resolve-Path .\AllFilerData.xml -ea 0).Path
C:\Users\Andy.Schneider\Documents\WindowsPowerShell\Scripts\AllFilerData.xml
6 > (Resolve-Path .\DoesNotExist -ea 0).Path
7 >
There is an accepted answer here, but it is quite lengthy and there is a simpler alternative available.
In any recent version of Powershell, you can use Test-Path -IsValid -Path 'C:\Probably Fake\Path.txt'
This simply verifies that there are no illegal characters in the path and that the path could be used to store a file. If the target doesn't exist, Test-Path won't care in this instance -- it's only being asked to test if the provided path is potentially valid.
Both most popular answers don't work correctly on paths on not existing drives.
function NormalizePath($filename)
{
$filename += '\'
$filename = $filename -replace '\\(\.?\\)+','\'
while ($filename -match '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\') {
$filename = $filename -replace '\\([^\\.]|\.[^\\.]|\.\.[^\\])[^\\]*\\\.\.\\','\'
}
return $filename.TrimEnd('\')
}
Check if the file exists before resolving:
if(Test-Path .\newdir\newfile.txt) { (Resolve-Path .\newdir\newfile.txt).Path }