Skip to content
Menu
The Lonely Administrator
  • PowerShell Tips & Tricks
  • Books & Training
  • Essential PowerShell Learning Resources
  • Privacy Policy
  • About Me
The Lonely Administrator

Converting PowerShell Scripts to Functions

Posted on December 10, 2021December 10, 2021

Recently, I shared some PowerShell code to export a function to a file. It was a popular post. My friend Richard Hicks (no relation) thought we was joking when he asked about converting files to functions. His thought was to take a bunch of PowerShell scripts, turn them into a group of functions which could then be organized into a module. This is not that far-fetched. So I spent some time the last few days and came up with a PowerShell function to take an existing PowerShell script file and turn it into a PowerShell function.

Manage and Report Active Directory, Exchange and Microsoft 365 with
ManageEngine ADManager Plus - Download Free Trial

Exclusive offer on ADManager Plus for US and UK regions. Claim now!

Practically speaking, there is no difference between running the code inside a function and a script. A function is at its core a scriptblock with a name that makes it easier to run. The code inside the scriptblock is no different than what you might have in a stand-alone script. Depending on your script, you could simply wrap your script code in a Function declaration. Of course, the best practice is for functions to do a single task and write a single type of object to the pipeline so you might still need to edit your resulting function. What I came up with was a PowerShell tool using the AST to accelerate the process.

Basic Example

Here is an extremely simple PowerShell script.

#requires -version 3.0

#this is a sample script

Write-Host "this is a sample script that doesn't do anything but write a random number" -ForegroundColor Yellow

Get-Random -Minimum 1 -Maximum 1000

I want to turn this into a PowerShell function, so I'll use my new tool.

My Convert-ScriptToFunction command obviously needs the path to file. I also need to specify a name for the new function. The conversion generates a new function definition complete with comment-based help. The original script didn't have defined parameters, so the conversion defined them. If I wanted to save this to a file all I need to do is run the command and pipe to Out-File. I can then open the file in my scripting editor to polish it.

Getting Requirements

You'll notice that the new code included #requires statements from the original file. I wrote a separate function, also using the AST to get any requirements. In my New-SystemReport.ps1 script, I have these requirements.

#requires -version 5.1
#requires -module CimCmdlets

I can use this function to discover them.

Function Get-PSRequirements {
    [cmdletbinding()]
    Param(
        [Parameter(Position = 0, Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [string]$Path
    )
    Begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        New-Variable astTokens -Force
        New-Variable astErr -Force
    }
    Process {
        $Path = Convert-Path $path
        Write-Verbose "Processing $path"

        $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)
        #add the Path as a property
        if ($ast.ScriptRequirements) {
            $ast.ScriptRequirements | Add-Member -MemberType NoteProperty -Name "Path" -Value $Path-Force -PassThru
        }
        else {
            Write-Verbose "No requirements detected in $Path."
        }
    }
    End {
        Write-Verbose "Ending $($MyInvocation.MyCommand)"
    }
}

Comment-Based Help

I also wanted to be able to generate comment-based help if it wasn't already defined. Using the AST ParamBlock I create such a block. I'm always telling people to use the HelpMessage attribute, and if you do, it will be used for the .Parameter definition.

Function New-CommentHelp {
    [cmdletbinding()]
    [OutputType("string")]
    Param([System.Management.Automation.Language.ParamBlockAst]$ParamBlock)

    $h = [System.Collections.Generic.List[string]]::new()
    $h.Add("<#")
    $h.Add("`t.Synopsis")
    $h.Add("`t  <short description>")
    $h.add("`t.Description")
    $h.add("`t  <long description>")

    foreach ($p in $ParamBlock.Parameters) {
        $paramName = $p.name.variablepath.userpath
        $h.Add("`t.Parameter $paramName")
        $paramHelp = $p.Attributes.namedArguments.where({ $_.argumentname -eq 'helpmessage' })
        if ($paramHelp) {
            $h.add("`t  $($paramHelp.argument.value)")
        }
        else {
            $h.Add("`t  <enter a parameter description>")
        }
    }
    $h.add("`t.Example")
    $h.Add("`t  PS C:\> $Name")
    $h.Add("`t  <output and explanation>")
    $h.Add("`t.Link")
    $h.Add("`t  <enter a link reference>")
    $h.Add("#>")

    $h
}

