Compartir a través de


Capítulo 9: Funciones

Los comandos de una línea de PowerShell y los scripts que tienen que modificarse a menudo son buenos candidatos para convertirlos en funciones reutilizables.

Escriba funciones siempre que sea posible porque están más orientadas a herramientas. Puede agregar las funciones a un módulo de script, colocar ese módulo en una ubicación definida en la $env:PSModulePath y llamar a las funciones sin necesidad de localizar dónde guardó las funciones. Con el módulo PowerShellGet, es fácil compartir sus módulos de PowerShell en un repositorio de NuGet. PowerShellGet viene incluido con PowerShell versión 5.0 y posteriores. También está disponible como descarga independiente de la versión 3.0 de PowerShell y de versiones posteriores.

No compliques demasiado las cosas. Elija la manera más directa de hacer una tarea y simplifique los procesos. Evite los alias y los parámetros posicionales en todo código que reutilice. Dé formato al código para facilitar la lectura. No codifique los valores; use parámetros y variables. No escriba código innecesario, aunque no perjudique a nada. Agrega una complejidad innecesaria. La atención a los detalles es crucial al escribir cualquier código en PowerShell.

Nomenclatura

Al asignar nombres a las funciones de PowerShell, use un nombre en Pascal case con un verbo aprobado y un sustantivo en singular. Para obtener una lista de verbos aprobados en PowerShell, ejecute Get-Verb. El siguiente ejemplo ordena los resultados de Get-Verb por la propiedad Verbo.

Get-Verb | Sort-Object -Property Verb

La propiedad Group le da una idea de cómo se deben usar los verbos.

Verb        Group
----        -----
Add         Common
Approve     Lifecycle
Assert      Lifecycle
Backup      Data
Block       Security
Checkpoint  Data
Clear       Common
Close       Common
Compare     Data
Complete    Lifecycle
Compress    Data
Confirm     Lifecycle
Connect     Communications
Convert     Data
ConvertFrom Data
ConvertTo   Data
Copy        Common
Debug       Diagnostic
Deny        Lifecycle
Disable     Lifecycle
Disconnect  Communications
Dismount    Data
Edit        Data
Enable      Lifecycle
Enter       Common
Exit        Common
Expand      Data
Export      Data
Find        Common
Format      Common
Get         Common
Grant       Security
Group       Data
Hide        Common
Import      Data
Initialize  Data
Install     Lifecycle
Invoke      Lifecycle
Join        Common
Limit       Data
Lock        Common
Measure     Diagnostic
Merge       Data
Mount       Data
Move        Common
New         Common
Open        Common
Optimize    Common
Out         Data
Ping        Diagnostic
Pop         Common
Protect     Security
Publish     Data
Push        Common
Read        Communications
Receive     Communications
Redo        Common
Register    Lifecycle
Remove      Common
Rename      Common
Repair      Diagnostic
Request     Lifecycle
Reset       Common
Resize      Common
Resolve     Diagnostic
Restart     Lifecycle
Restore     Data
Resume      Lifecycle
Revoke      Security
Save        Data
Search      Common
Select      Common
Send        Communications
Set         Common
Show        Common
Skip        Common
Split       Common
Start       Lifecycle
Step        Common
Stop        Lifecycle
Submit      Lifecycle
Suspend     Lifecycle
Switch      Common
Sync        Data
Test        Diagnostic
Trace       Diagnostic
Unblock     Security
Undo        Common
Uninstall   Lifecycle
Unlock      Common
Unprotect   Security
Unpublish   Data
Unregister  Lifecycle
Update      Data
Use         Other
Wait        Lifecycle
Watch       Common
Write       Communications

Es importante utilizar un verbo aprobado para sus funciones de PowerShell. Los módulos que contienen funciones con verbos no aprobados generan un mensaje de advertencia cuando se importan en una sesión de PowerShell. Ese mensaje de advertencia hará que las funciones no parezcan profesionales. Los verbos no aprobados también limitan la detectabilidad de las funciones.

Una función simple

Una función en PowerShell se declara con la palabra clave function seguida del nombre de la función y luego una llave de apertura y cierre ({ }). El código ejecutado por la función está contenido dentro de esas llaves.

function Get-Version {
    $PSVersionTable.PSVersion
}

