Questions/Problems related to set-winuserlanguagelist command - powershell

I'm building a code to fix keyboard layout situation on windows 10. With automated solution, I decided to use powershell. But the problem is that I'm pretty new in it and face certain problems. I managed to dig a script to change keyboard layouts, however it changes only to one language. As I try to create array with 2 languages:
$langlist=$lang_en,$lang_ru
set-winuserlanguagelist $langlist
It simply returns me next error:
Set-WinUserLanguageList : Cannot convert 'Microsoft.InternationalSettings.Commands.WinUserLanguage' to the type
'Microsoft.InternationalSettings.Commands.WinUserLanguage' required by parameter 'LanguageList'. Specified method is
not supported.
At line:1 char:25
+ set-winuserlanguagelist $langlist
+ ~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Set-WinUserLanguageList], ParameterBindingException
+ FullyQualifiedErrorId : CannotConvertArgument,Microsoft.InternationalSettings.Commands.SetWinUserLanguageListCommand
When I tried to use next command: $test = Get-WinUserLanguageList, the command works well with set-winuserlanguagelist.
The full script:
$keys='0809:00020409', '0419:00000419'
$lang_en=new-winuserlanguagelist en-gb
$lang_en[0].inputmethodtips.clear()
$lang_en[0].inputmethodtips.add($keys[0])
$lang_ru=new-winuserlanguagelist ru
$lang_ru[0].inputmethodtips.clear()
$lang_ru[0].inputmethodtips.add($keys[1])
$langlist=$lang_en,$lang_ru
set-winuserlanguagelist $langlist

