Why is Powershell Write-Output Date Format not the System Setting? - date

We have a Windows Server with the Region settings for short dates set to dd.MM.yyyy. However powershell outputs the dates as MM/dd/yyyy:
$d = (Get-Item .\somefile.txt).CreationTime
Write-Output "$d" # => 09/26/2016 15:35:35
Also, the toString() function returns a different (correct) format
Write-Output "$($d.toString())" # => 26.09.2016 15:35:35
Questions:
Why does powershell use MM/dd/yyyy?
Why are the 2 formats above different?
I know we can set the format in our powershell profile but is there no "System" setting which determines it?

Scripts are often used for automation, rarely for interaction with users or creating UIs. The automatic conversion to string that happens when you put a variable inside a string, e.g. "$d" will always use the invariant culture and never the user's preference. Same goes for numbers, for example. This is precisely to avoid issues that arise where a string would contain a different format for a different user or on a different machine.
If you need control over the format, convert to string explicitly, not implicitly.
The same holds for parsing, incidentally. You can cast a string to a datetime, or number, but this requires a certain format to work. If you want to use the user's preference, then use [DateTime]::Parse instead.

Related

Are you able to use PtrToStringAuto to decrypt a secure string in Powershell 7 on macOS?

I have had no success in getting the following code snippet to output "Hello World!" in PS7
$string = $("Hello World!" | ConvertTo-SecureString -AsPlainText -Force)
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($string))
The above code is an example of decrypting a secure string without specifying a length.
This same code works in PS6 and PS5 to fully decrypt the Secure String, but does not work in PS7. The only way around this I have found is to use PtrToStringBSTR. Then it works as expected across all versions of PS for this use case.
I raised an issue at the Powershell repo on Github, but haven't had any responses. I'm honestly just looking for some confirmation that the behavior is the same for others.
https://github.com/PowerShell/PowerShell/issues/11953
I would think something like this would be a breaking change for a lot of code being ported to PS7.
Here is what I have found so far:
Documentation
https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.marshal.ptrtostringauto?view=netframework-4.8
According to the documentation, when specifying an integer, PtrToStringAuto:
Allocates a managed String and copies the specified number of characters from a string stored in unmanaged memory into it.
Specifying an int of 11 Returns "Hello", this is because every other char returned is Null. In this case, you must specify an int of 23 to return the complete string "Hello World!" using this method. I have stored the output in a variable to demonstrate this.
$String = $("Hello World!" | ConvertTo-SecureString -AsPlainText -Force)
[System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
[System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($string), 23)
$String[0] Returns H
$String[1] Returns NULL
$String[2] Returns E
$String[3] Returns NULL
etc....
If no integer is specified, PtrToStringAuto:
Allocates a managed String and copies all characters up to the first null character from a string stored in unmanaged memory into it.
I believe this suggests that either the Secure String is being stored with NULL values, whereas in PS6 it was not, or that the behavior of the PtrToStringAuto function has changed, and now adheres to the behavior the documentation describes above.
This is only an issue on macOS; however, using PtrToStringBSTR in place of PtrToStringAuto to decrypt the Secure String works as expected across windows and macOS.
This seems related: https://stackoverflow.com/a/11022662/4257163
I also do not see anywhere that a change was made.
Note that [securestring] is not recommended for new code anymore.
While on Windows secure strings offer limited protection - by storing the string encrypted in memory - via the DPAPI - and by shortening the window during which the plain-text representation is held in memory, no encryption at all is used on Unix-like platforms.[1]
The only way around this I have found is to use PtrToStringBSTR.
That is not only a way around the problem, PtrToStringBSTR is the method that should have been used to begin with, given that the input string is a BSTR.[2]
Do note that converting a secure string to and from a regular [string] instance defeats the very purpose of using [securestring] to begin with: you'll end up with a plain-text representation of your sensitive data in your process' memory whose lifetime you cannot control.
If you really want to do this, a simpler, cross-platform-compatible approach is:
[System.Net.NetworkCredential]::new('dummy', $string).Password
[1] This is especially problematic when you save a secure string in a file, via ConvertFrom-SecureString or Export-CliXml - see this answer.
[2] The Auto in PtrToStringAuto() means that the unmanaged input string is assumed to use a platform-appropriate character encoding, whereas BSTR is
a "Unicode" (UTF-16) string on all platforms. On Windows, an unmanaged string is assumed to have UTF-16 encoding (which is why the code works), whereas on Unix-like platforms it is UTF-8 since .NET Core 3.0 (PowerShell [Core] 7.0 is based on .NET Core 3.1), which explains your symptoms: the NUL chars. in the BSTR instance's UTF-16 code units are interpreted as characters in their own right when (mis)interpreted as UTF-8. Note that .NET Core 2.x (which is what PowerShell [Core] 6.x is based on) (inappropriately) defaulted to UTF-16, which this PR fixed, amounting to a breaking change.

Powershell Cleanest way to convert a string to mmddyyyy format

Thank you in advance...
I have String output:
2021-12-23
the string Base.Type is System.Object
I would like to convert it to:
12-23-2021
or
12/23/2021
any ideas?
Note: This question is a near-duplicate of Change date format from "yyyymmdd" to "mm/dd/yyyy"; while the two are closely related, this question is more overtly focused on both parsing from a string to a date and then back to a string, using a different format; by contrast, the linked question has the from aspect covered as an incidental part of the question.
Bali C, in a comment on the question, came up with the most concise solution, which only requires a small tweak:
# Note the "\"-escaped separators - see below.
PS> Get-Date -Date 2021-12-23 -Format dd\-MM\-yyyy # or dd\/MM\/yyyy
23-12-2021
This will work with most cultures in effect, because format yyyy-MM-dd (as exemplified by your sample input string, '2021-12-23') is a recognized format in all but a few cultures.
PowerShell generally tries to use the invariant culture in order for code to work the same across cultures, but in the case of cmdlet arguments actually is culture-sensitive, which is a historical accident that cannot be fixed without breaking backward compatibility.
See below for a truly culture-invariant solution, which also explains why separator chars. - and / should be escaped for maximum robustness.
Since PowerShell's casts use the invariant culture and format yyyy-MM-dd (as exemplified by your sample input string, '2021-12-23') happens to be a recognized format in that culture, you can simply:
cast your string to [datetime] to create a System.DateTime instance
and then call .ToString() with the desired format string to produce the desired string format, as shown in the many answers to this question.
The following should work irrespective of the specific culture in effect:
PS> ([datetime] '2021-12-23').ToString('MM\-dd\-yyyy')
12-23-2021
PS> ([datetime] '2021-12-23').ToString('MM"/"dd"/"yyyy')
12/23/2021
Note that the separator chars. - and / are escaped / quoted so as to indicate that they're to be used verbatim (you're free to choose between \-escaping and embedded quoting).
This is necessary, because characters such as / are by default interpreted as placeholders for the culture-appropriate date separator, for instance, so - depending on the culture in effect (as reflected in $PSCulture) - they may be translated to a different character, such as ., for instance.
If your original string representation were not recognized in the invariant culture, you can use System.DateTime.ParseExact to parse your input string into a [datetime] (System.DateTime) instance before calling .ToString() as described above:
[datetime]::ParseExact(
'2021-12-23',
'yyyy\-MM\-dd',
$null # current culture
).ToString('MM"-"dd"-"yyyy')
The above again yields '12-23-2021'.
Note that, due to use of $null, the current culture is used for parsing, which has no effect in this case, however, given that the separator chars. use embedded quoting, and given that only digits are involved, not (culture-dependent) names, such as month names.
If you do need the context of a specific culture, pass a [cultureinfo] instance (System.Globalization.CultureInfo) instead of $null; e.g., to parse in the context of the French (France) culture, pass [cultureinfo] 'fr-FR'.