Here's a sample script.

#requires -version 3.0

#this is a sample script

Param (
[Parameter(Position = 0,HelpMessage = "How many numbers do you want?")]
[int]$Count = 1,
[string]$Name,
[switch]$Demo
)

Write-Host "this is a sample script that doesn't do anything but write a random number" -ForegroundColor Yellow
    
#get numbers
1..$count | Foreach-Object {
Get-Random -Minimum 1 -Maximum 1000
}
    
Write-Host "Ending script" -ForegroundColor yellow

#eof

Using my conversion function I get this output.

#requires -version 3.0


# Function exported from C:\scripts\SampleScript3.ps1

Function Invoke-Sample {

<#
	.Synopsis
	  <short description>
	.Description
	  <long description>
	.Parameter Count
	  How many numbers do you want?
	.Parameter Name
	  <enter a parameter description>
	.Parameter Demo
	  <enter a parameter description>
	.Example
	  PS C:\> Invoke-Sample
	  <output and explanation>
	.Link
	  <enter a link reference>
#>


	[cmdletbinding()]
	Param (
	[Parameter(Position = 0,HelpMessage = "How many numbers do you want?")]
	[int]$Count = 1,
	[string]$Name,
	[switch]$Demo
	)


Write-Host "this is a sample script that doesn't do anything but write a random number" -ForegroundColor Yellow
1..$count | Foreach-Object {
Get-Random -Minimum 1 -Maximum 1000
}
Write-Host "Ending script" -ForegroundColor yellow

} #close Invoke-Sample

Function Names

Because I have to define a new function name, I wanted this to be as simple as possible. I use this helper function to format the name to the proper case. I'm assuming a Verb-Noun naming convention.

Function Format-FunctionName {
    [cmdletbinding()]
    Param (
        [ValidateScript({
        if ($_ -match "^\w+-\w+$") {
            $true
        }
        else {
            Throw "Your function name should have a Verb-Noun naming convention"
            $False
        }
    })]
    [string]$Name
    )

    $split = $name -split "-"
    "{0}{1}-{2}{3}" -f $split[0][0].ToString().ToUpper(), $split[0].Substring(1).Tolower(),$split[1][0].ToString().ToUpper(), $split[1].Substring(1).ToLower()

}

It isn't always perfect, especially if your noun is complex like this example. The function also doesn't validate that you are using a standard verb. However, I have an argument completer that will insert a standard verb.

Register-ArgumentCompleter -CommandName Convert-ScriptToFunction -ParameterName Name -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)

    #PowerShell code to populate $wordtoComplete
    Get-Verb | Where-Object {$_.verb -match "^$wordToComplete"} |
    ForEach-Object {
        #this will autocomplete with Verb-
        [System.Management.Automation.CompletionResult]::new("$($_.verb)-", $_.verb, 'ParameterValue', $_.Group)
    }
}

All I need to do is begin typing the verb name and hit Tab. The completion will insert the verb and the dash. All I need to type is the noun.

Working with Output

My function does exactly what it says it does. It converts. It is up to you to decide how you want to use the output. You can pipe to Out-File or Set-Clipboard. But since I expect you will be immediately editing your output, I added a dynamic parameter (use my New-PSDynamicParameterForm function) called ToEditor. This parameter will be defined if you run the function in the PowerShell ISE or VS Code. The output will be opened in a new, unsaved file.

Again, here's the source file. These are script files I created merely to test my function.

#requires -version 4.0
#requires -runasAdministrator

#this is a sample script

Param (
[Parameter(Position = 0,HelpMessage = "How many numbers do you want?")]
[ValidateRange(1,100)]
[int]$Count = 1
)
DynamicParam {
    #this is a sample dynamic parameter
    If ($True) {

    $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary

    # Defining parameter attributes
    $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
    $attributes = New-Object System.Management.Automation.ParameterAttribute
    $attributes.ParameterSetName = '__AllParameterSets'
    $attributeCollection.Add($attributes)

    # Defining the runtime parameter
    $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('Demo', [String], $attributeCollection)
    $paramDictionary.Add('Demo', $dynParam1)

    return $paramDictionary
} # end if
} #end DynamicParam

