In many code editors interaction with brackets – () {} [] – and quotes – "" '' – goes like this – you type the first one and the second one is added automatically, if you then delete the first bracket or quote using the Backspace key, the second one is also deleted. When you finished entering the content, you type the right bracket or quote and the editor doesn’t add another one but just steps over the existing one.
The same we can say about PowerShell console.
If we look in the SamplePSReadLineProfile.ps1 files, we can see many useful functions. But in this article we will examine only those, that relate to its subject.
Also, for the functionality this article describes to be available in every PowerShell session, it is worthwhile to place the code blocks to the PowerShell profile.
First, we will need the following lines.
using namespace System.Management.Automation using namespace System.Management.Automation.Language
We need them because the functions in the code refer to the .net classes by their names, without using their respective namespaces.
Let’s add the following code.
Set-PSReadLineKeyHandler -Key '"',"'" `
-BriefDescription SmartInsertQuote `
-LongDescription "Insert paired quotes if not already on a quote" `
-ScriptBlock {
param($key, $arg)
$quote = $key.KeyChar
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
# If text is selected, just quote it without any smarts
if ($selectionStart -ne -1)
{
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $quote + $line.SubString($selectionStart, $selectionLength) + $quote)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
return
}
$ast = $null
$tokens = $null
$parseErrors = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$ast, [ref]$tokens, [ref]$parseErrors, [ref]$null)
function FindToken
{
param($tokens, $cursor)
foreach ($token in $tokens)
{
if ($cursor -lt $token.Extent.StartOffset) { continue }
if ($cursor -lt $token.Extent.EndOffset) {
$result = $token
$token = $token -as [StringExpandableToken]
if ($token) {
$nested = FindToken $token.NestedTokens $cursor
if ($nested) { $result = $nested }
}
return $result
}
}
return $null
}
$token = FindToken $tokens $cursor
# If we're on or inside a **quoted** string token (so not generic), we need to be smarter
if ($token -is [StringToken] -and $token.Kind -ne [TokenKind]::Generic) {
# If we're at the start of the string, assume we're inserting a new string
if ($token.Extent.StartOffset -eq $cursor) {
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote ")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
return
}
# If we're at the end of the string, move over the closing quote if present.
if ($token.Extent.EndOffset -eq ($cursor + 1) -and $line[$cursor] -eq $quote) {
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
return
}
}
if ($null -eq $token -or
$token.Kind -eq [TokenKind]::RParen -or $token.Kind -eq [TokenKind]::RCurly -or $token.Kind -eq [TokenKind]::RBracket) {
if ($line[0..$cursor].Where{$_ -eq $quote}.Count % 2 -eq 1) {
# Odd number of quotes before the cursor, insert a single quote
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
}
else {
# Insert matching quotes, move cursor to be in between the quotes
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$quote$quote")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
return
}
# If cursor is at the start of a token, enclose it in quotes.
if ($token.Extent.StartOffset -eq $cursor) {
if ($token.Kind -eq [TokenKind]::Generic -or $token.Kind -eq [TokenKind]::Identifier -or
$token.Kind -eq [TokenKind]::Variable -or $token.TokenFlags.hasFlag([TokenFlags]::Keyword)) {
$end = $token.Extent.EndOffset
$len = $end - $cursor
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($cursor, $len, $quote + $line.SubString($cursor, $len) + $quote)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($end + 2)
return
}
}
# We failed to be smart, so just insert a single quote
[Microsoft.PowerShell.PSConsoleReadLine]::Insert($quote)
}
This code implements the following cases of interacting with quotes.
If we type a quote, then the second one is added automatically, and the cursor is placed between the quotes.
"|"
If we type another quote, the cursor will just step over the one, added in the previous step.
""|
If the cursor is placed before the first quote, and not the second one, as in the previous example, and we type another quote, then it is assumed that we want to add another pair of quotes before the existing pair.
|"" -> "|" ""
Also, if there are some quotes to the left of the cursor, then the behavior is depended on the following.
If there are odd number of quotes, then when we type a quote the result is one quote, and not two. This is because it is implied that we close some existing opening quote.
"left "inner" right| -> "left "inner" right"|
And if there are even number of quotes to the left of the current cursor position, then what will be added is a pair of quotes, just like in the first example.
"left "inner" right" | -> "left "inner" right" "|"
Also, if the cursor is placed before some token – a command, a variable, a keyword, or a number of characters – then typing the quote will result in the whole token being enclosed in the quotes.
|someword -> "someword"|
And if before entering a quote we will select some part of the command, then the selected part is what will be quoted.
Some phrase with selected part. -> Some phrase with "selected part".
And if the condition doesn’t fall under any of the aforementioned categories, then just a single quote is added.
Next, we need the following part.
Set-PSReadLineKeyHandler -Key '(','{','[' `
-BriefDescription InsertPairedBraces `
-LongDescription "Insert matching braces" `
-ScriptBlock {
param($key, $arg)
$closeChar = switch ($key.KeyChar)
{
'(' { [char]')'; break }
'{' { [char]'}'; break }
'[' { [char]']'; break }
}
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($selectionStart -ne -1)
{
# Text is selected, wrap it in brackets
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, $key.KeyChar + $line.SubString($selectionStart, $selectionLength) + $closeChar)
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
} else {
# No text is selected, insert a pair
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)$closeChar")
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
}
Its task is, when the opening bracket is added – ( { [ – to add the closing one – ) } ]. The cursor stays inside the brackets.
(|) [|] {|}
Also, if we select some part of the command, then typing the opening bracket will result in the whole selection be enclosed in the brackets.
Some phrase with selected part. -> Some phrase with (selected part).
Let’s move to the next code part.
Set-PSReadLineKeyHandler -Key ')',']','}' `
-BriefDescription SmartCloseBraces `
-LongDescription "Insert closing brace or skip" `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($line[$cursor] -eq $key.KeyChar)
{
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($cursor + 1)
}
else
{
[Microsoft.PowerShell.PSConsoleReadLine]::Insert("$($key.KeyChar)")
}
}
This code block’s area of response is closing brackets.
If the cursor is placed before some closing bracket, it is assumed that this bracket closes some opening one and when we enter the closing bracket character the cursor just steps over it without adding another one.
(text|) -> (text)|
But if the cursor is placed before some other character or at the end of the command, then the closing bracket will be added.
(text| -> (text)|
Now, to the next block of code.
Set-PSReadLineKeyHandler -Key Backspace `
-BriefDescription SmartBackspace `
-LongDescription "Delete previous character or matching quotes/parens/braces" `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($cursor -gt 0)
{
$toMatch = $null
if ($cursor -lt $line.Length)
{
switch ($line[$cursor])
{
'"' { $toMatch = '"'; break }
"'" { $toMatch = "'"; break }
')' { $toMatch = '('; break }
']' { $toMatch = '['; break }
'}' { $toMatch = '{'; break }
}
}
if ($toMatch -ne $null -and $line[$cursor-1] -eq $toMatch)
{
[Microsoft.PowerShell.PSConsoleReadLine]::Delete($cursor - 1, 2)
}
else
{
[Microsoft.PowerShell.PSConsoleReadLine]::BackwardDeleteChar($key, $arg)
}
}
}
It defines the Backspace key behavior, namely – if the cursor is located directly between the quotes – "" '' – or brackets – () {} [] – then pressing the Backspace key will delete both of them.
"|" -> |
{|} -> |
In other circumstances the Backspace key works as usual.
Let’s add one more useful code block.
Set-PSReadLineKeyHandler -Key 'Alt+(' `
-BriefDescription ParenthesizeSelection `
-LongDescription "Put parenthesis around the selection or entire line and move the cursor to after the closing parenthesis" `
-ScriptBlock {
param($key, $arg)
$selectionStart = $null
$selectionLength = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetSelectionState([ref]$selectionStart, [ref]$selectionLength)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
if ($selectionStart -ne -1)
{
[Microsoft.PowerShell.PSConsoleReadLine]::Replace($selectionStart, $selectionLength, '(' + $line.SubString($selectionStart, $selectionLength) + ')')
[Microsoft.PowerShell.PSConsoleReadLine]::SetCursorPosition($selectionStart + $selectionLength + 2)
}
else
{
[Microsoft.PowerShell.PSConsoleReadLine]::Replace(0, $line.Length, '(' + $line + ')')
[Microsoft.PowerShell.PSConsoleReadLine]::EndOfLine()
}
}
Its effect is that when we press Shift+Alt+9 – practically Alt+( – all the selected part of the command will be enclosed in parentheses, and in that it resembles one of the previous code blocks.
Some phrase with selected part. -> Some phrase with (selected part).
What differs is that when we press this hotkey without first selecting some part of the command, what is being enclosed is the whole command.
Some command -> (Some command)
It can be useful, for example, in the situation when you typed some command and then decided that all you need is the value of some property of the objects the command returns.
Get-Process -Name pwsh
You press Shift+Alt+9 and the command becomes the following.
(Get-Process -Name pwsh)
All you need to do now is to add the point and the name of the needed property.
(Get-Process -Name pwsh).Path
In this article we discussed several useful techniques based on the code from the SamplePSReadLineProfile.ps1 file which is part of the PSReadline module. These techniques make interaction with the PowerShell console even more convenient by adding functionality available predominantly in code editors.
]]>Get-Process | Get-Member
TypeName: System.Diagnostics.Process ...
Objects that result from PowerShell commands execution belong to some data type, but this doesn’t prevent us from adding a custom type.
It becomes even more useful when we work with custom objects – System.Management.Automation.PSCustomObject.
[PSCustomObject]@{
Name = 'Object'
Description = 'Object Description'
}
If we define some objects as ones that belong to some custom type, we then can refer to it in type and format files – types.ps1xml and format.ps1xml, respectively, and define additional properties, methods, and views.
But in this article we will concentrate on techniques of adding custom types to objects and will not describe purpose and structure of types and format files.
First, let’s discuss, how we can find out, what type and object belongs to.
As we saw in the first example, we can use the Get-Member cmdlet. But it returns only most specific type and not the whole type hierarchy. To find out, what types an object inherits from, we can use the pstypenames property.
It is needed to say that, because the pstypenames is a hidden property, autocompletion functionality by using Tab kay or Ctrl+Space shortcut will not be available.
$Process = Get-Process -Id $pid $Process.pstypenames
System.Diagnostics.Process System.ComponentModel.Component System.MarshalByRefObject System.Object
The same applies to PSCustomObject.
$PSCustomObject = [PSCustomObject]@{
Name = 'Object'
Description = 'Object Description'
}
$PSCustomObject.pstypenames
System.Management.Automation.PSCustomObject System.Object
One of the approaches to adding custom type to objects is using the Add-Member cmdlet.
$PSCustomObject | Add-Member -TypeName 'CustomType' $PSCustomObject.pstypenames
CustomType System.Management.Automation.PSCustomObject System.Object
Another way is to use the value of pstypenames property, which data type inherits from the string collection. So, the following methods are available: Add, Insert, Remove, RemoveAt, and Clear.
$PSCustomObject.pstypenames.Add("AnotherCustomType")
$PSCustomObject.pstypenames
CustomType System.Management.Automation.PSCustomObject System.Object AnotherCustomType
$PSCustomObject.pstypenames.Remove("AnotherCustomType")
$PSCustomObject.pstypenames
CustomType System.Management.Automation.PSCustomObject System.Object
$PSCustomObject.pstypenames.RemoveAt(0) $PSCustomObject.pstypenames
System.Management.Automation.PSCustomObject System.Object
$PSCustomObject.pstypenames.Insert(0, "AnotherCustomType") $PSCustomObject.pstypenames
AnotherCustomType System.Management.Automation.PSCustomObject System.Object
$PSCustomObject.pstypenames.Clear() $PSCustomObject.pstypenames
Also, in case of a custom object, we can define its type at the creation, by specifying the PSTypeName property.
$PSCustomObject = [PSCustomObject]@{
Name = 'Object'
Description = 'Object Description'
PSTypeName = 'OneMoreCustomType'
}
$PSCustomObject
Name Description ---- ----------- Object Object Description
$PSCustomObject.pstypenames
OneMoreCustomType System.Management.Automation.PSCustomObject System.Object
In this article we have discussed several approaches to adding custom data types to objects, which is one of the steps to extend available set of types and formats in PowerShell.
]]>Sounds familiar?
In this article we will talk about some techniques, that can help us out.
In order to not distract too much from our main theme, let’s decide, that our long and complex command is the following.
Get-Process -Name $ProcessName
And the missed variable is $ProcessName.
$ProcessName = 'pwsh'
First, we can add the required command just before the one we entered followed by the semicolon.
$ProcessName = 'pwsh'; Get-Process -Name $ProcessName
Or, after entering the Get-Process command we can use Ctrl+Enter shortcut to add an empty sting above the command and type the missed command there.
$ProcessName = 'pwsh' >> Get-Process -Name $ProcessName
If you want all your actions to be represented as single commands, you can copy the command to clipboard, execute all missed commands, and paste the command back.
You can do this by selecting the command, for example, using the Ctrl+a shortcut, and copy it by pressing Ctrl+c.
Also, you can copy the command without the need to select it beforehand by using the Ctrl+Shift+c shortcut. It is needed to say, that in PSReadline options this shortcut is referred to as Ctrl+C.
If we open a PSReadline module folder and look through the SamplePSReadLineProfile.ps1 file, among other things, we will find the following.
Set-PSReadLineKeyHandler -Key Alt+w `
-BriefDescription SaveInHistory `
-LongDescription "Save current line in history but do not execute" `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
[Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line)
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
}
This command binds Alt+w shortcut to the code, specified in the ScriptBlock parameter.
So, when we use the above shortcut, the command typed in the console will be added to the command history and removed from the prompt. And after we execute all the required commands, we can bring the command back by using techniques for working with the command history, for example Up Arrow key.
Concerning the proposed shortcut, you can define it as you see fit. I, personally, would like the key to be closer to the Enter, for example, Ctrl+’. In this case, the code will be as follows.
Set-PSReadLineKeyHandler -Key "Ctrl+'" `
-BriefDescription SaveInHistory `
-LongDescription "Save current line in history but do not execute" `
-ScriptBlock {
param($key, $arg)
$line = $null
$cursor = $null
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
[Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($line)
[Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
}
For you not to enter the aforementioned command in every session, you can add it to the profile.
We have discussed several approaches, that you can use when it occurs, that the command entered requires some other actions to be taken beforehand and we do not feel like entering it again.
Every technique can be of use in different situations, and, as we saw in the last example, we can use the PSReadline module to bind various scripts to keyboard shortcuts, that allows us some freedom to define how our PowerShell console will work.
]]>
To install the PlasterTemplates module use the following command.
Install-Module -Name PlasterTemplates
To get help use the following command.
Get-Help about_PlasterTemplates
PowerShellModule – create a new PowerShell module with Visual Studio Code, Pester, and Azure Pipelines support.
Use the following command to create a new module with a PowerShellModule template.
Invoke-Plaster -TemplatePath 'C:\Program Files\PowerShell\Modules\PlasterTemplates\Templates\PowerShellModule' -DestinationPath c:\PlasterCreatedModule\ -ModuleName PlasterCreatedModule -Description "Module created by Plaster" -Version 1.0 -Options VSCode, Pester, AzurePipelines, Helpers
The TemplatePath parameter value depends on the folder the PlasterTemplates module was installed into.
To list templates available on the machine invoke the following cmdlet from Plaster module.
Get-PlasterTemplate -IncludeInstalledModules
PowerShell itself keeps them in memory. In addition to this, the PSReadline module saves them in a file on the hard drive. that allows us to go back to command entered not only in the current session, but also executed some time ago and probably in different PowerShell version.
We can get the location of this file as follows.
Get-PSReadLineOption | Select-Object HistorySavePath
To look at the previously entered commands we can use the Up Arrow and Down Arrow keys. The former lets us go back in history, and the latter sends us in opposite direction. By using these keys, we can traverse the full history – both from memory and the file.
Unlike the Up Arrow and Down Arrow keys, the history cmdlet, like Get-History, Invoke-History, and Clear-History, interact only with the commands, stored in memory.
So, the Get-History cmdlet, or its aliases – h, history, and ghy – return only those commands, that was executed in the current session. For example.
Get-History
Id Duration CommandLine -- -------- ----------- 1 0.143 Get-Process pwsh 2 0.018 Get-Service WinRM 3 0.023 Get-Content C:\Windows\System32\drivers\etc\hosts
The Invoke-History cmdlet (its aliases – r and ihy) allows us to execute some command from the history.
Invoke-History 2
Status Name DisplayName ------ ---- ----------- Running WinRM Windows Remote Management (WS-Management)
And the Clear-History cmdlet (its alias – clhy) – clears the history. Of course, this affects only the history, stored in memory, and do not affect the content of the file in any way.
To execute a command from memory we can enter the # symbol, followed by the command id and press Tab or Ctrl+Space.
#2[Tab] => Get-Service WinRM
Also, we can use the # symbol for searching a command in the history. For example, like this.
#hosts[Tab] => Get-Content C:\Windows\System32\drivers\etc\hosts
If the fragment we specify occurs in several command, we can look through them by using Tab, or, to move in opposite direction – Shift+Tab. Also, to get all found commands at once we can use Ctrl+Space.
Concerning a search through the full history, we have several options provided by the PSReadline module.
To search for the commands, starting with the specified characters, we can use F8, and, again, to move in opposite direction – Shift+F8.
Get[F8] => Get-Content C:\Windows\System32\drivers\etc\hosts
To search by arbitrary part of the command we can use the Ctrl+R and Ctrl+S hotkeys. The former is used to go back in history, and the latter – to move forward.
[Ctrl+R]WinRM
By typing the above symbols, we can find the Get-Service WinRM command. To search deeper in the history, we can continue pressing Ctrl+R, or, to return to previous findings – Ctrl+S.
If we look at the SamplePSReadLineProfile.ps1 file located in the PSReadline module folder, we can find in there some suggestions, concerning the use of different features of the module.
One of them is assigning the Up Arrow and Down Arrow keys the functions of the F8 and Shift+F8, respectively.
Set-PSReadLineKeyHandler -Key UpArrow -Function HistorySearchBackward Set-PSReadLineKeyHandler -Key DownArrow -Function HistorySearchForward
If we add the above commands to the PowerShell profile, we will be able to use the Up Arrow and Down Arrow keys not only for traversing the history, like is was before, but also to search for the commands, starting with the specified symbols.
We discussed only a small part of what PowerShell and the PSReadline module offer to create a more comfort environment for working in the console, but the ability to reuse commands and traverse command history quickly and efficiently is one of the most importance.
]]>This array can be specified as a value of the -InputObject parameter, but in the majority of cases it is received from the pipeline.
We can use the ForEach-Object cmdlet to process several elements in parallel, or to get the value of some property or call some method of the elements passed to the cmdlet.
But in this article we concentrate on its classic use case – executing several scriptblocks on the received objects.
To access the element being processed we use automatic variables $_ or $PSItem, which are equivalent.
In the simplest case we use ForEach-Object cmdlet as follows.
Get-Service | ForEach-Object -Process { if ($_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped') { Start-Service $_ } }
Or we can use a variable.
$ScriptBlock = { if ($_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped') { Start-Service $_ } }
Get-Service | ForEach-Object -Process $ScriptBlock
In the examples we specified the scriptblock as a value of the -Process parameter, which is executed for every service object, passed by the pipeline.
The ForEach-Object cmdlet has two aliases – foreach and %. Thus, the following command are fully interchangeable.
Get-Service | ForEach-Object Name Get-Service | foreach Name Get-Service | % Name
However, back to our scriptblocks.
Except the -Process parameter we can specify the -Begin and -End parameters. Each of them is executed single time, despite the number of received objects: -Begin is invoked before processing the elements, and -End is executed after the processing is finished.
Both these parameters accept scriptblock, but they differ from the -Process parameter in two aspects: firstly, they accept a scalar value, but the -Process parameter accepts array of scriptblocks, and, secondly, the scriptblock specified as a value of any of these two parameters cannot access the elements by using the $_ or $PSItem automatic variables.
In essence, -Begin is used for preparing for the processing of elements, and -End is called for some finishing actions.
Get-Process | ForEach-Object -Begin { [long]$WorkingSet = 0 } -Process { $WorkingSet += $_.WS } -End { $WorkingSet }
In addition to -Begin, -Process, and -End, there is another parameter: -RemainingScripts. It, like the -Process parameter, accepts an array of scriptblocks, and, when the ForEach-Object cmdlet runs, they are added to the scriptblocks specified in the -Process parameter. Why it is needed we will talk later.
Because of the PowerShell’s ability to use positional parameters, in many cmdlets we can skip parameter names and specify only their arguments.
So, we can specify a command as follows.
"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"}
If we invoke the Trace-Command cmdlet for ParameterBinding component
Trace-Command -Name ParameterBinding -Expression { "Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} } -PSHost
we’ll see that the first argument – {“1: $_”} – is bound to the -Process parameter, and others are associated with -RemainingScripts.
This is because in the code of the -RemainingScripts parameter the ValueFromRemainingArguments attribute is specified as equal to true, and this means, that all the arguments not bound to other parameters is bound to this one.
If we execute the original command, we get the following result.
1: 2: Value 3:
Why is this?
As we talked earlier, when the ForEach-Object cmdlet runs, the arguments of the -RemainingScripts parameter are added to the common array of scriptblocks, after those, that was specified in the -Process parameter. After that, PowerShell checks whether the -Begin and -End parameters are specified. If not, then the first scriptblock in the array becomes the begin scriptblock, as if it was specified in the -Begin parameter, and the last scriptblock becomes the end scriptblock, which is similar to specifying it in the -End parameter.
If we pass the ForEach-Object cmdlet an array of two objects, we’ll see this more clearly.
"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"}
1: 2: Value1 2: Value2 3:
It is interesting, that if we specify two scriptblocks, then the first one will be the begin scriptblock, and other will be executed for every object.
"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"}
1: 2: Value1 2: Value2
If we specify more than two scriptblocks, then, as we saw earlier, the first and the last become the begin and the end scriptblock, respectively.
"Value1", "Value2" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} {"4: $_"}
1: 2: Value1 3: Value1 2: Value2 3: Value2 4:
In order to all the scriptblocks execute for the received objects, we can specify the -Begin and -End parameters in the command. For example, like this.
"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} -Begin {} -End {}
Or like this.
"Value" | ForEach-Object {"1: $_"} {"2: $_"} {"3: $_"} -Begin $null -End $null
In either case, the result is as follows.
1: Value 2: Value 3: Value
Also, we can add empty scriptblocks as the first and the last arguments.
"Value" | ForEach-Object {} {"1: $_"} {"2: $_"} {"3: $_"} {}
Considering all we have discussed, we can define the command we used in the beginning as follows.
Get-Process | ForEach-Object { [long]$WorkingSet = 0 } { $WorkingSet += $_.WS } { $WorkingSet }
The ForEach-Object cmdlet is very important element of PowerShell. It supports several different use models (as we said, we discussed only one of them), and two types of syntax. But it is its finesse and ability to hide its intrinsic complexity, providing in most cases quite simple solutions to different problems, that makes it incredibly easy to use.
]]>For example, like this.
Get-Service | Where-Object Description -like "*wsman*"
Or else
Get-Service | Where-Object Description -like "*w?man*"
By default, this operator is not case-sensitive, so the following commands are equivalent.
Get-Service | Where-Object Description -like "*wsman*" Get-Service | Where-Object Description -like "*WSman*" Get-Service | Where-Object Description -like "*WSMAN*"
If we want it to consider the case of the objects it examines, we can specify it as -clike.
Get-Service | Where-Object Description -clike "*wsman*"
On the other hand, if we want to be clear, that the operator is case-insensitive, we can spell it as -ilike.
Get-Service | Where-Object Description -ilike "*wsman*"
Besides, there are opposite operator in PowerShell, -notlike.
Get-Service | Where-Object StartupType -notlike "Auto*"
Which, as the -like operator, has two variations: -cnotlike and -inotlike.
Like many other comparison operators, -like returns different results, depending on if the object it acts upon is scalar or an array.
If argument is scalar, then the result is a bool object, i.e. True or False.
"Some string." -like "*string*"
True
"Some string." -like "*integer*"
False
But if we pass it an array, then the result will consist of the elements, that meet the condition.
"Winter", "Spring", "Summer", "Autumn" -like "*er"
Winter Summer
Besides simple comparisons, -like and -notlike operators can be used to specify more complex expressions.
Let’s start with getting all the services, that depend on the Remote Procedure Call (RPC) service.
Get-Service | Where-Object { $_.RequiredServices.Name -like "RpcSs" } | Select-Object Name, DisplayName, RequiredServices
Name DisplayName RequiredServices
---- ----------- ----------------
AppIDSvc Application Identity {RpcSs, CryptSvc, AppID}
Appinfo Application Information {RpcSs, ProfSvc}
AppVClient Microsoft App-V Client {AppvVfs, RpcSS, AppvStrm, netprofm}
AppXSvc AppX Deployment Service (AppXSVC) {rpcss, staterepository}
Audiosrv Windows Audio {AudioEndpointBuilder, RpcSs}
autotimesvc Cellular Time {rpcss}
AxInstSV ActiveX Installer (AxInstSV) {rpcss}
BFE Base Filtering Engine {RpcSs}
BITS Background Intelligent Transfer Service {RpcSs}
BrokerInfrastructure Background Tasks Infrastructure Service {DcomLaunch, RpcSs, RpcEptMapper}
...
As you can see, there are services that depend only on RpcSs, and services, that require some additional services.
Let’s get services, that depend on Remote Procedure Call (RPC) and some other services.
Get-Service | Where-Object { $_.RequiredServices.Name -like "RpcSs" -and $_.RequiredServices.Name -notlike "RpcSs" } | Select-Object Name, DisplayName, RequiredServices
Name DisplayName RequiredServices
---- ----------- ----------------
AppIDSvc Application Identity {RpcSs, CryptSvc, AppID}
Appinfo Application Information {RpcSs, ProfSvc}
AppVClient Microsoft App-V Client {AppvVfs, RpcSS, AppvStrm, netprofm}
AppXSvc AppX Deployment Service (AppXSVC) {rpcss, staterepository}
Audiosrv Windows Audio {AudioEndpointBuilder, RpcSs}
BrokerInfrastructure Background Tasks Infrastructure Service {DcomLaunch, RpcSs, RpcEptMapper}
...
Now we get the services, that depend on the RpcSs only.
Get-Service | Where-Object { $_.RequiredServices.Name -like "RpcSs" -and -not ($_.RequiredServices.Name -notlike "RpcSs") } | Select-Object Name, DisplayName, RequiredServices
Name DisplayName RequiredServices
---- ----------- ----------------
autotimesvc Cellular Time {rpcss}
AxInstSV ActiveX Installer (AxInstSV) {rpcss}
BFE Base Filtering Engine {RpcSs}
BITS Background Intelligent Transfer Service {RpcSs}
...
So, we can use the -like, -notlike, and -not operators to define rather uncommon conditions.
]]>
This type is used, for example, by the Property parameter of the Measure-Object cmdlet.
Get-Process pwsh | Measure-Object -Property CPU -Average
Count : 3 Average : 39.8645833333333 Sum : Maximum : Minimum : StandardDeviation : Property : CPU
In this case, as a -Property parameters value, we specified a string, but, the specificity of this type is that besides the strings, it can accept scriptblocks, and this allows us to pick for measurements not only already existing properties of object, but also the results of their transformations.
Get-Process pwsh | Measure-Object -Property {$_.Threads.Count} -Sum
Count : 3 Average : Sum : 71 Maximum : Minimum : StandardDeviation : Property : $_.Threads.Count
PowerShell 6.2 have brought us the Join-String cmdlet, that also uses PSPropertyExpression as a value type of the -Property parameter.
Get-Service R* | Join-String -Property { $_.DisplayName + "`n" + $_.Description } -Separator "`n`n"
Remote Access Auto Connection Manager Creates a connection to a remote network whenever a program references a remote DNS or NetBIOS name or address. Remote Access Connection Manager Manages dial-up and virtual private network (VPN) connections from this computer to the Internet or other remote networks. If this service is disabled, any services that explicitly depend on it will fail to start. ...
Also, this type is implicitly used by several cmdlets, and one of which is Select-Object.
Now let’s take a look at how we can use the Microsoft.PowerShell.Commands.PSPropertyExpression type.
But firstly, we get the pwsh process object, which we will use in the following examples.
$ps = Get-Process pwsh | Select-Object -First 1
We can create PSPropertyExpression object as follows.
$PathProperty = [pspropertyexpression]::new("Path")
To get the value of the specified property (in this case it is – Path) we use the GetValues method.
$PathProperty.GetValues($ps)
Result ResolvedExpression Exception ------ ------------------ --------- C:\Program Files\PowerShell\7\pwsh.exe Path
As a result we receive a Microsoft.PowerShell.Commands.PSPropertyExpressionResult object, which consists of the following properties: Result, which contains the value of the specified property, ResolvedExpression, where that property name is located, and Exception, which contains the exception, should one come across during the process of getting the property value.
If all we need is a value of the property specified, then we can get only the Result property of the received object.
$PathProperty.GetValues($ps).Result
C:\Program Files\PowerShell\7\pwsh.exe
We also can use a string with wildcards as an argument to create a new PSPropertyExpression object.
$MemorySize = [pspropertyexpression]::new("*MemorySize64")
The HasWildCardCharacters property of the PSPropertyExpression object now contains True.
$MemorySize.HasWildCardCharacters
True
Now, as in the previous example, let’s call the GetValues method.
$MemorySize.GetValues($ps)
Result ResolvedExpression Exception
------ ------------------ ---------
66896 NonpagedSystemMemorySize64
61038592 PagedMemorySize64
381112 PagedSystemMemorySize64
61190144 PeakPagedMemorySize64
2204026884096 PeakVirtualMemorySize64
61038592 PrivateMemorySize64
2204008886272 VirtualMemorySize64
When create a new PSPropertyExpression object using a string, we also can specify the second parameter of the bool type. Setting it to True will let object constructor know, that the first argument is in its final state and there is no need to try resolving its wildcards. This can be useful, for example, if your objects have properties with wildcard characters.
$WildcardProperty = [pspropertyexpression]::new("Property*Name?", $true)
One more feature of the PSPropertyExpression type is that it allows us to specify Property Sets.
For example, the process object has two property sets – PSConfiguration and PSResources.
Get-Process | Get-Member -MemberType PropertySet | Select-Object -Property Name
Name ---- PSConfiguration PSResources
The first of them – PSConfiguration references following properties.
$ps.PSConfiguration.ReferencedPropertyNames
Name Id PriorityClass FileVersion
Let’s create a PSPropertyExpression object using this property set.
$PSConfiguration = [pspropertyexpression]::new("PSConfiguration")
Now, if we call the GetValues method, then as a result we receive four PSPropertyExpressionResult objects, each of which corresponds to one of the properties referenced by PSConfiguration property set.
$PSConfiguration.GetValues($ps)
Result ResolvedExpression Exception ------ ------------------ --------- pwsh Name 12752 Id Normal PriorityClass 7.0.1.0 FileVersion
The GetValues method also have the second overload, which allows us to specify three parameters: target – as in the previous examples, it is the object, values of properties of which we get, expand – setting it to $false prevents resolving property set to its referenced properties, and eatExceptions – setting which to $false will terminate the method execution, should there be any exception throwed. By default, exceptions will only be seen in the Exception property of the PSPropertyExpressionResult object.
$PSConfiguration.GetValues($ps, $false, $false)
As you can guess, there will be no output from the previous command.
It is interesting, that using the PSPropertyExpression object based on the property set, we can get the PSPropertyExpression object for every property that property set references. We can do it by using the ResolveNames method.
$PSConfiguration.ResolveNames($ps)
Script HasWildCardCharacters
------ ---------------------
False
False
False
False
And while by default we can’t say, which one corresponds to which property, but we can find this out by using the ToString method.
$PSConfiguration.ResolveNames($ps) | ForEach-Object ToString
Name Id PriorityClass FileVersion
Also, the ResolveNames method have the second overload, that allows us to specify the expand parameter, setting which to $false, as in the case of the GetValues method, prevent the property set resolution to its referenced properties.
$PSConfiguration.ResolveNames($ps, $false)
As we already mentioned, besides the strings, the PSPropertyExpression type can accept a scriptblock, that opens even more possibilities for getting and transforming the values of different properties.
$WorkingSet = [pspropertyexpression]::new({$_.WorkingSet64 / 1mb})
$WorkingSet.GetValues($ps)
Result ResolvedExpression Exception
------ ------------------ ---------
98.66015625 $_.WorkingSet64 / 1mb
$WorkingSet.GetValues($ps).Result
98.66015625
So, what exactly the PSPropertyExpression type brings to us?
Indeed, concerning the case with specifying the existing property
[pspropertyexpression]::new("Path").GetValues($ps)
we definitely can get its value in the usual way.
$ps.Path
But when we specify a string containing wildcards, a property set, or a scriptblock, the PSPropertyExpression object allows us to get the values of the required properties without the necessity to use ForEach-Object or Select-Object cmdlets.
So, instead of
$ps | Select-Object -Property *MemorySize64
$ps | ForEach-Object -Process {$_.WorkingSet64 / 1mb}
we can use aforementioned commands
[pspropertyexpression]::new("*MemorySize64").GetValues($ps)
[pspropertyexpression]::new({$_.WorkingSet64 / 1mb}).GetValues($ps)
and to some extent reduce the amount of required resources and the execution time.
]]>Why is this needed? One of the cases – is that we can use delegate as a parameter for another method.
Is PowerShell supports this? Starting with version 6.1 – yes.
Let’s create a class, which would have some method for transforming strings, that it will receive as a parameter value. The second parameter of this method will accept method definition, which will actually transform the aforementioned string.
class TransformEngine
{
static [string] Transform([string] $string, [Func[string, string]] $function)
{
return $function.Invoke($string)
}
}
The first line defines the class name. Let it be TransformEngine.
Then we define the Transform method. For simplicity let’s make it static.
This method returns a string object – the transformation result of some string. Its first parameter – $string – is a source string, and the second parameter – $function – is the actual method for transforming this string. This method’s signature we specify in the $function parameter declaration.
Let’s look at this more closely.
The $function parameter’s data type is [Func[string, string]]. This means, that as a value we expect some method, which return type is the latter data type of the expression – [Func[string, string]], and it accepts parameters which correspond to all but the latter data type – [Func[string, string]]. In our case both the data types are strings.
If for example $function parameter’s type was [Func[int, string, bool]], that would mean that the parameter accepts methods with parameters of type int and string respectively and return type of bool.
In the Transform method’s body, we invoke the method specified in the $function parameter, pass the string from the $string parameter and return the execution result.
Now, let’s create a class, which will contain the methods we will use for string transformation. We’ll call it Transformers.
Strictly speaking, nothing prevents us from defining needed methods in the TransformEngine class, but in order to showcase, that these methods can come from anywhere, we place them in different class.
class Transformers
{
static [string] TitleCase([string] $string)
{
return (Get-Culture).TextInfo.ToTitleCase($string)
}
static [string] Reverse([string] $string)
{
[string] $reversedString = [string]::Empty
for ($i = $string.Length - 1; $i -ge 0; $i--)
{
$reversedString += $string[$i]
}
return $reversedString
}
static [string] WordCount([string] $string)
{
$wordCount = $string.Split().Count
return "The string consists of $wordCount words."
}
static [string] Highlight([string] $string)
{
$firstSpaceIndex = $string.IndexOf(" ")
return "`e[31m$($string.Substring(0, $firstSpaceIndex))`e[0m$($string.Substring($firstSpaceIndex))"
}
}
The first of the methods – TitleCase returns the string, where the first letter of every word is capital, and all the other are lowercase.
The Reverse method reverses the string.
WordCount reports how many words the string consists of.
And the Highlight method uses escape symbols to display the first word of the string in red.
As you can see, all of the methods accept the string as the only parameter and returns this same type of object.
For the string to transform let’s use the first sentence of the article about rabbits on Wikipedia.
$string = "Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika)."
Now let’s call the Transform method and specify every method from the Transformers class as $function parameter value.
[TransformEngine]::Transform($string, [Transformers]::TitleCase)
Rabbits Are Small Mammals In The Family Leporidae Of The Order Lagomorpha (Along With The Hare And The Pika).
$engine = [TransformEngine] $transformers = [Transformers] $engine::Transform($string, $transformers::Reverse)
.)akip eht dna erah eht htiw gnola( ahpromogaL redro eht fo eadiropeL ylimaf eht ni slammam llams era stibbaR
$wordCount = [Transformers]::WordCount [TransformEngine]::Transform($string, $wordCount)
The string consists of 19 words.
$highlight = [Transformers]::Highlight $transform = [TransformEngine]::Transform $transform.Invoke($string, $highlight)
Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).
Now let’s suppose we intend to separate the highlight functionality into its own classes – HighlightEngine and Highlighters, and besides already existing capability to highlight the first word with red, allow users to specify the number of words to highlight, and, additionally – to choose the color.
class HighlightEngine
{
static [string] Highlight([string] $string, [Func[string, string]] $function)
{
return $function.Invoke($string)
}
static [string] Highlight([string] $string, [int] $numberOfWords, [Func[string, int, string]] $function)
{
return $function.Invoke($string, $numberOfWords)
}
static [string] Highlight([string] $string, [int] $numberOfWords, [int] $color, [Func[string, int, int, string]] $function)
{
return $function.Invoke($string, $numberOfWords, $color)
}
}
So, the Highlight method, defined in HighlightEngine class, have three overloads:
static [string] Highlight([string] $string, [Func[string, string]] $function)
The first overload, as in the previous example, accepts a source string and a method used for its transformation.
static [string] Highlight([string] $string, [int] $numberOfWords, [Func[string, int, string]] $function)
The second one, besides the source string and the method definition, accepts the number of words to highlight. As you can see, the signature of the method for the $function parameter is different.
static [string] Highlight([string] $string, [int] $numberOfWords, [int] $color, [Func[string, int, int, string]] $function)
The third overload allows to specify the string, the number of words, the color and the transformation method. In this case the delegate’s signature – [Func[string, int, int, string]] – contains even more parameter types, required for string transformation.
Now let’s define the transformation methods. We’ll place them in the Highlighters class.
class Highlighters
{
static [int] GetIndex([string] $string, [int] $numberOfWords)
{
$index = 0
for ($i = 0; $i -lt $numberOfWords; $i++)
{
$index = $string.IndexOf(" ", ++$index)
}
return $index
}
static [string] Highlighter([string] $string)
{
$firstSpaceIndex = $string.IndexOf(" ")
return "`e[31m$($string.Substring(0, $firstSpaceIndex))`e[0m$($string.Substring($firstSpaceIndex))"
}
static [string] Highlighter([string] $string, [int] $numberOfWords)
{
$index = [Highlighters]::GetIndex($string, $numberOfWords)
return "`e[31m$($string.Substring(0, $index))`e[0m$($string.Substring($index))"
}
static [string] Highlighter([string] $string, [int] $numberOfWords, [int] $color)
{
$index = [Highlighters]::GetIndex($string, $numberOfWords)
return "`e[${color}m$($string.Substring(0, $index))`e[0m$($string.Substring($index))"
}
}
The Highlighters class consists of the GetIndex method, which is used for getting location of the space symbol, following the specified number of words, and also three overloads of the Highlighter method – one for every use case, as defined in the HighlightEngine class.
Now, if we call every one of the Highlight method overload, and in each case specify the same Highlighter method, we’ll see, that PowerShell automatically choose the overload, which signature corresponds the one, defined in the $function parameter.
[HighlightEngine]::Highlight($string, [Highlighters]::Highlighter)
Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).
[HighlightEngine]::Highlight($string, 4, [Highlighters]::Highlighter)
Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).
[HighlightEngine]::Highlight($string, 4, 32, [Highlighters]::Highlighter)
Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika).
Now, let’s suppose, we have decided to transfer word count calculation functionality into separate classes – CalculateEngine and Calculators – and, like in the previous example, to extend its capabilities.
The CalculateEngine class contains the method Calculate, that accepts the source string and the method to perform required calculations.
class CalculateEngine
{
static [object] Calculate([string] $string, [Func[string, object]] $function)
{
return $function.Invoke($string)
}
}
The Calculators class consists of the three methods.
class Calculators
{
static [string] WordCount([string] $string)
{
$wordCount = $string.Split().Count
return "The string consists of $wordCount words."
}
static [object] WordCountInt([string] $string)
{
return $string.Split().Count
}
static [pscustomobject] WordCountObject([string] $string)
{
$wordCount = $string.Split().Count
return [pscustomobject]@{
String = $string
WordCount = $wordCount
CharacterCount = $string.Length
}
}
}
First method – WordCount, is the same method we saw earlier, and it returns string object, which informs us about the quantity of words the string consist of.
The WordCountInt method returns the word count in a numerical form, specifically – int. In order for us to be able to return the int object using delegates, we specify [object] as a return type of the WordCountInt method.
The third method – WordCountObject – returns the PowerShell custom object – PSCustomObject, which contains three properties: the source string – String, its word count – WordCount, and the character count – CharacterCount.
Because every method returns a different type of objects, and we are going to specify them as the $function parameter of the same Calculate method, we need to take advantage of one of the features delegates possess – covariance.
Covariance means that delegates can be used with methods that have return types that are derived from the return type in the delegate signature.
This is why we specify object as a return type in the $function parameter signature.
[Func[string, object]] $function
Also, in order for the Calculate method to return different types of objects, its return type is specified as object too.
static [object] Calculate([string] $string, [Func[string, object]] $function)
Let’s test the methods.
[CalculateEngine]::Calculate($string, [Calculators]::WordCount)
The string consists of 19 words.
[CalculateEngine]::Calculate($string, [Calculators]::WordCountInt) 19
[CalculateEngine]::Calculate($string, [Calculators]::WordCountObject)
String WordCount CharacterCount ------ --------- -------------- Rabbits are small mammals in the family Leporidae of the order Lagomorpha (along with the hare and the pika). 19 109
Let’s make this example a little bit more complex and add the ability to use not only the string objects per se, but also to get string from the properties of some objects, say, instances of the MSFT_NetFirewallRule WMI class. In order to do that we are going to add the second overload to the Calculate method.
class CalculateEngine
{
static [object] Calculate([string] $string, [Func[string, object]] $function)
{
return $function.Invoke($string)
}
static [object] Calculate([CimInstance] $firewallRule, [Func[CimInstance, object]] $function)
{
return $function.Invoke($firewallRule)
}
}
This additional overload instead of string accepts a CimInstance object, which is an ancestor of the MSFT_NetFirewallRule class, and as a calculation method – the one, that accepts CimInstance and returns object.
Because we are not looking forward to creating three new calculation methods specifically for CimInstalnce objects in the Calculators class, we are going to make use of another feature of delegates – contravariance.
This means that delegates can be used with methods that have parameters whose types are base types of the delegate signature parameter type.
class Calculators
{
static [string] GetString([object] $input)
{
if ($input.pstypenames[0] -eq 'Microsoft.Management.Infrastructure.CimInstance#root/standardcimv2/MSFT_NetFirewallRule')
{
return $input.Description
}
else
{
return $input
}
}
static [string] WordCount([object] $input)
{
$string = [Calculators]::GetString($input)
$wordCount = $string.Split().Count
return "The string consists of $wordCount words."
}
static [object] WordCountInt([object] $input)
{
$string = [Calculators]::GetString($input)
return $string.Split().Count
}
static [pscustomobject] WordCountObject([object] $input)
{
$string = [Calculators]::GetString($input)
$wordCount = $string.Split().Count
return [pscustomobject]@{
String = $string
WordCount = $wordCount
CharacterCount = $string.Length
}
}
}
Calculators class now has additional method – GetString, which retrieves the string from the Description property of the MSFT_NetFirewallRule.
Concerning the three existing methods, besides adding a call to the GetString method, there were changes in the parameters name – it is $input now, instead of $string, and – most importantly – their type. Now it is [object], which, as we talked earlier, is an ancestor to string and CimInstance types, specified in the Calculate method overloads.
static [string] WordCount([object] $input)
static [object] WordCountInt([object] $input)
static [pscustomobject] WordCountObject([object] $input)
Before we try this out, let’s define the $firewallRule variable and set its value to the MSFT_NetFirewallRule object, corresponding to FPS-ICMP4-ERQ-In firewall rule.
$firewallRule = Get-NetFirewallRule -Name "FPS-ICMP4-ERQ-In"
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCount)
The string consists of 11 words.
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCountInt) 11
[CalculateEngine]::Calculate($firewallRule, [Calculators]::WordCountObject)
String WordCount CharacterCount ------ --------- -------------- Echo Request messages are sent as ping requests to other nodes. 11 63
As you can see, the changes in PowerShell 6.1 allow us to use delegates with classes and methods, which even further expands the number of available tools for developing in PowerShell.
]]>We can specify Primary and Secondary site collection administrators in the SharePoint Central Administration portal by clicking on Application Management and then on Change site collection administrators in the Site Collections section.
Primary and secondary administrators are those who, among other things, receive e-mail notifications about various SharePoint event, like when a site collection reaches its storage limit.
Also, you can specify list of other administrators in the Site Settings. To do this, on the site collection click the cog at the upper right part of the page and then click Site Settings. If there is no such a setting in the menu, click on the Site information and then on the View all site settings. Finally, click on the Site collection administrators in the Users and Permissions section.
Now, let’s do it with PowerShell.
The Primary site collections administrator is specified in the Owner property, and the Secondary administrator is in the SecondaryContact property of the Microsoft.SharePoint.SPSite object, which is returned by the Get-SPSite cmdlet.
Get-SPSite -Identity http://ServerName/SitePath | Select-Object -Property Owner, SecondaryContact
The values of both properties are the Microsoft.SharePoint.SPUser objects, so if you need a more detailed information about these administrators, you can use the following commands.
Get-SPSite -Identity http://ServerName/SitePath | Select-Object -ExpandProperty Owner Get-SPSite -Identity http://ServerName/SitePath | Select-Object -ExpandProperty SecondaryContact
On the other hand, if all you need is a username of the primary administrator, you can get the value of the OwnerLoginName property of the Microsoft.SharePoint.SPSite object.
Get-SPSite -Identity http://ServerName/SitePath | Select-Object -Property OwnerLoginName
You can change the Primary and Secondary administrators using Set-SPSite cmdlet and its -OwnerAlias and -SecondaryOwnerAlias parameters, respectively.
Set-SPSite -Identity http://ServerName/SitePath -OwnerAlias NewAdmin@domain.com -SecondaryOwnerAlias NewSecondaryAdmin@domain.com
You can get the list of administrators using the same Get-SPSite cmdlet. To do this, you should get the value of the RootWeb property of the Microsoft.SharePoint.SPSite object, and then get the SiteAdministrators property of the acquired object.
For example.
$site = Get-SPSite -Identity http://ServerName/SitePath $site.RootWeb.SiteAdministrators
Need to say, that this list contains all site collection administrators, including the Primary and Secondary administrators.
Also you can get site collection administrators list using the Get-SPUser cmdlet.
Get-SPUser -Web http://ServerName/SitePath | Where-Object -Property IsSiteAdmin
In order to specify a user as a site collection administrator, you can use Set-SPUser cmdlet and its -IsSiteCollectionAdmin parameter. Since it is a switch parameter, no value required.
Set-SPUser -Web http://ServerName/SitePath -Identity NewAdminIdentity -IsSiteCollectionAdmin
For example, the command might look like this.
Set-SPUser -Web http://sharepointserver/sites/site -Identity 'i:0#.w|domain\newadmin' -IsSiteCollectionAdmin
Removing a user from administrators occurs in a similar way. However, since, as we mentioned before, the -IsSiteCollectionAdmin is a switch parameter, in this case we should specify it as follows: -IsSiteCollectionAdmin:$false.
Set-SPUser -Web http://ServerName/SitePath -Identity NewAdminIdentity -IsSiteCollectionAdmin:$false
Links:
GitHub: https://github.com/sethvs
Twitter: https://twitter.com/vsseth
Facebook: https://fb.com/inpowershell
VK: https://vk.com/inpowershell