La función que se muestra en el ejemplo siguiente es un ejemplo sencillo que devuelve la versión de PowerShell.

Get-Version
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Cuando se usa un nombre genérico para las funciones, como Get-Version, podría provocar conflictos de nomenclatura. Los comandos predeterminados agregados en el futuro o los comandos que otros podrían escribir podrían entrar en conflicto con ellos. Anteponga un prefijo a la parte del sustantivo de los nombres de las funciones para evitar conflictos de nombres. Por ejemplo: <ApprovedVerb>-<Prefix><SingularNoun>.

En el ejemplo siguiente se usa el prefijo PS.

function Get-PSVersion {
    $PSVersionTable.PSVersion
}

Más allá del nombre, esta función es idéntica a la anterior.

Get-PSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Puede seguir teniendo un conflicto de nombres aunque haya agregado un prefijo al nombre. Me gusta añadir como prefijo a los nombres de mis funciones mis iniciales. Desarrolle un estándar y cúmplalo.

function Get-MrPSVersion {
    $PSVersionTable.PSVersion
}

Esta función no es diferente de las dos anteriores, excepto por el uso de un nombre más único para intentar evitar conflictos de nomenclatura con otros comandos de PowerShell.

Get-MrPSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
5      1      14393  693

Una vez cargadas en la memoria, puede ver las funciones en el PSDrive Función.

Get-ChildItem -Path Function:\Get-*Version
CommandType     Name                                               Version
-----------     ----                                               -------
Function        Get-Version
Function        Get-PSVersion
Function        Get-MrPSVersion

Si quiere quitar estas funciones de la sesión actual, tendrá que quitarlas del PSDrive Función o cerrar PowerShell y volver a abrirlo.

Get-ChildItem -Path Function:\Get-*Version | Remove-Item

Compruebe que las funciones se hayan quitado bien.

Get-ChildItem -Path Function:\Get-*Version

Si las funciones se cargaron como parte de un módulo, puede descargar dicho módulo para quitarlas.

Remove-Module -Name <ModuleName>

El cmdlet Remove-Module quita los módulos de PowerShell de la memoria en la sesión actual de PowerShell. No los quita del sistema ni del disco.

Parámetros

No asigne valores de forma estática. En su lugar, use parámetros y variables. Al asignar nombres a los parámetros, use el mismo nombre que los cmdlets predeterminados para los nombres de parámetro siempre que sea posible.

En la función siguiente, observe que he usado ComputerName y no Computer, ServerName o Host para el nombre del parámetro. El uso de ComputerName estandariza el nombre del parámetro para que coincida con el nombre del parámetro y el caso como los cmdlets predeterminados.

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

La siguiente función consulta todos los comandos del sistema y devuelve el número con nombres de parámetro específicos.

function Get-MrParameterCount {
    param (
        [string[]]$ParameterName
    )

    foreach ($Parameter in $ParameterName) {
        $Results = Get-Command -ParameterName $Parameter -ErrorAction SilentlyContinue

        [pscustomobject]@{
            ParameterName   = $Parameter
            NumberOfCmdlets = $Results.Count
        }
    }
}

Como puede ver en los resultados siguientes, hay 39 comandos que tienen un parámetro ComputerName. No hay ningún comando que tenga parámetros como Computer, ServerName, Host o Machine.

Get-MrParameterCount -ParameterName ComputerName, Computer, ServerName,
    Host, Machine
ParameterName NumberOfCmdlets
------------- ---------------
ComputerName               39
Computer                    0
ServerName                  0
Host                        0
Machine                     0

Utilice las mismas mayúsculas y minúsculas para los nombres de parámetros que para los cmdlets predeterminados. Por ejemplo, use ComputerName, no computername. Este esquema de nomenclatura ayuda a las personas familiarizadas con PowerShell a descubrir tus funciones y a proporcionar una experiencia similar a los cmdlets predeterminados.

La instrucción param permite definir uno o más parámetros. Una coma (,) separa las definiciones de parámetros. Para obtener más información, consulte about_Functions_Advanced_Parameters.

Funciones avanzadas

Convertir una función en una avanzada en PowerShell es sencillo. Una de las diferencias entre una función y una función avanzada es que las avanzadas tienen parámetros comunes que se agregan automáticamente. Los parámetros comunes incluyen parámetros como Verbose y Debug.

