PowerShell Param string always in single quote - powershell

I have a script with mandatory parameters which we use to install some SQL components including user name and passwords like below:
param(
[Parameter(Mandatory=$True,HelpMessage="SQL Server password")]
[ValidateNotNullOrEmpty()]
[string] $SqlServerPassword
)
So when a user runs this script, he/she will need to include the -SqlServerPassword 'SpecialCharacters' variable string. I know best practice is to place the string inside a single quote, but its been a hard path training some of our installation managers and it messes up because our password vault includes special characters which without single quotes causes issues.
How can I re-write the above to ensure that even if the user passes the password without it being in single quotes, that it will be in single quotes? Thanks!

What you're asking for cannot be done, if the string is to be passed as an argument, because that would require deactivating the command-line parser - the very mechanism that recognizes individual arguments, evaluates variable references and subexpressions contained in them, and binds them to parameters.
With a limited set of special characters you could ignore the value bound by the parser and manually parse $MyInvocation.Line, the raw command line, but not only is that ill-advised, it would break with characters such as | and ;
However, you can achieve what you want via an interactive prompt.
While you also get such a prompt with your code if the user happens not to pass a -SqlServerPassword argument on the command line, it doesn't prevent potentially incorrect command-line use.
(Also, this automatic prompting is not user-friendly and has quirks, such as not being able to start a value with !).
If feasible, you could require users to enter the value interactively (and not also allow passing a value as an argument) using Read-Host, in which case quoting need not - and must not - be used:
param(
# Do NOT define $SqlServerPassword as a parameter (but define others)
)
# Prompt the user for the password.
# The input is invariably treated as a literal.
Write-Host "Please enter the SQL Server password:"
[string] $SqlServerPassword = Read-Host
# Validate the user's input.
if (-not $SqlServerPassword) { Throw 'Aborted.' }
# ...
Note: Read-Host has a -Prompt parameter, which accepts a prompt string directly, but if you were to use it, you wouldn't be able to enter values that start with !; therefore, the prompt was written with a separate Write-Host statement above.

Related

Is it possible to retrieve an #argument in a Powershell shell from within a program?

I am writing a program prog.exe that retrieves all arguments that are passed to it (in the form of a "sentence", not standalone arguments).
I just realized that in some cases only part of the line is retrieved, and this is when there are #parameters:
PS > ./prog.exe this is a #nice sentence
Only this, is and a are retrieved. In case I do not use # I get all of them. I presume this is because everything after the # is interpreted by Powershell as a comment.
Is there a way to retrieve everything that is on the command line?
If this makes a difference, I code in Go and get the arguments via os.Args[1:].
You can prevent PowerShell from interpreting # as a comment token by explicitly quoting the input arguments:
./prog.exe one two three '#four' five
A better way exists, though, especially if you don't control the input: split the arguments into individual strings then use the splatting operator # on the array containing them:
$paramArgs = -split 'one two three #four five'
./prog.exe #paramArgs
Finally, using the --% end-of-parsing token in a command context will cause the subsequent arguments on the same line to be passed as-is, no parsing of language syntax:
./prog.exe --% one two three #four five

Unable to evaluate Powershell password via interpolation

