Get arguments passed to powershell.exe - powershell

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

Related

In Powershell, How to print only the list of parameters when no parameters are provided?

In Powershell, How to print only the command synax when no parameters are provided? Example:
PS> .\sign.ps1
sign.ps1 [-Trace] [-New] [-All] [[-File] <string[]>] [<CommonParameters>]
Instead I get the complete Help page for the command... Here's what I'm doing.
# sign.ps1
<#
.SYNOPSIS
Script for Signing Powershell Scripts
.PARAMETER New
Create a new code signing certificate for signing powershell scripts.
(requires Powershell to be run in admin mode in order to make the certificate)
.PARAMETER File
List of Powershell scripts to sign
.PARAMETER All
Sign all ps1 files in the current directory
.EXAMPLE
# Creates a new certificate stored in current direcctory as sign.pfx
PS> sign -New
# Sign all ps1 files in current directory
PS> sign -File *.ps1
#>
param(
[switch]$Trace,
[switch]$New,
[switch]$All,
[string[]]$File
)
if ($PSBoundParameters.Count -eq 0) {
Get-Help $MyInvocation.MyCommand.Path
exit 1
}
To offer an alternative to Jeff Zeitlin's effective solution:
Get-Command -Syntax is designed to report a command's syntax diagram, and it derives it directly from the target command's definition, not from its help. Therefore, it works whether or not the target command has (comment-based) help defined for it.
It is also useful in interactive use (e.g., Get-Command -Syntax Get-ChildItem, or, using aliases and elastic syntax, gcm -sy gci).
if ($PSBoundParameters.Count -eq 0) {
Get-Command -Syntax $PSCommandPath
exit 1
}
Note the use of the automatic $PSCommandPath variable as a simpler way to refer to the running script's full path.
You can apply the technique to a function too:
if ($PSBoundParameters.Count -eq 0) {
$MyInvocation.MyCommand | Get-Command -Syntax
return
}
However, this isn't fully robust, because - unfortunately - $MyInvocation.MyCommand, which is an unambiguous System.Management.Automation.FunctionInfo instance, is stringified in the process, which means that Get-Command sees the only the function name, and not also its module context, so at least hypothetically Get-Command could act on a different function with the same name, from a different module / from the non-module scope domain.
This problematic Get-Command behavior - the inability to recognize System.Management.Automation.CommandInfo instances (as well as of derived classes such as FunctionInfo) as such in its pipeline input - is the subject of GitHub issue #11017.
If this were to get fixed, $MyInvocation.MyCommand | Get-Command -Syntax would work robustly in both scripts and functions.
The output of the Get-Help command is an object which has several member properties. One of those properties is "syntax". Thus...
if ($PSBoundParameters.Count -eq 0) {
(Get-Help $MyInvocation.MyCommand.Path).syntax
exit 1
}
...should do the trick.
(a little experimentation shows that this probably won't work for "built-in" commands, only for script cmdlets/advanced functions)

How to pass an array to the arguments in Start-Process in Powershell?

I am writing a script to play certain files in a player. I use Get-ChildItem to get an array of file names. Then I want to use Start-Process to play these files. However, how can I add these file names to the arguments of the player program?
I used Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $selected_items but it seems it doesn't work and the files are not played.
Notice there are spaces in the file names.
Syntax-wise, your approach should work, but doesn't, due to an unfortunate bug, still present in PowerShell 7.2 - see GitHub issue #5576.
While passing an array of arguments to Start-Process's -ArgumentList parameter does cause the array elements to be passed as individual arguments (which is usually how external CLIs expect multiple file arguments), the necessary double-quoting around elements with spaces is not applied when the command line ultimately used for invocation is constructed behind the scenes.
Also, for robustness you should use the .FullName property of the objects stored in $selected_items, so as to ensure that full paths are passed, because - in Windows PowerShell, situationally - Get-ChildItem's output objects may stringify to the file name only - see this answer.
Workaround: Pass a single argument to -ArgumentList, in which you encode all pass-through arguments, using embedded double-quoting.
Start-Process `
-FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" `
-ArgumentList ($selected_items.ForEach({ '"{0}"' -f $_.FullName }) -join ' ')
Taking a step back:
If PotPlayerMini64.exe is a Windows GUI(-subsystem) application, you don't need Start-Process at all, because even direct invocation will then act asynchronously (i.e., the program will launch, and control will return to PowerShell right away; conversely, if you wanted to wait for the program to exit, use Start-Process -Wait).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" $selected_Items.FullName
Note that in direct invocations such as this, PowerShell does perform the necessary double-quoting behind the scenes, on demand.
Note: I'm unclear on whether passing multiple file paths to PotPlayerMini64.exe alone also starts playback - the alternative solution in the next section may ensure that.
Alternative, clipboard-based solution:
Judging by PotPlayerMini64.exe's available command-line options[1], the following may work (I cannot personally verify):
/clipboard :Appends content(s) from clipboard into playlist and starts playback immediately.
# Copy the full names of the files of interest to the clipboard.
Set-Clipboard -Value $selected_items.FullName
# Launch the player and tell it to start playback of the files on the clipboard.
# Parameters -FilePath and -ArgumentList are positionally implied.
Start-Process 'C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe' /clipboard
There are file-arguments-related options such as /new, /insert, and /add, but it's unclear to me whether they - or omitting them altogether, as in your attempt - automatically start playback (may depend on the application's persistent configuration).
[1] Note that this is not the official documentation; I couldn't find the latter.
You can ForEach-Object:
Get-ChildItem . | ForEach-Object {Start-Process -FilePath "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe" -ArgumentList $_.FullName}
You don't need start-process (plus, -argumentlist doesn't handle filenames with spaces as easily).
& "C:\Program Files\DAUM\PotPlayer\PotPlayerMini64.exe"
C:\Program` Files\DAUM\PotPlayer\PotPlayerMini64.exe
$env:path += ';C:\Program Files\DAUM\PotPlayer'; PotPlayerMini64
Even using start-process, it varies with the program. For example, Emacs can take multiple file arguments, separated by spaces. If the filename has a space, it would need to be double quoted. External programs don't know what arrays are, and start-process converts them to one string with spaces in between each element.
start emacs file1,file2,'"my file"'
get-wmiobject win32_process | ? name -eq emacs.exe | % commandline
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"
ps emacs | % commandline # ps 7
"c:\program files\emacs\bin\emacs.exe" file1 file2 "my file"

Make Start-Process respect Verbose preference of parent script

Here are my scripts
Parent.ps1
[CmdletBinding(SupportsShouldProcess=$true)]
Param()
Write-Verbose 'Triggering Child Process...'
Start-Process PowerShell.exe '.\Child.ps1'
Child.ps1
[CmdletBinding(SupportsShouldProcess=$true)]
Param()
Write-Verbose 'Child Process Triggered.' # I want output from this line to be displayed
Write-Output 'Child Process Triggered.'
Start-Sleep 10
I'm calling the parent script as below
powershell Parent.ps1 -Verbose
Actual outpt:
VERBOSE: Triggering Child Process...
Child Process Triggered.
Desired Output:
VERBOSE: Triggering Child Process...
VERBOSE: Child Process Triggered.
Child Process Triggered.
If you really want to run .\Child.ps1 via another PowerShell instance, in a new window, asynchronously:
Start-Process PowerShell.exe "-c .\Child.ps1 -Verbose:`$$($VerbosePreference -eq 'Continue')"
Note the use of -c (-Command) to signal what PowerShell CLI parameter the command string is passed to, to distinguish it from -f (-File). While not strictly necessary, because -c is the default in Windows PowerShell (powershell.exe), it helps to clarify, especially given that PowerShell (Core) 7+ (pwsh) now defaults to -f.
When you invoke your Parent.ps1 script with -Verbose (-vb), PowerShell translates this switch to a script-scoped $VerbosePreference variable with value Continue.
To propagate a switch value - on or off - programmatically, you can follow the switch name with : and a Boolean, e.g. -Verbose:$true.
Caveat: While something like -Verbose:$false is typically the same as not passing the switch at all, there are exceptions, and this is one of them: -Verbose:$false explicitly overrides a caller's $VerbosePreference preference variable to deactivate verbose output - see this answer.
That said, this isn't a concern in your case, given that you're launching a new PowerShell instance, and there's no session-internal caller.
The above uses an expandable string to translate the value of $VerbosePreference into the appropriate Boolean; note that the subexpression ($(...)) is prefixed with `$, i.e. an escaped $ character to be retained verbatim, because stringifying a Boolean results in either True or False, so the $ prefix is needed to turn it back into a Boolean the way it needs to be represented as a literal in source code.
Note that if you were to invoke .\Child.ps1 directly from your parent script, it would automatically "inherit" the parent's $VerbosePreference value (it would see the same value by default, due to PowerShell's dynamic scoping).
A note on -c (-Command) vs. -f (-File) and PowerShell (Core) 7+:
For invoking a PowerShell script file (.ps1) via the CLI, it is generally sufficient and preferable for robust passing of verbatim arguments to use the -f (-File) parameter; -c (-Command) is only needed if you need the command string to be evaluated as PowerShell code.
In Windows PowerShell (powershell.exe), the -f parameter doesn't recognize Boolean argument values, unfortunately, which is why -c is used in the solution above. This limitation has been fixed in PowerShell (Core) 7+ (pwsh.exe).
See also:
Guidance on when to use -c (-Command) vs. -f (-File)
An overview of PowerShell's CLI; covers both editions.

Executing powershell command under cmd and INFO: Could not find files for the given pattern(s)

I am trying to execute the following powershell commands from CMD, for example:
powershell Get-WmiObject Win32_PnPSignedDriver
powershell Get-WmiObject Win32_PnPSignedDriver > test.txt
which both work correctly.
But when I do a query, for example:
powershell (Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
I am getting this message the cause of which I can not pin down:
INFO: Could not find files for the given pattern(s)
This seems to work for me (I was seeing the same error as you originally):
powershell -command "(Get-WmiObject Win32_PnPSignedDriver | where {$_.location -like '*PCI bus 0, device 22, function 0*'}).DeviceName"
To complement the existing answers with general guidelines for passing commands to powershell.exe from cmd.exe (a Command Prompt):
Enclose the entire PowerShell command in "..." (double quotes).
This protects its contents from unwanted up-front interpretation by cmd.exe - as happened with | in this case, as explained in aschipfl's answer.
However, cmd.exe-style environment-variable references (e.g., %USERNAME%) are still expanded.
For quoting that is embedded in the PowerShell command:
Use '...' (single quotes) where feasible; this is what Mark Wragg's helpful answer does, but it is only an option if the quoted string is meant to be a literal (doesn't contain variable references or subexpressions).
If you do need embedded "...", escape it as \"...\"
Note that while inside PowerShell it is the ` (backtick) that serves as the escape character, when passing strings from the outside PowerShell requires \.
Simplified examples (run from cmd.exe):
Note that passing a command without a parameter name implies the -Command parameter; run powershell -? to see the command-line syntax.
You may also want to use -NoProfile so that the user profile isn't loaded every time.
Commands that don't need embedded quoting - simply double-quote:
powershell -noprofile "get-date"
powershell -noprofile "(get-date).Date"
powershell -noprofile "get-date | get-member"
Commands with embedded quoting - use '...' or \"...\":
powershell -noprofile "(get-date).Year -like '*17'"
powershell -noprofile "$v=17; (get-date).Year -like \"*$v\""
Commands that incorporate the value of a cmd.exe environment variable:
:: # Nothing special to do: cmd.exe expands the reference irrespective of quoting.
set "v=17"
powershell -noprofile "(get-date).Year -like '*%v%'"
:: # More robust variant that passes the value as an argument to a script block.
set "v=17"
powershell -noprofile ". { param($v) (get-date).Year -like \"*$v\" }" "%v%"
Optional reading: calling powershell from POSIX-like shells such as bash:
The above rules apply analogously, except that you'll typically use '...' (single quotes) to enclose the entire PowerShell command, which categorically prevents up-front interpretation by the shell.
Using "..." is an option if up-front expansions of shell-variable references and command substitutions are explicitly desired, but the potential for confusion is great, because both POSIX-like shells and PowerShell use sigil $ to refer to variables - it may not be obvious what is expanded when.
POSIX-like shells categorically do not support embedding ' instances in side '...' strings, which necessitates a somewhat awkward workaround (see below).
Simplified examples (run from a POSIX-like shell such as bash):
Commands that don't need embedded quoting - simply single-quote:
powershell -noprofile 'get-date'
powershell -noprofile '(get-date).Date'
powershell -noprofile 'get-date | get-member'
Commands with embedded quoting - replace embedded ' instances with '\'' (sic) and use embedded " as-is:
powershell -noprofile '(get-date).Year -like '\''*17'\'''
powershell -noprofile '$v=17; (get-date).Year -like "*$v"'
Commands that incorporate the value of a shell/environment variable:
# By using a double-quoted string, the shell expands $v up front.
v=17
powershell -noprofile "(get-date).Year -like '*$v'"
# It gets trickier if you want to reference PS variables too.
# Note how the PS variable's $ is \-escaped so that the shell doesn't interpret it.
v=$HOME
powershell -noprofile "\$HOME -eq '$v'"
# More robust variant that passes the value as an argument to a script block.
v=17
powershell -noprofile '. { param($v) (get-date).Year -like "*$v" }' "$v"
I am pretty sure that the pipe character | is the problem here, because cmd tries to process it.
Simply escape it by preceding with ^:
powershell (Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}).DeviceName
If this code is used within a parenthesised block of code in cmd, you may need to escape the closing ) as well (the opening ( can be escaped too, but there is no need):
powershell ^(Get-WmiObject Win32_PnPSignedDriver ^| where {$_.location -like "*PCI bus 0, device 22, function 0*"}^).DeviceName

Run Powershell script that uses -List (parameter) with alternate credentials that

I was struggling to get this simple (?) function working today.
I have a PowerShell script that reads computer names from a txt-based file. It works fine when run from a PowerShell session by the following one-liner:
./"Server Health Check.ps1" -List One-off.txt
As you can see, it's got a long file name, so it's wrapped with quotes.
However, I'm building a PowerShell GUI form with radio boxes that will pass on a choice for a text file that will be used to a call the script. Trick is, the script needs to be run with alternate admin account, and i'm not clear how to make that work.
For another script I've got that doesn't use I know i can use something along the lines of the following, this uses the old DOS "runas", however, it doesn't work with the -list function.
invoke-command -scriptblock {runas.exe /user:domain\$Env:Username"admin" "powershell.exe -file \"\\Server\c$\LONG FOLDER\Server Health Check.PS1""}
So, in a nutshell, how do get a script to launch with alternate credentials that reads a parameter (-List) from the command line? I'm also keen to preserve my directory structure, which includes folders with spaces. The script is titled: "Server health check.ps1"
The last thing I tried was the following
$ScriptPath = "C:\SCRIPTS FOLDER\Server Health Check.ps1"
$ArgList = "-List C:\SCRIPTS FOLDER\One-off.txt"
Invoke-Command -filepath $ScriptPath -Credential DragonBallDomain\$Env:UserName"Admin" -ArgumentList $ArgList
The result was the following message:
Invoke-Command : Parameter set cannot be resolved using the specified named parameters.
I'm almost certain this is do-able by invoke-command or start-process, it's just a matter of getting the correct formatting? I'm probably missing a / or a ' or "" somewhere in my trials with start-process or invoke-command.
Any help appreciated!
Update for April 30:
I've tried some more to make this work, i'm close, but still not quite there.
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = \\UNC\PATH TO FOLDER\WITH LONG NAME\
start-process -filepath powershell.exe -argumentlist " -file``"$($FilePath.path)`"" -cred DOMAIN\USERID -WorkingDirectory "$LongFolderPath"
Adding the -credential is what causes an error that states that the -file parameter is invalid. I'm sure there's a way to do this.
Note: Completely rewritten after the requirements became clearer.
To run a command as a different user locally, use Start-Process -Credential ...
That is what you've attempted in your update in principle, but there are problems with how you're passing parameters; try this instead:
$LongScriptPath = resolve-path Script.ps1
$LongFolderPath = '\\UNC\PATH TO FOLDER\WITH LONG NAME\'
start-process `
powershell.exe `
-ArgumentList '-file', $LongScriptPath, '-List', 'One-off.txt' `
-Credential DOMAIN\USERID `
-WorkingDirectory $LongFolderPath
The key to making this work is to pass all parameters to pass to powershell.exe as an array via Start-Process's -ArgumentList parameter, which means that the parameters must be ,-separated.
Note how an array is always parsed in expression mode, which means that literal string elements such as -file and -List must be quoted.
It is important in general to understand the difference between PowerShell's two fundamental parsing modes, argument mode and expression mode, and which is applied when - see https://technet.microsoft.com/en-us/library/hh847892.aspx
Add -Wait to wait for the script to finish; Start-Process is asynchronous by default (all PS cmdlets named Start-* are).
Caveat: For commands invoked as a different user, you can only wait from an elevated prompt.
If it isn't, the command will still execute, but will do so asynchronously, and you'll get an Access denied error message in the current console; in effect, -Wait is ignored.
Only if not running as a different user: Add -NoNewWindow -Wait if you want to run the script in the current console window; Start-Process opens a new window by default for console applications such as powershell.exe and cmd.exe.
If you do run the command as a different user, -NoNewWindow is quietly ignored.
As for the original symptom and why using Invoke-Command to run a command locally as a different user is ill-advised:
Invoke-Command -Credential ... requires that the -ComputerName parameter be specified too.
Run Get-Help Invoke-Command to see all parameter sets that involve the -Credential parameter. The OP's original command had only -Credential, but not -ComputerName, which caused PS to complain that no parameter set could be unambiguously identified.
Once you use -ComputerName, PowerShell remoting is invariably used, even if you specify . - the local computer - as the only computer to target.
Using remoting has two implications:
Remoting is not available by default, and must be configured on the target computer (the local computer, in this case).
Using remoting requires invocation with admin privileges.
In short:
While you can perform purely local invocations with Invoke-Command, you cannot do so as another user, because that invariably involves remoting.
Start-Process, by contrast, solely exists to run commands locally, optionally as a different user.