How to require argument be supplied when script is executed - powershell

So this was my script:
$ans=Read-Host "What process would you like to query?"
Get-WmiObject win32-process -Filter "name='$ans'" | Format-Table HandleCount,VirtualSize,UserModeTime,KernelModeTime,ProcessID,Name
Now I need to create a script which requires the argument be passed when the script is executed. I'm a little confused on how to do this successfully. This is what I'm trying to work with:
#!/bin/bash
echo $1
Get-WmiObject win32_process -Filter "name='$1'" | Format-Table HandleCount,VirtualSize,UserModeTime,ProcessID,Name

To make a parameter mandatory (required) in PowerShell, you must use an advanced script or function; to create a script file, save your code in a .ps1 file[1].
param(
[Parameter(Mandatory)]
[string] $Name
)
Get-CimInstance win32_process -Filter "name='$Name'"
Note:
The code uses Get-CimInstance instead of Get-WmiObject, because the CIM cmdlets superseded the WMI cmdlets in PowerShell v3 (released in September 2012). Therefore, the WMI cmdlets should be avoided, not least because PowerShell [Core] (version 6 and above), where all future effort will go, doesn't even have them anymore. For more information, see this answer.
The Format-Table call was intentionally omitted, because Format-* cmdlets should only ever be used to format data for display, never for subsequent programmatic processing, i.e. never for outputting data - see this answer.
Outputting just data means that PowerShell controls in what format your data is displayed; for information on how to control this format, see this answer.
[1] This is enough to make a plain-text file executable from PowerShell, without needing to include the .ps1 extension in the invocation. On Unix-like platforms, you can create an executable shell script without a filename extension via a shebang line such as #!/usr/bin/env pwsh and chmod a+x some that can be called from outside PowerShell as well.

Related

Invoke-command and running ps1 with parameters

