I migrated from an Apple MacBook to a Windows 10 laptop earlier in the year. Ever since I’ve been busy grokking the world of PowerShell and .NET Core .
As this little learning project has to fit around all my other activities, it’s very much still a work in progress. But I’ve started to share some of my discoveries with PaperCut colleagues, and thought I’d also share them with you.
Note these tips assume you have some PowerShell experience. However, even if you haven’t used PowerShell , you might find it useful to cut and paste these examples into your TEST PaperCut MF/NG server system to see how they work.
There are a number of PowerShell details I’ve glossed over, so further experimentation and reading are very much encouraged.
Tip 1: Create a PowerShell function to make it easy to call the PaperCut server-command utility
Tip 2: The location of the profile is hard to remember
Tip 3: List the PaperCut MF/NG Windows services
Tip 4: Make frequent use of the Get-Member cmdlet!
Tip 5: Use the Select-Object and Where-Object cmdlets to locate and process data
Tip 6: If life gives you lemons strings, make lemonade custom objects*
Tip 7: Use server-command’s get-group-members subcommand instead of list-user-accounts
Tip 1: Create a PowerShell function to make it easy to call the PaperCut server-command utility
Add the following to your PowerShell profile:
Function server-command { &'C:\Program Files\PaperCut MF\server\bin\win\server-command.exe' $Args }
Note the server-command program only runs on the PaperCut MF/NG application server.
Tip 2: The location of the profile is hard to remember
It also varies depending on which version of PowerShell and which operating system you are using. Just use the “magic” variable $PROFILE
instead. For example:
code $PROFILE
(I use VS Code to edit my files)
Tip 3: List the PaperCut MF/NG Windows services
You can find a list of the running PaperCut processes with the Get-Service cmdlet as follows:
Get-Service -DisplayName *PaperCut*
The cmdlet displays a limited amount of information by default, so perhaps more usefully.
Get-Service -DisplayName *PaperCut* | Format-Table -Property Name, Status, StartType
Here’s more about Format-Table
Tip 4: Make frequent use of the Get-Member
cmdlet
The biggest difference between PowerShell and other shells (for instance cmd.exe or Bash) is that PowerShell processes objects, while other shells only process text strings (technically that’s an oversimplification, but it will do for now).
This means that the type of the objects passed between PowerShell commands matters a lot! You can discover the details of an object type using the Get-Member cmdlet.
Let’s compare a couple of the previous examples, but add the Get-Member command to see the output type instead:
Get-Service | Where-Object -Property DisplayName -like -Value "PaperCut*" | Get-Member
And we get:
TypeName: System.ServiceProcess.ServiceController Name MemberType Definition ---- ---------- ---------- Name AliasProperty Name = ServiceName RequiredServices AliasProperty RequiredServices = Services... Disposed Event System.EventHandler Dispose... Close Method void Close() Continue Method void Continue() …
But if we run the pipeline …
Get-Service -DisplayName *PaperCut* | Format-Table -Property Name, Status, StartType | Get-Member
… the output type is very different:
`TypeName: Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
Name MemberType Definition
-— ———- ———- Equals Method bool Equals(Syst…
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
autosizeInfo Property Microsoft.PowerS…
ClassId2e4f51ef21dd47e99d3c952918aff9cd Property string ClassId2e…
groupingEntry Property Microsoft.PowerS…
pageFooterEntry Property Microsoft.PowerS…
pageHeaderEntry Property Microsoft.PowerS…
shapeInfo Property Microsoft.PowerS…
TypeName: Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
Name MemberType Definition
-— ———- ———- Equals Method bool Equals(Syst…
…`
So Format-Table
creates output suitable for display purposes and should not be used in the middle of a pipeline when data requires further processing. Which leads us to …
Tip 5: Use the Select-Object and Where-Object cmdlets to locate and process data
See the previous tip for some hints on why. Here’s an example:
Get-Service | Where-Object -Property DisplayName -like -Value "PaperCut*" | Select-Object -Property ServiceName,Status,StartType
The output from this pipeline is still a ServiceController type, which can be further processed, so, for example, we can convert it to JSON:
Get-Service | Where-Object -Property DisplayName -like -Value "PaperCut*" | Select-Object -Property ServiceName,Status,StartType | ConvertTo-Json
Tip 6: If life gives you lemons strings, make lemonade some custom objects*
* With apologies to Elbert Hubbard
The server-command utility usually returns strings, and often we need to make multiple calls to discover the various pieces of information we need. For example, suppose we want to create a list of users with a Boolean flag to indicate if they are internal or external user accounts. This involves using the subcommand list-user-accounts to get a list of all the user names, and then iterating over the list to get the value of the user’s internal property. Note that the property value is returned as a string (“true”/“false”) rather than a Boolean. How can we wrap this to make it more PowerShell compatible?
The “trick” is that we can convert a string “true” or “false into the corresponding Boolean value with the
[System.Convert]::ToBoolean
method. So to discover if a user is an internal user let’s try:
pc-sc get-user-property <user-name> internal
over all the user accounts, and see what types are returned:
pc-sc list-user-accounts | ForEach-Object {pc-sc get-user-property $_ internal} | Get-Member
Gives us:
TypeName: System.String
Now use ToBoolean()
and convert strings to Boolean after each get-user-property
call:
pc-sc list-user-accounts | ForEach-Object {[System.Convert]::ToBoolean((pc-sc get-user-property $_ internal))} | Get-Member
(Yes, you really do need the ((.. )) double parentheses).
True
So now I can use Foreach-Object
to create a hash table of the user name and a Boolean flag for the internal status:
`pc-sc list-user-accounts |
Foreach-Object {
@{
user=$;
internal=[System.Convert]::ToBoolean((pc-sc get-user-property $ internal))
}
}
Name Value
-— —– user ahmed
internal False
user guest-1
internal True`
And we use the Get-Member
command to get the type:
TypeName: System.Collections.Hashtable
We can now take it one stage further and convert the hash table (name value pairs) into a custom PowerShell object:
`pc-sc list-user-accounts |
Foreach-Object {
[PSCustomObject]@{
user=$;
internal=[System.Convert]::ToBoolean((pc-sc get-user-property $ internal))
}
}
user internal
-— ——– ahmed False
guest-1 True
guest-2 True
jane False
john False`
And Get-Member
tells us:
`TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
-— ———- ———-
Equals Method bool Equals(System.Object obj)
GetHashCode Method int GetHashCode()
GetType Method type GetType()
ToString Method string ToString()
internal NoteProperty bool internal=False
user NoteProperty System.String user=ahmed`
So what’s the point? Well, now I can wrap it in a function and use it to solve a number of admin tasks. I’ve extended the above example into a function that returns an object for each user with properties for:
- username
- internal flag
- account balance
Function get-pc-users-balance-and-account-type { pc-sc list-user-accounts | Foreach-Object { [PSCustomObject]@{ user=$_; internal= [System.Convert]::ToBoolean((pc-sc get-user-property $_ internal)); balance= [System.Convert]::ToSingle((pc-sc get-user-property $_ balance)); } } }
And then, for instance, I can use the new function to give an extra $5.50 of credit to all internal users who have a balance less than $5.00:
get-pc-users-balance-and-account-type | Where-Object {$.internal -and $.balance -le 5.0 } | Foreach-Object { pc-sc adjust-user-account-balance $.user 5.5 “By Powershell”; Write-Output “$($.user) got an extra `$5.5” }
Tip 7: Use server-command’s get-group-members
subcommand instead of list-user-accounts
In tip 6 we found the balance and internal flag for every user account in PaperCut MF/NG, but then only processed internal users. This is often a very small subset of all the users in the system, though. We could be wasting a lot of time retrieving information we never need.
So let’s change the example a bit:
Function get-pc-
internal-users
-and-balance { pc-sc
get-group-members
"!!Internal Users!!" | Foreach-Object { [PSCustomObject]@{ user=$_; internal=$True; balance= [System.Convert]::ToSingle((pc-sc get-user-property $_ balance)); } } }
Note I’ve hard-coded the internal flag …
internal=$True;
… so that the original business logic is the same:
get-pc-
internal-users
-and-balance | Where-Object {$.internal -and $.balance -le 5.0 } | Foreach-Object { pc-sc adjust-user-account-balance $.user 5.5 “By Powershell”; Write-Output “$($.user) got an extra `$5.5” }
<Aaaaaand that’s enough PowerShell tips for now – Ed>
In a future post, I’ll show you how you can use the PaperCut MF/NG health API in PowerShell to get more detailed information about the state of your application server (hint, use the Invoke-RestMethod cmdlet).
Future Work: I’m currently trying to work out how to use the XML-RPC web services API directly from PowerShell via .NET Core. The current approach depends on .NET Foundation which will become a legacy framework at some point.