Please check the following commented code snippets:
PS D:\PShell> ### Type mismatch
$langlist=$lang_en,$lang_ru
### Note the difference in type:
$langlist.gettype().Name ### Object[]
(Get-WinUserLanguageList).gettype().Name ### List`1
Object[]
List`1
PS D:\PShell> ### Use the following:
$langlist = Get-WinUserLanguageList
$langlist.Clear()
$langlist.Add($lang_en[0])
$langlist.Add($lang_ru[0])
$langlist.gettype().Name ### List`1
List`1
PS D:\PShell> <### The next cmdlet should work now:
set-winuserlanguagelist $langlist
<##>

The problem is that you're using New-WinUserLanguageList twice, with each call returning a list, so that $langlist = $lang_en, $lang_ru mistakenly created an array of (single-item) lists rather than a single list with two items, which caused the (nonsensically-sounding) type-mismatch error you saw.
Very awkwardly, however, cmdlet New-WinUserLanguageList only allows you to specify one language, even though it returns a list type ([Collections.Generic.List[Microsoft.InternationalSettings.Commands.WinUserLanguage]]).
That is, the following should work, but doesn't:
# Try to create the list with *2* entries
$langlist = New-WinUserLanguageList en-gb, ru # !! Doesn't work, parameter type is [string]
Instead, you have to initialize with 1 language and then add additional ones later, using the .Add() method:
# Create the list with initially just 'en-gb'...
$langlist = New-WinUserLanguageList en-gb
# ... and then add the other language, 'ru'
# Because the list is strongly typed, it is sufficient to pass the language
# identifier, which implicitly creates a new
# [Microsoft.InternationalSettings.Commands.WinUserLanguage] instance.
$langlist.Add('ru')
# Now you can modify the properties of $langlist[0] (en-gb)
# and $langlist[1] (ru)
# ...
# ... and pass the list of modified languages to Set-WinUserLanguageList:
Set-WinUserLanguageList $langlist
Alternatively, to avoid the .Add() call, you could have used:
$langlist = (New-WinUserLanguageList en-gb)[0], (New-WinUserLanguageList ru)[0]
Even though $langlist is then technically an array (a [System.Object[]] instance whose elements are of type Microsoft.InternationalSettings.Commands.WinUserLanguage), passing it to Set-WinUserLanguageList works, because it is implicitly converted to the required list type.

Related

Error when running a dot-sourced script after executing it

I've got very weird behaviour which I suppose is related to dot-sourcing somehow, but I cannot wrap my head around it. Here's what I have:
A script sourced.ps1 which contains the two functions and a class:
class MyData {
[string] $Name
}
function withClass() {
$initialData = #{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[MyData]
foreach ($item in $initialData.Keys) {
$d = [MyData]::new()
$d.Name = $item
$list.Add($d)
}
}
function withString() {
$initialData = #{
Name1 = "1";
Name2 = "2";
}
$list = New-Object Collections.Generic.List[string]
foreach ($item in $initialData.Keys) {
$list.Add($item)
}
}
I also have a script caller.ps1 which dot-sources the one above and calls the function:
$ErrorActionPreference = 'Stop'
. ".\sourced.ps1"
withClass
I then call the caller.ps1 by executing .\caller.ps1 in the shell (Win terminal with PS Core).
Here's the behaviour I cannot explain: if I call .\caller.ps1, then .\sourced.ps1 and then caller.ps1 again, I get the error:
Line |
14 | $list.Add($d)
| ~~~~~~~~~~~~~
| Cannot find an overload for "Add" and the argument count: "1".
However, if I change the caller.ps1 to call withString function instead, everything works fine no matter how many times I call caller.ps1 and sourced.ps1.
Furthermore, if I first call caller.ps1 with withString, then change it to withClass, there is no error whatsoever.
I suppose using modules would be more correct, but I'm interested in the reason for such weird behaviour in the first place.
Written as of PowerShell 7.2.1
A given script file that is both dot-sourced and directly executed (in either order, irrespective of how often) creates successive versions of the class definitions in it - these are distinct .NET types, even though their structure is identical. Arguably, there's no good reason to do this, and the behavior may be a bug.
These versions, which have the same full name (PowerShell class definitions created in the top-level scope of scripts have only a name, no namespace) but are housed in different dynamic (in-memory) assemblies that differ by the last component of their version number, shadow each other, and which one is effect depends on the context:
Other scripts that dot-source such a script consistently see the new version.
Inside the script itself, irrespective of whether it is itself executed directly or dot-sourced:
In PowerShell code, the original version stays in effect.
Inside binary cmdlets, notably New-Object, the new version takes effect.
If you mix these two ways to access the class inside the script, type mismatches can occur, which is what happened in your case - see sample code below.
While you can technically avoid such errors by consistently using ::new() or New-Object to reference the class, it is better to avoid performing both direct execution and dot-sourcing of script files that contain class definitions to begin with.
Sample code:
Save the code to a script file, say, demo.ps1
Execute it twice.
First, by direct execution: .\demo.ps1
Then, via dot-sourcing: . .\demo.ps1
The type-mismatch error that you saw will occur during that second execution.
Note: The error message, Cannot find an overload for "Add" and the argument count: "1", is a bit obscure; what it is trying to express that is that the .Add() method cannot be called with the argument of the given type, because it expects an instance of the new version of [MyData], whereas ::new() created an instance of the original version.
# demo.ps1
# Define a class
class MyData { }
# Use New-Object to instantiate a generic list based on that class.
# This passes the type name as a *string*, and instantiation of the
# type happens *inside the cmdlet*.
# On the second execution, this will use the *new* [MyData] version.
Write-Verbose -Verbose 'Constructing list via New-Object'
$list = New-Object System.Collections.Generic.List[MyData]
# Use ::new() to create an instance of [MyData]
# Even on the second execution this will use the *original* [MyData] version
$myDataInstance = [MyData]::new()
# Try to add the instance to the list.
# On the second execution this will *fail*, because the [MyData] used
# by the list and the one that $myDataInstance is an instance of differ.
$list.Add($myDataInstance)
Note that if you used $myDataInstance = New-Object MyData, the type mismatch would go away.
Similarly, it would also go away if you stuck with ::new() and also used it to instantiate the list: $list = [Collections.Generic.List[MyData]]::new()

Param in CmdletBinding not mandatory and error/misspelled checking

Hey all I have a powershell script that I need to send some parameters to. However, sometimes I do not need to send any or maybe one out of the two.
[CmdletBinding()]
param (
[Parameter(ParameterSetName='SkipAutoLoader')][switch]$SkipAutoLoader,
[Parameter(ParameterSetName='AppPool')][switch]$AppPool
)
[more code here....]
if (-not $SkipAutoLoader) {
$services += "Auto Loader Service"
}
[more code here....]
The above works just fine as long as I have either:
.\Start-AkkServides.ps1 -SkipAutoLoader
or
.\Start-AllServices -AppPool
If I have both together:
.\Start-AllServices -SkipAutoLoader -AppPool
It errors out.
C:\src\Start-AllServices.ps1 : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ .\Start-AllServices.ps1 -SkipAutoLoader -AppPool
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Start-AllServices.ps1], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,Start-AllServices.ps1
I also want to be able to determine if the param has the valid -SkipAutoLoader and/or -AppPool but with something else tagged like -SkipAutoLoader43
I would like to say that -SkipAutoLoaderbob is an invalid param. But do not show an error if either param is not present.
Loads Fine:
.\Start-AllServides.ps1 -SkipAutoLoader
Loads Fine:
.\Start-AllServides.ps1 -AppPool
Does Not Load Fine/causes error:
.\Start-AllServides.ps1 -SkipAutoLoader -AppPool
Does Not Load Fine/casues error:
.\Start-AllServides.ps1
Does not say param is not valid:
.\Start-AllServides.ps1 -SkipAutoLoaderbob
Does not say param is not valid:
.\Start-AllServides.ps1 -AppPool7
Possible to do with powershell?
Your parameters are in separate parameter sets (only[1]), so by design they cannot be used together.
If your parameters can be freely combined, you don't need to define parameter sets at all.
Since your script is an advanced one, thanks to [CmdletBinding()] and/or [Parameter()] attributes, calling with non-declared parameter names is automatically prevented.
However, the lack of a default parameter-set designation in the [CmdletBinding()] attribute causes a more fundamental error if you only specify an unsupported parameter name (such as -SkipAutoLoaderbob): PowerShell then doesn't know which of the two defined parameter sets to select, because no declared parameter can be bound (before even considering whether the parameter name given is valid, perhaps surprisingly)
Use [CmdletBinding(DefaultParameterSetName='AppPool')], for instance, to designate the default parameter set.
Assuming that your two parameters can be freely combined and neither is mandatory (which [switch] parameters shouldn't be anyway), your code can be simplified to:
[CmdletBinding()]
param (
[switch] $SkipAutoLoader,
[switch] $AppPool
)
# Output the name of the active parameter set.
$PSCmdlet.ParameterSetName
Note that non-mandatory parameters without explicit parameter-set membership (and other non-default parameter properties) do not require [Parameter()] attributes.
When you invoke the script, you'll see that PowerShell implicitly defines a parameter set in the absence of explicitly declared ones, named __AllParameterSets.
[1] Note that a parameter can belong to multiple explicitly specified parameter sets, via multiple [Parameter(ParameterSetName= '...')] attributes. Any parameter without an explicit parameter-set membership is implicitly part of all parameter sets.

Parameters issue in script

Can someone tell what I am doing wrong in the below I wrote:
function set-harden {
[CmdletBinding(DefaultParameterSetName='NormalHardening')]
param (
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[Parameter(ParameterSetName='PermitHTTP' ,Mandatory=$True)]
[Parameter(ParameterSetName='PermitHTTPS' ,Mandatory=$True)]
[switch]$DONOTRemovefromdomain,
[Parameter(ParameterSetName='PermitHTTP')]
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[switch]$Permithttp,
[Parameter(ParameterSetName='PermitHTTPS')]
[Parameter(ParameterSetName='DoNotRemoveFromDomain')]
[switch]$Permithttps,
[Parameter(ParameterSetName='NormalHardening')]
$NormalHardening
)}
If($NormalHardening -eq ""){
Write-Host "Excellent!"
}
All I want to do is to let the user select -DONOTRemovefromdomain or -Permithttp or even -Permithttps. There could be a variety of options the user has to choose from.
When I run this below I get an error:
PS C:\Temp> set-harden -DONOTRemovefromdomain -Permithttp
set-harden : Parameter set cannot be resolved using the specified named parameters.
At line:1 char:1
+ set-harden -DONOTRemovefromdomain -Permithttp
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [set-harden], ParameterBindingException
+ FullyQualifiedErrorId : AmbiguousParameterSet,set-harden
Also, if I do not specify anything (so it should just go to the parameter NormalHardening) I get an nothing back:
PS C:\Temp> set-harden
PS C:\Temp>
You've specified two flags, DONOTRemovefromDomain and Permithttp that belong to two parameter sets, DoNotRemoveFromDomain and PermitHttp. The command parser has no way of knowing which parameter set you mean, so you get an error.
The reason you don't get an error when you don't specify anything is because you've set the default parameter set explicitly to NormalHardening. You've not set the Mandatory flag on the single parameter in this parameter set, and by default parameters are not mandatory so you're not seeing an error.
Instead of having all these parameter sets why not just have 2, one for the default and one for all the flags you want to set:
function set-harden {
[CmdletBinding(DefaultParameterSetName='NormalHardening')]
param (
[Parameter(ParameterSetName='Options')]
[switch]$DONOTRemovefromdomain,
[Parameter(ParameterSetName='Options')]
[switch]$Permithttp,
[Parameter(ParameterSetName='Options')]
[switch]$Permithttps,
[Parameter(ParameterSetName='NormalHardening')]
$NormalHardening
)}
If($PSCmdlet.ParameterSetName -eq "Options"){
Write-Host "Excellent!"
}
How, if the parameter set name is set to Options you can check and apply the flags. If it's set to NormalHarding then you know to use the $NormalHardening parameter.
Sean gave a good answer already about what's going on in your specific case, but I want to include some tips for troubleshooting parameter sets.
Get Help
Or more specifically, Get-Help. The parameter set syntax is automatically generated from the param block, so running Get-Help myFunction will show you how PowerShell is interpreting your parameter sets (how many, which parameters are mandatory or not in each set, etc.).
Trace the Call
If the sets look right but you're getting errors and aren't sure why, let PowerShell show you how it's binding parameters:
Trace-Command -Name ParameterBinding -Expression { Set-Harden -Permithttp } -PSHost
That can give you great insight on what's going on, and lead you to how you might fix that (or help you realize that you can't).

You cannot call a method on a null-valued expression - general

You create a script, it works for some time, then out of the blue it starts crashing with "You cannot call a method on a null-valued expression" or "The property 'property name' cannot be found on this object. Verify that the property exists and can be set.". What does this mean?
This is a Powershell version of "null pointer exception". This exception arises every time you attempt to query a variable that appears to be null. To determine what variable is null and where, you need to read the stack trace and the line/symbol numbers of the line in question. An example:
You cannot call a method on a null-valued expression.
At E:\temp\testsest.ps1:35 char:12
+ If($Search.value() -contains $SearchString)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
Let's parse the error message. First, there is a wording that is in the title of this question. If you are about to ask a question with this wording, you will get a set of similar questions proposed by StackOverflow. But there is more in the error description. Second line shows script, line and character number of the first character of an expression that generates this exception. Here, a request is made to $Search.value() querying if it -contains $SearchString. The wavy underline separates the expression in full, although the proper way would be underlining only $Search.value(). Next, there is a CategoryInfo and FullyQualifiedErrorId, the latter saying "Invoke method on null", omitting "pointer" or "variable".
Now, let's debug the message. Here, the only method that's about to be called is value(), this means $Search is equal to null. Therefore, we need to get upwards from the line 35 of the script and find a place where a value is last assigned to the variable in question. This particular script had a query to Range.Find() which returns null if there's no match to the searched string. An excerpt:
$Excel = New-Object -ComObject Excel.Application
$Excel.Visible = $true
$ExcelWorkBook = $Excel.Workbooks.Open($ExcelPath)
$ExcelWorkSheet = $Excel.WorkSheets.item("$location")
$Range = $ExcelWorkSheet.Range("A1").EntireColumn
$Search = $Range.find($user) # <<< here we get null
If($Search.value() -contains $user)
So, we have found where do we receive a null.
Remedies vary, but all include checks against $null. In this case it's enough to check $Search for null, and return "Nothing found" if it is indeed null. It might be not as simple, there might be more structures that can be null, like in $a.b.c.someMethod() - here either $a, $a.b or $a.b.c is null, so you need to check all of the outcomes. There are also situations where a complex structure is returned, and is expected to have a value in a certain field, but the field is not populated, therefore trying to use that field's value will produce an exception.
The moral is: If you receive an exception speaking about "null-valued", you have not expected something to return null, and you have to add checks for null (or, in fact, any unexpected) values before attempting to use the result.

Why would a variable in PowerShell lose its value after it is referenced?

With the following 2 lines of code:
$meta = New-Object System.Management.Automation.CommandMetadata (Get-Command Get-Event)
$parametersInCmdlet = $meta.Parameters.GetEnumerator()
The $parametersInCmdlet variable is set as can be seen by referencing it.
$parametersInCmdlet
Key Value
--- -----
SourceIdentifier System.Management.Automation.ParameterMetadata
EventIdentifier System.Management.Automation.ParameterMetadata
When I reference it again immediately after that, it appears empty (and confirmed if piped to Get-Member).
$parametersInCmdlet | gm
gm : No object has been specified to the get-member cmdlet.
At line:1 char:23
+ $parametersInCmdlet | gm
+ ~~
+ CategoryInfo : CloseError: (:) [Get-Member], InvalidOperationException
+ FullyQualifiedErrorId : NoObjectInGetMember,Microsoft.PowerShell.Commands.GetMemberCommand
There is nothing else [that should be] touching that variable in between those references. This occurs in the console and ISE for both PS 2.0 and 3.0 so that makes me think it is more user misunderstanding than a bug.
What would cause the value to be lost in this case?
The object returned by GetEnumerator() methods is pretty much always an IEnumerator. The job of an IEnumerator is to hand back elements of a collection, one at a time, until that collection is depleted. At that point, it is the correct behavior for the IEnumerator to return back nothing when asked for the next item.
Powershell unrolls the entire collection when you look at it the first time. Thus, by default, it is expected that you can't look at the collection again, since the IEnumerator has already been "spent."
The workaround is to call Reset() on the IEnumerator if you want it to start over. Assuming the IEnumerator is properly implemented, this will allow you to re-read the collection from the beginning again.
So, try calling $parametersInCmdlet.Reset() before using it again.