Create new absolute path from absolute path + relative or absolute path - powershell

I am working on a build script using psake and I need to create an absolute path from the current working directory with an inputted path which could either be a relative or absolute path.
Suppose the current location is C:\MyProject\Build
$outputDirectory = Get-Location | Join-Path -ChildPath ".\output"
Gives C:\MyProject\Build\.\output, which isn't terrible, but I would like without the .\. I can solve that issue by using Path.GetFullPath.
The problem arises when I want to be able to provide absolute paths
$outputDirectory = Get-Location | Join-Path -ChildPath "\output"
Gives C:\MyProject\Build\output, where I need C:\output instead.
$outputDirectory = Get-Location | Join-Path -ChildPath "F:\output"
Gives C:\MyProject\Build\F:\output, where I need F:\output instead.
I tried using Resolve-Path, but this always complains about the path not existing.
I'm assuming Join-Path is not the cmdlet to use, but I have not been able find any resources on how to do what I want. Is there a simple one-line to accomplish what I need?

You could use GetFullPath(), but you would need to use a "hack" to make it use you current location as the current Directory(to resolve relative paths). Before using the fix, the .NET method's current directory is the working directory for the process, and not the location you have specified inside the PowerShell process. See Why don't .NET objects in PowerShell use the current directory?
#Hack to make .Net methods use the shells current directory instead of the working dir for the process
[System.Environment]::CurrentDirectory = (Get-Location)
".\output", "\output", "F:\output" | ForEach-Object {
[System.IO.Path]::GetFullPath($_)
}
Output:
C:\Users\Frode\output
C:\output
F:\output
Something like this should work for you:
#Hack to make .Net methods use the shells current directory instead of the working dir for the process
[System.Environment]::CurrentDirectory = (Get-Location)
$outputDirectory = [System.IO.Path]::GetFullPath(".\output")

I don't think there's a simple one-liner. But I assume you need the path created anyway, if it doesn't exist yet? So why not just test and create it?
cd C:\
$path = 'C:\Windows', 'C:\test1', '\Windows', '\test2', '.\Windows', '.\test3'
foreach ($p in $path) {
if (Test-Path $p) {
(Get-Item $p).FullName
} else {
(New-Item $p -ItemType Directory).FullName
}
}

Related

Powershell Script "$dirs"