I am using Powershell to request a password from a user if not provided, based upon another answer. I then pass the password (no pun intended) to some program, do-something.exe. Rather than have an intermediate variable, I tried to convert the password to a normal string "inline":
[CmdletBinding()]
Param(
[Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
do-something password=${[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))}
That doesn't work. I could only get it to work using a temporary, intermediate variable:
[CmdletBinding()]
Param(
[Parameter(Mandatory, HelpMessage="password?")] [SecureString]$password
)
$pwd=[Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
do-something.exe password=$pwd
Did I make a mistake trying to evaluate the password inline when invoking do-something.exe? How can this be done?
${...} is a variable reference, and whatever ... is is taken verbatim as a variable name.
Enclosing a vairable name in {...} is typically not necessary, but is required in two cases: (a) if a variable name contains special characters and/or (b) in the context of an expandable string ("..."), to disambiguate the variable name from subsequent characters - see this answer
In order to embed an expression or command as part of an argument, use $(...), the subexpression operator, and preferably enclose the entire argument in "..." - that way, the entire argument is unambiguously passed as a single argument, whereas an unquoted token that starts with a $(...) subexpression would be passed as (at least) two arguments (see this answer).
If an expression or command by itself forms an argument, (...), the grouping operator is sufficient and usually preferable - see this answer
Therefore:
[CmdletBinding()]
param(
[Parameter(Mandatory, HelpMessage="password?")]
[SecureString] $password
)
# Note the use of $(...) and the enclosure of the whole argument in "..."
do-something "password=$([Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)))"
Also note:
On Windows it doesn't make a difference (and on Unix [securestring] instances offer virtually no protection and should be avoided altogether), but it should be [Runtime.InteropServices.Marshal]::PtrToStringBSTR(), not [Runtime.InteropServices.Marshal]::PtrToStringAuto()
As Santiago Squarzon points out, there is an easier way to convert a SecureString instance to its plain-text equivalent (which should generally be avoided[1], however, and, more fundamentally, use of [securestring] in new projects is discouraged[2]):
[pscredential]::new('unused', $password).GetNetworkCredential().Password
[1] A plain-text representation of a password stored in a .NET string lingers in memory for an unspecified time that you cannot control. More specifically, if it is part of a process command line, as in your case, it can be discovered that way. Of course, if the CLI you're targeting offers no way to authenticate other than with a plain-text password, you have no other choice.
[2] See this answer, which links to this .NET platform-compatibility recommendation.

PowerShell - Merge two variables into one

I'm learning PowerShell so please forgive (what I'm sure is) a simple question.
I'm used to coding BATCH scripts and if I wanted to merge %USERDOMAIN% and %USERNAME% I would:
set zFullUsername=%USERDOMAIN%\%USERNAME%
echo %zFullUsername%
How can I do the same in PowerShell?
Thank you for your time.
On a supported Operating System, I wouldn't even bother with environment variables for this:
$zFullUsername = whoami
Then just access it as required:
$zFullUsername
In PowerShell, you can access environment variables in a few different ways. The way I recommend is to use the $env:VAR variable to access them.
$user = $env:USERNAME
$domain = $env:USERDOMAIN
echo "$domain\$user"
Note: \ is not an escape character in the PowerShell parser, ` is.
Similarly to rendering the echo command (echo is an alias of Write-Output btw) you can create a username variable like so:
$fullUserName = "$domain\$user"
Or you can skip right to creating $fullUserName straight from the environment variables:
$fullUserName = "${env:USERDOMAIN}\${env:USERNAME}"
Note: When variables have non-alphanumeric characters in them, the ${} sequence tells PowerShell everything between the ${} is part of the variable name to expand.
It seems the : in $env:VAR is actually an exception to this rule, as"Username: $env:USERNAME" does render correctly. So the ${} sequence above is optional.
To avoid confusion when trying to apply this answer in other areas, if you needed to insert the value of an object property or some other expression within a string itself, you would use a sub-expression within the string instead, which is the $() sequence:
$someVar = "Name: $($someObject.Name)"
When using either ${} or $(), whitespace is not allowed to pad the outer {} or ().

How do I run this in Powershell?

Here is the command that works in command prompt.
C:\Temp\Agent.exe CustomerId={9c0-4ab1-123-102423a} ActivationId={9c0-4ab1-123-102423a} WebServiceUri=https://Agent/
Here is the error. (I have tried invoke-command and arguments but I think the { is causing issues.
Error:
Agent.exe: The command parameter was already specified.
You are certainly not required to use Start-Process (although it may "work," with some limitations, in some scenarios). The simplest and most straightforward answer is to quote the arguments:
C:\Temp\Agent.exe 'CustomerId={9c0-4ab1-123-102423a}' 'ActivationId={9c0-4ab1-123-102423a}' 'WebServiceUri=https://Agent/'
If the executable you want to run is in a path that contains spaces (or the executable filename itself contains spaces), quote the command and use the & (call/invocation) operator; e.g.:
& 'C:\Temp Dir\Agent.exe' 'CustomerId={9c0-4ab1-123-102423a}' 'ActivationId={9c0-4ab1-123-102423a}' 'WebServiceUri=https://Agent/'
Remarks:
If you need string interpolation (i.e., automatic expansion of $variable names inside strings), then use " instead of ' as your quote character. Use ' instead of " (as in the examples above) to prevent string interpolation.
Parameter quoting in this case is required because the { and } symbols have special meaning in PowerShell.
The proper way to run external programs is to use Start-Process. It gives you a couple of additional options like a separate ArgumentList parameter, running-as another user, or redirecting outputs:
Start-Process -FilePath 'C:\Temp\Agent.exe' -ArgumentList #(
# Arguments are space-separated when run. You could also just use one big string.
'CustomerId={9c0-4ab1-123-102423a}',
'ActivationId={9c0-4ab1-123-102423a}',
'WebServiceUri=https://Agent/'
)

Powershell use Read-Host variables in command

I have a very simple .ps1-script:
$prename = Read-Host 'What is your prename?'
$lastname = Read-Host 'What is your lastname?'
.\migration.exe -name=$prename -password=$lastname
Regardless of what the user enters, $prename and $lastname don't get passed to migration.exe - they are empty!
What am I doing wrong?
$prename and $lastname don't get passed to migration.exe - they are empty!
These variable references are not empty; they're - unexpectedly - not expanded (interpolated), as of PowerShell 7.1; that is, external program .\migration unexpectedly receives the following arguments verbatim: -name=$prename and -password=$lastname.
This should be considered a bug - see GitHub issue #14587.
The workaround is to use explicit string interpolation ("..."):
.\migration.exe -name="$prename" -password="$lastname"
Note: On Windows, the partial quoting is not guaranteed to be passed through as-is to the target program; instead, PowerShell performs its own parsing first and then performs ("-based) re-quoting on demand behind the scenes, depending on whether the resulting argument contains whitespace. For instance, if $prename's value is foo,
verbatim -name=foo is passed to migration.exe; if the value is foo bar,
verbatim "-name=foo bar" is passed - which the target program may or may not process properly; if not, you'll have to use --%, the stop-parsing symbol operator - see this answer.
You are passing the objects $prename and $lastname. You need expand the variables so you pass the value in the variables.
.\migration.exe -name=$($prename) -password=$($lastname)