Empiece con la función Test-MrParameter, que se usó en la sección anterior.

function Test-MrParameter {

    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Hay un par de maneras diferentes de ver los parámetros comunes. Una es visualizando la sintaxis con Get-Command.

Get-Command -Name Test-MrParameter -Syntax

Observe que la función Test-MrParameter no tiene ningún parámetro común.

Test-MrParameter [[-ComputerName] <Object>]

Otra es profundizar en la propiedad parámetros de Get-Command.

(Get-Command -Name Test-MrParameter).Parameters.Keys
ComputerName

Agregue el atributo CmdletBinding para convertir la función en una avanzada.

function Test-MrCmdletBinding {

    [CmdletBinding()] # Turns a regular function into an advanced function
    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Al especificar CmdletBinding, los parámetros comunes se agregan automáticamente. CmdletBinding requiere un bloque param, pero el bloque param puede estar vacío.

Get-Command -Name Test-MrCmdletBinding -Syntax
Test-MrCmdletBinding [[-ComputerName] <Object>] [<CommonParameters>]

Al profundizar en la propiedad parámetros de Get-Command se muestran los nombres reales de los parámetros, incluidos los comunes.

(Get-Command -Name Test-MrCmdletBinding).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable

SoportaDebeProcesar

El atributo SupportsShouldProcess agrega WhatIf y Confirm, que son los parámetros de mitigación de riesgos. Estos parámetros solo son necesarios para los comandos que hacen cambios.

function Test-MrSupportsShouldProcess {

    [CmdletBinding(SupportsShouldProcess)]
    param (
        $ComputerName
    )

    Write-Output $ComputerName

}

Observe que ahora hay parámetros WhatIf y Confirm.

Get-Command -Name Test-MrSupportsShouldProcess -Syntax
Test-MrSupportsShouldProcess [[-ComputerName] <Object>] [-WhatIf] [-Confirm]
[<CommonParameters>]

Una vez más, también puede usar Get-Command para devolver una lista de los nombres reales de los parámetros, incluidos los comunes, junto con WhatIf y Confirm.

(Get-Command -Name Test-MrSupportsShouldProcess).Parameters.Keys
ComputerName
Verbose
Debug
ErrorAction
WarningAction
InformationAction
ErrorVariable
WarningVariable
InformationVariable
OutVariable
OutBuffer
PipelineVariable
WhatIf
Confirm

Validación de parámetros

Valide la entrada bien al principio. No permita que su código continúe por un camino cuando no pueda completarse sin una entrada válida.

Especifique siempre un tipo de datos para las variables usadas para los parámetros. En el ejemplo siguiente, se especifica String como tipo de datos para el parámetro ComputerName. Esta validación lo limita a permitir que solo se especifique un solo nombre de equipo para el parámetro ComputerName.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [string]$ComputerName
    )

    Write-Output $ComputerName

}

Se genera un error si se especifica más de un nombre de equipo.

Test-MrParameterValidation -ComputerName Server01, Server02
Test-MrParameterValidation : Cannot process argument transformation on
parameter 'ComputerName'. Cannot convert value to type System.String.
At line:1 char:42
+ Test-MrParameterValidation -ComputerName Server01, Server02
+                                          ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-MrParameterValidation]
   , ParameterBindingArgumentTransformationException
    + FullyQualifiedErrorId : ParameterArgumentTransformationError,Test-MrP
   arameterValidation

El problema con la definición actual es que es válido omitir el valor del parámetro ComputerName, pero se requiere un valor para que la función se complete correctamente. Este escenario es donde el atributo de parámetro Mandatory es beneficioso.

La sintaxis usada en el ejemplo siguiente es compatible con la versión 3.0 de PowerShell y con versiones posteriores. Podría especificarse [Parameter(Mandatory=$true)] para que la función sea compatible con la versión 2.0 de PowerShell o posteriores.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$ComputerName
    )

    Write-Output $ComputerName

}

Ahora que se requiere ComputerName, si no se especifica uno, la función solicita uno.

Test-MrParameterValidation
cmdlet Test-MrParameterValidation at command pipeline position 1
Supply values for the following parameters:
ComputerName:

Si desea permitir más de un valor para el parámetro ComputerName, use el tipo de datos String, pero agregue corchetes ([]) al tipo de datos para permitir una matriz de cadenas.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string[]]$ComputerName
    )

    Write-Output $ComputerName

}

Es posible que quiera especificar un valor predeterminado para el parámetro ComputerName, si no se especifica uno. El problema es que los valores predeterminados no se pueden usar con parámetros obligatorios. En su lugar, use el atributo de validación de parámetros ValidateNotNullOrEmpty con un valor predeterminado.

Intente no usar valores estáticos, ni siquiera cuando establezca un valor predeterminado. En el ejemplo siguiente, se usa $env:COMPUTERNAME como valor predeterminado, que se traduce automáticamente al nombre del equipo local si no se proporciona un valor.

function Test-MrParameterValidation {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    Write-Output $ComputerName

}

Salida detallada

Los comentarios insertados son útiles si está escribiendo código complejo, pero los usuarios no los ven a menos que examinen el código.

La función del ejemplo siguiente tiene un comentario en línea en el bucle foreach. Aunque es posible que este comentario concreto no sea tan difícil de encontrar, imagínese que la función incluyese cientos de líneas de código.

function Test-MrVerboseOutput {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    foreach ($Computer in $ComputerName) {
        #Attempting to perform an action on $Computer <<-- Don't use
        #inline comments like this, use write verbose instead.
        Write-Output $Computer
    }

}

Una mejor opción es usar Write-Verbose en lugar de comentarios en línea.

function Test-MrVerboseOutput {

    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName = $env:COMPUTERNAME
    )

    foreach ($Computer in $ComputerName) {
        Write-Verbose -Message "Attempting to perform an action on $Computer"
        Write-Output $Computer
    }

}

La salida detallada no se muestra cuando se llama a la función sin el parámetro Verbose.

Test-MrVerboseOutput -ComputerName Server01, Server02

La salida detallada se muestra cuando se llama a la función con el parámetro Verbose.

Test-MrVerboseOutput -ComputerName Server01, Server02 -Verbose

Entrada de pipeline

El código adicional es necesario cuando desea que la función acepte la entrada de canalización. Como se ha mencionado anteriormente en este libro, los comandos pueden aceptar la entrada de canalización por valor (por tipo) o por nombre de propiedad. Puede escribir sus funciones de la misma manera que los comandos nativos, para que acepten uno o ambos tipos de entrada.

Para aceptar la entrada de canalización por valor, especifique el atributo de parámetro ValueFromPipeline para ese parámetro específico. Solo se puede aceptar la entrada por valor de un parámetro de cada tipo de datos. Si tiene dos parámetros que aceptan entradas de cadena, solo uno de ellos puede aceptar entradas de pipeline por valor. Si especificó como valor para ambos parámetros de cadena, la entrada no sabría a qué parámetro vincular. Este escenario es otra de las razones por las que llamo a este tipo de entrada por tipo en lugar de por valor.

La entrada de pipeline se recibe de elemento en elemento, de forma similar a como se administran los elementos en un bucle foreach. Se requiere un bloque process para procesar cada elemento si la función acepta una matriz como entrada. Si la función solo acepta un valor único como entrada, no es necesario un bloque de process, pero se recomienda para la coherencia.

function Test-MrPipelineInput {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline)]
        [string[]]$ComputerName
    )

    process {
        Write-Output $ComputerName
    }

}

Aceptar la entrada del pipeline por nombre de propiedad es similar, excepto que se especifica ValueFromPipelineByPropertyName con el atributo de parámetro, y se puede especificar para cualquier número de parámetros independientemente del tipo de datos. La clave es que la salida del comando por el que se canaliza debe tener un nombre de propiedad que coincida con el nombre del parámetro o un alias de un parámetro de tu función.

function Test-MrPipelineInput {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
            Write-Output $ComputerName
    }

}

los bloques begin y end son opcionales. Se especifica begin antes que el bloque process y se usa para realizar cualquier trabajo inicial antes de recibir los elementos de la canalización. Los valores que se canalizan no son accesibles en el bloque begin. El bloque end se especifica después del bloque process y se usa para la limpieza tras procesar todos los elementos canalizados.

Control de errores