Begin {
    Write-Host "this is a sample script that doesn't do anything but write a random number" -ForegroundColor Yellow
}
Process {
    #get numbers
    1..$count | Foreach-Object {
    Get-Random -Minimum 1 -Maximum 1000
    }
}
End {
    write-host "Ending script" -ForegroundColor yellow
}
#eof

I'll run this code in the integrated PowerShell terminal in VS Code to create a new file. The function will also get a new alias.

Convert-ScriptToFunction .\SampleScript4.ps1 -Name Invoke-Sample -Alias ins -ToEditor

I can then use VSCode to clean up the file such as formatting, expanding aliases, and converting tabs to spaces. Then I can save the file. The output includes a reference to the original source file.

Convert-ScriptToFunction

Here is the conversion function.

Function Convert-ScriptToFunction {
    <#
    .Synopsis
    Convert a script file to a PowerShell function.
    .Description
    This command takes the body of a script file and wraps it in a function
    declaration. The command will insert missing elements like [cmdletbinding()]
    and comment-based help. You will most likely need to edit and clean up the
    result in your scripting editor.

    If you run this command in the PowerShell ISE or the VS Code PowerShell
    integrated terminal, you can use the dynamic parameter ToEditor to open a
    new file with with the output. You can edit and save the file manually.

    It is assumed that your script file is complete and without syntax errors.
    .Parameter Path
    Enter the path to your PowerShell script file.
    .Parameter Name
    What is the name of your new function? It should have a Verb-Noun name.
    .Parameter Alias
    Define an optional alias for your new function.
    .Parameter ToEditor
    If you run this command in the PowerShell ISE or the VS Code PowerShell
    integrated terminal, you can use this dynamic parameter to open a new
    file with with the output. You can edit and save the file manually.
    .Example
    PS C:\> Convert-ScriptToFunction c:\scripts\Daily.ps1 -name Invoke-DailyTask | Set-Clipboard

    Convert Daily.ps1 to a function called Invoke-DailyTask and copy the
    results to the Windows clipboard. You can then paste the results into
    scripting editor.
    .Example
    PS C:\> Convert-ScriptToFunction c:\scripts\systemreport.ps1 -name New-SystemReport | Out-File c:\scripts\New-SystemReport.ps1

    Convert the SystemReport.ps1 script file to a function called
    New-SystemReport and save the results to a file.
    .Example
    PS C:\> Convert-ScriptToFunction c:\scripts\systemreport.ps1 -name New-System -alias nsr | Tee-Object -variable f

    Convert the script to a function called New-System and tee the output to $f.
    This will also define an function alias of nsr.
    #>
    [cmdletbinding()]
    [Outputtype("System.String")]
    [alias('csf')]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName,
            HelpMessage = "Enter the path to your PowerShell script file."
        )]
        [ValidateScript({Test-Path $_ })]
        [ValidatePattern("\.ps1$")]
        [string]$Path,

        [Parameter(
            Position = 1,
            Mandatory,
            ValueFromPipelineByPropertyName,
            HelpMessage = "What is the name of your new function?")]
        [ValidateScript({
            if ($_ -match "^\w+-\w+$") {
                $true
            }
            else {
                Throw "Your function name should have a Verb-Noun naming convention"
                $False
            }
        })]
        [string]$Name,

        [Parameter(ValueFromPipelineByPropertyName,HelpMessage = "Specify an optional alias for your new function. You can define multiple aliases separated by commas.")]
        [ValidateNotNullOrEmpty()]
        [string[]]$Alias
    )
    DynamicParam {
        <#
        If running this function in the PowerShell ISE or VS Code,
        define a ToEditor switch parameter
        #>
        If ($host.name -match "ISE|Code") {

            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary

            # Defining parameter attributes
            $attributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $attributes = New-Object System.Management.Automation.ParameterAttribute
            $attributes.ParameterSetName = '__AllParameterSets'
            $attributeCollection.Add($attributes)

            # Defining the runtime parameter
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter('ToEditor', [Switch], $attributeCollection)
            $paramDictionary.Add('ToEditor', $dynParam1)

            return $paramDictionary
        } # end if
    } #end DynamicParam
    Begin {
        Write-Verbose "Starting $($MyInvocation.MyCommand)"
        Write-Verbose "Initializing"
        New-Variable astTokens -Force
        New-Variable astErr -Force
        $new = [System.Collections.Generic.list[string]]::new()
    } #begin
    Process {
        #normalize
        $Path = Convert-Path $path
        $Name = Format-FunctionName $Name

        Write-Verbose "Processing $path"
        $AST = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$astTokens, [ref]$astErr)

        if ($ast.extent) {
            Write-Verbose "Getting any comment based help"
            $ch = $astTokens | Where-Object { $_.kind -eq 'comment' -AND $_.text -match '\.synopsis' }

            if ($ast.ScriptRequirements) {
                Write-Verbose "Adding script requirements"
               if($ast.ScriptRequirements.RequiredPSVersion) {
                   $new.Add("#requires -version $($ast.ScriptRequirements.RequiredPSVersion.ToString())")
               }
               if ($ast.ScriptRequirements.RequiredModules) {
                    Foreach ($m in $ast.ScriptRequirements.RequiredModules) {
                        #test for version requirements
                        $ver = $m.psobject.properties.where({$_.name -match 'version' -AND $_.value})
                        if ($ver) {
                            $new.Add("#requires -module @{ModuleName = '$($m.name)';$($ver.Name) = '$($ver.value)'}")
                        }
                        else {
                            $new.add("#requires -module $($m.Name)")
                        }
                    }
               }
               if ($ast.ScriptRequirements.IsElevationRequired) {
                    $new.Add("#requires -RunAsAdministrator")
               }
               If ($ast.ScriptRequirements.requiredPSEditions) {
                    $new.add("#requires -PSEdition $($ast.ScriptRequirements.requiredPSEditions)")
               }

               $new.Add("`n")
            }
            else {
                Write-Verbose "No script requirements found"
            }


           $head = @"
# Function exported from $Path

Function $Name {

"@
        $new.add($head)

            if ($ch) {
                $new.Add($ch.text)
                $new.Add("`n")
            }
            else {
                Write-Verbose "Generating new comment based help from parameters"
                New-CommentHelp -ParamBlock $ast.ParamBlock | Foreach-Object { $new.Add("$_")}
                $new.Add("`n")
            }

            [regex]$rx = "\[cmdletbinding\(.*\)\]"
            if ($rx.Ismatch($ast.Extent.text)) {
                Write-Verbose "Using existing cmdletbinding"
                #use the first match
                $cb = $rx.match($ast.extent.text).Value
                $new.Add("`t$cb")
            }
            else {
                 Write-Verbose "Adding [cmdletbinding()]"
               $new.Add("`t[cmdletbinding()]")
            }

           if ($alias) {
                Write-Verbose "Adding function alias definition $($alias -join ',')"
                $new.Add("`t[Alias('$($alias -join "','")')]")
           }
            if ($ast.ParamBlock) {
                Write-Verbose "Adding defined Param() block"
                [void]($ast.ParamBlock.tostring().split("`n").Foreach({$new.add("`t$_")}) -join "`n")
                $new.Add("`n")
            }
            else {
                Write-Verbose "Adding Param() block"
                $new.add("`tParam()")
            }
            if ($ast.DynamicParamBlock) {
                #assumes no more than 1 dynamic parameter
                Write-Verbose "Adding dynamic parameters"
                [void]($ast.DynamicParamBlock.tostring().split("`n").Foreach({$new.Add($_)}) -join "`n")
            }

            if ($ast.BeginBlock.Extent.text) {
                Write-Verbose "Adding defined Begin block"
                [void]($ast.BeginBlock.Extent.toString().split("`n").Foreach({$new.Add($_)}) -join "`n")
                $UseBPE = $True
            }

            if ($ast.ProcessBlock.Extent.text) {
                Write-Verbose "Adding defined Process block"
                [void]($ast.ProcessBlock.Extent.ToString().split("`n").Foreach({$new.add($_) }) -join "`n")
            }

            if ($ast.EndBlock.Extent.text) {
                if ($UseBPE) {
                    Write-Verbose "Adding opening End{} block"
                    $new.Add("`tEnd {")
                }
                    Write-Verbose "Adding the remaining code or defined endblock"
                    [void]($ast.Endblock.Statements.foreach({ $_.tostring() }).Foreach({ $new.Add($_)}))
                if ($UseBPE) {
                Write-Verbose "Adding closing End {} block"
                    $new.Add("`t}")
                }
            }
            else {
                $new.Add("End { }")
            }
            Write-Verbose "Closing the function"
           $new.Add( "`n} #close $name")

           if ($PSBoundParameters.ContainsKey("ToEditor")) {
                Write-Verbose "Opening result in editor"
                if ($host.name -match "ISE") {
                    $newfile = $psise.CurrentPowerShellTab.Files.add()
                    $newfile.Editor.InsertText(($new -join "`n"))
                    $newfile.editor.select(1,1,1,1)
                }
                elseif ($host.name -match "Code") {
                    $pseditor.Workspace.NewFile()
                    $ctx = $pseditor.GetEditorContext()
                    $ctx.CurrentFile.InsertText($new -join "`n")
                }
                else {
                    $new -join "`n" | Set-Clipboard
                    Write-Warning "Can't detect the PowerShell ISE or VS Code. Output has been copied to the clipboard."
                }
        }
        else {
            Write-Verbose "Writing output [$($new.count) lines] to the pipeline"
            $new -join "`n"
        }
        } #if ast found
        else {
            Write-Warning "Failed to find a script body to convert to a function."
        }

    } #process
    End {
        Write-Verbose "Ending $($MyInvocation.mycommand)"
    }
}