I'm trying to run a script using invoke-command to install defender for endpoint with some associated parameters.
If I run a standard ps1 using invoke-command it works with no issues. However, if I run the following:
Invoke-Command -ComputerName NAME -FilePath \\srv\share\install.ps1 -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
I receive "A parameter cannot be found that matches parameter name 'OnboardingScript'". Can someone please help me understand how I invoke a command and run a script with parameters?
Parameters already defined in the install.Ps1 file
https://github.com/microsoft/mdefordownlevelserver/blob/main/Install.ps1
Many thanks in advance
Your Invoke-Command call has a syntax problem, as Santiago Squarzon points out:
Any pass-through arguments - those to be seen by the script whose path is passed to -FilePath - must be specified via the -ArgumentList (-Args) parameter, as an array.
# Simplified example with - of necessity - *positional* arguments only.
# See below.
Invoke-Command -ComputerName NAME -FilePath .\foo.ps1 -Args 'bar', 'another arg'
The same applies to the more common invocation form that uses a script block ({ ... }), via the (potentially positionally implied) -ScriptBlock parameter.
However, there's a catch: Only positional arguments can be passed that way, which:
(a) requires that the target script support positional argument binding for all arguments of interest...
(b) ... which notably precludes passing switch parameters (type [switch]), such as -Passive in your call.
(c) requires you to pass the invariably positional arguments in the correct order.
Workaround:
Use a -ScriptBlock-based invocation, which allows for regular argument-passing with the usual support for named arguments (including switches):
If, as in your case, the script file is accessible by a UNC path visible to the remote session as well, you can simply call it from inside the remote script block.
Note: It isn't needed in your case, but you generally may need $using: references in order to incorporate values from the local session into the arguments - see further below for an example.
Invoke-Command -ComputerName NAME {
& \\srv\share\install.ps1 -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
}
Otherwise (typically, a script file local to the caller):
Use a $using: reference to pass the content (source code) of your script file to the remote session, parse it into a script block there, and execute that script block with the arguments of interest :
$scriptContent = Get-Content -Raw \\srv\share\install.ps1
Invoke-Command -ComputerName NAME {
& ([scriptblock]::Create($using:scriptContent)) -OnboardingScript \\srv\share\WindowsDefenderATPonboardingscript.cmd -Passive
}
Small caveat: Since the original script file's source code is executed in memory in the remote session, file-related reflection information won't be available, such as the automatic variables that report a script file's full path and directory path ($PSCommandPath and $PSScriptRoot).
That said, the same applies to use of the -FilePath parameter, which essentially uses the same technique of copying the source code rather than a file to the remote session, behind the scenes.
thanks for your reply. I have managed to get this working by adding -ScriptBlock {. "\srv\share etc}

Get arguments passed to powershell.exe

Is there a way to determine, in a Profile script, what arguments were passed to the powershell executable?
Use-case
I'd like to check whether the WorkingDirectory parameter was set, before overriding it with my own cd in my user profile.
Attempts
I've made a few helpless attempts to get variable values from within the profile script, with no luck. None of them seem to give me any information about whether pwsh.exe was invoked with a -wd parameter or not:
echo $PSBoundParameters
echo $ArgumentList
echo (Get-Variable MyInvocation -Scope 0).Value;
To inspect PowerShell's own invocation command line, you can use:
[Environment]::CommandLine (single string)
or [Environment]::GetCommandLineArgs() (array of arguments, including the executable as the first argument).
These techniques also work on Unix-like platforms.
Caveat: As of PowerShell Core 7 (.NET Core 3.1), it is pwsh.dll, not pwsh[.exe] that is reported as the executable.
To check in your $PROFILE file if a working directory was specified on startup could look like this, though do note that the solution is not foolproof:
$workingDirSpecified =
($PSVersionTable.PSEdition -ne 'Desktop' -and
[Environment]::GetCommandLineArgs() -match '^-(WorkingDirectory|wd|wo|wor|work|worki|workin|working|workingd|workingdi|workingdir|workingdire|workingdirec|workingdirect|workingdirecto|workingdirector)') -or
[Environment]::CommandLine -match
'\b(Set-Location|sl|cd|chdir|Push-Location|pushd|pul)\b'
In PowerShell Core, a working directory may have been specified with the -WorkingDirectory / -wd parameter (which isn't supported in Windows PowerShell); e.g.,
pwsh -WorkingDirectory /
Note: Given that it is sufficient to specify only a prefix of a parameter's name, as long as that prefix uniquely identifies the parameter, it is necessary to also test for wo, wor, work, ...
In both PowerShell Core and Windows PowerShell, the working directory may have been set with a cmdlet call (possibly via a built-in alias) as part of a -c / -Command argument (e.g.,
pwsh -NoExit -c "Set-Location /")
Note: In this scenario, unlike with -WorkingDirectory, the working directory has not yet been changed at the time the $PROFILE file is loaded.
It is possible, but unlikely for the above to yield false positives; to use a contrived example:
pwsh -NoExit -c "'Set-Location inside a string literal'"
How about (powershell.exe or pwsh.exe?):
get-ciminstance win32_process | where name -match 'powershell.exe|pwsh.exe' |
select name,commandline

What is shortest possible way to download script from HTTP and run it with parameters using Powershell?

I have a PowerShell script file stored in an internal artifact server. The script URL is http://company-server/bootstrap.ps1.
What is a concise way to download that script and execute with a custom parameter?
I want to send such a command to users, who will copy-paste it over and over, so it must be a single-line and should be short.
What I currently have works, but it is long and unwieldy:
$c=((New-Object System.Net.WebClient).DownloadString('http://company-server/bootstrap.ps1'));Invoke-Command -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList 'RunJob'
I am wondering if there is shorter way to do this.
Note: From a code golf perspective, the solutions below could be shortened further, by eliminating insignificant whitespace; e.g., &{$args[0]}hi instead of & { $args[0] } hi. However, in the interest of readability such whitespace was kept.
A short formulation of a command that downloads a script via HTTP and executes it locally, optionally with arguments is probably this, taking advantage of:
alias irm for Invoke-RestMethod, in lieu of (New-Object System.Net.WebClient).DownloadString()
omitting quoting where it isn't necessary
relying on positional parameter binding
& ([scriptblock]::Create((irm http://company-server/bootstrap.ps1))) RunJob
RunJob is the OP's custom argument to pass to the script.
An even shorter, but perhaps more obscure approach is to use iex, the built-in alias for Invoke-Expression, courtesy of this GitHub comment.
iex "& { $(irm http://company-server/bootstrap.ps1) } RunJob"
As an aside: in general use, Invoke-Expression should be avoided.
The command uses an expandable string ("...", string interpolation) to create a string with the remote script's content enclosed in a script block { ... }, which is then invoked in a child scope (&). Note how the arguments to pass to the script must be inside "...".
However, there is a general caveat (which doesn't seem to be a problem for you): if the script terminates with exit, the calling PowerShell instance is exited too.
There are two workarounds:
Run the script in a child process:
powershell { iex "& { $(irm http://company-server/bootstrap.ps1) } RunJob" }
Caveats:
The above only works from within PowerShell; from outside of PowerShell, you must use powershell -c "..." instead of powershell { ... }, but note that properly escaping embedded double quotes, if needed (for a URL with PS metacharacters and/or custom arguments with, say, spaces), can get tricky.
If the script is designed to modify the caller's environment, the modifications will be lost due to running in a child process.
Save the script to a temporary file first:
Note: The command is spread across multiple lines for readability, but it also works as a one-liner:
& {
$f = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName() + '.ps1');
irm http://company-server/bootstrap.ps1 > $f;
& $f RunJob;
ri $f
}
The obvious down-side is that the command is much longer.
Note that the command is written with robustness and cross-platform compatibility in mind, so that it also works in PowerShell Core, on all supported platforms.
Depending on what platforms you need to support / what assumptions you're willing to make (e.g., that the current dir. is writeable), the command can be shortened.
Potential future enhancements
GitHub issue #5909, written as of PowerShell Core 6.2.0-preview.4 and revised as of PowerShell Core 7.0, proposes enhancing the Invoke-Command (icm) cmdlet to greatly simplify download-script-and-execute scenarios, so that you could invoke the script in question as follows:
# WISHFUL THINKING as of PowerShell Core 7.0
# iwr is the built-in alias for Invoke-WebRequest
# icm is the built-in alias for Invoke-Command.
iwr http://company-server/bootstrap.ps1 | icm -Args RunJob
GitHub issue #8835 goes even further, suggesting an RFC be created to introduce a new PowerShell provider that allows URLs to be used in places where only files were previously accepted, enabling calls such as:
# WISHFUL THINKING as of PowerShell Core 7.0
& http://company-server/bootstrap.ps1 RunJob
However, while these options are very convenient, there are security implications to consider.
Here is a shorter solution (158 chars.)
$C=(New-Object System.Net.WebClient).DownloadString("http://company-server/bootstrap.ps1");icm -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 121
$C=(curl http://company-server/bootstrap.ps1).content;icm -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 108
$C=(curl http://company-server/bootstrap.ps1).content;icm ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 98
$C=(iwr http://company-server/bootstrap.ps1).content;icm -sc([Scriptblock]::Create($c)) -ar RunJob
Thanks to Ansgar Wiechers

Determine if PowerShell script has been dot-sourced

From a PowerShell script, how can I determine if the script has been dot-sourced, i.e. it has been called with
. .\myscript.ps1
rather than
.\myscript.ps1
NOTE an interesting blog post (also) about this: http://poshoholic.com/2008/03/18/powershell-deep-dive-using-myinvocation-and-invoke-expression-to-support-dot-sourcing-and-direct-invocation-in-shared-powershell-scripts/
To complement mjolinor's helpful answer:
tl;dr
$isDotSourced = $MyInvocation.InvocationName -eq '.' -or $MyInvocation.Line -eq ''
While $MyInvocation.InvocationName -eq '.' mostly tells you whether a given script is being dot-sourced, there is one exception:
When you run a script from the - obsolescent[1] - Windows PowerShell ISE with Debug > Run/Continue (F5), it is implicitly sourced, yet $MyInvocation.InvocationName contains the full script filename rather than . However, you can detect this case by checking if $MyInvocation.Line is empty.
(The PIC (PowerShell Integrated Console) that comes with Visual Studio Code's PowerShell extension used to behave this way, but as of at least version v2023.1.0 submits explicit dot-sourcing commands).
Note: Detecting whether a function is being dot-sourced is not subject to the exception above, so testing for $MyInvocation.InvocationName -eq '.' is sufficient (but the above will work too).
[1] The PowerShell ISE is no longer actively developed and there are reasons not to use it (bottom section), notably not being able to run PowerShell (Core) 7+. The actively developed, cross-platform editor that offers the best PowerShell development experience is Visual Studio Code with its PowerShell extension.
Check $myinvocation.line
It will show the line that was used to call the script.
PS C:\scripts\test> gc test.ps1
$myinvocation.line
PS C:\scripts\test> ./test.ps1
./test.ps1
PS C:\scripts\test> . ./test.ps1
. ./test.ps1
You can also check the .invocationname property. If the script was dot-sourced, it will just be a dot. If not, is will be ./scriptname.ps1

Best practices for writing PowerShell scripts for local and remote usage

What are some of the best practices for writing scripts that will execute in a remote context?
For instance, I just discovered that built-in var $Profile doesn't exist during remote execution.
Profile
You've discovered one main difference, $profile not being configured.
Buried in MSDN here are some FAQs about remote powershell, or do get-help about_Remote_FAQ.
Under the "WHERE ARE MY PROFILES?" (heh) it explains:
For example, the following command runs the CurrentUserCurrentHost profile
from the local computer in the session in $s.
invoke-command -session $s -filepath $profile
The following command runs the CurrentUserCurrentHost profile from
the remote computer in the session in $s. Because the $profile variable
is not populated, the command uses the explicit path to the profile.
invoke-command -session $s {. "$home\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1"}
Serialization
Another difference that may affect you is that instead of the .NET objects returned by commands being just directly returned, when you run them remotely and return them, they get serialized and deserialized over the wire. Many objects support this fine, but some do not. Powershell automatically removes methods on objects that are no longer "hooked up", and they're basically data structures then... but it does re-hook methods on some types like DirectoryInfo.
Usually you do not have to worry about this, but if you're returning complex objects over a pipe, you might...
Script blocks don't act as closures, like they do normally:
$var = 5
$sb={ $var }
&$sb # 5
Start-Job $sb | Wait-Job | Receive-Job # nothing