How to convert absolute path to relative path in PowerShell? - powershell

I'd like to convert a path to a relative path in a PowerShell script.
How do I do this using PowerShell?
For example:
Path to convert: c:\documents\mynicefiles\afile.txt
Reference path: c:\documents
Result: mynicefiles\afile.txt
And
Path to convert: c:\documents\myproject1\afile.txt
Reference path: c:\documents\myproject2
Result: ..\myproject1\afile.txt

I found something built in, Resolve-Path:
Resolve-Path -Relative
This returns the path relative to the current location. A simple usage:
$root = "C:\Users\Dave\"
$current = "C:\Users\Dave\Documents\"
$tmp = Get-Location
Set-Location $root
Resolve-Path -relative $current
Set-Location $tmp

Using the built-in System.IO.Path.GetRelativePath is simpler than the accepted answer:
[System.IO.Path]::GetRelativePath($relativeTo, $path)

There is a good answer here, but it changes your current directory (it reverts back), but if you really need to isolate that process, code example below can help. It does the same thing, just in a new PowerShell instance.
function Get-RelativePath($path, $relativeTo) {
$powershell = (Get-Process -PID $PID | Get-Item)
if ([string]::IsNullOrEmpty($powershell)) {
$powershell = "powershell.exe"
}
& $powershell -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command "& { Set-Location `"$relativeTo`"; Resolve-Path `"$path`" -Relative}"
}
It's really slow though, you should really use the other version unless you absolutely have to use this.

