Powershell $PSScriptRoot empty when using Invoke-Command - powershell

When running a .ps1 on a remote computer using Invoke-Command, $PSScriptRoot is empty on the remote computer.
Is this normal? How can one find the .ps1 path when using Invoke-Command?
Further, other methods, such as through $MyInvocation are also empty.

Is this normal? How can one find the .ps1 path when using Invoke-Command?
There is no script file involved on the remote computer when you call
Invoke-Command -ComputerName $someComputer -FilePath c:\path\to\some\script.ps1
What PowerShell does in this case is to send the contents (parsed source code)[1] of the caller-local script file to the remote machine and executes it there.
Therefore, $PSScriptRoot (the directory in which a script file resides) doesn't apply and returns the empty string.
The same applies to other script-file-related automatic variables, such as $PSCommandPath and $MyInvocation.MyCommand.Name (see about_Automatic_Variables).
To put it differently: When the script file's code executes remotely, it doesn't know anything about the file it came from on the caller's side.
Thus, the only information available you there is $PWD, i.e., the current (working) location (directory).
[1] From the docs: "When you use this [the -FilePath] parameter, PowerShell converts the contents of the specified script file to a script block, transmits the script block to the remote computer, and runs it on the remote computer."

Related

Script location of a remotely executed script?

How can I get a remotely executed script to know it's own location? I'm using Invoke-Command to run a script on a remote server. The script needs to create files in the directory in which it lives. Running with relative addressing doesn't work (i.e. .\output.log), the scripts generally end up in my user profile on the remote server. I tried all the methods outlined in this question but none of them seem to work when the script is remote.
Update: Provided script invocation code per request
$server='ad1hfdahp802'
$remotepath='\\ad1hfdahp802\d$\AHP\pi_exceed_presentation\pi_exceed_presentation_deploy.ps1'
$SDFEnvironment='INT'
Invoke-Command -ComputerName $server -FilePath $remotepath -ArgumentList($SDFEnvironment,$remotepath)
The remote script takes the $remotepath and turns it into a file system path.
Using -FilePath with Invoke-Command means that you read the script locally and send the content as the scriptblock to the remote computer. $PSScriptRoot only works when the script is executed directly on the target. You could try using:
Invoke-Command - ComputerName "computer1" -Scriptblock { & '\\server\path\to\script.ps1' } -Authentication Credssp
Be aware that you need CredSSP to make this work since the remote computer can't use your credentials to access network-resources without it. As an alternative, you could use psexec (or start a process remotely). Ex.
psexec \\computer1 powershell -noprofile -file \\server\path\to\script.ps1
After trying some of the changes proposed I've come to understand that the Invoke-Command isn't actually running the remote script at its original location, but rather loading it from the original location and then running it under the context of PowerShell as the user running the local script. The "script directory" is actually a directory in the user's workspace regardless of where the script originally lived.
This clarifies things for me somewhat. While there may be ways to divine where the script originally came from or to actually start a session on the remote server then run the script as a "local" script there, the need for the remote script to further access other servers, creating multiple hops in authentication, means I have to add CredSSP to the mix.
It seems my original plan, to pass the path I'm using to locate the script to the script so it can place output files in the original directory, is probably the best approach given that I also have to add CredSSP to the mix.
I'm open to refutation, but I don't think any of the proposed solutions actually improve the functionality of the remote script so I'm going to stick with what I started with for now. Thanks to everyone for their contributions.
Enter a session on the remote server, and call the script from there.
local PS> Enter-PSSession -ComputerName $server ...
remote PS> powershell d:\AHP\...\script.ps1
remote PS> exit
local PS>
Then you can use $PSScriptRoot in the script in the remote server to get the local path of the directory of the script on the remote server.
EDIT:
To locate the script on the remote server, you can use your knowledge of the network path of the script file, and parse the output of net share to map network path to local path on the remote server.
remote PS> net share | where { $_.StartsWith('D$ ') } | foreach { [regex]::Split($_, " +")[1]}

How to call a sub script from the main script on a remote server

