Setting the Script folder location using a Function fails - powershell

I have created a function with a piece of code that works fine in a script but fails when it is converted to a function.
The code sets the location where the script resides as current location.
Function Set-ScriptFolder
{
<#
.SYNOPSIS
Sets the location to the folder where the script folder resides.
.DESCRIPTION
Sets the location to the folder where the script folder resides.
.EXAMPLE
Set-ScriptFolder
.EXAMPLE
Set-ScriptFolder
#>
$location = Split-Path $MyInvocation.MyCommand.Path
set-location $location
}
The output is:
Split-Path : Cannot bind argument to parameter 'Path' because it is null.
set-location : Cannot process argument because the value of argument "path" is null. Change the value of argument
"path" to a non-null value.
Any idea how to convert:
$location = Split-Path $MyInvocation.MyCommand.Path
set-location $location
to a function?
Thanks

If you look at the documentation for $MyInvocation you would see ....
$MyInvocation is populated only for scripts, function, and script blocks.
You can use the information in the System.Management.Automation.InvocationInfo
object that $MyInvocation returns in the current script, such as the path
and file name of the script ($MyInvocation.MyCommand.Path) or the name of a
function ($MyInvocation.MyCommand.Name) to identify the current command.
You can see the output of $MyInvocation.MyCommand.Name inside your function would return:
Set-ScriptFolder
Alternate solutions
You could use the automatic variable $PSScriptRoot instead.
Set-Location $PSScriptRoot
That should work inside the function as well. If you prefer you could also use the propery PSScriptRoot of $MyInvocation as long as you have PowerShell 3.0 or higher.
Set-Location $MyInvocation.PSScriptRoot
Function for just one line does seem a little overkill.

Related

Batch file to change directory run from within powershell does nothing