So I do not have a degree or any formal training in any programming language but my job has had me slowly learn the basics of SQL and I have now been given a new task at work. The previous person in charge of this task ran powershell scripts to combine and rename PDFs. I get the macro level of how this all works. The script sets a loop through the parent directory into all the children directory concatenates the PDFs using PDFtk Server then renames the combined PDFs to child directory they are named in. However, I cannot figure out how to specify the dirs (I think thats the term). As it stands now I can only successfully run the powershell script in the folder in which Console2 is located.
$path = Split-Path -parent $MyInvocation.MyCommand.Definition
Function mergeFiles
{
# Loop through all directories
$dirs = dir $path -Recurse | Where { $_.psIsContainer -eq $true }
$cmd = 'C:\Program Files (x86)\PDFtk Server\bin\pdftk.exe'
$In1 = 'A.pdf'
$In2 = 'B.pdf'
$Out1 = 'C.pdf'
Foreach ($dir In $dirs)
This is the first part of the merge files function. Can someone help me figure out how to identify a specfic "dirs"? (Like if I had the PDF in a folder on my desktop)
The location that is populating the directory it will search based on the location of your script, based on this line:
$path = Split-Path -parent $MyInvocation.MyCommand.Definition
$MyInvocation.MyCommand.Definition is the full path to the running script and the command Split-Path -parent will return the parent directory. You could just change that line to be the the location you want i.e.:
$path = 'C:\Users\JC\Desktop\PDF'
but you probably don't want to hardcode that path. What you want to do is add the path as an input argument to the script. To do that, add the following to the top of your script:
PARAM($path)
Then when you invoke your script, you just pass the path you are interested in:
.\theScript.ps1 C:\Users\JC\Desktop\PDF
You can then get more advanced and specify a default value, for example, if you want the default to be the location the script is run:
PARAM($path = '.')

How to follow a shortcut in powershell

In powershell, you use cd dir to go into the directory dir.
But if dir is a shortcut to a directory, cd dir and cd dir.lnk both give an error, saying that the directory doesn't exist.
So how do I follow that shortcut?
(In Linux cd dir just works. In Windows, I've got no idea)
Using the shell com-object, you can get the target path and from there, do what you wish. Get-ShortcutTargetPath
function Get-ShortcutTargetPath($fileName) {
$sh = New-Object -COM WScript.Shell
$targetPath = $sh.CreateShortcut($fileName).TargetPath
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($sh) | Out-Null
return $targetPath
}
$file = 'Path\to\Filename.lnk'
$TargetPath = Get-shortcutTargetPath($file)
if (Test-Path -PathType Leaf $TargetPath) {
$TargetPath = Split-Path -Path $TargetPath
}
Set-Location $TargetPath
The other answers don't actually work for me, the first does nothing and the second gives me the error "The shortcut pathname must end with .lnk or .url."
However, after digging through MSDN for a while I worked out a solution. I have no idea why I can't find any answers to this on Google, but I even travelled to the untouched second and third pages and tried multiple keywords... I guess the search results are just buried for some reason. But here's how I solved the problem. Using a while loop and the Get-Item command, you can follow a symbolic link to it's true path! Works for directory links, too.
So far the double-cd approach is the most reliable/universal I can find for handling a situation of mixed relative/absolute paths in the filenames and targets, and I tested several scenarios.
function getLinkTarget($fn) {
$op=$PWD #Save original path
while($t=(Get-Item $fn).Target) { #Get link target
cd (Split-Path -Parent $fn) #cd to parent of file/dir
cd (Split-Path -Parent $t) #cd again to parent of target
$fn=(Split-Path -Leaf $t) #Set filename to relative target
}
$fn=(Join-Path $PWD $fn) #Make path absolute
cd $op #Change back to original path
return $fn
}
In Windows 10 cd directory_name or cd dir* if you only want part of the name assuming no other directories start with the same dir*.

How can I use spaces in a fully qualified pathname in Powershell?

I have a script that copies a number of files from different sources to a single directory for backup. The only step of the script the errors out has a space in both the path and file names: \\server\Network Shares\Transfer\tu3\tu3 Code.mdb
I get the error copy-item : Cannot find path '\\server\Network Shares\Transfer\tu3\tu3 Code.mdb' because it does not exist. and I'm assuming it's because of the spaces in either the path or filename. Does PowerShell allow spaces in a fully qualified path? If not, how can I get at the file?
Here's the relevant code (My$Destis defined as a global variable for the script):
$TU3CodeUpdatedPathname = "\\server\Network Shares\Transfer\tu3\"
$TU3CodeUpdatedFilename = "tu3 Code.mdb"
$TU3CodeUpdated = $TU3CodeUpdatedPathname + $TU3CodeUpdatedFilename
#
$Source = $TU3CodeUpdated
$Dest = $VMShareSpacePathname
#
copy-item $Source $Dest
Try being more explicit, and wrap the parameter values in quotes. Adding -Verbose might help with debugging. If it's complaining the file doesn't exist, maybe double check that the file is indeed accessible when your script runs under the account, if it's not the same as your user account.
Copy-Item -Path "$Source" -Destination "$Dest"
Just to ensure, you might have mixed up the variable names TU3/HS3?
$TU3CodeUpdatedPathname = "\\server\Network Shares\Transfer\tu3\"
$TU3CodeUpdatedFilename = "tu3 Code.mdb"
$TU3CodeUpdated = Join-Path -Path $TU3CodeUpdatedPathname -ChildPath $TU3CodeUpdatedFilename
Otherwise I can't see anything wrong with your code.
Spaces are just fine within quotes as you did write it.
I would guess the running user from the script does not have access rights to the file/share.
This post might help in that case.
This worked for me to copy folder with space in its name. I am using powershell 4.0
$Source = "D:\test\Test cases"
$Dest = "D:\bck\Test cases"
Copy-Item -Path "$Source" "$Dest" -Recurse

Powershell climb directory tree backwards

I have script located in C:\projects\bacon\packages\build\run.ps1 and I am trying to locate the solution folder (in this case bacon). Everything I've found shows how to climb forward if you know the folder name. But I don't know the project name, so I need to climb backwards until I find the first containing folder that has a packages or dependencies folder within the given script's path.
The closest function I've found is to use Split-Path $script:MyInvocation.MyCommand.Path to get my script's path and perhaps loop backwards somehow? but I can't find anyway of looping the folders backwards until I find the "packages" or "dependencies" folder.
You can use a combination of Get-Item and Get-ChildItem. Get-Item returns an object that has a Parent property. You can limit Get-ChildItem to just directory objects. You can then use this to trek backwards:
$current = Get-Item .
Write-Host $Current.Parent
do
{
$parent = Get-Item $current.Parent.FullName
$childDirectories = $parent | Get-ChildItem -Directory | ? { $_.Name -in #("dependencies","packages") }
$current = $parent
} until ($childDirectories)
$bacon = $parent.FullName
I should say that the first line $current = Get-Item . will only work as is if the current path for the PowerShell runspace is at the end of the tree you are working with.
In your script, if you are using v3, you can replace the . with $PSScriptRoot.

How to get the current directory of the cmdlet being executed

This should be a simple task, but I have seen several attempts on how to get the path to the directory where the executed cmdlet is located with mixed success. For instance, when I execute C:\temp\myscripts\mycmdlet.ps1 which has a settings file at C:\temp\myscripts\settings.xml I would like to be able to store C:\temp\myscripts in a variable within mycmdlet.ps1.
This is one solution which works (although a bit cumbersome):
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$settingspath = $directorypath + '\settings.xml'
Another one suggested this solution which only works on our test environment:
$settingspath = '.\settings.xml'
I like the latter approach a lot and prefer it to having to parse the filepath as a parameter each time, but I can't get it to work on my development environment. What should I do? Does it have something to do with how PowerShell is configured?
Yes, that should work. But if you need to see the absolute path, this is all you need:
(Get-Item .).FullName
The reliable way to do this is just like you showed $MyInvocation.MyCommand.Path.
Using relative paths will be based on $pwd, in PowerShell, the current directory for an application, or the current working directory for a .NET API.
PowerShell v3+:
Use the automatic variable $PSScriptRoot.
The easiest method seems to be to use the following predefined variable:
$PSScriptRoot
about_Automatic_Variables and about_Scripts both state:
In PowerShell 2.0, this variable is valid only in script modules (.psm1). Beginning in PowerShell 3.0, it is valid in all scripts.
I use it like this:
$MyFileName = "data.txt"
$filebase = Join-Path $PSScriptRoot $MyFileName
You can also use:
(Resolve-Path .\).Path
The part in brackets returns a PathInfo object.
(Available since PowerShell 2.0.)
Try :
(Get-Location).path
or:
($pwd).path
Path is often null. This function is safer.
function Get-ScriptDirectory
{
$Invocation = (Get-Variable MyInvocation -Scope 1).Value;
if($Invocation.PSScriptRoot)
{
$Invocation.PSScriptRoot;
}
Elseif($Invocation.MyCommand.Path)
{
Split-Path $Invocation.MyCommand.Path
}
else
{
$Invocation.InvocationName.Substring(0,$Invocation.InvocationName.LastIndexOf("\"));
}
}
Get-Location will return the current location:
$Currentlocation = Get-Location
I like the one-line solution :)
$scriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
Try this:
$WorkingDir = Convert-Path .
In Powershell 3 and above you can simply use
$PSScriptRoot
If you just need the name of the current directory, you could do something like this:
((Get-Location) | Get-Item).Name
Assuming you are working from C:\Temp\Location\MyWorkingDirectory>
Output
MyWorkingDirectory
Most answers don't work when debugging in the following IDEs:
PS-ISE (PowerShell ISE)
VS Code (Visual Studio Code)
Because in those the $PSScriptRoot is empty and Resolve-Path .\ (and similars) will result in incorrect paths.
Freakydinde's answer is the only one that resolves those situations, so I up-voted that, but I don't think the Set-Location in that answer is really what is desired. So I fixed that and made the code a little clearer:
$directorypath = if ($PSScriptRoot) { $PSScriptRoot } `
elseif ($psise) { split-path $psise.CurrentFile.FullPath } `
elseif ($psEditor) { split-path $psEditor.GetEditorContext().CurrentFile.Path }
For what it's worth, to be a single-line solution, the below is a working solution for me.
$currFolderName = (Get-Location).Path.Substring((Get-Location).Path.LastIndexOf("\")+1)
The 1 at the end is to ignore the /.
Thanks to the posts above using the Get-Location cmdlet.
this function will set the prompt location to script path, dealing with the differents way to get scriptpath between vscode, psise and pwd :
function Set-CurrentLocation
{
$currentPath = $PSScriptRoot # AzureDevOps, Powershell
if (!$currentPath) { $currentPath = Split-Path $pseditor.GetEditorContext().CurrentFile.Path -ErrorAction SilentlyContinue } # VSCode
if (!$currentPath) { $currentPath = Split-Path $psISE.CurrentFile.FullPath -ErrorAction SilentlyContinue } # PsISE
if ($currentPath) { Set-Location $currentPath }
}
You would think that using '.\' as the path means that it's the invocation path. But not all the time. Example, if you use it inside a job ScriptBlock. In which case, it might point to %profile%\Documents.
This is what I came up with. It's an array including multiple methods of finding a path, uses the current location, filters out null\empty results, and returns the first not-null value.
#((
($MyInvocation.MyCommand.Module.ModuleBase),
($PSScriptRoot),
(Split-Path -Parent -Path $MyInvocation.MyCommand.Definition -ErrorAction SilentlyContinue),
(Get-Location | Select-Object -ExpandProperty Path)
) | Where-Object { $_ })[0]
To only get the current folder name, you can also use:
(Split-Path -Path (Get-Location) -Leaf)
To expand on #Cradle 's answer: you could also write a multi-purpose function that will get you the same result per the OP's question:
Function Get-AbsolutePath {
[CmdletBinding()]
Param(
[parameter(
Mandatory=$false,
ValueFromPipeline=$true
)]
[String]$relativePath=".\"
)
if (Test-Path -Path $relativePath) {
return (Get-Item -Path $relativePath).FullName -replace "\\$", ""
} else {
Write-Error -Message "'$relativePath' is not a valid path" -ErrorId 1 -ErrorAction Stop
}
}
I had similar problems and it made me a lot of trouble since I am making programs written in PowerShell (full end user GUI applications) and I have a lot of files and resources I need to load from disk.
From my experience, using . to represent current directory is unreliable. It should represent current working directory, but it often does not.
It appears that PowerShell saves location from which PowerShell has been invoked inside ..
To be more precise, when PowerShell is first started, it starts, by default, inside your home user directory. That is usually directory of your user account, something like C:\USERS\YOUR USER NAME.
After that, PowerShell changes directory to either directory from which you invoked it, or to directory where script you are executing is located before either presenting you with PowerShell prompt or running the script. But that happens after PowerShell app itself originally starts inside your home user directory.
And . represents that initial directory inside which PowerShell started. So . only represents current directory in case if you invoked PowerShell from the wanted directory. If you later change directory in PowerShell code, change appears not to be reflected inside . in every case.
In some cases . represents current working directory, and in others directory from which PowerShell (itself, not the script) has been invoked, what can lead to inconsistent results.
For this reason I use invoker script. PowerShell script with single command inside:
POWERSHELL.
That will ensure that PowerShell is invoked from the wanted directory and thus make . represent current directory. But it only works if you do not change directory later in PowerShell code.
In case of a script, I use invoker script which is similar to last one I mentioned, except it contains a file option:
POWERSHELL -FILE DRIVE:\PATH\SCRIPT NAME.PS1.
That ensures that PowerShell is started inside current working directory.
Simply clicking on script invokes PowerShell from your home user directory no matter where script is located.
It results with current working directory being directory where script is located, but PowerShell invocation directory being C:\USERS\YOUR USER NAME, and with . returning one of these two directories depending on the situation, what is ridiculous.
But to avoid all this fuss and using invoker script, you can simply use either $PWD or $PSSCRIPTROOT instead of . to represent current directory depending on weather you wish to represent current working directory or directory from which script has been invoked.
And if you, for some reason, want to retrieve other of two directories which . returns, you can use $HOME.
I personally just have invoker script inside root directory of my apps I develop with PowerShell which invokes my main app script, and simply remember to never ever change current working directory inside my source code of my app, so I never have to worry about this, and I can use . to represent current directory and to support relative file addressing in my applications without any problems.
This should work in newer versions of PowerShell (newer than version 2).
Mine was a short, so unplug everything but USB from it and recompile