The function assumes your script file is syntactically complete and without error. Understand that not every script file and can be converted into a PowerShell function that you can immediately use. The convert function is making the best effort possible. You should look at this as a tool to accelerate your scripting work. I assume I will need to edit the new file. But this is a good start.

I can take an old PowerShell script like this:

#requires -version 3.0

#Basic-HotFixReport.ps1

Param([string[]]$Computername = $env:COMPUTERNAME)

$ErrorActionPreference = "SilentlyContinue"

Get-Hotfix -ComputerName $Computername | 
Select-Object -Property PSComputername,HotFixID,Description,InstalledBy,InstalledOn,
@{Name="Online";Expression={$_.Caption}}

And create a new function:

csf .\Basic-HotfixReport.ps1 -Name Get-HotFixReport -Alias ghfr | out-file c:\scripts\get-hotfixreport.ps1

My function has an alias of csf. After editing the file to bring it up-to-date, I have something like this.

#requires -version 5.1

# Function exported from C:\scripts\Basic-HotfixReport.ps1

Function Get-HotfixReport {

    <#
    .Synopsis
      Get a hotfix report
    .Description
      Use this command to get a report of installed hotfixes on a computer.
    .Parameter Computername
     Enter the name of a computer.
    .Example

PS C:\scripts> Get-HotfixReport thinkp1 | format-table


Computername HotFixID  Description InstalledBy         InstalledOn            Online
------------ --------  ----------- -----------         -----------            ------
THINKP1      KB5006363 Update      NT AUTHORITY\SYSTEM 11/6/2021 12:00:00 AM  http://support.microsoft.com/?kbid=5006363
THINKP1      KB5004567 Update      NT AUTHORITY\SYSTEM 7/4/2021 12:00:00 AM   https://support.microsoft.com/help/5004567
THINKP1      KB5008295 Update      NT AUTHORITY\SYSTEM 11/6/2021 12:00:00 AM  https://support.microsoft.com/help/5008295
THINKP1      KB5007262 Update      NT AUTHORITY\SYSTEM 11/22/2021 12:00:00 AM https://support.microsoft.com/help/5007262
THINKP1      KB5007414 Update      NT AUTHORITY\SYSTEM 11/13/2021 12:00:00 AM

    .Link
      Get-HotFix
#>

    [cmdletbinding()]
    [Alias('ghfr')]
    Param([string[]]$Computername = $env:COMPUTERNAME)

    Try {
        Get-HotFix -ComputerName $Computername -ErrorAction Stop |
        Select-Object -Property @{Name = "Computername"; Expression = { $_.CSName } },
        HotFixID, Description, InstalledBy, InstalledOn,
        @{Name = "Online"; Expression = { $_.Caption } }
    }
    Catch {
        Throw $_
    }

} #close Get-Hotfixreport