I have a small "dev.bat" batch file on my PATH which I run to switch to my development project directory in W:\. This works fine from CMD but not when run from PowerShell (or PWSH).
I have no other problems running .bat files from PowerShell.
PS C:\> type C:\dev.bat
W:
CD W:\dev
PS C:\> dev.bat
me#computer C:\
> W:
me#computer W:\dev
> CD W:\dev
PS C:\> echo "Why did dev.bat not change directory??"
Why did dev.bat not change directory??
PS C:\> W:
PS W:\>
No, cmd /c dev.bat makes no difference.
When run from PowerShell, batch files invariably run in a (cmd.exe) child process[1], given that PowerShell itself doesn't understand the batch language.
Changing the working directory in a child process is limited to that child process (and its own children), and has no effect on the calling process; a child process cannot change the calling process' working directory.
Your only option is to:
have your batch file echo (print) the desired working directory
capture that path in PowerShell and pass it to Set-Location
If you don't want to change your batch file, use the following workaround:
Set-Location -LiteralPath (cmd /c 'dev.bat >NUL && cd')
# Or if you want to use the 'cd' alias for Set-Location and
# are confident that path never has "[" characters in it (so that
# it can't be mistaken for a wildcard expression):
cd (cmd /c 'dev.bat >NUL && cd')
If batch files needn't be involved at all, and you just want a convenient way to create custom functions that change to a predefined location (working directory), place the following function in your $PROFILE file:
# Helper function to place in $PROFILE, which generates custom quick-cd
# functions, based on a function name and target directory path.
function New-QuickCD ($Name, $LiteralPath) {
$funcDef = #"
function global:$Name { Push-Location -LiteralPath "$LiteralPath" } # quick-CD function
"#
Invoke-Expression $funcDef # define in current session too
$funcDef >> $PROFILE # append to $PROFILE
}
Note:
The generated functions use Push-Location rather than Set-Location to enable easy returning to the previous location with Pop-Location (popd).
For convenience, generated functions are also defined in the current session via Invoke-Expression[2] on creation, so you don't have to reload (dot-source) $PROFILE or open a new session before you can call the newly generated function.
Blindly appending to $PROFILE with >> means that if you redefine a function, the new definition will take effect, but the obsolete previous one will linger in the file, requiring manual cleanup; the comment # quick-CD function placed after each generated function is meant to facilitate that - see the bottom section for a more sophisticated version of New-QuickCD that updates old definitions in place.
You can make the function more robust and convenient in a variety of ways: making the parameters mandatory, verifying the path's existence (by default), resolving the path to an absolute one - again, see the bottom section.
E.g., to create a function named dev that switches to W:\dev, you'd then call:
# Generate function 'dev', which switches to 'W:\dev',
# append it to your $PROFILE file, and also define it in this session:
New-QuickCD dev W:\dev
# Call it:
dev # changes the current location to W:\dev; use 'popd' to return.
More robust, flexible New-QuickCD function:
It improves on the above version as follows:
It makes the parameters mandatory.
It verifies the existence of the target directory path.
It defines the functions with support for a -PrintOnly switch that merely prints the function's target directory, without changing to it.
It resolves a relative path to an absolute one first, so that you can run New-QuickCD foo . to define a function that switches to the absolute path of the current location.
When you redefine a function, the previous definition is automatically updated:
In order to enable this functionality $PROFILE is rewritten as a whole, using the > redirection operator.
To remove functions, you must still edit $PROFILE manually.
It comes with comment-based help; run help New-QuickCD -Examples, for instance.
function New-QuickCD {
<#
.SYNOPSIS
Creates a custom quick-CD function.
.DESCRIPTION
Creates a custom quick-CD function and appends it your $PROFILE file.
Such a function changes to a fixed location (directory) stored inside the
function, specified at creation time to allow for quickly changing to
frequently used directories using a short name.
For convenience, a newly created function is also defined for the running
session (not just for all future sessions).
The quick-CD functions use Push-Location to change location, which
enables you to easily return to the previously active location with
Pop-Location (popd).
To determine what location a given quick-CD function *would* change to,
invoke it with the -PrintOnly switch.
.PARAMETER FunctionName
The name of the quick-CD function to define.
.PARAMETER DirectoryPath
The literal path of the directory the quick-CD function should change to.
If given a relative path, it is resolved to an absolute one first.
For convenience, you may specify a *file* path, in which case that file's
parent path is used.
.NOTES
Your $PROFILE file is recreated every time you use this function, using the
> redirection operator, so as to support updating functions in place.
To *remove* a quick-CD function, edit $PROFILE manually.
.EXAMPLE
New-QuickCD dev W:\dev
Adds a 'dev' function to $PROFILE, which on invocation changes the current
location to W:\dev
* Call just 'dev' to change to W:\dev. Use popd to return to the previous
location.
* Call 'dev -PrintOnly' to print what location function 'dev' *would*
change to.
.EXAMPLE
New-QuickCD proj .
Adds a 'proj' function to $PROFILE, which on invocation changes to the
the location that is current at the time of calling New-QuickCd.
#>
param(
[Parameter(Mandatory)] [string] $FunctionName,
[Parameter(Mandatory)] [string] $DirectoryPath
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
# Resolve the path to a full path. Fail if it doesn't exist.
$fullPath = (Resolve-Path -ErrorAction Stop -LiteralPath $DirectoryPath).Path
# As a courtesy, if the path is a *file*, we use its parent path instead.
if (Test-Path -PathType Leaf $fullPath) {
$fullPath = [IO.Path]::GetDirectoryName($fullPath)
}
# Define a comment that identifies the functions we add to $PROFILE as
# quick-CD functions.
$idComment = '<# quick-CD function generated with New-QuickCD #>'
# Generate the new function's source code...
# * on a *single line*, which enables easy filtering when updating $PROFILE below
# * with a distinctive comment at the end of the line that identifies the
# function as a quick-CD function.
# * with the global: scope specifier, which makes it easier to call the
# same definition with Invok-Expression to make the function available in the
# current session too.
$newFuncDef = #"
$idComment function global:$FunctionName { param([switch] `$PrintOnly) if (`$PrintOnly) { "$fullPath" } else { Push-Location -LiteralPath "$fullPath" } }
"#
# ... define it in the current session (doing this *before* updating $PROFILE ensures early exit if the function name is invalid)
Invoke-Expression $newFuncDef
# ... and update $PROFILE:
# Get the current content of $PROFILE
[string] $currentProfileContent = if (Test-Path -LiteralPath $PROFILE) { Get-Content -Raw -LiteralPath $PROFILE }
# Try to replace an existing definition.
$newProfileContent = $currentProfileContent -replace ('(?m)^{0} function global:{1} .+$' -f [regex]::Escape($idComment), [regex]::Escape($FunctionName)), $newFuncDef
if (-not $currentProfileContent -or $newProfileContent -ceq $currentProfileContent) { # Profile didn't exist or nothing was replaced -> we must append the new definition.
$newProfileContent = $newProfileContent.TrimEnd() + [Environment]::NewLine * 2 + $newFuncDef
}
# Write the file.
$newProfileContent > $PROFILE
}
[1] By contrast, batch files run in-process when invoked from cmd.exe, analogous to how PowerShell runs its *.ps1 scripts in-process.
POSIX-like shells such as Bash, on the other hand, by default run their scripts in a child process, except when sourcing is used (., source)
[2] While this is a safe use of Invoke-Expression, it should generally be avoided.
Another simple way to do that is create a dev.ps1 (PowerShell Script file) rather batch file, whith the follow code Set-Location -Path "W:\dev"
Note: batch file runs on CMD process as a child process even if you runs it on powershell
#mkelement is correct: there is no simple way to do this from a .bat file on your path - that is old school. The proper PowerShell way is to create an alias to a function which does what you want.
Borrowing from this answer my solution is:
Step 1: Create a reusable function to make an alias:
PS> echo 'function myAlias {
$g=[guid]::NewGuid();
$alias = $args[0]; $commands = $args[1];
echo "function G$g { $commands }; New-Alias -Force $alias G$g">>$profile
};'>>$profile
Re-start powershell (to load the above function) and then define your dev shortcut as follows:
Step 2: Create a dev shortcut/alias which gets you where you want to be:
PS> myAlias dev "Set-Location W:\dev"
Step 3: Happily use dev
PS C:\> dev
PS W:\dev>