Add sexagesimal times in powershell

I'm using ffprobe to get the sexagesimal time and after trimming the last three (unnecessary) digits I get the following format:
#examples
0:05:51.15
11:03:15.24
Is there a way to add these 2 so that the result is 11:09:06.39 ?
You can cast the strings to type [timespan], which allows you to perform arithmetic on the resulting objects, and to apply custom formatting on output:
PS> ([timespan] ' 0:05:51.15' + [timespan] '11:03:15.24').ToString('h\:mm\:ss\.ff')
11:09:06.39
Note: If there's a chance that the resulting time span exceeds 24 hours, more work is needed.[1]
Note how \-escaping must be used to specify the output separators as literals.
In this case, the simpler .ToString('g') would have yielded the same output, but only in cultures that use . as the decimal separator, because the standard g format specifier is culture-sensitive.
See the [timespan]::ToString() documentation as well as the documentation on standard and custom time-span format specifiers.
By contrast, PowerShell uses the invariant culture when interpreting the input format cast to [timespan], where . is the decimal separator; similarly, using a [timespan] instance in expandable strings yields a culture-invariant representation; e.g.:
[timespan] '11:03:15.24' always works, irrespective of the current culture, because the invariant culture expects . as the decimal separator.
"$([timespan] '1' - 1)" always yields 23:59:59.9999999, irrespective of the current culture[2].
As No Refunds No Returns notes, if you're dealing with differently formatted input, possibly from a different culture, you can use [timespan]::Parse() / [timespan]::ParseExact() / [timespan]::TryParseExact()
Parsing the standard formats of a given culture:
[timespan]::Parse(' 0:05:51,15', 'fr-FR') # OK
Note the , as the decimal separator.
If you omit the culture argument (or pass $null), the current culture is applied. Note how that differs from using a [timespan] cast, which is always culture-invariant (and assumes . as the decimal separator).
Parsing with a custom format:
[timespan]::ParseExact(' 0:05:51,15'.Trim(), 'h\:mm\:ss\,ff', $null) # OK
Note that using such a literal custom format is never culture-sensitive, because all the separators must be specified as - escaped - literals (e.g., \:), so $null is passed as the culture argument (IFormatProvider).
Conversely, passing a specific culture only makes sense with the culture-sensitive standard format specifiers, g and G.
Parsing with a culture-aware custom format:
If you don't know what culture will be in effect at runtime, but you want to respect that culture's decimal separator in combination with a custom format, you need to dynamically embed the current culture's decimal separator in your custom format string:
$tsText = ' 0:05:51.15'
[timespan]::ParseExact($tsText.Trim(),
('h\:mm\:ss\{0}ff' -f [cultureinfo]::CurrentCulture.NumberFormat.NumberDecimalSeparator),
$null
) # OK in cultures that use "." as the decimal separator
[1] h and hh only ever reflect the hours not included in full days in the input time span. To reflect the number of days too, prepend something like d\. - there is no format specifier that allows you express the total number of hours across multiple days, but you can use general-purpose string formatting to achieve that - do note, however, that you'll also need custom parsing code in order to convert the resulting string back to a [timespan] instance:
$ts = [timespan] ' 1:05:51.15' + [timespan] '23:03:15.24'
'{0}:{1:mm\:ss\.ff}' -f [Math]::Floor($ts.TotalHours), $ts
[2] At the .NET level, calling .ToString() on objects typically yields a culture-sensitive representation (if the type supports it), but with [timespan] the output happens to be culture-invariant. By contrast, PowerShell explicitly uses the invariant culture behind the scenes in casts and string interpolation, so that "$var" and $var.ToString() may not yield the same representation - see this answer for details.

