Get original script name included script name and possibly others - powershell

I've been testing $MyInvocation, $MyCommand and inspecting some other variables but would like a good example of how to do the following:
Get the name of the current script file
Get the name of the caller script file that included the current script file
Get the name of the original script file that ultimately resulted in #1 being executed
I have used this for original but maybe it will also be caller?
$myInvocation.MyCommand.Name #original
Even if there are more files involved, I only need the current, caller and original. Consider three scripts; if you were to execute first.ps1:
first.ps1
. "$PSScriptRoot\second.ps1"
second.ps1
. "$PSScriptRoot\third.ps1"
#get $Current = second.ps1
#get $Caller = first.ps1
#get $Original = first.ps1
third.ps1
#get $Current = third.ps1
#get $Caller = second.ps1
#get $Original = first.ps1
I have worked out how to get the filename from the path etc...
Also, why is syntax highlighting wonky on SO for PowerShell?

Place the following in second.ps1 and third.ps1, which utilizes the automatic $PSCommandPath variable and Get-PSCallStack:
# Get the full paths of the scripts on the call stack (the calling scripts).
$callingScripts = (Get-PSCallStack).InvocationInfo.PSCommandPath -ne $null
# For illustration, use a custom object to show the current script's
# file name, that of the immediate caller, and that of the original caller.
[pscustomobject] #{
Current = Split-Path -Leaf $PSCommandPath
Caller = Split-Path -Leaf $callingScripts[0]
Original = Split-Path -Leaf $callingScripts[-1]
}
Your third.ps1 script would then output the following:
Current Caller Original
------- ------ --------
third.ps1 second.ps1 first.ps1
Explanation:
The automatic $PSCommandPath variable contains the running script's own full file-system path.
Get-PSCallStack outputs the call stack in the form of System.Management.Automation.CallStackFrame instances, bottom-up: that is, the instance at index 0 represents the immediate caller, whereas index -1 - the last array element - represents the top of the call stack and therefore the original caller.
.InvocationInfo.PSCommandPath is an instance of member-access enumeration, which extracts the caller script path's across all stack frames (across all array elements).
Non-script-file callers may be involved as well, notably the initial script block used behind the scenes to submit a command from the interactive prompt, and the .PSCommandPath property of such contains $null.
-ne $null is used to filter out these $null values, so that the array returned contains only the paths of script-file callers.
Split-Path -Leaf is used to extract just the file-name portion from the full paths.
why is syntax highlighting wonky on SO for PowerShell?
Good question. It seems that some heuristic is being implied to guess the language, and it frequently guesses wrong; As of this writing, explicitly specifying ```powershell at the start of a fenced code block doesn't help, suggesting that PowerShell isn't a supported language. For now, ```sh, even though it's the wrong language, works reasonably well in practice.

Related

PowerShell, auto load functions from internet on demand

