Avoiding overriding variables when importing a subscript using dot notation - powershell

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

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>

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.

Setting the Script folder location using a Function fails

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.

Path SQLSERVER:\7za.exe does not exist?

I have the following code in a powershell script. And I have file 7za.exe in the same directory of the script.
param($sql)
$temp = [System.IO.Path]::GetTempFileName()
Invoke-Sqlcmd -ServerInstance sqlserver1 $sql | ConvertTo-Csv > $temp
.\7za.exe a '$temp.zip' '$temp'
However, it got the following error. What's the right approach to execute an executable in the same folder right after invoke-sqlcmd?
SQL Server PowerShell provider error: Path SQLSERVER:\7za.exe does not exist. Please specify a valid path.
+ CategoryInfo : OperationStopped: (:) [], GenericProviderException
+ FullyQualifiedErrorId : Microsoft.SqlServer.Management.PowerShell.GenericProviderException
+ PSComputerName : localhost
If you look at the path in the error message, it is "SQLSERVER:\7za.exe" -- so the current working directory for the instance of PowerShell running this script (which you can get using the automatic variable $pwd) is SQLSERVER:\, which is the PSDrive created by the SQL module\snap-in that must be loaded to run the Invoke-SqlCmd cmdlet.
PowerShell scripts do NOT use their current location as the current working directory by default. In PowerShell version 3 and later, however, you can get the script's directory using the automatic variable $PSScriptRoot. In earlier versions, you can generate $PSScriptRoot yourself with:
$PSScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Definition
Then you could use this variable in your script like so:
& (Join-Path $PSScriptRoot '7za.exe') "$temp.zip" "$temp"
Note that I used double quotes around the $temp variable, so that PowerShell would automatically expand the variable into the correct name.
#jbsmith is right about what path to use. Contributing to this issue is that the SQLPS module changes the current directory to SQLSERVER: when it is imported.

Remove alias in script

I can remove an alias like so:
Remove-Item Alias:wget
Then trying the alias gives the expected result:
PS > wget
wget : The term 'wget' is not recognized as the name of a cmdlet, function,
script file, or operable program.
However, if I put the same into a script,
PS > cat wget.ps1
Remove-Item Alias:wget
wget
it gives unexpected result
cmdlet Invoke-WebRequest at command pipeline position 1
Supply values for the following parameters:
Uri:
The following seems to work
If (Test-Path Alias:wget) {Remove-Item Alias:wget}
If (Test-Path Alias:wget) {Remove-Item Alias:wget}
wget
The first line removes the Script level alias, and the second removes the Global level alias. Note that both need to be tested, because if the Global doesn't exist the Script isn't created.
Also this misbehaves in that it modifies the parent without being dot sourced.
Alternatively you can just dot source your original and that would work
. .\wget.ps1
The reason behind this is scope...
There are four scopes: local, script, private, and global
The rules for variables, functions and aliases say that if they are not defined in the current scope then PowerShell will search the parent scopes.
The default for an alias is set to allscope, which make it visible from any child scopes and also they are inherited into any new child scope (I think that would be a fair definition).
Get-Alias -Name wget | select Name,options
Name Options
wget AllScope
Once you remove the alias that is the script scope (scope 0), it will then find the alias from the global/parent scope (scope 1).
When you dot source it, you are just saying to run the script in the calling/global scope in the first place, so you are removing the Global alias by default.
Try these...
E.g #1.
1.1) Remove the alias from the global scope.
Remove-Item -Path Alias:\wget
1.2) Create a new one (global scope) and make it private.
New-Alias -Name wget -Value dir -Scope private
1.3) Now the alias is not visible from the nested scope.
So try run the script and it will not be found.
E.g #2.
2.1) Remove the alias from the global scope.
Remove-Item -Path Alias:\wget
2.2) Create a new one and make it AllScope
New-Alias -Name wget -Value dir -Option AllScope
2.3) Now run your script and it will work fine (using the new alias dir from the parent scope)
You could try the same with variables as well. They should be easier to demo (and play around with) since you can more easily play with the scope parameter when you use New/Get/Set/Remove -scope 1 or scope 0 OR use the scope modifiers, for example, $global:a.
Get-Command -Noun variable -ParameterName scope
Clearly, some of the other answers are on-track and have identified the fact that the alias is present in several scopes.
But if you're trying to invoke wget.exe rather than Invoke-WebRequest (or wget.ps1 ) in some script, the simplest answer might be to put the ".exe" extension on the command name when you invoke it. That way you don't have to fiddle with the aliases.
It looks like when you run the script from the PowerShell console it inherits the alias drive from that session. So if wget is there it works, and if it's not it fails. If instead you run the script in a new process it gets removed normally.
Here's the code I used for my demo:
Remove-Item -Path Alias:\wget -Force
wget
Start-Sleep -Seconds 100
The start-sleep will keep your console from closing immediately. Try running that script in a new process. I used the run prompt and PowerShell itself by typing `powershell.exe -file pathtoscript '