HI i am having problem in calling a sub script from the main script using powershell on a remote server
I have the main script which is call 1.ps1
the other script is called 2.ps1
Here are the scripts
1.ps1 contains this code &("C:\Users\test\Desktop\remotely run scripts\2.ps1")
2.ps1 contains this code new-item -type file c:\itworksagain.txt
I am running the command as follows:
invoke-command -computer "remoteServer" -filepath "C:\Users\test\Desktop\remotely run scripts\1.ps1"
I am getting an error
The term 'C:\Users\test\Desktop\remotely run scripts\2.ps1 is not recognized as the name of a cmdlet, function, ...
I should have noticed this before. You have the following in the script
&("C:\Users\test\Desktop\remotely run scripts\2.ps1")
The brackets in that line are telling PowerShell to run a subexpression called "C:\Users\test\Desktop\remotely run scripts\2.ps1" which is not a cmdlet, exe.... etc.
I think your issue might be addressed if you change the line to the following.
& "C:\Users\test\Desktop\remotely run scripts\2.ps1"
Update from comments
Your first script calls the second. The invoke command runs the first on the remote server. The script executing on the remote server calls the second script. The call is origination on the remote server so the call is relative to that server. It is looking for 2.ps1 on the remote server. You need to move that script to a central location and then call it from your first with a UNC path which powershell supports. For testing you can move 2.ps1 to the remote server and it should work.

Is it possible to call a powershell script within another script as a variable?

I have a Powershell script where the user passes in a script as a parameter. After that is passed in, I cannot call the script by using $scriptvariable. Is there any way to call a Powershell script from within another Powershell script, when the one script needs to be called from a variable.
param(
[string]hostval,
[string]$scriptpath
)
Invoke-Command -Computer $hostval -Scriptblock { $scriptpath } -credential $cred
This does not work, and I'm not sure if what I want is possible. Is there a parameter type (ex: [script]$scriptpath) that I can use so the script can be called from $scriptpath?
It sounds like you need to use the -FilePath parameter, instead of -Scriptblock:
-FilePath <String>
Runs the specified local script on one or more remote computers. Enter the path and file name of the script, or pipe a script path to Invoke-Command. The script must reside on the local computer or in a directory that the local computer can access. Use the ArgumentList parameter to specify the values of parameters in the script.

Get script directory in PowerShell when script is invoked with Invoke-Command

I have a set of PowerShell scripts that include a "common" script, located in the same folder, like this:
# some-script.ps1
$scriptDir = Split-Path -Parent $myinvocation.mycommand.path
. "$scriptDir\script-utils.ps1"
This is fine if the script is called directly, e.g.
.\some-script.ps1
However, if the script is called with Invoke-Command, this does not work:
Invoke-Command -ComputerName server01 -FilePath "X:\some-script.ps1"
In this case, infact, $myinvocation.mycommand contains the contents of the script, and $myinvocation.mycommand.path is null.
How can I determine the script's directory in a way that works also when the script is invoked with Invoke-Command?
NOTE
In the end, this is the solution I actually used:
Invoke-Command -ComputerName server01 `
{param($scriptArg); & X:\some-script.ps1 $scriptArg } `
-ArgumentList $something
This also allows passing parameters to the script.
I don't believe you can, from within the invoked script. From get-help invoke-command:
-FilePath
Runs the specified local script on one or more remote computers. Enter the path and file name of the script, or
pipe a script path to Invoke-Command. The script must reside on the local computer or in a directory that the
local computer can access. Use the ArgumentList parameter to specify the values of parameters in the script.
**When you use this parameter, Windows PowerShell converts the contents of the specified script file to a script
block, transmits the script block to the remote computer, and runs it on the remote computer.**
When you use invoke-command using the -filepath parameter, the script is read from the file on the local computer, converted to a script block, and that's what gets passed to the remote computer. The remote computer doesn't have any way of knowing if that script block was read from a file.
For the remote computer to know what that original file path was, you'll have to tell it. I think the easiest way to do that would be to write a function to do the invocation, and have it pass the filename to the invoked script as a parameter.
alternatively rather than using filepath.. you could pass in a scriptblock, that dotsources the script from a UNC path that all machines have access to. However each machine will need to have the appropriate executionpolicy set so that they can run that script from the UNC path
let me be clear though, that i'm not saying to run the script from the UNC path as if that does the remoting, but still using invoke-command or start-job to run a scriptblock on a remote computer. It just happens that that scriptblock will run the script from a UNC path for convenience.