It was pointed out to me (in PowerShell, replicate bash parallel ping) that I can load a function from the internet as follows:
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
The url referenced Test-ConnectionAsync.ps1 contains two functions: Ping-Subnet and Test-ConnectionAsync
This made me wonder if I could then define bypass functions in my personal module that are dummy functions that will be permanently overridden as soon as they are invoked. e.g.
function Ping-Subnet <mimic the switches of the function to be loaded> {
if <function is not already loaded from internet> {
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
}
# Now, somehow, permanently overwrite Ping-Subnet to be the function that loaded from the URL
Ping-Subnet <pass the switches that we mimicked to the required function that we have just loaded>
}
This would very simply allow me to reference a number of useful scripts directly from my module but without having to load them all from the internet upon loading the Module (i.e. the functions are only loaded on demand, when I invoke them, and I will often never invoke the functions unless I need them).
You could use the Parser to find the functions in the remote script and load them into your scope. This will not be a self-updating function, but should be safer than what you're trying to accomplish.
using namespace System.Management.Automation.Language
function Load-Function {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[uri] $URI
)
process {
try {
$funcs = Invoke-RestMethod $URI
$ast = [Parser]::ParseInput($funcs, [ref] $null, [ref] $null)
foreach($func in $ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true)) {
if($func.Name -in (Get-Command -CommandType Function).Name) {
Write-Warning "$($func.Name) is already loaded! Skipping"
continue
}
New-Item -Name "script:$($func.Name)" -Path function: -Value $func.Body.GetScriptBlock()
}
}
catch {
Write-Warning $_.Exception.Message
}
}
}
Load-Function https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1
Ping-Subnet # => now is available in your current session.
function Ping-Subnet{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
function Test-ConnectionAsync{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
Ping-Subnet -Result Success
Test-ConnectionAsync -Computername $env:COMPUTERNAME
Result:
Computername Result
------------ ------
192.168.1.1 Success
192.168.1.2 Success
192.168.1.146 Success
Computername IPAddress Result
------------ --------- ------
HOME-PC fe80::123:1234:ABCD:EF12 Success
Yes, it should work. Calling Test-ConnectionAsync.ps1 from with-in a function will create the functions defined with-in, in the wrapping function's scope. You will be able to call any wrapped functions until the function's scope ends.
If you name the wrapper and wrapped functions differently, you can check whether the function has been declared with something like...
Otherwise, you need to get more creative.
This said, PROCEED WITH CAUTION. Remote code execution, like this, is fraught with security issues, especially in the way we're talking about it i.e., no validation of Test-ConnectionAsync.ps1.
Fors1k's answer deserves the credit for coming up with the clever fundamentals of the approach:
Download and execute the remote script's content in a dynamic module created with New-Module (whose built-in alias is nmo), which causes the script's functions to be auto-exported and to become available session-globally[1]
Note that dynamic modules aren't easy to discover, because they're not shown in Get-Module's output; however, you can discover them indirectly, via the .Source property of the command-info objects output by Get-Command:
Get-Command | Where Source -like __DynamicModule_*
That the downloaded functions become available session-globally may be undesired if you're trying to use the technique inside a script that shouldn't affect the session's global state - see the bottom section for a solution.
Then re-invoke the function, under the assumption that the original stub function has been replaced with the downloaded version of the same name, passing the received arguments through.
While Fors1k's solution will typically work, here is a streamlined, robust alternative that prevents potential, inadvertent re-execution of code:
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Define and session-globally import a dynamic module based on the remote
# script's content.
# Any functions defined in the script would automatically be exported.
# However, unlike with persisted modules, *aliases* are *not* exported by
# default, which the appended Export-ModuleMember call below compensates for.
# If desired, also add -Variable * in order to export variables too.
# Conversely, if you only care about functions, remove the Export-ModuleMember call.
$dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
)
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name uses the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName #args
}
Specifically, this implementation ensures:
That aliases defined in the remote script are exported as well (just remove + "`nExport-ModuleMember -Function * -Alias *" from the code above if that is undesired.
That the re-invocation robustly targets the new, module-defined implementation of the function - even if the stub function runs in a child scope, such as in a (non-dot-sourced) script.
When run in a child scope, $MyInvocation.Line|IEX (iex is a built-in alias of the Invoke-Expression cmdlet) would result in an infinite loop, because the stub function itself is still in effect at that time.
That all received arguments are passed through on re-invocation without re-evaluation.
Using the built-in magic of splatting the automatic $args variable (#args) passes only the received, already expanded arguments through, supporting both named and positional arguments.[2]
$MyInvocation.Line|IEX has two potential problems:
If the invoking command line contained multiple commands, they are all repeated.
You can solve this particular problem by substituting (Get-PSCallStack)[1].Position.Text for $MyInvocation.Line, but that still wouldn't address the next problem.
Both $MyInvocation.Line and (Get-PSCallStack)[1].Position.Text contain the arguments that were passed in unexpanded (unevaluated) form, which causes their re-evaluation by Invoke-Expression, and the perils of that are that, at least hypothetically, this re-evaluation could involve lengthy commands whose output served as arguments or, worse, commands that had side effects that cannot or should not be repeated.
Scoping the technique to a given local script:
That the downloaded functions become available session-globally may be undesired if you're trying to use the technique inside a script that shouldn't affect the session's global state; that is, you may want the functions exported via the dynamic module to disappear when the script exits.
This requires two extra steps:
Piping the dynamic module to Import-Module, which is the prerequisite for being able to unload it before exiting with Remove-Module
Calling Remove-Module with the dynamic module before exiting in order to unload it.
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Save the module in a script-level variable, and pipe it to Import-Module
# so that it can be removed before the script exits.
$script:dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
) | Import-Module -PassThru
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name use the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName #args
}
# Sample commands to perform in the script.
Ping-Subnet -?
Get-Command Ping-Subnet, Test-ConnectionAsync | Format-Table
# Before exiting, remove (unload) the dynamic module.
$dynMod | Remove-Module
[1] This assumes that the New-Module call itself is made outside of a module; if it is made inside a module, at least that module's commands see the auto-exported functions; if that module uses implicit exporting behavior (which is rare and not advisable), the auto-exported functions from the dynamic module would be included in that module's exports and therefore again become available session-globally.
[2] This magic has one limitation, which, however, will only rarely surface: [switch] parameters with a directly attached Boolean argument aren't supported (e.g., -CaseSensitive:$true) - see this answer.