I'd probably continue to refine the function.

Building a Module

I'll wrap this up with a proof-of-concept. Assuming I know the script files I want to use and the function names I want to assign, I could use a PowerShell script to quickly build a module.

#a proof of concept to convert scripts to a new module

#dot source the conversion functions
. C:\scripts\dev-scripttofunction.ps1

$NewModuleName = "PSMagic"
$Description = "A sample module"
$ParentPath = "C:\work"
$path = New-Item -Name $NewModuleName -Path $ParentPath -ItemType Directory -Force

#create the module structure
"docs", "functions", "en-us", "formats" |
ForEach-Object { New-Item -Path $path -Name $_ -ItemType Directory }

#file data
$data = @"
"Path","Name"
"C:\scripts\SampleScript.ps1","Get-Foo"
"C:\scripts\SampleScript2.ps1","Set-Foo"
"C:\scripts\SampleScript3.ps1","Invoke-Foo"
"C:\scripts\SampleScript4.ps1","Remove-Foo"
"C:\scripts\SampleScript5.ps1","Test-Foo"
"@

$csv = $data | ConvertFrom-Csv
foreach ($item in $csv) {
    $out = Join-Path $path\functions "$($item.name).ps1"
    $item | Convert-ScriptToFunction |  Out-File -FilePath $out
    Get-Item $out

} #foreach item