La función que se muestra en el siguiente ejemplo genera una excepción no controlada cuando no se puede establecer contacto con un equipo.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            Test-WSMan -ComputerName $Computer
        }
    }

}

Hay un par de formas de controlar los errores en PowerShell. Try/Catch es la forma más moderna de controlar los errores.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            try {
                Test-WSMan -ComputerName $Computer
            }
            catch {
                Write-Warning -Message "Unable to connect to Computer: $Computer"
            }
        }
    }

}

Aunque la función que se muestra en el ejemplo anterior usa el control de errores, genera una excepción no controlada porque el comando no genera un error de terminación. Solo se detectan los errores de terminación. Especifique el parámetro ErrorAction con Stop como valor para convertir un error de no terminación en uno de terminación.

function Test-MrErrorHandling {

    [CmdletBinding()]
    param (
        [Parameter(Mandatory,
                   ValueFromPipeline,
                   ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

    process {
        foreach ($Computer in $ComputerName) {
            try {
                Test-WSMan -ComputerName $Computer -ErrorAction Stop
            }
            catch {
                Write-Warning -Message "Unable to connect to Computer: $Computer"
            }
        }
    }

}

No modifique la variable global $ErrorActionPreference a menos que sea absolutamente necesario. Si lo cambia en un ámbito local, se revierte al valor anterior al salir de ese ámbito.

Si usa algo parecido a .NET directamente desde la función de PowerShell, no puede especificar el parámetro ErrorAction en el propio comando. Puede cambiar la variable $ErrorActionPreference justo antes de llamar al método .NET.

Ayuda basada en comentarios

Agregar ayuda a las funciones se considera un procedimiento recomendado. La ayuda permite que las personas con las que los comparte sepan cómo utilizarlos.

function Get-MrAutoStoppedService {

<#
.SYNOPSIS
    Returns a list of services that are set to start automatically, are not
    currently running, excluding the services that are set to delayed start.

.DESCRIPTION
    Get-MrAutoStoppedService is a function that returns a list of services
    from the specified remote computer(s) that are set to start
    automatically, are not currently running, and it excludes the services
    that are set to start automatically with a delayed startup.

.PARAMETER ComputerName
    The remote computer(s) to check the status of the services on.

.PARAMETER Credential
    Specifies a user account that has permission to perform this action. The
    default is the current user.

.EXAMPLE
     Get-MrAutoStoppedService -ComputerName 'Server1', 'Server2'

.EXAMPLE
     'Server1', 'Server2' | Get-MrAutoStoppedService

.EXAMPLE
     Get-MrAutoStoppedService -ComputerName 'Server1' -Credential (Get-Credential)

.INPUTS
    String

.OUTPUTS
    PSCustomObject

.NOTES
    Author:  Mike F. Robbins
    Website: https://mikefrobbins.com
    Twitter: @mikefrobbins
#>

    [CmdletBinding()]
    param (

    )

    #Function Body

}

Al añadir ayuda basada en comentarios a tus funciones, se puede recuperar la ayuda para estas de manera similar a los comandos integrados por defecto.

Toda la sintaxis para escribir una función en PowerShell puede parecer abrumadora para alguien que se inicia. Si no puede recordar la sintaxis de algo, abra una segunda instancia del entorno de scripting integrado (ISE) de PowerShell en un monitor independiente y vea el fragmento de código "Cmdlet (función avanzada): completa" mientras escribe el código para las funciones. Se puede acceder a los fragmentos de código en el ISE de PowerShell mediante la combinación de teclas Ctrl + J.

Resumen

En este capítulo, ha aprendido los conceptos básicos de la escritura de funciones en PowerShell, incluido cómo:

  • Crear funciones avanzadas
  • Usar la validación de parámetros
  • Utilizar salida detallada
  • Admitir entrada de pipeline
  • Manejo de errores
  • Crear ayuda basada en comentarios

Revisar

  1. ¿Cómo se obtiene una lista de verbos aprobados en PowerShell?
  2. ¿Cómo se convierte una función de PowerShell en avanzada?
  3. ¿Cuándo se deben agregar los parámetros WhatIf y Confirm a las funciones de PowerShell?
  4. ¿Cómo conviertes un error no terminado en uno terminante?
  5. ¿Por qué debe agregar ayuda basada en comentarios a sus funciones?

Referencias