Dot-sourcing a self-elevate script

I have a self elevate snippet which is quite wordy, so I decided instead of duplicating it at the top of every script that needs to be run as admin to move it into a separate .ps1:
function Switch-ToAdmin {
# Self-elevate the script if required
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$Cmd = #(
"-Command Set-Location `"$(Get-Location)`"; & `"$PSCommandPath`""
"-$($PSBoundParameters.Keys)"
)
$ProcArgs = #{
FilePath = 'PowerShell.exe'
Verb = 'RunAs'
ArgumentList = $Cmd
}
Start-Process #ProcArgs
Exit
}
}
}
So for every script that needs elevation I'd prepend
. "$PSScriptRoot\self-elevate.ps1"
Switch-ToAdmin
# rest of script
Doing above successfully procs the UAC prompt, but the rest of the script won't get executed.
Is this sorta stuff disallowed?
Darin and iRon have provided the crucial pointers:
Darin points out that the automatic $PSCommandPath variable variable in your Switch-ToAdmin function does not contain the full path of the script from which the function is called, but that of the script in which the function is defined, even if that script's definitions are loaded directly into the scope of your main script via ., the dot-sourcing operator.
The same applies analogously to the automatic $PSScriptRoot variable, which reflects the defining script's full directory path.
Also, more generally, the automatic $PSBoundParameters variable inside a function reflects that function's bound parameters, not its enclosing script's.
iRon points out that the Get-PSCallStack cmdlet can be used to get information about a script's callers, starting at index 1; the first object returned - index 0, when Get-PSCallStack output is captured in an array, represents the current command. Index 1 therefore refers to the immediate caller, which from the perspective of your dot-sourced script is your main script.
Therefore:
Replace $PSCommandPath with $MyInvocation.PSCommandPath, via the automatic $MyInvocation variable. $MyInvocation.PSCommandPath truly reflects the caller's full script path, irrespective of where the called function was defined.
Alternatively, use (Get-PSCallStack)[1].ScriptName, which despite what the property name suggests, returns the full path of the calling script too.
Replace $PSBoundParameters with (Get-PSCallStack)[1].InvocationInfo.BoundParameters
Note that there's also (Get-PSCallStack)[1].Arguments, but it seems to contain a single string only, containing a representation of all arguments that is only semi-structured and therefore doesn't allow robust reconstruction of the individual parameters.
As an aside:
Even if $PSBoundParameters contained the intended information, "-$($PSBoundParameters.Keys)" would only succeed in passing the bound parameters through if your script defines only one parameter, if that parameter is a [switch] parameter, and if it is actually passed in every invocation.
Passing arguments through robustly in this context is hard to do, and has inherent limitations - see this answer for a - complex - attempt to make it work as well as possible.

How to set PowerShell PSDefaultParameterValues conditionally based on other parameters