Sometimes I have multiple "roots" from which I want to generate relative file paths. I have found Resolve-Path -Relative unusable in this kind of situation. Changing a global setting, current location, in order to generate a relative file path seems error-prone and (if you're writing parallel code) possibly not thread-safe.
The following should work in early or recent versions of Powershell and Powershell Core, doesn't change your current directory, even temporarily, and is OS-independent and thread-safe.
It doesn't address the second example from OP (inserting .. as required.)
function Get-RelativePath {
param($path, $relativeTo)
# strip trailing slash
$relativeTo = Join-Path `
(Split-Path -Parent $relativeTo) `
(Split-Path -Leaf $relativeTo)
$relPath = Split-Path -Leaf $path
$path = Split-Path -Parent $path
do {
$leaf = Split-Path -Leaf $path
$relPath = Join-Path $leaf $relPath
$path = Split-Path -Parent $path
} until (($path -eq $relativeTo) -Or ($path.Length -eq 0))
$relPath
}
An example:
PS> $configRoot = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $queryPath = 'C:\Users\P799634t\code\RMP\v2\AWD\config_queries\LOAD_UNQ_QUEUE_QUERY2.sql'
PS> Write-Host (Get-RelativePath $queryPath $configRoot)
config_queries\LOAD_UNQ_QUEUE_QUERY2.sql
It behaves reasonably when one file path is not a sub-path of the other:
PS> $root = 'C:\Users\P799634t\code\RMP\v2\AWD'
PS> $notRelated = 'E:\path\to\origami'
PS> Write-Host (Get-RelativePath $notRelated $root)
E:\path\to\origami

A quick and easy way would be :
$current -replace [regex]::Escape($root), '.'
Or if you want the relative path from your actual current location
$path -replace [regex]::Escape((pwd).Path), '.'
This assumes all your paths are valid.

Here is an alternative approach
$pathToConvert1 = "c:\documents\mynicefiles\afile.txt"
$referencePath1 = "c:\documents"
$result1 = $pathToConvert1.Substring($referencePath1.Length + 1)
#$result1: mynicefiles\afile.txt
And
$pathToConvert2 = "c:\documents\myproject1\afile.txt"
#$referencePath2 = "c:\documents\myproject2"
$result2 = "..\myproject" + [regex]::Replace($pathToConvert2 , ".*\d+", '')
#$result2: ..\myproject\afile.txt
Note: in the second case ref path wasn't used.

Related

Converting a line of cmd to powershell

EDIT2: Final code below
I need help on converting some codes as I am very new to mkvmerge, powershell and command prompt.
The CMD code is from https://github.com/Serede/mkvtoolnix-batch/blob/master/mkvtoolnix-batch.bat
for %%f in (*.mkv) do %mkvmerge% #options.json -o "mkvmerge_out/%%f" "%%f"
What I've managed so far
$SourceFolder = "C:\tmp" #In my actual code, this is done using folder browser
$SourceFiles = Get-ChildItem -LiteralPath $SourceFolder -File -Include *.mkv
$SourceFiles | foreach
{
start-process "F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe"
}
I'd be grateful for any help as I'm having trouble understanding and converting while learning both sides. Thank you very much.
**EDIT 2:**Here's my final working code.
Function Get-Folder($initialDirectory) {
#Prompt to choose source folder
[void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')
$FolderBrowserDialog = New-Object System.Windows.Forms.FolderBrowserDialog
$FolderBrowserDialog.Description = 'Choose the video folder'
$FolderBrowserDialog.RootFolder = 'MyComputer'
if ($initialDirectory) { $FolderBrowserDialog.SelectedPath = $initialDirectory }
[void] $FolderBrowserDialog.ShowDialog()
return $FolderBrowserDialog.SelectedPath
}
Function ExitMessage
{
#endregion Function output
Write-Host "`nOperation complete";
Write-Host -NoNewLine 'Press any key to continue...';
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');
Exit;
}
($SourceFolder = Get-Folder | select )
#Check for output folder and create if unavailable
$TestFile = "$SourceFolder" + "\mkvmerge_out"
if ((Test-Path -LiteralPath $TestFile) -like "False")
{
new-item -Path $SourceFolder -name "mkvmerge_out" -type directory
Write-Host 'Folder created';
}
#Checking for the presence of a Json file
$TestFile = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.json)
if ($TestFile.count -eq 0)
{
Write-Host 'json file not found';
ExitMessage;
}
$TestFile = "$SourceFolder" + "\$TestFile"
#Getting the total number of files and start timer.
[Int] $TotalFiles = 0;
[Int] $FilesDone = 0;
$TotalFiles = (Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv).count
$PercentFiles = 0;
$Time = [System.Diagnostics.Stopwatch]::StartNew()
#Start mkvmerge process with progress bar
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$JsonFile = "$TestFile" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
Write-Host "Processing $_"
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe -q `#$JsonFile -o $of $f
$FilesDone++
}
Remove-Item -LiteralPath $JsonFile #Remove this line if you want to keep the Json file
$PercentFiles = [math]::truncate(($FilesDone/$TotalFiles)*100)
Write-Progress -Activity mkvmerge -Status ("{0}% Completed; {1}/{2} done; Time Elapsed: {3:d2}:{4:d2}:{5:d2}" -f $PercentFiles, $FilesDone, $TotalFiles, $Time.Elapsed.Hours, $Time.Elapsed.minutes, $Time.Elapsed.seconds) -PercentComplete $PercentFiles;
ExitMessage;
$mkvmergeExe = 'F:\Desktop\#progs\mkvtoolnix\mkvmerge.exe'
$optionsFile = "$SourceFolder\options.json" # alternatively, use Join-Path
Get-ChildItem -LiteralPath $SourceFolder -File -Filter *.mkv | ForEach-Object {
$f = $_.FullName
$of = "$SourceFolder\mkvmerge_out\$($_.Name)"
& $mkvmergeExe `#$optionsFile -o $of $f
}
Note that your cmd code assumes that it's operating in the current directory, while your PowerShell code passes a directory explicitly via $SourceFolder; therefore, the options.json file must be looked for in $SourceFolder and too, and the output file path passed to -o must be prefixed with $SourceFolder too which is achieved via expandable strings ("...") .
The main points to consider:
for %%f in (*.mkv) has no direct counterpart in PowerShell; you correctly used Get-ChildItem instead, to get a list of matching files, which are returned as System.IO.FileInfo instances.
However, -Include won't work as intended in the absence of -Recurse (unless you append \* - see this GitHub issue; -Filter does, and is also the faster method, but it has its limitations and legacy quirks (see this answer).
While PowerShell too allows you to execute commands whose names or paths are stored in a variable (or specified as a quoted string literal), you then need &, the call operator, to invoke it, for syntactic reasons.
Inside a script block ({ ... }) passed to the ForEach-Object cmdlet, automatic variable $_ represents the pipeline input object at hand.
$_.FullName ensures that the System.IO.FileInfo input instances are represented by their full path when used in a string context.
This extra step is no longer necessary in PowerShell [Core] 6+, where System.IO.FileInfo instances thankfully always stringify as their full paths.
The # character is preceded by ` (backtick), PowerShell's escape character, because # - unlike in cmd - is a metacharacter, i.e. a character with special syntactic meaning. `# ensures that the # is treated verbatim, and therefore passed through to mkvmerge.
Alternatively, you could have quoted the argument instead of escaping just the #: "#$optionsFile"
See this answer for background information.
You generally do not need to enclose arguments in "..." in PowerShell, even if they contain spaces or other metacharacters.