PowerShell: Run command from script's directory

I have a PowerShell script that does some stuff using the script’s current directory. So when inside that directory, running .\script.ps1 works correctly.
Now I want to call that script from a different directory without changing the referencing directory of the script. So I want to call ..\..\dir\script.ps1 and still want that script to behave as it was called from inside its directory.
How do I do that, or how do I modify a script so it can run from any directory?
Do you mean you want the script's own path so you can reference a file next to the script? Try this:
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
Write-host "My directory is $dir"
You can get a lot of info from $MyInvocation and its properties.
If you want to reference a file in the current working directory, you can use Resolve-Path or Get-ChildItem:
$filepath = Resolve-Path "somefile.txt"
EDIT (based on comment from OP):
# temporarily change to the correct folder
Push-Location $dir
# do stuff, call ant, etc
# now back to previous directory
Pop-Location
There's probably other ways of achieving something similar using Invoke-Command as well.
There are answers with big number of votes, but when I read your question, I thought you wanted to know the directory where the script is, not that where the script is running. You can get the information with powershell's auto variables
$PSScriptRoot # the directory where the script exists, not the
# target directory the script is running in
$PSCommandPath # the full path of the script
For example, I have a $profile script that finds a Visual Studio solution file and starts it. I wanted to store the full path, once a solution file is started. But I wanted to save the file where the original script exists. So I used $PsScriptRoot.
If you're calling native apps, you need to worry about [Environment]::CurrentDirectory not about PowerShell's $PWD current directory. For various reasons, PowerShell does not set the process' current working directory when you Set-Location or Push-Location, so you need to make sure you do so if you're running applications (or cmdlets) that expect it to be set.
In a script, you can do this:
$CWD = [Environment]::CurrentDirectory
Push-Location $MyInvocation.MyCommand.Path
[Environment]::CurrentDirectory = $PWD
## Your script code calling a native executable
Pop-Location
# Consider whether you really want to set it back:
# What if another runspace has set it in-between calls?
[Environment]::CurrentDirectory = $CWD
There's no foolproof alternative to this. Many of us put a line in our prompt function to set [Environment]::CurrentDirectory ... but that doesn't help you when you're changing the location within a script.
Two notes about the reason why this is not set by PowerShell automatically:
PowerShell can be multi-threaded. You can have multiple Runspaces (see RunspacePool, and the PSThreadJob module) running simultaneously withinin a single process. Each runspace has it's own $PWD present working directory, but there's only one process, and only one Environment.
Even when you're single-threaded, $PWD isn't always a legal CurrentDirectory (you might CD into the registry provider for instance).
If you want to put it into your prompt (which would only run in the main runspace, single-threaded), you need to use:
[Environment]::CurrentDirectory = Get-Location -PSProvider FileSystem
This would work fine.
Push-Location $PSScriptRoot
Write-Host CurrentDirectory $CurDir
I often used the following code to import a module which sit under the same directory as the running script. It will first get the directory from which powershell is running
$currentPath=Split-Path ((Get-Variable
MyInvocation -Scope
0).Value).MyCommand.Path
import-module "$currentPath\sqlps.ps1"
I made a one-liner out of #JohnL's solution:
$MyInvocation.MyCommand.Path | Split-Path | Push-Location
Well I was looking for solution for this for a while, without any scripts just from CLI. This is how I do it xD:
Navigate to folder from which you want to run script (important thing is that you have tab completions)
..\..\dir
Now surround location with double quotes, and inside them add cd, so we could invoke another instance of powershell.
"cd ..\..\dir"
Add another command to run script separated by ;, with is a command separator in powershell
"cd ..\..\dir\; script.ps1"
Finally Run it with another instance of powershell
start powershell "cd..\..\dir\; script.ps1"
This will open new powershell window, go to ..\..\dir, run script.ps1 and close window.
Note that ";" just separates commands, like you typed them one by one, if first fails second will run and next after, and next after... If you wanna keep new powershell window open you add -noexit in passed command . Note that I first navigate to desired folder so I could use tab completions (you couldn't in double quotes).
start powershell "-noexit cd..\..\dir\; script.ps1"
Use double quotes "" so you could pass directories with spaces in names e.g.,
start powershell "-noexit cd '..\..\my dir'; script.ps1"