Avoiding overriding variables when importing a subscript using dot notation

When I write complex PowerShell scripts, I use to split the scripts into separate files:
main.ps1 to contains the logic
imports\xxxx.ps1, imports\yyyy.ps1, ... to provides custom functions
The main.ps1 file imports in the current scope the functions using the dot command after having computed the path relative to the current directory. Ex:
$CurrentDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition
. "$CurrentDirectory\Imports\Maths.ps1"
Process-Multiply -x 10 -y 10 # assuming there's a function in the maths.ps1
This is working as long as I don't overwrite the variables within the imported scripts. For example, if my subscripts again declare a $CurrentDirectory variable, it actually overwrite the variable from the main script. I guess it's because of the dot notation (in opposition to the & notation).
Is there a way to avoid this ? Is it possible to have "local" variable for a script ?
Here's a full reproductible sample:
File main.ps1 :
$CurrentDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition
Write-Host "`$CurrentDirectory from main.ps1 : $CurrentDirectory"
. "$CurrentDirectory\Imports\Maths.ps1"
Write-Host "`$CurrentDirectory from main.ps1 : $CurrentDirectory"
Process-Multiply -x 10 -y 10
File Imports\Maths.ps1 :
$CurrentDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition
function Process-Multiply{
param(
[Parameter(Mandatory=$true)][int]$x,
[Parameter(Mandatory=$true)][int]$y
)
$x * $y
}
When running the main.ps1 script, the output is :
$CurrentDirectory from main.ps1 : C:\temp\xx
$CurrentDirectory from main.ps1 : C:\temp\xx\Imports
100
As you can see, the $CurrentDirectory variable is overriden in the sub script.
PS: I know I could rename the variable in the subscript, but in a larger scope with centralized scripts, it would be hard to ensure uniqueness of variable names.
PPS: the real world scenario where I need directory path of current script is to load a custom module from a c# assembly. The assembly is located in a subdirectory of a loader script.
PPPS (!) : I have to stick to PS v4

Is it possible to get the caller's directory in PowerShell script when calling from CMD or batch file?