Defining path to a file with $PSScriptRoot

This is not exactly asking for help, more like kind of curiosity. I have a path to a file defined like this in my script:
$RootDir = $PSScriptRoot
$ExcelFile = $RootDir + "\" + $File + ".xlsx"
The $File variable is defined earlier in the script. This version works perfectly fine, but when I tried to simplify it like this to a single row, it didn't work:
$ExcelFile = $PSScriptRoot + "\" + $File + ".xlsx"
Any idea why the second version does not work? It says that the file could not be found. I can't see any logical reason for it.
For building paths use the Join-Path cmdlet:
$ExcelFile = Join-Path $PSScriptRoot ($File + '.xlsx')
In your particular case you could also go with a simple string with variables:
$ExcelFile = "${PSScriptRoot}\${File}.xlsx"
But Join-Path is the more resilient approach, because it automatically takes care of the path separator between the elements.
Thought this might help someone
# Get Current working path
if($PSScriptRoot){$PSRootPath = $PSScriptRoot}Else{Try { $PSRootPath = (Split-Path $psISE.CurrentFile.FullPath -Parent)} Catch {$PSRootPath = (Split-Path $MyInvocation.MyCommand.Path -Parent)}}

In PowerShell, how can I test if a user-supplied string represents a full absolute path? [duplicate]

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 }

Removing path and extension from filename in PowerShell