PowerShell Param string always in single quote

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.

Should I use colons for all arguments of a function/script call?

I recently started using PowerShell, and noticed that I could pass argument values using a space between the argument name and value, or using a colon, like this:
MyFunction -Parameter value
or
MyFunction -Parameter:value
I started using the colon because it differentiates the line a bit more (for readability reasons), but from what I've seen, most people do not use it.
I've read a bit also about the differences between these approaches when working with switch typed arguments, that normally do not need values. In that situation, one needs to use the colon, or else the command will not work. This is another reason why I'm leaning towards using the colon for every parameter, for consistency reasons.
Is there something else I should keep in mind?
Generally speaking, when I need to execute a function with a switch parameter set to false, I simply omit the switch. That's the design intent of a switch parameter. The only time I think I would ever use the colon in a parameter is when I need to programmatically determine the value of a switch.
For example, let's say I need to get a regular directory listing on even days, and a recursive directory listing on odd days:
Get-ChildItem -Path $Path -Recurse:$((Get-Date).Day % 2 -eq 1) | ForEach-Object {...}
Beyond that, I personally wouldn't bother with the colon unless it significantly added to the readability of a given statement. It's not a commonly used syntax, so people who read your code later are more likely to be confused by it.
In general I would leave the colon off. Only use it in the situation of setting switch a parameter (typically when you want to pass a variable to it, like -Verbose:$someVariable.
Basically, I think you should be consistent with the more accepted style, which is what I've described.
If you really want to set all parameters in an internally consistent way which allows for variables for switch parameters, and is an accepted (though less known) way of passing parameters, consider splatting like so:
$params = #{
'Param1' = $value1
'Param2' = 5
'WhatIf' = $true
}
Some-Cmdlet #params