Let's say I type this in CMD from C:\source:
powershell.exe Set-ExecutionPolicy RemoteSigned -File C:\test\test.ps1
In test.ps1 I try to get C:\source as directory without success.
$script_folder = $PSScriptRoot
$myDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$myDir
$PSScriptRoot
Both $myDir and $PSScriptRoot returns C:\test\ instead of C:\source.
You could use $PWD which is the Automatic variable for Present Working Directory. When you open PowerShell it should continue to use the same working directory.
From about_automatic_variables
$PWD
Contains a path object that represents the full path of the current directory.
Also MS-DOS is an Operating System which cannot run PowerShell. This is different from cmd.exe aka Command Prompt in Windows.
The automatic variables you are using are information about the script invocation. The location from which the command to launch the script was initiated is part of the environment.
$PWD contains information about the present working directory (nod to posix pwd command). Specifically, $PWD.Path.
Per the about_automatic_variables page (or Get-Help about_automatic_variables), $PSScriptRoot, $PSCommandPath, are properties of $MyInvocation.
See here for an example of using Split-Path -Path $($Global:$MyInvocation.MyCommand.Path) to get the current path.
Recommend a test script:
# TestInvocationAndPWDPaths.ps1
function Test-MyInvocation {
$MyInvocation
}
function Test-PWD {
$PWD
}
'$MyInvocation from script:'
$MyInvocation
'$MyInvocation from function:'
Test-MyInvocation
'$PWD from script:'
$PWD
'$PWD from function'
Test-PWD
Has interesting results. Running this from powershell console, and from ISE, and from command prompt will show you the differences in $MyInvocation.
$MyInvocation.PSScriptRoot gives you the caller scripts folder.
When the caller is command line, this will return $null.
You should be able to use these two facts.
Just want to add that it is a general pitfall in powershell to use $pwd/get-location inside psm1 functions.
Instead inject the full paths as parameters.

Passing Arguments between script files

I need to pass a few arguments from one script in file to another one. I load path to my current script file to variable and add name, arguments of other script that I want to call.
Here is the sample of calling and passing argument that I got in Script1.ps1:
Param([string]$argument)
$thisScript = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
.($thisScript+'\anotherScript.ps1 -passedArgument '+$argument)
Here is the part of the script Script2.ps1 that I'm calling:
Param([string]$passedArgument)
$passedArgument = "do some work with it HERE"
When I start the first script like this
C:\Users\user1\Desktop\Script1.ps1 -argument datatopass
it writes the error
The term 'C:\Users\user1\Desktop\Script2.ps1 -passedArgument datatopass' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
When I try use the script manually like this
C:\Users\user1\Desktop\Script2.ps1 -passedArgument datatopass
it works fine and doesn't report any error with wrong path or name.
I don't know where the problem is, and I couldn't find anything about this error.
You dont have to concat the passedArgument with its value to a string. Try:
& (Join-Path $thisScript 'anotherScript.ps1') -passedArgument $argument

$MyInvocation.MyCommand.Name Providing Strange Results

Why does $MyInvocation.MyCommand.Name give you the name of the function when executed from within a function?
According to this it is meant to give you the script file name.
Here is the code I'm using:
function startScript
{
$ScriptName = $MyInvocation.MyCommand.Name
$ScriptName
}
$ScriptName = $MyInvocation.MyCommand.Name
$ScriptName
startScript
<#
Output:
testing.ps1
startScript
#>
According to PowerShell documentation:
$MyInvocation
Contains an information about the current command, such as the name,
parameters, parameter values, and information about how the command was
started, called, or "invoked," such as the name of the script that called
the current command.
$MyInvocation is populated only for scripts, function, and script blocks.
You can use the information in the System.Management.Automation.InvocationInfo
object that $MyInvocation returns in the current script, such as the path
and file name of the script ($MyInvocation.MyCommand.Path) or the name of a
function ($MyInvocation.MyCommand.Name) to identify the current command.
This is particularly useful for finding the name of the current script.
Also see the following paragraph if you are interested in the script path:
Beginning in Windows PowerShell 3.0, $MyInvocation has the following new
properties.
-- PSScriptRoot: Contains the full path to the script that invoked the
current command. The value of this property is populated only when
the caller is a script.