Without going much into details on why am I even trying this out, is it possible to set PSDefaultParameterValues conditionally based on other parameter values?
Let's say I would like to set -Force if ItemType is Directory in New-Item call.
$PSDefaultParameterValues = #{ "New-Item:Force" = {
# TODO: if Itemtype is Directory, return $true
# else return default: false
return $false
}
}
New-Item -ItemType Directory
Problem is, that I can get the parameters used in $args but I do not have access to their values.
As you've observed, the argument passed to your script block via the automatic $args variable contains the names of the bound parameters in the New-Item call at hand, but lacks their values.
This looks like an oversight, which GitHub proposal #16011 aims to correct.
The following workaround isn't foolproof, but may suffice in practice:
$PSDefaultParameterValues = #{
'New-Item:Force' = {
($false, $true)[
$args.BoundParameters.Contains('ItemType') -and
(Get-PSCallStack)[1].Position.Text -match '\bDirectory\b'
]
}
}
You could tweak the regex to be stricter, but note that PowerShell's elastic syntax and parameter aliases make it hard to match a parameter name reliably; e.g., -Type Directory, -it Directory and -ty Directory are all acceptable variations of -ItemType Directory.
A caveat is that this won't work if you pass the Directory argument to -ItemType in New-Item calls via a variable; e.g., $type='Directory'; New-Item -ItemType $type ... would not be recognized by the script block. Handling that case would require substantially more work.
Note:
The parent call-stack entry, which you can obtain as the 2nd element of the call-stack array returned by Get-PSCallStack, contains the raw command text of the New-Item call at hand (in property .Position.Text), which the solution above examines.
However, since it is the raw command text, it doesn't include the expanded argument values that are ultimately seen by the command; that is, what variable references and expression evaluate to isn't directly available.
You could perform your own expansion, assuming you've reliably identified the variable reference / subexpression of interest, but note that, at least in principle, evaluating a subexpression can have side effects (and possibly also take a long time to execute), so effectively executing it twice may be undesirable.

How do I copy a list of files and rename them in a PowerShell Loop