#create the root module
$psm1 = @"

Get-Childitem `$psscriptroot\functions\*.ps1 |
Foreach-Object {
. `$_.FullName
}

"@

$psm1 | Out-File "$path\$newmodulename.psm1"

#create the module manifest
$splat = @{
    Path                 = "$path\$newmodulename.psd1"
    RootModule           = "$path\$newmodulename.psm1"
    ModuleVersion        = "0.1.0"
    Author               = "Jeff Hicks"
    Description          = $Description
    FunctionsToExport    = $csv.name
    PowerShellVersion    = "5.1"
    CompatiblePSEditions = "Desktop"
}
New-ModuleManifest @splat

Get-ChildItem $path

Running this script quickly builds my module.

Naturally, there would still be editing and revisions, but this gives me a huge jump start on the process.

Next Steps

I hope some of you will give this code a spin and let me know what you think. Remember, it probably won't generate perfect PowerShell functions. I think I now have enough commands that I might bundle all of this together into a new module. In fact, I can use the tools themselves to build the module. Talk about meta!


Behind the PowerShell Pipeline

Share this:

  • Click to share on X (Opens in new window) X
  • Click to share on Facebook (Opens in new window) Facebook
  • Click to share on Mastodon (Opens in new window) Mastodon
  • Click to share on LinkedIn (Opens in new window) LinkedIn
  • Click to share on Pocket (Opens in new window) Pocket
  • Click to share on Reddit (Opens in new window) Reddit
  • Click to print (Opens in new window) Print
  • Click to email a link to a friend (Opens in new window) Email

Like this:

Like Loading...

Related

2 thoughts on “Converting PowerShell Scripts to Functions”

  1. Pingback: Converting PowerShell Scripts to Functions - The Lonely Administrator - Syndicated Blogs - IDERA Community
  2. Pingback: Building a PowerShell Module Inception-Style • The Lonely Administrator

Comments are closed.

reports

Powered by Buttondown.

Join me on Mastodon

The PowerShell Practice Primer
Learn PowerShell in a Month of Lunches Fourth edition


Get More PowerShell Books

Other Online Content

github



PluralSightAuthor

Active Directory ADSI Automation Backup Books CIM CLI conferences console Friday Fun FridayFun Function functions Get-WMIObject GitHub hashtable HTML Hyper-V Iron Scripter ISE Measure-Object module modules MrRoboto new-object objects Out-Gridview Pipeline PowerShell PowerShell ISE Profile prompt Registry Regular Expressions remoting SAPIEN ScriptBlock Scripting Techmentor Training VBScript WMI WPF Write-Host xml

©2025 The Lonely Administrator | Powered by SuperbThemes!
%d