I have a series of strings which are full paths to files. I'd like to save just the filename, without the file extension and the leading path. So from this:
c:\temp\myfile.txt
to
myfile
I'm not actually iterating through a directory, in which case something like PowerShell's basename property could be used, but rather I'm dealing with strings alone.
Way easier than I thought to address the issue of displaying the full path, directory, file name or file extension.
## Output:
$PSCommandPath ## C:\Users\user\Documents\code\ps\test.ps1
(Get-Item $PSCommandPath ).Extension ## .ps1
(Get-Item $PSCommandPath ).Basename ## test
(Get-Item $PSCommandPath ).Name ## test.ps1
(Get-Item $PSCommandPath ).DirectoryName ## C:\Users\user\Documents\code\ps
(Get-Item $PSCommandPath ).FullName ## C:\Users\user\Documents\code\ps\test.ps1
$ConfigINI = (Get-Item $PSCommandPath ).DirectoryName+"\"+(Get-Item $PSCommandPath ).BaseName+".ini"
$ConfigINI ## C:\Users\user\Documents\code\ps\test.ini
Other forms:
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
split-path -parent $PSCommandPath
Split-Path $script:MyInvocation.MyCommand.Path
split-path -parent $MyInvocation.MyCommand.Definition
[io.path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
There's a handy .NET method for that:
C:\PS> [io.path]::GetFileNameWithoutExtension("c:\temp\myfile.txt")
myfile
Inspired by an answer of #walid2mi:
(Get-Item 'c:\temp\myfile.txt').Basename
Please note: this only works if the given file really exists.
or
([io.fileinfo]"c:\temp\myfile.txt").basename
or
"c:\temp\myfile.txt".split('\.')[-2]
you can use basename property
PS II> ls *.ps1 | select basename
Starting with PowerShell 6, you get the filename without extension like so:
split-path c:\temp\myfile.txt -leafBase
#Keith,
here another option:
PS II> $f="C:\Downloads\ReSharperSetup.7.0.97.60.msi"
PS II> $f.split('\')[-1] -replace '\.\w+$'
PS II> $f.Substring(0,$f.LastIndexOf('.')).split('\')[-1]
Expanding on René Nyffenegger's answer, for those who do not have access to PowerShell version 6.x, we use Split Path, which doesn't test for file existence:
Split-Path "C:\Folder\SubFolder\myfile.txt" -Leaf
This returns "myfile.txt". If we know that the file name doesn't have periods in it, we can split the string and take the first part:
(Split-Path "C:\Folder\SubFolder\myfile.txt" -Leaf).Split('.') | Select -First 1
or
(Split-Path "C:\Folder\SubFolder\myfile.txt" -Leaf).Split('.')[0]
This returns "myfile".
If the file name might include periods, to be safe, we could use the following:
$FileName = Split-Path "C:\Folder\SubFolder\myfile.txt.config.txt" -Leaf
$Extension = $FileName.Split('.') | Select -Last 1
$FileNameWoExt = $FileName.Substring(0, $FileName.Length - $Extension.Length - 1)
This returns "myfile.txt.config".
Here I prefer to use Substring() instead of Replace() because the extension preceded by a period could also be part of the name, as in my example. By using Substring we return the filename without the extension as requested.
Given any arbitrary path string, various static methods on the System.IO.Path object give the following results.
strTestPath = C:\Users\DAG\Documents\Articles_2018\NTFS_File_Times_in_CMD\PathStringInfo.ps1
GetDirectoryName = C:\Users\DAG\Documents\Articles_2018\NTFS_File_Times_in_CMD
GetFileName = PathStringInfo.ps1
GetExtension = .ps1
GetFileNameWithoutExtension = PathStringInfo
Following is the code that generated the above output.
[console]::Writeline( "strTestPath = {0}{1}" ,
$strTestPath , [Environment]::NewLine );
[console]::Writeline( "GetDirectoryName = {0}" ,
[IO.Path]::GetDirectoryName( $strTestPath ) );
[console]::Writeline( "GetFileName = {0}" ,
[IO.Path]::GetFileName( $strTestPath ) );
[console]::Writeline( "GetExtension = {0}" ,
[IO.Path]::GetExtension( $strTestPath ) );
[console]::Writeline( "GetFileNameWithoutExtension = {0}" ,
[IO.Path]::GetFileNameWithoutExtension( $strTestPath ) );
Writing and testing the script that generated the above uncovered some quirks about how PowerShell differs from C#, C, C++, the Windows NT command scripting language, and just about everything else with which I have any experience.
Here is one without parentheses
[io.fileinfo] 'c:\temp\myfile.txt' | % basename
This script searches in a folder and sub folders and rename files by removing their extension
Get-ChildItem -Path "C:/" -Recurse -Filter *.wctc |
Foreach-Object {
rename-item $_.fullname -newname $_.basename
}
This can be done by splitting the string a couple of times.
#Path
$Link = "http://some.url/some/path/file.name"
#Split path on "/"
#Results of split will look like this :
# http:
#
# some.url
# some
# path
# file.name
$Split = $Link.Split("/")
#Count how many Split strings there are
#There are 6 strings that have been split in my example
$SplitCount = $Split.Count
#Select the last string
#Result of this selection :
# file.name
$FilenameWithExtension = $Split[$SplitCount -1]
#Split filename on "."
#Result of this split :
# file
# name
$FilenameWithExtensionSplit = $FilenameWithExtension.Split(".")
#Select the first half
#Result of this selection :
# file
$FilenameWithoutExtension = $FilenameWithExtensionSplit[0]
#The filename without extension is in this variable now
# file
$FilenameWithoutExtension
Here is the code without comments :
$Link = "http://some.url/some/path/file.name"
$Split = $Link.Split("/")
$SplitCount = $Split.Count
$FilenameWithExtension = $Split[$SplitCount -1]
$FilenameWithExtensionSplit = $FilenameWithExtension.Split(".")
$FilenameWithoutExtension = $FilenameWithExtensionSplit[0]
$FilenameWithoutExtension
The command below will store in a variable all the file in your folder, matchting the extension ".txt":
$allfiles=Get-ChildItem -Path C:\temp\*" -Include *.txt
foreach ($file in $allfiles) {
Write-Host $file
Write-Host $file.name
Write-Host $file.basename
}
$file gives the file with path, name and extension: c:\temp\myfile.txt
$file.name gives file name & extension: myfile.txt
$file.basename gives only filename: myfile
Here are a couple PowerShell 5.1 one-liner options that put the path at the start of the line.
'c:\temp\myfile.txt' |%{[io.fileinfo]$_ |% basename}
OR
"c:\temp\myfile.txt" | Split-Path -Leaf | %{$_ -replace '\.\w+$'}

How to get the Parent's parent directory in Powershell?

So if I have a directory stored in a variable, say:
$scriptPath = (Get-ScriptDirectory);
Now I would like to find the directory two parent levels up.
I need a nice way of doing:
$parentPath = Split-Path -parent $scriptPath
$rootPath = Split-Path -parent $parentPath
Can I get to the rootPath in one line of code?
Version for a directory
get-item is your friendly helping hand here.
(get-item $scriptPath ).parent.parent
If you Want the string only
(get-item $scriptPath ).parent.parent.FullName
Version for a file
If $scriptPath points to a file then you have to call Directory property on it first, so the call would look like this
(get-item $scriptPath).Directory.Parent.Parent.FullName
Remarks
This will only work if $scriptPath exists. Otherwise you have to use Split-Path cmdlet.
I've solved that like this:
$RootPath = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
You can split it at the backslashes, and take the next-to-last one with negative array indexing to get just the grandparent directory name.
($scriptpath -split '\\')[-2]
You have to double the backslash to escape it in the regex.
To get the entire path:
($path -split '\\')[0..(($path -split '\\').count -2)] -join '\'
And, looking at the parameters for split-path, it takes the path as pipeline input, so:
$rootpath = $scriptpath | split-path -parent | split-path -parent
You can use
(get-item $scriptPath).Directoryname
to get the string path or if you want the Directory type use:
(get-item $scriptPath).Directory
You can simply chain as many split-path as you need:
$rootPath = $scriptPath | split-path | split-path
simplest solution
Here's the simplest solution
"$path\..\.."
If you want to get an absolute path, you can
"$path\..\.." | Convert-Path
reusable solution
Here is a reusable solution, first define the getParent function, then call it directly.
function getParent($path, [int]$deep = 1) {
$result = $path | Get-Item | ForEach-Object { $_.PSIsContainer ? $_.Parent : $_.Directory }
for ($deep--; $deep -gt 0; $deep--) { $result = getParent $result }
return $result
}
getParent $scriptPath 2
In PowerShell 3, $PsScriptRoot or for your question of two parents up,
$dir = ls "$PsScriptRoot\..\.."
Split-Path -Path (Get-Location).Path -Parent
To extrapolate a bit on the other answers (in as Beginner-friendly a way as possible):
String objects that point to valid paths can be converted to DirectoryInfo/FileInfo objects via functions like Get-Item and Get-ChildItem.
.Parent can only be used on a DirectoryInfo object.
.Directory converts a FileInfo object to a DirectoryInfo object (targeting the file's directory), and will return null if used on any other type (even another DirectoryInfo object).
.DirectoryName converts a FileInfo object to a String object (targeting the file's directory), and will return null if used on any other type (even another String object).
.FullName converts a DirectoryInfo/FileInfo object to a String object, and will return null if used on any other type (even another DirectoryInfo/FileInfo object).
.Path converts a PathInfo object to a String object, and will return null if used on any other type (even another PathInfo object).
Check the object type with the GetType Method to see what you're working with: $scriptPath.GetType()
Lastly, a quick tip that helps with making one-liners: Get-Item has the gi alias and Get-ChildItem has the gci alias.
If you want to use $PSScriptRoot you can do
Join-Path -Path $PSScriptRoot -ChildPath ..\.. -Resolve
In powershell :
$this_script_path = $(Get-Item $($MyInvocation.MyCommand.Path)).DirectoryName
$parent_folder = Split-Path $this_script_path -Leaf