We are copying a long list of files from their different directories into a single location (same server). Once there, I need to rename them.
I was able to move the files until I found out that there are duplicates in the list of file names to move (and rename). It would not allow me to copy the file multiple times into the same destination.
Here is the list of file names after the move:
"10.csv",
"11.csv",
"12.csv",
"13.csv",
"14.csv",
"15.csv",
"16.csv",
"17.csv",
"18.csv",
"19.csv",
"20.csv",
"Invoices_Export(16) - Copy.csv" (this one's name should be "Zebra.csv")
I wrote a couple of foreach loops, but it is not working exactly correctly.
The script moves the files just fine. It is the rename that is not working the way I want. The first file does not rename; the other files rename. However, they leave the moved file in place too.
This script requires a csv that has 3 columns:
Path of the file, including the file name (eg. c:\temp\smefile.txt)
Destination of the file, including the file name (eg. c:\temp\smefile.txt)
New name of the file. Just the name and extention.
# Variables
$Path = (import-csv C:\temp\Test-CSV.csv).Path
$Dest = (import-csv C:\temp\Test-CSV.csv).Destination
$NN = (import-csv C:\temp\Test-CSV.csv).NewName
#Script
foreach ($D in $Dest) {
$i -eq 0
Foreach ($P in $Path) {
Copy-Item $P -destination C:\Temp\TestDestination -force
}
rename-item -path "$D" -newname $NN[$i] -force
$i += 1
}
There were no error per se, just not the outcome that I expected.
Welcome to Stack Overflow!
There are a couple ways to approach the duplicate names situation:
Check if the file exists already in the destination with Test-Path. If it does, start a while loop that appends a number to the end of the name and check if that exists. Increment the number you append after each check with Test-Path. Keep looping until Test-Path comes back $false and then break out of the loop.
Write an error message and skip that row in the CSV.
I'm going to show a refactored version of your script with approach #2 above:
$csv = Import-Csv 'C:\temp\Test-CSV.csv'
foreach ($row in $csv)
{
$fullDestinationPath = Join-Path -Path $row.Destination -ChildPath $row.NewName
if (Test-Path $fullDestinationPath)
{
Write-Error ("The path '$fullDestinationPath' already exists. " +
"Skipping row for $($row.Path).")
continue
}
# You may also want to check if $row.Path exists before attempting to copy it
Copy-Item -Path $row.Path -Destination $fullDestinationPath
}
Now that your question is answered, here are some thoughts for improving your code:
Avoid using acronyms and abbreviations in identifiers (variable names, function names, etc.) when possible. Remember that code is written for humans and someone else has to be able to understand your code; make everything as obvious as possible. Someone else will have to read your code eventually, even if it's Future-You™!
Don't Repeat Yourself (called the "DRY" principle). As Lee_daily mentioned in the comments, you don't need to import the CSV file three times. Import it once into a variable and then use the variable to access the properties.
Try to be consistent. PowerShell is case-insensitive, but you should pick a style and stick to it (i.e. ForEach or foreach, Rename-Item or rename-item, etc.). I would recommend PascalCase as PowerShell cmdlets are all in PascalCase.
Wrap literal paths in single quotes (or double quotes if you need string interpolation). Paths can have spaces in them and without quotes, PowerShell interprets a space as you are passing another argument.
$i -eq 0 is not an assignment statement, it is a boolean expression. When you run $i -eq 0, PowerShell will return $true or $false because you are asking it if the value stored in $i is 0. To assign the value 0 to $i, you need to write it like this: $i = 0.
There's nothing wrong with $i += 1, but it could be shortened to $i++, if you want to.
When you can, try to check for common issues that may come up with your code. Always think about what can go wrong. "If I copy a file, what can go wrong? Does the source file or folder exist? Is the name pulled from the CSV a valid path name or does it contain characters that are invalid in a path (like :)?" This is called defensive programming and it will save you so so many headaches. As with anything in life, be careful not to go overboard. Only check for likely scenarios; rare edge-cases should just raise errors.
Write some decent logs so you can see what happened at runtime. PowerShell provides a pair of great cmdlets called Start-Transcript and Stop-Transcript. These cmdlets log all the output that was sent to the PowerShell console window, in addition to some system information like the version of PowerShell installed on the machine. Very handy!

Why does PowerShell code behave different when called from a function?

Experienced C# developer learning PowerShell here so I am sure I am just missing something silly. What I am trying to do is write a function that simply writes it's input to a temporary file in JSON format. I have code that works fine if I run it 'inline', but that same code writes an empty file when called in a function.
Here is the code:
function Dump-File {
param (
[Parameter(Mandatory=$true)]
$Input
)
$tmp = New-TemporaryFile
$Input | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "Dump file written: $($tmp.FullName)"
}
$args = #{}
$args.Add('a', 1)
$args.Add('b', 2)
$args.Add('c', 3)
$args.Add('d', 4)
# results in json written to temp file
$tmp = New-TemporaryFile
$args | ConvertTo-Json | Out-File $tmp.FullName
Write-Output "args dumped: $($tmp.FullName)"
# results in empty temp file
Dump-File $args
Can anyone help me understand why the code called inline works but the same code does not work when I wrap it up as a function?
$Input is an automatic variable.
Changing the name of your Dump-File parameter to $somethingelse will resolve your problem. Never use $input as a parameter or variable name.
Automatic variables are to be considered read-only.
About Automatic Variables
SHORT DESCRIPTION
Describes variables that store state information for
PowerShell. These variables are created and maintained by PowerShell.
LONG DESCRIPTION
Conceptually, these variables are considered to be
read-only. Even though they can be written to, for backward
compatibility they should not be written to.
Here is a list of the automatic variables in PowerShell:
...
$INPUT
Contains an enumerator that enumerates all input that is passed to a function. The $input variable is available only to functions and script blocks (which are unnamed functions). In the Process block of a function, the $input variable enumerates the object that is currently in the pipeline. When the Process block completes, there are no objects left in the pipeline, so the $input variable enumerates an empty collection. If the function does not have a Process block, then in the End block, the $input variable enumerates the collection of all input to the function.
Source: About_Automatic_Variables
This information is also avaible through Get-help command
Get-Help about_Automatic_Variables