Capítulo 1. El intérprete de comandos interactivo PowerShell

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

1.0 Introducción

Por encima de todo lo demás, el diseño de PowerShell da prioridad a su uso como shell interactivo eficaz y potente. Incluso su lenguaje de scripting desempeña un papel crítico en este esfuerzo, ya que favorece en gran medida el uso interactivo.

Lo que sorprende a la mayoría de la gente cuando inicia PowerShell por primera vez es su similitud con el símbolo del sistema que existe desde hace mucho tiempo como parte de Windows. Las herramientas conocidas siguen ejecutándose. Los comandos conocidos siguen ejecutándose. Incluso las teclas de acceso rápido son las mismas. Pero detrás de esta interfaz familiar hay un potente motor que te permite realizar con facilidad tareas administrativas y de scripting que antes eran engorrosas.

Este capítulo presenta PowerShell desde la perspectiva de su shell interactivo.

1.1 Instalar PowerShell Core

Problema

En querrás instalar la versión más reciente de PowerShell en tu sistema Windows, Mac o Linux.

Solución

Visita el sitio web de Microsoft para encontrar las instrucciones de instalación para el sistema operativo y la plataforma en la que quieras instalar. Para los más habituales:

Windows

Instala PowerShell desde Microsoft a través de la aplicación Microsoft Store en el Menú Inicio. A continuación, instala Windows Terminal desde Microsoft a través de la aplicación Microsoft Store del Menú Inicio.

Mac

Instala PowerShell desde Homebrew:

brew install --cask powershell

Linux

Las instrucciones de instalación varían según la distribución de Linux, pero la distribución más común entre los usuarios de PowerShell Core es Ubuntu:

# Update the list of packages
sudo apt-get update

# Install pre-requisite packages.
sudo apt-get install -y wget apt-transport-https software-properties-common

# Download the Microsoft repository GPG keys
wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb

# Register the Microsoft repository GPG keys
sudo dpkg -i packages-microsoft-prod.deb

# Update the list of packages after we added packages.microsoft.com
sudo apt-get update

# Install PowerShell
sudo apt-get install -y powershell

Debate

PowerShell ya ha tenido una vida larga y emocionante. Durante sus primeros 15 años de existencia, se le conoció como "Windows PowerShell": una fantástica plataforma y shell de gestión basada en objetos que facilitaba y divertía el trabajo de administradores, desarrolladores y usuarios avanzados.

En sus primeras etapas, este soporte formaba parte de "Windows Management Framework": una descarga independiente que proporcionaba esta funcionalidad tan necesaria en Windows. Con el tiempo, Windows PowerShell pasó a formar parte del propio Windows, y ha sido una parte esencial del sistema operativo desde Windows 7.

En 2016, PowerShell dio un giro tremendo al anunciar que distribuiríaPowerShell en múltiples plataformas de sistemas operativos y, de paso, ¡hizo que todo el proyecto fuera de código abierto al mismo tiempo! Windows PowerShell recibió un nuevo nombre con su nuevo futuro: simplemente PowerShell. Este gran cambio abrió las puertas a una innovación mucho más rápida, a la participación de la comunidad y a la disponibilidad a través de vías que antes nunca habrían sido posibles. Aunque el Windows PowerShell clásico sigue incluido por defecto en el sistema operativo, ya no recibe actualizaciones y debe evitarse.

Instalar y ejecutar PowerShell en Windows

Como se menciona en la Solución, la mejor forma de obtener PowerShell es instalarlo a través de Microsoft Store. Esto facilita su instalación y actualización. Una vez instalado, puedes encontrar PowerShell en el Menú Inicio como harías con cualquier otraaplicación.

Nota

Si quieres instalar una versión de PowerShell en todo el sistema para la automatización y otras tareas de administración, probablemente prefieras el mecanismo de instalación basado en MSI. Para más información, consulta el sitio web de Microsoft.

Aunque en estás instalando PowerShell desde la Tienda Microsoft, ahora es un buen momento para instalar la aplicación Terminal de Windows desde la Tienda Microsoft. La interfaz de consola tradicional (la ventana dentro de la que se ejecuta PowerShell) incluida en Windows tiene tantas herramientas y aplicaciones que dependen de sus peculiaridades exactas que es casi imposible cambiarla de forma significativa. Se ha quedado lamentablemente atrás en lo que cabría esperar de una interfaz de consola de terminal, por lo que la aplicación Terminal de Windows de la Tienda Microsoft, como se muestra en la Figura 1-1, es la solución. Al igual que PowerShell, es de código abierto, un foco de rápida innovación y una gran mejora de lo que Windows incluye por defecto.

wps4 0101
Figura 1-1. Terminal de Windows ejecutando PowerShell, Bash, ¡e incluso Azure Cloud Shell!

Puedes ejecutar muchos shells dentro de pestañas en el Terminal de Windows: PowerShell, Windows PowerShell, cmd.exe, Bash (si has activado el Subsistema de Windows para Linux), e incluso una conexión a Azure Cloud Shell. Windows Terminal utiliza por defecto PowerShell si lo tienes instalado.

Personalizar PowerShell en el Terminal de Windows

Hay dos cambios en la instalación por defecto de Windows Terminal + PowerShell que realmente mejoran la experiencia: la fijación de la barra de tareas y los temas.

Fijación de la barra de tareas

Cuando inicies el Terminal de Windows, haz clic con el botón derecho del ratón en el icono de su barra de tareas. Selecciona "Anclar a la barra de tareas" y, a continuación, arrastra el icono al extremo izquierdo de la barra de tareas. A partir de ahora, cada vez que pulses al mismo tiempo la tecla Windows + 1, iniciarás Windows Terminal y PowerShell (si no está ya abierto), o lo activarás. Es una forma increíble de tener a mano tu shell favorito.

Temas

Windows PowerShell tiene un precioso tema Azul Noble. Es agradable a la vista, rápido de identificar y lo distingue de las docenas de shells que existen. PowerShell Core no lleva este esquema de colores por defecto, pero aún es posible personalizar tu instalación. Desde el Terminal de Windows, pulsa Ctrl+Coma o haz clic en la flecha hacia abajo situada a la derecha de la barra de pestañas para abrir el cuadro de diálogo Configuración del Terminal de Windows. El archivo que contiene esta configuración se abrirá en tu editor de texto predeterminado. En Profiles, busca el elemento con Windows.Terminal.​Power⁠shellCore como fuente, y añade Campbell Powershell como colorScheme. El resultado debería tener este aspecto:

{
    "guid": ...
    "hidden": false,
    "name": "PowerShell",
    "colorScheme": "Campbell Powershell",
    "source": "Windows.Terminal.PowershellCore"
},

Presta atención a las mayúsculas, las comillas, los dos puntos y las comas, ¡y tus sesiones PowerShell volverán a parecer nobles en un santiamén!

Instalar y ejecutar PowerShell en Mac y Linux

En su mayor parte, la instalación de PowerShell en Mac y Linux sigue los patrones con los que probablemente ya estés familiarizado.

En Mac, el método de instalación recomendado es a través del popular gestor de paquetes Homebrew. Homebrew no está instalado por defecto en macOS, pero su instalación es bastante sencilla. Si aún no has instalado Homebrew, puedes encontrar instrucciones en el sitio oficial de Homebrew.

En Linux, los métodos de instalación varían en función de la distribución que te interese. En la mayoría de los casos, la instalación es tan sencilla como registrar el repositorio de Microsoft para el gestor de paquetes de tu distribución y, a continuación, instalar PowerShell. La Solución proporciona un ejemplo específico para Ubuntu 20.04, pero puedes obtener instrucciones específicas para tu distribución y versión concreta en el sitio web de Microsoft.

1.2 Ejecutar programas, scripts y herramientas existentes

Problema

dependes de mucho esfuerzo invertido en tus herramientas actuales. Tienes ejecutables tradicionales, scripts Perl, VBScript y, por supuesto, un sistema de compilación heredado que ha crecido orgánicamente hasta convertirse en un enredo de archivos por lotes. Quieres utilizar PowerShell, pero no quieres renunciar a todo lo que ya tienes.

Solución

Para ejecutar un programa, script, archivo por lotes u otro comando ejecutable en la ruta del sistema, introduce su nombre de archivo. Para estos tipos de ejecutables, la extensión es opcional:

Program.exe arguments
ScriptName.ps1 arguments
BatchFile.cmd arguments

Para ejecutar un comando que contenga un espacio en su nombre, encierra su nombre de archivo entre comillas simples (') y precede al comando con un ampersand (&), conocido en PowerShell como operador de invocación:

& 'C:\Program Files\Program\Program.exe' arguments

Para que ejecute un comando en el directorio actual, coloca .\ delante de su nombre de archivo:

.\Program.exe arguments

Para ejecutar un comando con espacios en su nombre desde el directorio actual, prepáralo con un ampersand y .\:

& '.\Program With Spaces.exe' arguments

Debate

En este caso, la solución consiste principalmente en utilizar tus herramientas actuales como siempre lo has hecho. La única diferencia es que las ejecutas en el intérprete de comandos interactivo PowerShell en lugar de cmd.exe.

Especificar el nombre del comando

Los tres últimos consejos de la Solución merecen una atención especial. Son las características de PowerShell con las que tropiezan muchos nuevos usuarios a la hora de ejecutar programas. El primero es la ejecución de comandos que contienen espacios. En cmd.exe, la forma de ejecutar un comando que contiene espacios es rodearlo de comillas:

"C:\Program Files\Program\Program.exe"

En PowerShell, sin embargo, colocar texto entre comillas forma parte de una función que te permite evaluar expresiones complejas en el prompt, como se muestra en el Ejemplo 1-1.

Ejemplo 1-1. Evaluar expresiones en el indicador PowerShell
PS > 1 + 1
2
PS > 26 * 1.15
29.9
PS > "Hello" + " World"
Hello World
PS > "Hello World"
Hello World
PS > "C:\Program Files\Program\Program.exe"
C:\Program Files\Program\Program.exe
PS >

Por tanto, un nombre de programa entre comillas no es diferente de cualquier otra cadena entre comillas. Es sólo una expresión. Como se ha mostrado anteriormente, la forma de ejecutar un comando en una cadena es preceder esa cadena con el operador de invocación (&). Si el comando que quieres ejecutar es un archivo por lotes que modifica su entorno, consulta la Receta 3.5.

Nota

Por defecto, las políticas de seguridad de PowerShell impiden la ejecución de scripts. Sin embargo, cuando empieces a escribir o utilizar scripts, deberás configurar esta política a algo menos restrictivo. Para obtener información sobre cómo configurar tu política de ejecución, consulta la Receta 18.1.

El segundo comando con el que a veces tropiezan los nuevos usuarios (¡y los veteranos antes del café!) es ejecutar comandos desde el directorio actual. En cmd.exe, el directorio actual se considera parte de la ruta: la lista de directorios que Windows busca para encontrar el nombre del programa que has escrito. Si estás en el directorio C:\Programas, cmd.exe busca en C:\Programas (entre otros lugares) aplicaciones para ejecutar.

PowerShell, como la mayoría de los shells Unix, requiere que indiques explícitamente tu deseo de ejecutar un programa desde el directorio actual. Para ello, utiliza la sintaxis .\Program.exe, como se ha mostrado anteriormente. Esto evita que los usuarios malintencionados de tu sistema ensucien tu disco duro con programas malignos que tienen nombres similares (o iguales) a los comandos que podrías ejecutar al visitar ese directorio.

Para ahorrarse tener que escribir la ubicación de los scripts y programas más utilizados, muchos usuarios colocan las utilidades de uso común junto con sus scripts de PowerShell en un directorio de "herramientas", que añaden a la ruta de su sistema. Si PowerShell puede encontrar un script o utilidad en la ruta de tu sistema, no necesitas especificar explícitamente su ubicación.

Si quieres que PowerShell busque automáticamente scripts en tu directorio de trabajo actual, puedes añadir un punto (.) a tu variable de entorno PATH.

Para más información sobre cómo actualizar la ruta de tu sistema, consulta la Receta 16.2.

Si quieres capturar la salida de un comando, puedes guardar los resultados en una variable o guardarlos en un archivo. Para guardar los resultados en una variable, consulta la Receta 3.3. Para guardar los resultados en un archivo, consulta la Receta 9.2.

Especificar los argumentos del comando

Para especificar en los argumentos de un comando, puedes escribirlos como lo harías en otros shells. Por ejemplo, para hacer que un archivo especificado sea de sólo lectura (dos argumentos para attrib.exe), simplemente escribe:

attrib +R c:\path\to\file.txt

Donde muchos scripters se despistan cuando se trata de argumentos de comandos es en cómo cambiarlos dentro de tus scripts. Por ejemplo, ¿cómo se obtiene el nombre de archivo de una variable PowerShell? La respuesta es definir una variable que contenga el valor del argumento y utilizarla en el lugar donde escribiste el argumento del comando:

$filename = "c:\path\to\other\file.txt"
attrib +R $filename

Puedes utilizar la misma técnica cuando llames a un cmdlet, script ofunción de PowerShell:

$filename = "c:\path\to\other\file.txt"
Get-Acl -Path $filename

Si en ves una solución que utiliza el cmdlet Invoke-Expression para componer argumentos de comando, es casi seguro que es incorrecta. El cmdlet Invoke-Expression toma la cadena que le das y la trata como un script completo de PowerShell. Como sólo un ejemplo de los problemas que esto puede causar, considera lo siguiente: se permite que los nombres de archivo contengan el carácter punto y coma (;), pero cuando Invoke-Expression ve un punto y coma, asume que se trata de una nueva línea de script PowerShell. Por ejemplo, prueba a ejecutar esto

$filename = "c:\file.txt; Write-Warning 'This could be bad'"
Invoke-Expression "Get-Acl -Path $filename"

Dado que estos argumentos dinámicos suelen proceder de la entrada del usuario, utilizar Invoke-Expression para componer comandos puede (en el mejor de los casos) provocar resultados impredecibles en los scripts. Peor aún, podría provocar daños en tu sistema o una vulnerabilidad de seguridad.

Además de permitirte suministrar argumentos a través de variables de uno en uno, PowerShell también te permite suministrar varios de ellos a la vez mediante una técnica conocida como splatting. Para más información sobre splatting, consulta la Receta 11.14.

1.3 Ejecutar un comando PowerShell

Problema

En querrás ejecutar un comando PowerShell.

Solución

Para ejecutar un comando PowerShell, escribe su nombre en el símbolo del sistema. Por ejemplo

PS > Get-Process

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
     14     3.47      10.55       0.00    6476   0 AGMService
     14     3.16      10.57       0.00    3704   0 AGSService
     37    40.12      40.51       2.06   17676   1 ApplicationFrameHost

Debate

El comando Get-Process es un ejemplo de comando nativo de PowerShell, llamado cmdlet. En comparación con los comandos tradicionales, los cmdlets ofrecen importantes ventajas tanto a los administradores como a los desarrolladores:

  • Comparten una sintaxis de línea de comandos común y habitual.

  • Admiten escenarios de canalización enriquecidos (utilizando la salida de un comando como entrada de otro).

  • Producen una salida basada en objetos fácilmente manejable, en lugar de una salida de texto plano propensa a errores.

Como el cmdlet Get-Process genera una salida rica basada en objetos, puedes utilizar su salida para muchas tareas relacionadas con los procesos.

Todos los comandos de PowerShell te permiten proporcionar información al comando a través de sus parámetros. Para más información sobre cómo introducir datos en los comandos, consulta "Ejecutar comandos".

El cmdlet Get-Process es sólo uno de los muchos que admite PowerShell. Consulta la Receta 1.12 para aprender técnicas para encontrar comandos adicionales compatibles con PowerShell.

Para más información sobre cómo trabajar con clases del .NET Framework, consulta la Receta 3.8.

1.4 Resolver errores al llamar a ejecutables nativos

Problema

En tienes una línea de comandos que funciona desde cmd.exe, y quieres resolver los errores que se producen al ejecutar ese comando en PowerShell.

Solución

Encierra cualquier argumento de comando afectado entre comillas simples para evitar que PowerShell lo interprete, y sustituye las comillas simples del comando por dos comillas simples:

PS > cmd /c echo '!"#$%&''()*+,-./09:;<=>?@AZ[\]^_`az{|}~'
!"#$%&'()*+,-./09:;<=>?@AZ[\]^_`az{|}~

Para los comandos complicados de en los que esto no funcione, utiliza la sintaxis de argumento literal(--% ):

PS > cmd /c echo 'quotes' "and" $variables @{ etc = $true }
quotes and System.Collections.Hashtable

PS > cmd --% /c echo 'quotes' "and" $variables @{ etc = $true }
'quotes' "and" $variables @{ etc = $true }

Debate

Uno de los principales objetivos de PowerShell ha sido siempre la coherencia de los comandos. Por eso, los cmdlets son muy regulares en la forma en que aceptan parámetros. Los ejecutables nativos escriben su propio análisis sintáctico de parámetros, por lo que nunca sabes qué esperar cuando trabajas con ellos. Además, PowerShell ofrece muchas funciones que te hacen más eficiente en la línea de comandos: sustitución de comandos, expansión de variables y mucho más. Como muchos ejecutables nativos se escribieron antes de que se desarrollara PowerShell, pueden utilizar caracteres especiales que entren en conflicto con estas funciones.

Como ejemplo, el comando que aparece en la Solución utiliza todos los caracteres especiales disponibles en un teclado típico. Sin las comillas, PowerShell trata algunos de ellos como características del lenguaje, como se muestra en la Tabla 1-1.

Tabla 1-1. Muestra de caracteres especiales
Carácter especial Significado

"

El principio (o final) del texto citado

#

El comienzo de un comentario

$

El inicio de una variable

&

El operador de canalización de fondo

( )

Paréntesis utilizados para subexpresiones

;

Separador de declaraciones

{ }

Bloque de guión

|

Separador de tuberías

`

Carácter de escape

Cuando están rodeados de comillas simples, PowerShell acepta estos caracteres tal y como están escritos, sin el significado especial.

A pesar de estas precauciones, puede que a veces te encuentres con un comando que no parece funcionar cuando se invoca desde PowerShell. En la mayoría de los casos, puedes resolverlos con el marcador de argumento literal (--%) que impide que PowerShell interprete cualquiera de los caracteres restantes de la línea. Puedes colocar este marcador en cualquier lugar de los argumentos del comando, lo que te permitirá beneficiarte de las construcciones de PowerShell cuando proceda. El siguiente ejemplo utiliza una variable PowerShell para algunos de los argumentos del comando, pero utiliza argumentos literales para el resto:

PS > $username = "Lee"
PS > cmd /c echo Hello $username with 'quotes' "and" $variables @{ etc = $true }
Hello Lee with quotes and System.Collections.Hashtable
PS > cmd /c echo Hello $username `
     --% with 'quotes' "and" $variables @{ etc = $true }
Hello Lee with 'quotes' "and" $variables @{ etc = $true }

Mientras está en este modo, PowerShell también acepta variables de entorno al estilo cmd.exe, ya que éstas se utilizan con frecuencia en comandos que "simplemente solían funcionar":

PS > $env:host = "localhost"
PS > ping %host%
Ping request could not find host %host%. Please check the name and try again.
PS > ping --% %host%

Pinging localhost [127.0.1.1] with 32 bytes of data:
(...)

1.5 Suministrar valores por defecto para los parámetros

Problema

En querrás definir un valor por defecto para un parámetro en un comando PowerShell.

Solución

Añade una entrada a la tabla hash PSDefaultParameterValues:

PS > Get-Process

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    150      13     9692       9612    39    21.43    996 audiodg
   1013      84    45572      42716   315     1.67   4596 WWAHost
(...)

PS > $PSDefaultParameterValues["Get-Process:ID"] = $pid
PS > Get-Process

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    584      62   132776     157940   985    13.15   9104 powershell

PS > Get-Process -Id 0

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
      0       0        0         20     0               0 Idle

Debate

En PowerShell, muchos comandos (cmdlets y funciones avanzadas) tienen parámetros que te permiten configurar su comportamiento. Para una descripción completa de cómo proporcionar entradas a los comandos, consulta "Ejecutar comandos". A veces, sin embargo, proporcionar valores para esos parámetros en cada invocación resulta incómodo o repetitivo.

En las primeras versiones de de PowerShell, era responsabilidad del autor de cada cmdlet reconocer las propiedades de configuración incómodas o repetitivas y crear soporte para "variables de preferencia" en el propio cmdlet. Por ejemplo, el cmdlet Send-MailMessage busca la variable $PSEmailServer si no proporcionas un valor para su parámetro-SmtpServer parámetro.

Para que este soporte sea más coherente y configurable, PowerShell admite la variable de preferencia PSDefaultParameterValues. Esta variable de preferencia es una hashtable. Como todas las demás hashtables de PowerShell, las entradas constan de dos partes: la clave y el valor.

Las claves de la tabla hash PSDefaultParameterValues deben coincidir con el patrón cmdlet:parameter-es decir, un nombre de cmdlet y un nombre de parámetro, separados por dos puntos. Cualquiera de ellos (o ambos) puede utilizar comodines, y se ignoran los espacios entre el nombre del comando, los dos puntos y el parámetro.

Los valores de los pares cmdlet/parámetro pueden ser un valor de parámetro simple (una cadena, un valor booleano, un número entero, etc.) o un bloque de secuencia de comandos. Los valores de parámetros simples son los que utilizarás más a menudo.

Si necesitas que el valor por defecto cambie dinámicamente en función de los valores de los parámetros que se proporcionen hasta el momento, puedes utilizar un bloque de secuencia de comandos como valor por defecto. Al hacerlo, PowerShell evalúa el bloque de secuencia de comandos y utiliza su resultado como valor por defecto. Si tu bloque de secuencia de comandos no devuelve un resultado, PowerShell no aplica un valor por defecto.

Cuando PowerShell invoca tu bloque de script, $args[0] contiene información sobre los parámetros vinculados hasta el momento: BoundDefaultParameters, BoundParameters, y BoundPositionalParameters. Como ejemplo de esto, considera la posibilidad de proporcionar valores por defecto al parámetro -Credential en función del ordenador al que se esté conectando. Aquí tienes una función que simplemente muestra la credencial que se está utilizando:

function RemoteConnector
{
    param(
        [Parameter()]
        $ComputerName,

        [Parameter(Mandatory = $true)]
        $Credential)

    "Connecting as " + $Credential.UserName
}

Ahora puedes definir un mapa de credenciales:

PS > $credmap = @{}
PS > $credmap["RemoteComputer1"] = Get-Credential
PS > $credmap["RemoteComputer2"] = Get-Credential

A continuación, crea un parámetro por defecto para todos los parámetros de Credential que busque el parámetroComputerName parámetro vinculado:

$PSDefaultParameterValues["*:Credential"] = {
    if($args[0].BoundParameters -contains "ComputerName")
    {
        $cred = $credmap[$PSBoundParameters["ComputerName"]]
        if($cred) { $cred }
    }
}

Aquí tienes un ejemplo de su uso:

PS > RemoteConnector -ComputerName RemoteComputer1
Connecting as UserForRemoteComputer1
PS > RemoteConnector -ComputerName RemoteComputer2
Connecting as UserForRemoteComputer2
PS > RemoteConnector -ComputerName RemoteComputer3

cmdlet RemoteConnector at command pipeline position 1
Supply values for the following parameters:
Credential: (...)

Para más información sobre cómo trabajar con hashtables en PowerShell, consulta "Hashtables (matrices asociativas)".

1.6 Invocar un comando de larga ejecución o en segundo plano

Problema

En quieres invocar un comando de larga ejecución en un ordenador local o remoto.

Solución

Invocar el comando como Job para que PowerShell lo ejecute en segundo plano:

PS > Start-Job { while($true) { Get-Random; Start-Sleep 5 } } -Name Sleeper

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
1               Sleeper         Running    True            localhost

PS > Receive-Job Sleeper
671032665
1862308704
PS > Stop-Job Sleeper

O, si tu comando es una única canalización, coloca un carácter & al final de la línea para ejecutar esa canalización en segundo plano:

PS > dir c:\windows\system32 -recurse &

Id     Name            PSJobTypeName   State         HasMore
                                                     Data
--     ----            -------------   -----         -------
1      Job1            BackgroundJob   Running       True

PS > 1+1
2

PS > Receive-Job -id 1 | Select -First 5

    Directory: C:\Windows\System32

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----           12/7/2019  1:50 AM                0409
d----           11/5/2020  7:09 AM                1028
d----           11/5/2020  7:09 AM                1029
d----           11/5/2020  7:09 AM                1031
d----           11/5/2020  7:09 AM                1033

Debate

Los cmdlets de trabajo de PowerShell proporcionan una forma coherente de crear tareas en segundo plano e interactuar con ellas. En la Solución, utilizamos el cmdlet Start-Job para lanzar un trabajo en segundo plano en el ordenador local. Le damos el nombre de Sleeper, pero por lo demás no personalizamos mucho su entorno de ejecución.

En además de permitirte personalizar el nombre del trabajo, el cmdlet Start-Job también te permite lanzar el trabajo bajo credenciales de usuario alternativas o como un proceso de 32 bits (si se ejecuta originalmente desde un proceso de 64 bits).

Como alternativa al cmdlet Start-Job, también puedes utilizar el cmdlet Start-ThreadJob. El cmdlet Start-ThreadJob es un poco más rápido a la hora de iniciar trabajos en segundo plano y también te permite suministrar e interactuar con objetos vivos en los trabajos que crees. Sin embargo, consume recursos de tu proceso PowerShell actual y no te permite ejecutar tu trabajo con credenciales de usuario alternativas.

Una vez que hayas lanzado un trabajo, puedes utilizar los demás cmdlets de Job para interactuar con él:

Get-Job

Obtiene todos los trabajos asociados a la sesión actual. Además, los parámetros -Before,-After, -Newest, y -State te permiten filtrar los trabajos en función de su estado o tiempo de finalización.

Wait-Job

Espera por un trabajo hasta que tenga salida lista para ser recuperada.

Receive-Job

Recupera cualquier salida que el trabajo haya generado desde la última llamada a Receive-Job.

Stop-Job

Deja de un trabajo.

Remove-Job

Elimina un trabajo de la lista de trabajos activos.

Nota

En además del cmdlet Start-Job, también puedes utilizar el parámetro -AsJob en muchos cmdlets para que realicen sus tareas en segundo plano. Dos de los ejemplos más útiles son el cmdlet Invoke-Command (cuando se opera contra ordenadores remotos) y el cmdlet ForEach-Object.

Si tu trabajo genera un error, el cmdlet Receive-Job te lo mostrará cuando recibas los resultados, como se muestra en el Ejemplo 1-2. Si quieres investigar más a fondo estos errores, el objeto devuelto por Get-Job los expone a través de la propiedad Error.

Ejemplo 1-2. Recuperar errores de un Trabajo
PS > Start-Job -Name ErrorJob { Write-Error Error! }

Id              Name            State      HasMoreData     Location
--              ----            -----      -----------     --------
1               ErrorJob        Running    True            localhost


PS > Receive-Job ErrorJob
Write-Error: Error!

PS > $job = Get-Job ErrorJob
PS > $job | Format-List *

State         : Completed
HasMoreData   : False
StatusMessage :
Location      : localhost
Command       :  Write-Error Error!
JobStateInfo  : Completed
Finished      : System.Threading.ManualResetEvent
InstanceId    : 801e932c-5580-4c8b-af06-ddd1024840b7
Id            : 1
Name          : ErrorJob
ChildJobs     : {Job2}
Output        : {}
Error         : {}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}

PS > $job.ChildJobs[0] | Format-List *
State         : Completed
StatusMessage :
HasMoreData   : False
Location      : localhost
Runspace      : System.Management.Automation.RemoteRunspace
Command       :  Write-Error Error!
JobStateInfo  : Completed
Finished      : System.Threading.ManualResetEvent
InstanceId    : 60fa85da-448b-49ff-8116-6eae6c3f5006
Id            : 2
Name          : Job2
ChildJobs     : {}
Output        : {}
Error         : {Microsoft.PowerShell.Commands.WriteErrorException,Microso
                ft.PowerShell.Commands.WriteErrorCommand}
Progress      : {}
Verbose       : {}
Debug         : {}
Warning       : {}


PS > $job.ChildJobs[0].Error
Write-Error: Error!

PS >

Como muestra este ejemplo , a veces los trabajos son contenedores de otros trabajos, llamados trabajos hijo. Los trabajos creados mediante el cmdlet Start-Job siempre serán trabajos hijo adjuntos a un contenedor genérico. Para acceder a los errores devueltos por estos trabajos, accede en su lugar a los errores de su primer trabajo hijo (llamado trabajo hijo número cero).

Además de los trabajos de larga duración que se ejecutan bajo el control de la sesión actual de PowerShell, es posible que quieras registrar y controlar trabajos que se ejecutan según una programación, o independientemente de la sesión actual de PowerShell. PowerShell tiene un puñado de comandos que te permiten trabajar con trabajos programados de este tipo; para más información, consulta la Receta 27.14.

1.7 Programa: Monitorear los cambios de un comando

Por muy emocionantes que sean nuestras vidas, algunos días se reducen a ejecutar un comando una y otra y otra vez. ¿Han terminado ya de copiarse los archivos? ¿Ha terminado la construcción? ¿Sigue funcionando el sitio?

Normalmente, la respuesta a estas preguntas viene de ejecutar un comando, mirar su salida y decidir si cumple tus criterios. Y normalmente esto significa esperar a que cambie la salida, esperar a que aparezca algún texto o esperar a que desaparezca algún texto.

Afortunadamente, el Ejemplo 1-3 automatiza este tedioso proceso por ti.

Ejemplo 1-3. Observar-Comando.ps1
##############################################################################
##
## Watch-Command
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Watches the result of a command invocation, alerting you when the output
either matches a specified string, lacks a specified string, or has simply
changed.

.EXAMPLE

PS > Watch-Command { Get-Process -Name Notepad | Measure } -UntilChanged
Monitors Notepad processes until you start or stop one.

.EXAMPLE

PS > Watch-Command { Get-Process -Name Notepad | Measure } -Until "Count    : 1"
Monitors Notepad processes until there is exactly one open.

.EXAMPLE

PS > Watch-Command {
     Get-Process -Name Notepad | Measure } -While 'Count    : \d\s*\n'
Monitors Notepad processes while there are between 0 and 9 open
(once number after the colon).

#>

[CmdletBinding(DefaultParameterSetName = "Forever")]
param(
    ## The script block to invoke while monitoring
    [Parameter(Mandatory = $true, Position = 0)]
    [ScriptBlock] $ScriptBlock,

    ## The delay, in seconds, between monitoring attempts
    [Parameter()]
    [Double] $DelaySeconds = 1,

    ## Specifies that the alert sound should not be played
    [Parameter()]
    [Switch] $Quiet,

    ## Monitoring continues only while the output of the
    ## command remains the same.
    [Parameter(ParameterSetName = "UntilChanged", Mandatory = $false)]
    [Switch] $UntilChanged,

    ## The regular expression to search for. Monitoring continues
    ## until this expression is found.
    [Parameter(ParameterSetName = "Until", Mandatory = $false)]
    [String] $Until,

    ## The regular expression to search for. Monitoring continues
    ## until this expression is not found.
    [Parameter(ParameterSetName = "While", Mandatory = $false)]
    [String] $While
)

Set-StrictMode -Version 3

$initialOutput = ""
$lastCursorTop = 0
Clear-Host

## Start a continuous loop
while($true)
{
    ## Run the provided script block
    $r = & $ScriptBlock

    ## Clear the screen and display the results
    $buffer = $ScriptBlock.ToString().Trim() + "`r`n"
    $buffer += "`r`n"
    $textOutput = $r | Out-String
    $buffer += $textOutput

    [Console]::SetCursorPosition(0, 0)
    [Console]::Write($buffer)

    $currentCursorTop = [Console]::CursorTop
    $linesToClear = $lastCursorTop - $currentCursorTop
    if($linesToClear -gt 0)
    {
        [Console]::Write((" " * [Console]::WindowWidth * $linesToClear))
    }

    $lastCursorTop = [Console]::CursorTop
    [Console]::SetCursorPosition(0, 0)

    ## Remember the initial output, if we haven't
    ## stored it yet
    if(-not $initialOutput)
    {
        $initialOutput = $textOutput
    }

    ## If we are just looking for any change,
    ## see if the text has changed.
    if($UntilChanged)
    {
        if($initialOutput -ne $textOutput)
        {
            break
        }
    }

    ## If we need to ensure some text is found,
    ## break if we didn't find it.
    if($While)
    {
        if($textOutput -notmatch $While)
        {
            break
        }
    }

    ## If we need to wait for some text to be found,
    ## break if we find it.
    if($Until)
    {
        if($textOutput -match $Until)
        {
            break
        }
    }

    ## Delay
    Start-Sleep -Seconds $DelaySeconds
}

## Notify the user
if(-not $Quiet)
{
    [Console]::Beep(1000, 1000)
}

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.8 Notifícate de la finalización del trabajo

Problema

Quieres notificarte cuando finaliza un trabajo de larga duración.

Solución

Utiliza el comando Register-TemporaryEvent que aparece en la Receta 31.3 para registrarte en el evento StateChanged:

PS > $job = Start-Job -Name TenSecondSleep { Start-Sleep 10 }
PS > Register-TemporaryEvent $job StateChanged -Action {
     [Console]::Beep(100,100)
     Write-Host "Job #$($sender.Id) ($($sender.Name)) complete."
}

PS > Job #6 (TenSecondSleep) complete.
PS >

Debate

Cuando un trabajo finaliza, lanza un evento StateChanged para notificar a los suscriptores que su estado ha cambiado. Podemos utilizar los cmdlets de gestión de eventos de PowerShell para registrarnos para recibir notificaciones sobre este evento, pero no están orientados a este tipo de gestión de eventos puntuales. Para solucionarlo, utilizamos el comando Register-TemporaryEvent que aparece en la Receta 31.3.

En nuestro bloque de acción de ejemplo de la Solución, simplemente emitimos un pitido y escribimos un mensaje diciendo que el trabajo se ha completado.

Como otra opción, también puedes actualizar tu función prompt para resaltar los trabajos que están completos pero que todavía tienen salida que no has procesado:

$psJobs = @(Get-Job -State Completed | ? { $_.HasMoreData })
if($psJobs.Count -gt 0) {
    ($psJobs | Out-String).Trim() | Write-Host -Fore Yellow }

Para más información sobre los eventos y este tipo de gestión automática de eventos, consulta el Capítulo 31.

1.9 Personalizar tu Shell, Perfil y Prompt

Problema

quieres personalizar la experiencia interactiva de PowerShell con un prompt personalizado, alias y mucho más.

Solución

Cuando quieras personalizar aspectos de PowerShell, coloca esas personalizaciones en tu script de perfil personal. PowerShell facilita el acceso a este script de perfil almacenando su ubicación en la variable $profile.

Nota

Por defecto, las políticas de seguridad de PowerShell impiden la ejecución de scripts (incluido tu perfil). Sin embargo, una vez que empieces a escribir scripts, deberás configurar esta política a algo menos restrictivo. Para obtener información sobre cómo configurar tu política de ejecución, consulta la Receta 18.1.

Para crear un nuevo perfil (y sobrescribir uno si ya existe):

New-Item -type file -force $profile

Para editar tu perfil (en Visual Studio Code, si lo tienes instalado):

code $profile

Para ver tu archivo de perfil:

Get-ChildItem $profile

Una vez crees un script de perfil, puedes añadir una función llamada prompt que devuelva una cadena. PowerShell muestra la salida de esta función como indicador de tu línea de comandos:

function prompt
{
    "PS [$env:COMPUTERNAME] >"
}

Este ejemplo muestra el nombre de tu ordenador y se parece a PS [LEE-DESK] >.

También puede resultarte útil añadir alias a tu perfil. Los alias te permiten referirte a comandos comunes con un nombre que tú elijas. Los scripts de perfil personal te permiten definir automáticamente alias, funciones, variables o cualquier otra personalización que puedas establecer interactivamente desde el indicador de PowerShell. Los alias se encuentran entre las personalizaciones más comunes, ya que te permiten referirte a los comandos PowerShell (y a tus propios scripts) con un nombre más fácil de escribir.

Nota

Si quieres definir un alias para un comando pero también necesitas modificar los parámetros de ese comando, entonces define una función en su lugar. Para más información, consulta la Receta 11.14.

Por ejemplo:

Set-Alias new New-Object
Set-Alias browse 'C:\Users\lee\AppData\Local\Microsoft\*\MicrosoftEdge.exe'

Tus cambios se harán efectivos cuando guardes tu perfil y reinicies PowerShell. Alternativamente, puedes recargar tu perfil inmediatamente ejecutando este comando:

. $profile

Las funciones también son personalizaciones muy comunes, siendo la más popular la función prompt.

Debate

La Solución analiza tres técnicas para realizar personalizaciones útiles en tu entorno PowerShell: alias, funciones y un prompt a medida. Puedes (y a menudo lo harás) aplicar estas técnicas en cualquier momento de tu sesión PowerShell, pero tu script de perfil es el lugar estándar para poner las personalizaciones que quieras aplicar a cada sesión.

Nota

Para eliminar un alias o una función, utiliza el cmdlet Remove-Item:

Remove-Item function:\MyCustomFunction
Remove-Item alias:\new

Aunque la función prompt devuelve una simple cadena, también puedes utilizarla para tareas más complejas. Por ejemplo, muchos usuarios actualizan el título de la ventana de la consola (cambiando la variable $host.UI.RawUI.WindowTitle ) o utilizan el cmdlet Write-Host para dar salida al prompt en color. Si tu función prompt se encarga ella misma de la salida en pantalla, aún debe devolver una cadena (por ejemplo, un solo espacio) para evitar que PowerShell utilice su valor por defecto. Si no quieres que este espacio extra aparezca en tu prompt, añade un espacio extra al final de tu comando Write-Host y devuelve el carácter de retroceso ("`b"), como se muestra en el Ejemplo 1-4.

Ejemplo 1-4. Ejemplo de instrucción PowerShell
##############################################################################
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

Set-StrictMode -Version 3

function prompt
{
    $id = 1
    $historyItem = Get-History -Count 1
    if($historyItem)
    {
        $id = $historyItem.Id + 1
    }

    Write-Host -ForegroundColor DarkGray "`n[$(Get-Location)]"
    Write-Host -NoNewLine "PS:$id > "
    $host.UI.RawUI.WindowTitle = "$(Get-Location)"

    "`b"
}

En además de mostrar la ubicación actual, este indicador también muestra el ID de ese comando en tu historial. Esto te permite localizar e invocar comandos anteriores con relativa facilidad:

[C:\]
PS:73 >5 * 5
25

[C:\]
PS:74 >1 + 1
2

[C:\]
PS:75 >Invoke-History 73
5 * 5
25

[C:\]
PS:76 >

Aunque el perfil al que hace referencia $profile es el que casi siempre querrás utilizar, PowerShell admite en realidad cuatro scripts de perfil distintos. Para más detalles sobre estos scripts (junto con otras opciones de personalización del shell), consulta "Puntos comunes de personalización".

1.10 Personalizar el comportamiento de la entrada de usuario de PowerShell

Problema

En quieres anular la forma en que PowerShell lee y gestiona la entrada en el prompt.

Solución

Utiliza el cmdlet Set-PSReadLineOption para configurar propiedades como EditMode (Windows, VI, Emacs) y la gestión del historial. Por ejemplo, para que la línea de continuación de una entrada incompleta sea un poco más roja de lo habitual:

Set-PSReadLineOption -Colors @{ ContinuationPrompt = "#663333" }

Utiliza el comando Set-PSReadLineKeyHandler para configurar cómo responde PSReadLine a tus pulsaciones reales. Por ejemplo, para añadir navegación hacia delante y hacia atrás en el historial de directorios para Alt+Comma y Alt+Period:

Set-PSReadLineKeyHandler -Chord 'Alt+,' -ScriptBlock {
    Set-Location -
    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}

Set-PSReadLineKeyHandler -Chord 'Alt+.' -ScriptBlock {
    Set-Location +
    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
}

Debate

Cuando PowerShell apareció en escena, los usuarios de Unix fueron de los primeros en darse cuenta. Llevaban años disfrutando de un potente shell y de una vigorosa herencia de automatización, y "cuando me veo obligado a usar Windows, PowerShell mola" es una frase que hemos oído muchas veces.

Esta asimilación natural no fue un error. Hay muchos miembros del equipo que provienen de una profunda formación Unix, y las similitudes con los shells Unix tradicionales eran intencionadas. Sin embargo, para la gente que viene de otros shells, todavía oímos alguna queja ocasional de que alguna función resulta extraña. ¿Alt+P no inicia la utilidad de paginación integrada? ¿Ctrl+XX no se mueve entre el principio de la línea y la posición actual del cursor? ¡Abominable!

En las primeras versiones de PowerShell, no había nada que pudieras hacer razonablemente para solucionar esto. En esas versiones, PowerShell leía su entrada desde la consola en lo que se conoce como Modo Cocinado -dondeel subsistema de consola de Windows se encarga de todas las pulsaciones de teclas, los menús F7 extravagantes y demás-. Cuando pulsas Intro o Tabulador, PowerShell obtiene el texto de lo que has escrito hasta ese momento, pero eso es todo. No hay forma de que sepa que has pulsado las teclas (tipo Unix) Ctrl+R, Ctrl+A, Ctrl+E o cualquier otra.

En versiones posteriores de PowerShell, la mayoría de estas cuestiones han desaparecido con la introducción del fantástico módulo PSReadLine que PowerShell utiliza para la entrada de línea de comandos. PSReadLine añade un rico resaltado de sintaxis, completado de tabulaciones, navegación por el historial y mucho más.

El módulo PSReadLine te permite configurarlo hasta un grado increíble. El cmdletSet-PSReadLineOption cmdlet admite opciones para su interfaz de usuario, modo de manejo de la entrada, procesamiento del historial y mucho más:

EditMode                       BellStyle
ContinuationPrompt             CompletionQueryItems
HistoryNoDuplicates            WordDelimiters
AddToHistoryHandler            HistorySearchCaseSensitive
CommandValidationHandler       HistorySaveStyle
HistorySearchCursorMovesToEnd  HistorySavePath
MaximumHistoryCount            AnsiEscapeTimeout
MaximumKillRingCount           PromptText
ShowToolTips                   ViModeIndicator
ExtraPromptLineCount           ViModeChangeHandler
DingTone                       PredictionSource
DingDuration                   Colors

En, además de permitirte configurar su comportamiento en tiempo de ejecución, también puedes configurar cómo tus pulsaciones de teclas hacen que reaccione. Para ver todos los comportamientos que puedes asignar a las pulsaciones de teclas, ejecuta Get-PSReadLineKeyHandler . PSReadLine ofrece páginas de opciones -muchas de ellas no asignadas actualmente a ninguna pulsación de tecla:

PS > Get-PSReadLineKeyHandler

Basic editing functions
=======================

Key              Function           Description
---              --------           -----------
Enter            AcceptLine         Accept the input or move to the next line if
                                    input is missing a closing token.
Shift+Enter      AddLine            Move the cursor to the next line without
                                    attempting to execute the input
Backspace        BackwardDeleteChar Delete the character before the cursor
Ctrl+h           BackwardDeleteChar Delete the character before the cursor
Ctrl+Home        BackwardDeleteLine Delete text from the cursor to the start of
                                    the line
Ctrl+Backspace   BackwardKillWord   Move the text from the start of the current
                                    or previous word to the cursor to the kill
                                    ring
Ctrl+w           BackwardKillWord   Move the text from the start of the current
                                    or previous word to the cursor to...
(...)

Para configurar cualquiera de estas funciones, utiliza el comando Set-PSReadLineKeyHandler. Por ejemplo, para configurar Ctrl+Shift+C para que capture regiones coloreadas del búfer en tu portapapeles, ejecuta:

Set-PSReadLineKeyHandler -Chord Ctrl+Shift+C -Function CaptureScreen

Si en no hay una función predefinida para hacer lo que quieres, puedes utilizar el parámetro-ScriptBlock para que PSReadLine ejecute cualquier código de tu elección cuando pulses una tecla o combinación de teclas. El ejemplo de la Solución lo demuestra añadiendo la navegación hacia delante y hacia atrás en el historial de directorios.

Para que cualquiera de estos cambios persista, sólo tienes que añadir estos comandos a tu Perfil PowerShell.

Aunque realmente sólo sirve para escenarios extremadamente avanzados ahora que PSReadLine cubre casi todo lo que podrías necesitar, puedes personalizar o aumentar aún más esta funcionalidad mediante la función PSConsoleHostReadLine. Cuando defines este método en el host de la consola PowerShell, PowerShell llama a esa función en lugar de a la funcionalidad de entrada por defecto de Windows en Modo Cocinado. La versión por defecto de esta función lanza PSReadLine's ReadLine input handler. Pero si deseas redefinir esto completamente, eso es todo: el resto depende de ti. Si quieres implementar un método de entrada personalizado, la libertad (y la responsabilidad) es toda tuya.

Cuando definas esta función, debe procesar la entrada del usuario y devolver el comando resultante. El Ejemplo 1-5 implementa un mecanismo de entrada de usuario basado en el Bloc de Notas un tanto ridículo:

Ejemplo 1-5. Un mecanismo de entrada de usuario basado en el Bloc de Notas
function PSConsoleHostReadLine
{
    $inputFile = Join-Path $env:TEMP PSConsoleHostReadLine
    Set-Content $inputFile "PS > "

    ## Notepad opens. Enter your command in it, save the file,
    ## and then exit.
    notepad $inputFile | Out-Null
    $userInput = Get-Content $inputFile
    $resultingCommand = $userInput.Replace("PS >", "")
    $resultingCommand
}

Para más información sobre el manejo de las pulsaciones de teclas y otras formas de entrada del usuario, consulta el Capítulo 13.

1.11 Personalizar el comportamiento de resolución de comandos de PowerShell

Problema

En querrás anular o personalizar el comando que PowerShell invoca antes de ser invocado.

Solución

Asigna a un bloque de guión a una o todas las propiedades PreCommandLookupAction, PostCommand​Loo⁠kupAction o CommandNotFoundAction de $executionContext.SessionState.InvokeCommand. El ejemplo 1-6 permite navegar fácilmente por el directorio padre cuando escribes varios puntos.

Ejemplo 1-6. Facilitar la navegación por la ruta padre mediante CommandNotFoundAction
##############################################################################
##
## Add-RelativePathCapture
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Adds a new CommandNotFound handler that captures relative path
navigation without having to explicitly call 'Set-Location'

.EXAMPLE

PS C:\Users\Lee\Documents>..
PS C:\Users\Lee>...
PS C:\>

#>

Set-StrictMode -Version 3

$executionContext.SessionState.InvokeCommand.CommandNotFoundAction = {
    param($CommandName, $CommandLookupEventArgs)

    ## If the command is only dots
    if($CommandName -match '^\.+$')
    {
        ## Associate a new command that should be invoked instead
        $CommandLookupEventArgs.CommandScriptBlock = {

            ## Count the number of dots, and run "Set-Location .." one
            ## less time.
            for($counter = 0; $counter -lt $CommandName.Length - 1; $counter++)
            {
                Set-Location ..
            }

        ## We call GetNewClosure() so that the reference to $CommandName can
        ## be used in the new command.
        }.GetNewClosure()

        ## Stop going through the command resolution process. This isn't
        ## strictly required in the CommandNotFoundAction.
        $CommandLookupEventArgs.StopSearch = $true
    }
}

Debate

Cuando invocas un comando en PowerShell, el motor pasa por tres fases distintas:

  1. Recupera el texto de la orden.

  2. Busca el comando para ese texto.

  3. Invoca el comando encontrado.

En PowerShell, la propiedad $executionContext.SessionState.InvokeCommand te permite anular cualquiera de estas etapas con bloques de script para interceptar alguna o todas las etapasPreCommandLookupActionPostCommandLookupAction o CommandNotFoundAction.

Cada bloque de script recibe dos parámetros: el nombre del comando y un objeto(CommandLookupEventArgs ) para controlar el comportamiento de búsqueda del comando. Si tu manejador asigna un bloque de secuencia de comandos a la propiedad CommandScriptBlock del Command​Looku⁠pEventArgs o asigna un CommandInfo a la propiedad Command del CommandLookupEventArgs, PowerShell utilizará ese bloque de secuencia de comandos o comando, respectivamente. Si tu bloque de secuencia de comandos asigna la propiedad StopSearch a true, PowerShell no realizará ninguna otra resolución de comandos.

PowerShell invoca el bloque de script PreCommandLookupAction cuando conoce el nombre de un comando (por ejemplo, Get-Process) pero aún no ha buscado el comando en sí. Puedes anular esta acción si quieres reaccionar basándote principalmente en el texto del nombre del comando o si quieres adelantarte a la resolución habitual de comandos o alias de PowerShell. Por ejemplo, el Ejemplo 1-7 muestra un PreCommandLookupAction que busca comandos con un asterisco delante de su nombre. Cuando encuentra uno, activa el parámetro-Verbose parámetro

Ejemplo 1-7. Personalización de la PreCommandLookupAction
$executionContext.SessionState.InvokeCommand.PreCommandLookupAction = {
    param($CommandName, $CommandLookupEventArgs)

    ## If the command name starts with an asterisk, then
    ## enable its Verbose parameter
    if($CommandName -match "\*")
    {
        ## Remove the leading asterisk
        $NewCommandName = $CommandName -replace '\*',''

        ## Create a new script block that invokes the actual command,
        ## passes along all original arguments, and adds in the -Verbose
        ## parameter
        $CommandLookupEventArgs.CommandScriptBlock = {
            & $NewCommandName @args -Verbose

        ## We call GetNewClosure() so that the reference to $NewCommandName
        ## can be used in the new command.
        }.GetNewClosure()
    }
}

PS > dir > 1.txt
PS > dir > 2.txt
PS > del 1.txt
PS > *del 2.txt
VERBOSE: Performing operation "Remove file" on Target "C:\temp\tempfolder\2.txt".

Después de que PowerShell ejecute el PreCommandLookupAction (si existe y no devuelve un comando), pasa por su resolución de comandos habitual. Si encuentra un comando, invoca el bloque de script asociado al PostCommandLookupAction. Puedes anular esta acción si quieres reaccionar principalmente a un comando que está a punto de ser invocado. El Ejemplo 1-8 muestra un PostCommandLookupAction que hace un recuento de los comandos que utilizas con más frecuencia.

Ejemplo 1-8. Personalización de la PostCommandLookupAction
$executionContext.SessionState.InvokeCommand.PostCommandLookupAction = {
    param($CommandName, $CommandLookupEventArgs)

    ## Stores a hashtable of the commands we use most frequently
    if(-not (Test-Path variable:\CommandCount))
    {
        $global:CommandCount = @{}
    }

    ## If it was launched by us (rather than as an internal helper
    ## command), record its invocation.
    if($CommandLookupEventArgs.CommandOrigin -eq "Runspace")
    {
        $commandCount[$CommandName] = 1 + $commandCount[$CommandName]
    }
}

PS > Get-Variable commandCount
PS > Get-Process -id $pid
PS > Get-Process -id $pid
PS > $commandCount

Name                           Value
----                           -----
Out-Default                    4
Get-Variable                   1
prompt                         4
Get-Process                    2

Si la resolución del comando no tiene éxito, PowerShell invoca el bloque de script CommandNotFound​Ac⁠tion si existe. En su forma más simple, puedes anular esta acción si quieres recuperarte o anular el comportamiento de error de PowerShell cuando no puede encontrar uncomando.

Como aplicación más avanzada, la CommandNotFoundAction te permite escribir extensiones PowerShell que alteran su comportamiento basándose en la forma del nombre, en lugar de en los argumentos que se le pasan. Por ejemplo, puede que quieras lanzar automáticamente URLs con sólo escribirlas o navegar por proveedores con sólo escribir ubicaciones de rutas relativas.

La Solución ofrece un ejemplo de implementación de este tipo de manejador. Aunque la navegación dinámica por rutas relativas no es una función incorporada en PowerShell, es posible obtener una alternativa muy razonable interceptando el CommandNotFoundAction. Si vemos un comando perdido que tiene un patrón que queremos manejar (una serie de puntos), devolvemos un bloque de secuencia de comandos que realiza la navegación por rutas relativas adecuada.

1.12 Encontrar una orden para realizar una tarea

Problema

En quieres realizar una tarea en PowerShell pero no conoces el comando o cmdlet para llevarla a cabo.

Solución

Utiliza el cmdlet Get-Command para buscar e investigar comandos.

Para obtener la información resumida sobre un comando concreto, especifica el nombre del comando como argumento:

Get-Command CommandName

Para obtener información detallada sobre un comando concreto, canaliza la salida deGet-Command al cmdlet Format-List:

Get-Command CommandName | Format-List

Para buscar todos los comandos con un nombre que contenga texto, rodea el texto con caracteres asterisco:

Get-Command *text*

Para buscar todos los comandos que utilicen el verbo Get, introduce Get en el parámetro-Verb parámetro:

Get-Command -Verb Get

Para buscar todos los comandos que actúan sobre un servicio, utiliza Service como valor del parámetro-Noun:

Get-Command -Noun Service

Debate

Una de las ventajas que PowerShell proporciona a los administradores es la coherencia de los nombres de sus comandos. Todos los comandos de PowerShell (llamados cmdlets) siguen un patrón Verbo-Sustantivo regular: por ejemplo, Get-Process, Get-Service y Set-Location. Los verbos de proceden de un conjunto relativamente pequeño de verbos estándar (enumerados en el Apéndice J) y describen la acción que realiza el cmdlet. Los sustantivos son específicos del cmdlet y describen sobre qué actúa el cmdlet.

Conociendo esta filosofía, puedes aprender fácilmente a trabajar con grupos de cmdlets. Si quieres iniciar un servicio en la máquina local, el verbo estándar para ello es Start. Una buena suposición sería probar primero con Start-Service (que en este caso sería correcto), pero escribir Get-Command -Verb Start también sería una forma eficaz de ver qué cosas puedes iniciar. Yendo en sentido contrario, puedes ver qué acciones son compatibles con los servicios escribiendo Get-Command -Noun Service.

Cuando utilizas el cmdlet Get-Command, PowerShell devuelve resultados de la lista de todos los comandos disponibles en tu sistema. Si en cambio quieres buscar sólo comandos de módulos que hayas cargado explícitamente o mediante carga automática, utiliza el parámetro-ListImported parámetro. Para más información sobre la carga automática de comandos de PowerShell, consulta la Receta 1.28.

Consulta la Receta 1.13 para ver una lista de todos los comandos junto con una breve descripción de lo que hacen.

El cmdlet Get-Command es uno de los tres comandos que utilizarás con más frecuencia a medida que explores PowerShell. Los otros dos comandos son Get-Help y Get-Member.

En hay un punto importante que debes tener en cuenta cuando busques un comando PowerShell para realizar una tarea concreta. Muchas veces, ese comando PowerShell no existe, porque la tarea se realiza mejor de la misma forma que siempre-por ejemplo, ipconfig.exe para obtener información de configuración IP, netstat.exe para listar estadísticas de protocolo y conexiones de red TCP/IP actuales, y muchos más.

Para más información sobre el cmdlet Get-Command, escribe Get-Help Get-Command.

Ver también

Receta 1.13

1.13 Obtener ayuda sobre una orden

Problema

quieres aprender cómo funciona un comando concreto y cómo utilizarlo.

Solución

El comando que proporciona ayuda e información de uso sobre un comando se llama Get-Help. Admite varias vistas diferentes de la información de ayuda, en función de tus necesidades.

Para obtener el resumen de la información de ayuda de un comando concreto, proporciona el nombre del comando como argumento al cmdlet Get-Help. Esto incluye principalmente su sinopsis, sintaxis y descripción detallada:

Get-Help CommandName

o:

CommandName -?

Para obtener la información de ayuda detallada de un comando concreto, proporciona la bandera -Detailed al cmdlet Get-Help. Además de la vista resumida, también incluye la descripción de sus parámetros y ejemplos:

Get-Help CommandName -Detailed

Para obtener la información de ayuda completa de un comando concreto, proporciona la bandera -Full al cmdlet Get-Help. Además de la vista detallada, también incluye la descripción completa de sus parámetros y notas adicionales:

Get-Help CommandName -Full

Para obtener sólo los ejemplos de un comando concreto, proporciona la bandera -Examples al cmdlet Get-Help:

Get-Help CommandName -Examples

Para que recupere la versión en línea más actualizada del tema de ayuda de un comando, proporciona la bandera -Online al cmdlet Get-Help:

Get-Help CommandName -Online

Para consultar en una vista gráfica de un tema de ayuda en la que se puedan realizar búsquedas, utiliza el parámetro -ShowWindow:

Get-Help CommandName -ShowWindow

Para buscar todos los temas de ayuda que contengan una palabra clave determinada, proporciona esa palabra clave como argumento al cmdlet Get-Help. Si la palabra clave no es también el nombre de un tema de ayuda concreto, éste devuelve todos los temas de ayuda que contengan la palabra clave, incluyendo su nombre, categoría y sinopsis:

Get-Help Keyword

Debate

El cmdlet Get-Help es la forma principal de interactuar con el sistema de ayuda en PowerShell. Al igual que el cmdlet Get-Command, el cmdlet Get-Help admite comodines. Si quieres listar todos los comandos cuyo contenido de ayuda coincida con un determinado patrón (por ejemplo, proceso), sólo tienes que escribir:

Get-Help *process*

Si el patrón coincide con un solo comando, PowerShell muestra la ayuda de ese comando. Aunque el comodín de comandos y la búsqueda por palabras clave son una forma útil de buscar en la ayuda de PowerShell, consulta la Receta 1.15 para ver un script que te permite buscar en el contenido de la ayuda un patrón especificado.

Aunque tienes a tu disposición miles de páginas de contenido de ayuda escrito a medida en , PowerShell sólo incluye por defecto la información que puede generar automáticamente a partir de la contenida en los propios comandos: nombres, parámetros, sintaxis y parámetros por defecto. Necesitas actualizar el contenido de la ayuda para recuperar el resto. Cuando ejecutes Get-Help para un comando para el que no hayas descargado contenido de ayuda, verás las siguientes observaciones como parte de esa ayuda:

REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer.
    It is displaying only partial help.
        -- To download and install Help files for the module that includes
    this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: "Get-Help
    Get-Process -Online" or
           go to https://go.microsoft.com/fwlink/?LinkID=2096814.

Ejecuta el cmdlet Update-Help, y PowerShell descargará e instalará automáticamente el contenido de ayuda más reciente para todos los módulos de tu sistema. Para más información sobre la ayuda actualizable, consulta la Receta 1.14.

Si quieres generar una lista de todos los cmdlets y alias (junto con sus breves sinopsis), ejecuta el siguiente comando:

Get-Help * -Category Cmdlet | Select-Object Name,Synopsis | Format-Table -Auto

Además de la ayuda basada en la consola, PowerShell también ofrece acceso en línea a su contenido de ayuda. La Solución muestra cómo acceder rápidamente al contenido de la ayuda en línea.

El cmdlet Get-Help es uno de los tres comandos que utilizarás con más frecuencia a medida que explores PowerShell. Los otros dos comandos son Get-Command y Get-Member.

Para más información sobre el cmdlet Get-Help, escribe Get-Help Get-Help.

1.14 Actualizar el contenido de la Ayuda del Sistema

Problema

En querrás actualizar el contenido de la ayuda de tu sistema a lo último disponible.

Solución

Ejecuta el comando Update-Help. Para recuperar la ayuda de una ruta local, utiliza el parámetro-SourcePath parámetro del cmdlet:

Update-Help

o:

Update-Help -SourcePath \\helpserver\help

Debate

Uno de los mayores puntos fuertes de PowerShell es el increíble detalle de su contenido de ayuda. Contando sólo el contenido de la ayuda y los temas de about_* que describen la funcionalidad básica, la ayuda de PowerShell incluye aproximadamente medio millón de palabras y abarcaría 1.200 páginas si se imprimiera.

El reto al que cada versión de PowerShell se ha visto obligada a enfrentarse es que este contenido de ayuda se escribe al mismo tiempo que la propia PowerShell. Dado que su objetivo es ayudar al usuario, el contenido que está listo en el momento del lanzamiento de una versión de PowerShell es la mejor estimación posible de aquello para lo que los usuarios necesitarán ayuda.

A medida que los usuarios se familiarizan con PowerShell, empiezan a tener preguntas. Algunas de ellas se abordan en los temas de ayuda, mientras que otras no. A veces, la ayuda es simplemente incorrecta debido a un cambio en el producto durante el lanzamiento. Para solucionar esto, PowerShell admite la ayuda actualizable en.

No sólo es posible actualizar la ayuda, sino que, de hecho, el comando Update-Help es la única forma de obtener ayuda en tu sistema. Fuera de la caja, PowerShell proporciona una experiencia derivada únicamente de lo que está incorporado en los propios comandos: nombre, sintaxis, parámetros y valores por defecto.

Cuando ejecute Get-Help para un comando para el que no hayas descargado contenido de ayuda, verás las siguientes observaciones como parte de esa ayuda:

REMARKS
    Get-Help cannot find the Help files for this cmdlet on this computer.
    It is displaying only partial help.
        -- To download and install Help files for the module that includes
    this cmdlet, use Update-Help.
        -- To view the Help topic for this cmdlet online, type: "Get-Help
    Get-Process -Online" or
           go to https://go.microsoft.com/fwlink/?LinkID=2096814.

Ejecuta el cmdlet Update-Help, y PowerShell descargará e instalará automáticamente el contenido de ayuda más reciente para todos los módulos de tu sistema.

Cuando ejecutas Update-Help, PowerShell busca en cada módulo de tu sistema, comparando la ayuda que tienes para ese módulo con la última versión en línea. Para los módulos en caja, PowerShell utiliza download.microsoft.com para recuperar el contenido actualizado de la ayuda. Otros módulos que descargues de Internet pueden utilizar la clave de módulo HelpInfoUri para respaldar su propia ayuda actualizable.

PowerShell almacena este contenido en el directorio PowerShell\Help de tus documentos de usuario o directorio personal.

Por defecto, el comando Update-Help recupera su contenido de Internet. Si quieres actualizar la ayuda en una máquina no conectada a Internet, puedes utilizar el parámetro-SourcePath del cmdlet Update-Help. Esta ruta representa un directorio o ruta UNC donde PowerShell debe buscar el contenido actualizado de la ayuda. Para rellenar este contenido, utiliza primero el cmdlet Save-Help para descargar los archivos y, a continuación, cópialos en la ubicación de origen.

Para más información sobre la ayuda de PowerShell, consulta la Receta 1.13.

1.15 Programa: Buscar ayuda por texto

Tanto como los cmdlets Get-Command y Get-Help te permiten buscar nombres de comandos que coincidan con un patrón determinado. Sin embargo, cuando no sepas exactamente qué partes del nombre de un comando estás buscando, te resultará más fácil buscar la respuesta en el contenido de la ayuda. En los sistemas Unix, este comando se llama Apropos.

El cmdlet Get-Help busca automáticamente referencias a palabras clave en la base de datos de ayuda cuando no encuentra un tema de ayuda para el argumento que le proporcionas. Además de eso, puede que quieras ampliarlo aún más para buscar patrones de texto o incluso temas de ayuda que hablen de temas de ayuda existentes. Las herramientas de ayuda de PowerShell admiten una versión de búsqueda de contenido con comodines, pero no admiten expresiones regulares completas.

Pero eso no tiene por qué detenernos, ya que podemos escribir la funcionalidad nosotros mismos.

Para ejecutar este programa, proporciona una cadena de búsqueda al script Search-Help (que aparece en el Ejemplo 1-9). La cadena de búsqueda puede ser un texto simple o una expresión regular. A continuación, el script muestra el nombre y la sinopsis de todos los temas de ayuda que coincidan. Para ver el contenido de la ayuda de ese tema, utiliza el cmdlet Get-Help.

Ejemplo 1-9. Buscar-Ayuda.ps1
##############################################################################
##
## Search-Help
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Search the PowerShell help documentation for a given keyword or regular
expression. For simple keyword searches in PowerShell version two or three,
simply use "Get-Help <keyword>"

.EXAMPLE

PS > Search-Help hashtable
Searches help for the term 'hashtable'

.EXAMPLE

PS > Search-Help "(datetime|ticks)"
Searches help for the term datetime or ticks, using the regular expression
syntax.

#>

param(
    ## The pattern to search for
    [Parameter(Mandatory = $true)]
    $Pattern
)

$helpNames = $(Get-Help * | Where-Object { $_.Category -ne "Alias" })

## Go through all of the help topics
foreach($helpTopic in $helpNames)
{
    ## Get their text content, and
    $content = Get-Help -Full $helpTopic.Name | Out-String
    if($content -match "(.{0,30}$pattern.{0,30})")
    {
        $helpTopic | Add-Member NoteProperty Match $matches[0].Trim()
        $helpTopic | Select-Object Name,Match
    }
}

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.16 Iniciar PowerShell en una ubicación específica

Problema

En querrás iniciar una sesión PowerShell en una ubicación concreta.

Solución

Tanto Windows como PowerShell ofrecen varias formas de iniciar PowerShell en una ubicación concreta:

  • Barra de direcciones del Explorador

  • Argumentos de la línea de comandos de PowerShell

  • Terminal de Windows Extensión de shell "Abrir en Terminal de Windows

Debate

Si estás navegando por el sistema de archivos con el Explorador de Windows, escribe pwsh.exe opowershell.exe en la barra de direcciones se inicia PowerShell en esa ubicación (como se muestra en la Figura 1-2).

wps4 0102
Figura 1-2. Iniciar PowerShell desde el Explorador de Windows

Ten en cuenta que lo que escribas debe terminar con la extensión .exe, de lo contrario el Explorador abrirá generalmente tu carpeta de documentos PowerShell. Además, puedes abrir Windows PowerShell directamente desde el menú Archivo, como se muestra en la Figura 1-3.

Para otra forma de iniciar PowerShell desde el Explorador de Windows, el Terminal de Windows (si lo has instalado) añade una opción "Abrir en el Terminal de Windows" cuando haces clic con el botón derecho en una carpeta desde el Explorador de Windows.

Si no estás navegando por la carpeta deseada con el Explorador de Windows, puedes utilizar Inicio→Ejecutar (o cualquier otro medio de lanzar una aplicación) para lanzar PowerShell en una ubicación específica. Para ello, utiliza el parámetro -NoExit de PowerShell, junto con el parámetro-Command de PowerShell. En el parámetro -Command, llama al cmdlet Set-Location para desplazarte inicialmente a la ubicación que desees.

pwsh -NoExit -Command Set-Location 'C:\Program Files'
wps4 0103
Figura 1-3. Iniciar PowerShell desde el Explorador

1.17 Invocar un comando o script PowerShell desde fuera de PowerShell

Problema

En quieres invocar un comando o script PowerShell desde un archivo por lotes, un script de inicio de sesión, una tarea programada o cualquier otra aplicación que no sea PowerShell.

Solución

Para invocar un comando PowerShell, utiliza el parámetro -Command:

pwsh -Command Get-Process; Read-Host

Para lanzar un script PowerShell, utiliza el parámetro -File:

pwsh -File 'full path to script' arguments

Por ejemplo:

pwsh -File 'c:\shared scripts\Get-Report.ps1' Hello World

Debate

Por defecto en, cualquier argumento a pwsh.exe se interpreta como un script a ejecutar. Si utilizas el parámetro -Command, PowerShell ejecuta el comando como si lo hubieras escrito en el shell interactivo y, a continuación, sale. Puedes personalizar este comportamiento suministrando otros parámetros a pwsh.exe, como -NoExit, -NoProfile, y otros.

Nota

Si eres el autor de un programa que necesita ejecutar scripts o comandos PowerShell, PowerShell te permite llamar a estos scripts y comandos de forma mucho más sencilla que llamando a su interfaz de línea de comandos. Para más información sobre este enfoque, consulta la Receta 17.10.

Puesto que lanzar un script es algo tan habitual, PowerShell proporciona el parámetro -File para eliminar las complejidades que surgen al tener que invocar un script desde el parámetro-Command parámetro. Esta técnica te permite invocar un script de PowerShell como objetivo de un script de inicio de sesión, una asociación avanzada de archivos, una tarea programada, etc.

Nota

Cuando PowerShell detecta que sus flujos de entrada o salida han sido redirigidos, suprime los avisos que podría mostrar normalmente. Si quieres alojar un aviso interactivo de PowerShell dentro de otra aplicación (como Emacs), utiliza - como argumento para el parámetro -File. En PowerShell (como en los shells Unix tradicionales), esto implica "tomado de la entrada estándar".

pwsh -File -

Si el script es para una automatización en segundo plano o una tarea programada, estos scripts a veces pueden interferir con el entorno del usuario (o verse influidos por él). Para estas situaciones, resultan útiles tres parámetros:

-NoProfile

Ejecuta el comando o script sin cargar los scripts de perfil de usuario. Esto hace que el script se inicie más rápido, pero principalmente evita que las preferencias del usuario (por ejemplo, alias y variables de preferencia) interfieran en el entorno de trabajo del script.

-WindowStyle

Ejecuta el comando o script con el estilo de ventana especificado -más comúnmente Hidden. Cuando se ejecuta con un estilo de ventana de Hidden, PowerShell oculta su ventana principal inmediatamente. Para conocer más formas de controlar el estilo de ventana desde PowerShell, consulta la Receta 24.3.

-ExecutionPolicy

Ejecuta el comando o script con una política de ejecución especificada aplicada sólo a esta instancia de PowerShell. Esto te permite escribir scripts PowerShell para gestionar un sistema sin tener que cambiar la política de ejecución de todo el sistema. Para más información sobre las políticas de ejecución de ámbito, consulta la Receta 18.1.

Si los argumentos del parámetro -Command se vuelven complejos, el manejo de caracteres especiales en la aplicación que llama a PowerShell (como cmd.exe) podría interferir con el comando que quieres enviar a PowerShell. Para esta situación, PowerShell admite un parámetro EncodedCommand : una representación codificada en Base64 de la cadena Unicode que quieres ejecutar. El Ejemplo 1-10 demuestra cómo convertir una cadena que contiene comandos PowerShell a una forma codificada en Base64.

Ejemplo 1-10. Convertir comandos PowerShell en una forma codificada en Base64
$commands = '1..10 | % { "PowerShell Rocks" }'
$bytes = [System.Text.Encoding]::Unicode.GetBytes($commands)
$encodedString = [Convert]::ToBase64String($bytes)

Una vez que tengas la cadena codificada, puedes utilizarla como valor del parámetro EncodedCommand, como se muestra en el Ejemplo 1-11.

Ejemplo 1-11. Iniciar PowerShell con un comando codificado desde cmd.exe
Microsoft Windows [Version 10.0.19041.685]
(c) 2020 Microsoft Corporation. All rights reserved.

C:\Users\Lee>PowerShell -EncodedCommand MQAuAC4AMQAwACAAfAAgACUAIAB7ACAAIgBQAG8A
   dwBlAHIAUwBoAGUAbABsACAAUgBvAGMAawBzACIAIAB9AA==
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks
PowerShell Rocks

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.18 Entender y Personalizar el Completado de Pestañas de PowerShell

Problema

En quieres personalizar cómo reacciona PowerShell al pulsar la tecla Tabulador y Ctrl+Espacio.

Solución

Crea una función personalizada llamada TabExpansion2. PowerShell invoca esta función cuando pulsas Tabulador o Ctrl+Espacio en la consola o en Visual Studio Code.

Debate

Cuando pulsas Tabulador, PowerShell invoca una función conocida como expansión del tabulador: sustituir lo que has escrito hasta ese momento por una versión expandida de ello (si se aplica alguna). Por ejemplo, si escribes Set-Location C:\ y luego pulsas Tabulador, PowerShell empieza a recorrer los directorios de C:\ para que navegues hasta ellos.

Las funciones que ofrece la expansión de pestañas incorporada de PowerShell son bastante ricas, como se muestra en la Tabla 1-2.

Tabla 1-2. Funciones de expansión de pestañas en PowerShell
Descripción Ejemplo

Completar comando. Completa los nombres de los comandos cuando el texto actual parece representar una invocación de comando.

Get-Ch <Tab>

Completar parámetros. Completa los parámetros del comando actual.

Get-ChildItem -Pat <Tab>

Completar argumentos. Completa los argumentos de comando para el parámetro de comando actual. Esto se aplica a cualquier argumento de comando que tome un conjunto fijo de valores (enumeraciones o parámetros que definen un atributo ValidateSet ). Además, PowerShell contiene completado de argumentos ampliado para nombres de módulo, temas de ayuda, clases CIM / WMI, nombres de registro de eventos, IDs y nombres de trabajo, IDs y nombres de proceso, nombres de proveedor, nombres de unidad, nombres de servicio y nombres de pantalla, y nombres de fuente de rastreo.

Set-ExecutionPolicy -ExecutionPolicy <Tab>

Completar texto del historial. Sustituye la entrada actual por elementos del historial de comandos que coincidan con el texto después del carácter #.

#Process <Tab>

Finalización del ID del historial. Sustituye la entrada actual por la línea de comando del número de elemento ID de tu historial de comandos.

#12 <Tab>

Completar nombre de archivo. Sustituye el valor del parámetro actual por nombres de archivo que coincidan con lo que has escrito hasta ahora. Cuando se aplica al cmdlet Set-Location, PowerShell filtra aún más los resultados a sólo directorios.

Set-Location C:\Windows\S <Tab>

Completar operador. Sustituye el texto actual por un operador coincidente. Esto incluye las banderas suministradas a la sentencia switch.

"Hello World" -rep<Tab>

switch - c <Tab>

Completar variables. Sustituye el texto actual por las variables PowerShell disponibles. PowerShell incorpora incluso variables del contenido de scripts que nunca han sido invocados.

$myGreeting = "Hello World"; $myGr <Tab>

Completar miembro. Sustituye los nombres de los miembros de la variable o tipo a los que se hace referencia en ese momento. Cuando PowerShell puede deducir los miembros a partir de comandos anteriores de la cadena, admite incluso la compleción de miembros dentro de bloques de secuencia de comandos.

[Console]::Ba <Tab>

Get-Process | Where-Object { $_.Ha <Tab>

Completar tipos. Sustituye los nombres abreviados de los tipos por su nombre cualificado por el espacio de nombres.

[PSSer <Tab>

$l = New-Object List[Stri <Tab>

Si quieres ampliar las capacidades de expansión de pestañas de PowerShell, define una función llamada TabExpansion2. Puedes añadirla directamente a tu perfil de PowerShell, o utilizarla como fuente de tu perfil. El Ejemplo 1-12 muestra un ejemplo de función personalizada de expansión de pestañas que amplía la funcionalidad ya incorporada en PowerShell.

Ejemplo 1-12. Ejemplo de implementación de TabExpansion2
##############################################################################
##
## TabExpansion2
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

function TabExpansion2
{
    [CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
    Param(
        [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)]
        [string] $inputScript,

        [Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)]
        [int] $cursorColumn,

        [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)]
        [System.Management.Automation.Language.Ast] $ast,

        [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)]
        [System.Management.Automation.Language.Token[]] $tokens,

        [Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)]
        [System.Management.Automation.Language.IScriptPosition] $positionOfCursor,

        [Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
        [Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
        [Hashtable] $options = $null
    )

    End
    {
        ## Create a new 'Options' hashtable if one has not been supplied.
        ## In this hashtable, you can add keys for the following options, using
        ## $true or $false for their values:
        ##
        ## IgnoreHiddenShares - Ignore hidden UNC shares (such as \\COMPUTER\ADMIN$)
        ## RelativePaths - When expanding filenames and paths, $true forces PowerShell
        ##     to replace paths with relative paths. When $false, forces PowerShell to
        ##     replace them with absolute paths. By default, PowerShell makes this
        ##     decision based on what you had typed so far before invoking tab completion.
        ## LiteralPaths - Prevents PowerShell from replacing special file characters
        ##     (such as square brackets and back-ticks) with their escaped equivalent.
        if(-not $options) { $options = @{} }

        ## Demonstrate some custom tab expansion completers for parameters.
        ## This is a hash table of parameter names (and optionally cmdlet names)
        ## that we add to the $options hashtable.
        ##
        ## When PowerShell evaluates the script block, $args gets the
        ## following: command name, parameter, word being completed,
        ## AST of the command being completed, and currently-bound arguments.
        $options["CustomArgumentCompleters"] = @{
            "Get-ChildItem:Filter" = { "*.ps1","*.txt","*.doc" }
            "ComputerName" = { "ComputerName1","ComputerName2","ComputerName3" }
        }

        ## Also define a completer for a native executable.
        ## When PowerShell evaluates the script block, $args gets the
        ## word being completed, and AST of the command being completed.
        $options["NativeArgumentCompleters"] = @{
            "attrib" = { "+R","+H","+S" }
        }

        ## Define a "quick completions" list that we'll cycle through
        ## when the user types '!!' followed by TAB.
        $quickCompletions = @(
            'Get-Process -Name PowerShell | ? Id -ne $pid | Stop-Process',
            'Set-Location $pshome',
            ('$errors = $error | % { $_.InvocationInfo.Line }; Get-History | ' +
                ' ? { $_.CommandLine -notin $errors }')
        )

        ## First, check the built-in tab completion results
        $result = $null
        if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
        {
            $result = [System.Management.Automation.CommandCompletion]::CompleteInput(
                <#inputScript#>  $inputScript,
                <#cursorColumn#> $cursorColumn,
                <#options#>      $options)
        }
        else
        {
            $result = [System.Management.Automation.CommandCompletion]::CompleteInput(
                <#ast#>              $ast,
                <#tokens#>           $tokens,
                <#positionOfCursor#> $positionOfCursor,
                <#options#>          $options)
        }

        ## If we didn't get a result
        if($result.CompletionMatches.Count -eq 0)
        {
            ## If this was done at the command-line or in a remote session,
            ## create an AST out of the input
            if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
            {
                $ast = [System.Management.Automation.Language.Parser]::ParseInput(
                    $inputScript, [ref]$tokens, [ref]$null)
            }

            ## In this simple example, look at the text being supplied.
            ## We could do advanced analysis of the AST here if we wanted,
            ## but in this case just use its text. We use a regular expression
            ## to check if the text started with two exclamations, and then
            ## use a match group to retain the rest.
            $text = $ast.Extent.Text
            if($text -match '^!!(.*)')
            {
                ## Extract the rest of the text from the regular expression
                ## match group.
                $currentCompletionText = $matches[1].Trim()

                ## Go through each of our quick completions and add them to
                ## our completion results. The arguments to the completion results
                ## are the text to be used in tab completion, a potentially shorter
                ## version to use for display (i.e.: intellisense in the ISE),
                ## the type of match, and a potentially more verbose description to
                ## be used as a tool tip.
                $quickCompletions | Where-Object { $_ -match $currentCompletionText } |
                    Foreach-Object { $result.CompletionMatches.Add(
                        (New-Object Management.Automation.CompletionResult $_,$_,
                            "Text",$_) )
                }
            }
        }

        return $result
    }
}

1.19 Programa: Aprende alias para comandos comunes

En el uso interactivo de , los nombres completos de los cmdlets (como Get-ChildItem) son engorrosos y lentos de escribir. Aunque los alias son mucho más eficientes, lleva un tiempo descubrirlos. Para aprender más fácilmente los alias, puedes modificar tu prompt para que te recuerde la versión abreviada de los comandos con alias que utilices.

Esto implica dos pasos:

  1. Añade el programa, Get-AliasSuggestion.ps1, que se muestra en el Ejemplo 1-13, a tu directorio de herramientas o a otro directorio.

    Ejemplo 1-13. Get-AliasSugerencia.ps1
    ##############################################################################
    ##
    ## Get-AliasSuggestion
    ##
    ## From PowerShell Cookbook (O'Reilly)
    ## by Lee Holmes (http://www.leeholmes.com/guide)
    ##
    ##############################################################################
    
    <#
    
    .SYNOPSIS
    
    Get an alias suggestion from the full text of the last command. Intended to
    be added to your prompt function to help learn aliases for commands.
    
    .EXAMPLE
    
    PS > Get-AliasSuggestion Remove-ItemProperty
    Suggestion: An alias for Remove-ItemProperty is rp
    
    #>
    
    param(
        ## The full text of the last command
        $LastCommand
    )
    
    Set-StrictMode -Version 3
    
    $helpMatches = @()
    
    ## Find all of the commands in their last input
    $tokens = [Management.Automation.PSParser]::Tokenize(
        $lastCommand, [ref] $null)
    $commands = $tokens | Where-Object { $_.Type -eq "Command" }
    
    ## Go through each command
    foreach($command in $commands)
    {
        ## Get the alias suggestions
        foreach($alias in Get-Alias -Definition $command.Content)
        {
            $helpMatches += "Suggestion: An alias for " +
                "$($alias.Definition) is $($alias.Name)"
        }
    }
    
    $helpMatches
  2. Añade el texto del Ejemplo 1-14 a la función Prompt de tu perfil. Si todavía no tienes una función Prompt, consulta la Receta 1.9 para aprender a añadir una.

    Ejemplo 1-14. Un prompt útil para enseñarte alias de comandos comunes
    function prompt
    {
        ## Get the last item from the history
        $historyItem = Get-History -Count 1
    
        ## If there were any history items
        if($historyItem)
        {
            ## Get the training suggestion for that item
            $suggestions = @(Get-AliasSuggestion $historyItem.CommandLine)
            ## If there were any suggestions
            if($suggestions)
            {
                ## For each suggestion, write it to the screen
                foreach($aliasSuggestion in $suggestions)
                {
                    Write-Host "$aliasSuggestion"
                }
                Write-Host ""
    
            }
        }
    
        ## Rest of prompt goes here
        "PS [$env:COMPUTERNAME] >"
    }

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.20 Programa: Aprender alias para parámetros comunes

Problema

Quieres aprender los alias definidos para los parámetros de los comandos.

Solución

Utiliza el script Get-ParameterAlias, como se muestra en el Ejemplo 1-15, para devolver todos los alias de los parámetros utilizados por el comando anterior en tu historial de sesiones.

Ejemplo 1-15. Get-ParámetroAlias.ps1
##############################################################################
##
## Get-ParameterAlias
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Looks in the session history, and returns any aliases that apply to
parameters of commands that were used.

.EXAMPLE

PS > dir -ErrorAction SilentlyContinue
PS > Get-ParameterAlias
An alias for the 'ErrorAction' parameter of 'dir' is ea

#>

Set-StrictMode -Version 3

## Get the last item from their session history
$history = Get-History -Count 1
if(-not $history)
{
    return
}

## And extract the actual command line they typed
$lastCommand = $history.CommandLine

## Use the Tokenizer API to determine which portions represent
## commands and parameters to those commands
$tokens = [System.Management.Automation.PsParser]::Tokenize(
    $lastCommand, [ref] $null)
$currentCommand = $null

## Now go through each resulting token
foreach($token in $tokens)
{
    ## If we've found a new command, store that.
    if($token.Type -eq "Command")
    {
        $currentCommand = $token.Content
    }

    ## If we've found a command parameter, start looking for aliases
    if(($token.Type -eq "CommandParameter") -and ($currentCommand))
    {
        ## Remove the leading "-" from the parameter
        $currentParameter = $token.Content.TrimStart("-")

        ## Determine all of the parameters for the current command.
        (Get-Command $currentCommand).Parameters.GetEnumerator() |

            ## For parameters that start with the current parameter name,
            Where-Object { $_.Key -like "$currentParameter*" } |

            ## return all of the aliases that apply. We use "starts with"
            ## because the user might have typed a shortened form of
            ## the parameter name.
            Foreach-Object {
                $_.Value.Aliases | Foreach-Object {
                    "Suggestion: An alias for the '$currentParameter' " +
                    "parameter of '$currentCommand' is '$_'"
                }
            }
    }
}

Debate

Para facilitar la escritura de parámetros de comandos, PowerShell te permite escribir sólo la parte del parámetro del comando que sea necesaria para desambiguarlo de otros parámetros de ese comando. Además de los acortamientos implícitos admitidos por el shell, los autores de cmdlets también pueden definir alias explícitos para sus parámetros; por ejemplo, CN como forma abreviada de ComputerName.

Aunque son útiles, estos alias son difíciles de descubrir.

Si quieres ver los alias de un comando concreto, puedes acceder a su colección Parameters:

PS > (Get-Command New-TimeSpan).Parameters.Values | Select Name,Aliases

Name                Aliases
----                -------
Start               {LastWriteTime}
End                 {}
Days                {}
Hours               {}
Minutes             {}
Seconds             {}
Verbose             {vb}
Debug               {db}
ErrorAction         {ea}
WarningAction       {wa}
InformationAction   {infa}
ErrorVariable       {ev}
WarningVariable     {wv}
InformationVariable {iv}
OutVariable         {ov}
OutBuffer           {ob}
PipelineVariable    {pv}

Si quieres aprender cualquier alias de los parámetros de tu comando anterior, simplemente ejecuta Get-ParameterAlias.ps1. Para que PowerShell lo haga automáticamente, añade una llamada a Get-ParameterAlias.ps1 en tu prompt.

Este script de se basa en dos características principales: La API Tokenizer de PowerShell y la rica información que devuelve el cmdlet Get-Command. La API Tokenizer de PowerShell examina su entrada y devuelve la interpretación que PowerShell hace de ella: comandos, parámetros, valores de parámetros, operadores y mucho más. Al igual que la rica salida producida por la mayoría de los comandos de PowerShell, Get-Command devuelve información sobre los parámetros de un comando, conjuntos de parámetros, tipo de salida (si se especifica) y mucho más.

Para más información sobre la API Tokenizer, consulta la Receta 10.10.

1.21 Acceder y Gestionar el Historial de tu Consola

Problema

Después de trabajar en el shell durante un tiempo, querrás invocar comandos de tu historial, ver tu historial de comandos y guardar tu historial de comandos.

Solución

Los atajos que se dan en la Receta 1.9 te permiten gestionar tu historial, pero PowerShell ofrece varias funciones que te ayudarán a trabajar con tu consola de forma aún más detallada.

Para obtener los comandos más recientes de tu sesión, utiliza el cmdlet Get-History (o su alias h):

Get-History

Para volver a ejecutar un comando específico de tu historial de sesiones, proporciona su ID al cmdlet Invoke-History (o su alias ihy):

Invoke-History ID

Para aumentar (o limitar) el número de comandos almacenados en tu historial de sesión, asigna un nuevo valor a la variable $MaximumHistoryCount:

$MaximumHistoryCount = Count

Para guardar tu historial de comandos en un archivo, canaliza la salida de Get-History al cmdlet Export-CliXml:

Get-History | Export-CliXml Filename

Para que añada un historial de comandos previamente guardado al historial de la sesión actual, llama al cmdlet Import-CliXml y luego canaliza esa salida al cmdlet Add-History:

Import-CliXml Filename | Add-History

Para borrar todos los comandos del historial de tu sesión, utiliza el cmdlet Clear-History:

Clear-History

Debate

A diferencia de las teclas rápidas del historial de la consola que se comentan en la Receta 1.9, el cmdlet Get-History produce objetos ricos que representan información sobre los elementos de tu historial. Cada objeto contiene el ID de ese elemento, la línea de comandos, la hora de inicio de ejecución y la hora de fin de ejecución.

Una vez que conozcas el ID de un elemento del historial (como se muestra en la salida de Get-History), puedes pasarlo a Invoke-History para volver a ejecutar ese comando. La función prompt de ejemplo que se muestra en la Receta 1.9 facilita el trabajo con elementos del historial anteriores, ya que el prompt de cada comando incluye el ID del historial que lo representará.

Nota

En puedes ver fácilmente cuánto tiempo se tardó en invocar una serie de comandos consultando la propiedad Duration. Es una forma estupenda de saber exactamente cuánto tiempo se tardó en invocar los comandos que acaban de ahorrarte horas de trabajo manual:

PS:29 > Get-History 27,28 | Format-Table *

Id CommandLine             StartExecutionTime   Duration
-- -----------             ------------------   --------
27 dir                     2/15/2021 5:12:49 PM 00:00:00.0319401
28 Start-Sleep -Seconds 45 2/15/2021 5:12:53 PM 00:00:45.0073792

Los IDs proporcionados por el cmdlet Get-History difieren de los IDs proporcionados por las teclas rápidas comunes del historial de la consola de Windows (como F7), porque sus técnicas de gestión del historial difieren.

Por defecto, PowerShell almacena las últimas 4.096 entradas de tu historial de comandos. Si quieres aumentar o disminuir esta cantidad, establece la variable $MaximumHistoryCount al tamaño que desees. Para que este cambio sea permanente, establece la variable en tu script de perfil de PowerShell.

Con diferencia, la función más útil del historial de comandos de PowerShell es para revisar la experimentación ad hoc y capturarla en un script que luego puedas utilizar una y otra vez. Para una visión general de ese proceso (y un script que ayuda a automatizarlo), consulta la Receta 1.22.

1.22 Programa: Crea Scripts a partir de tu Historial de Sesiones

Después de experimentando interactivamente en la línea de comandos durante un tiempo para resolver una tarea de varios pasos, a menudo querrás conservar o compartir los pasos exactos que utilizaste para resolver finalmente el problema. El script te sonríe desde el búfer del historial, pero desgraciadamente está rodeado de muchos más comandos que no quieres conservar.

Nota

Para ver un ejemplo de cómo utilizar el cmdlet Out-GridView para hacerlo gráficamente, consulta la Receta 2.4.

Para resolver este problema en, utiliza el cmdlet Get-History para ver los comandos recientes que has escrito. A continuación, llama a Copy-History con los ID de los comandos que quieras conservar, como se muestra en el Ejemplo 1-16.

Ejemplo 1-16. Copiar-Historial.ps1
##############################################################################
##
## Copy-History
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Copy selected commands from the history buffer into the clipboard as a script.

.EXAMPLE

PS > Copy-History
Copies the entire contents of the history buffer into the clipboard.

.EXAMPLE

PS > Copy-History -5
Copies the last five commands into the clipboard.

.EXAMPLE

PS > Copy-History 2,5,8,4
Copies commands 2,5,8, and 4.

.EXAMPLE

PS > Copy-History (1..10+5+6)
Copies commands 1 through 10, then 5, then 6, using PowerShell's array
slicing syntax.

#>

[CmdletBinding()]
param(
    ## The range of history IDs to copy
    [Alias("Id")]
    [int[]] $Range
)

Set-StrictMode -Version 3

$history = @()

## If they haven't specified a range, assume it's everything
if((-not $range) -or ($range.Count -eq 0))
{
    $history = @(Get-History -Count ([Int16]::MaxValue))
}
## If it's a negative number, copy only that many
elseif(($range.Count -eq 1) -and ($range[0] -lt 0))
{
    $count = [Math]::Abs($range[0])
    $history = (Get-History -Count $count)
}
## Otherwise, go through each history ID in the given range
## and add it to our history list.
else
{
    foreach($commandId in $range)
    {
        if($commandId -eq -1) { $history += Get-History -Count 1 }
        else { $history += Get-History -Id $commandId }
    }
}

## Finally, export the history to the clipboard.
$history | Foreach-Object { $_.CommandLine } | clip.exe

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.23 Invocar un Comando desde tu Historial de Sesiones

Problema

En quieres ejecutar un comando del historial de tu sesión actual.

Solución

Utiliza el cmdlet Invoke-History (o su alias ihy ) para invocar un comando específico por su ID:

Invoke-History ID

Para buscar en tu historial un comando que contenga text:

PS > #text<Tab>

Para repoblar tu comando con el texto de un comando anterior por su ID:

PS > #ID<Tab>

Debate

Cuando lleves un rato con el intérprete de comandos abierto, el búfer del historial se llenará rápidamente de comandos útiles. Las teclas de acceso rápido para la gestión del historial de descritas en la Receta 1.9 muestran una forma de navegar por tu historial, pero este tipo de navegación por el historial sólo funciona para las líneas de comandos que hayas escrito en esa sesión concreta. Si mantienes un historial de comandos persistente (como se muestra en la Receta 1.31), estos atajos no se aplican.

El cmdlet Invoke-History ilustra el ejemplo más sencillo de trabajar con tu historial de comandos. Dado un ID de historial concreto (quizás mostrado en tu función prompt), al llamar a Invoke-History con ese ID se ejecutará de nuevo ese comando. Para más información sobre esta técnica, consulta la Receta 1.9.

Como parte de su compatibilidad con el completado con tabulador, PowerShell también te facilita el acceso a comandos anteriores. Si antepones a tu comando el carácter #, la finalización con tabulador adopta uno de dos enfoques:

Finalización de ID

Si en tecleas un número, el tabulador de finalización encuentra la entrada de tu historial de comandos con ese ID y, a continuación, sustituye tu línea de comandos por el texto de esa entrada del historial. Esto es especialmente útil cuando quieres modificar ligeramente una entrada anterior del historial, ya que Invoke-History por sí mismo no admite eso.

Finalización del patrón

Si escribes cualquier otra cosa, el completado de tabulación busca entradas en tu historial de comandos que contengan ese texto. Bajo el capó, PowerShell utiliza el operador -like para hacer coincidir tus entradas de comandos, por lo que puedes utilizar todos los caracteres comodín que admite ese operador. Para más información sobre la búsqueda de patrones en el texto, consulta la Receta 5.7.

El completado de pestañas de PowerShell está impulsado en gran medida por la función Tab​Expansion2 totalmente personalizable. Puedes modificar fácilmente esta función para incluir funciones más avanzadas, o incluso sólo personalizar comportamientos concretos para adaptarlos a tus preferencias personales. Para más información, consulta la Receta 1.18.

1.24 Programa: Buscar un patrón en la salida formateada

Aunque las funciones de filtrado integradas de PowerShell son increíblemente flexibles (por ejemplo, el cmdlet Where-Object ), generalmente operan contra propiedades específicas del objeto entrante. Si buscas texto en la salida formateada del objeto, o no sabes qué propiedad contiene el texto que buscas, a veces resulta útil el sencillo filtrado basado en texto.

Para resolver este problema en, puedes canalizar la salida en el cmdlet Out-String antes de pasarla al cmdlet Select-String:

Get-Service | Out-String -Stream | Select-String audio

O, utilizando alias incorporados:

Get-Service | oss | sls audio

En forma de script, Select-TextOutput (mostrado en el Ejemplo 1-17) hace exactamente esto, y te permite buscar un patrón en la representación visual de la salida del comando.

Ejemplo 1-17. Seleccionar-TextoSalida.ps1
##############################################################################
##
## Select-TextOutput
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Searches the textual output of a command for a pattern.

.EXAMPLE

PS > Get-Service | Select-TextOutput audio
Finds all references to "Audio" in the output of Get-Service

#>

param(
    ## The pattern to search for
    $Pattern
)

Set-StrictMode -Version 3
$input | Out-String -Stream | Select-String $pattern

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.25 Ver y procesar interactivamente la salida de comandos

Problema

En querrás explorar y analizar gráficamente la salida de un comando.

Solución

Utiliza el cmdlet Out-GridView para explorar interactivamente la salida de un comando.

Debate

El cmdlet Out-GridView es uno de los pocos cmdlets de PowerShell que muestra una interfaz gráfica de usuario. Mientras que los cmdlets Where-Object y Sort-Object son la forma más habitual de ordenar y filtrar listas de elementos, el cmdlet Out-GridView es muy eficaz en el estilo de refinamiento repetido que a veces te ayuda a desarrollar consultas complejas. La Figura 1-4 muestra el cmdlet Out-GridView en acción.

wps4 0104
Figura 1-4. Out-GridView, listo para filtrar

Out-GridView permite a filtrar principalmente la salida de tus comandos de dos formas: una expresión de filtro rápido y un filtro de criterios.

Los filtros rápidos son bastante sencillos. Cuando escribes un texto en la ventana superior "Filtro", Out-GridView filtra la lista para que sólo contenga elementos que coincidan con ese texto. Si quieres restringir este filtrado de texto a columnas específicas, simplemente proporciona un nombre de columna antes de tu cadena de búsqueda y separa ambos con dos puntos. Puedes proporcionar varias cadenas de búsqueda, en cuyo caso Out-GridView sólo devolverá las filas que coincidan con todas las cadenas requeridas.

Nota

A diferencia de la mayoría de los cmdlets de filtrado de PowerShell, los filtros rápidos del cmdlet Out-GridView no admiten comodines ni expresiones regulares. Para este tipo de consulta avanzada, puede ser útil el filtrado basado en criterios.

Los filtros de criterios proporcionan a un control detallado sobre el filtrado utilizado por el cmdlet Out-GridView. Para aplicar un filtro de criterios, haz clic en el botón "Añadir criterios" y selecciona una propiedad sobre la que filtrar. Out-GridView añade una fila debajo del campo de filtro rápido y te permite elegir una de varias operaciones para aplicar a esta propiedad:

  • Inferior o igual a

  • Mayor o igual que

  • Entre

  • Es igual a

  • No es igual a

  • Contiene

  • No contiene

Además de estas opciones de filtrado, Out-GridView también te permite hacer clic y reorganizar las columnas de cabecera para ordenarlas.

Salida de procesamiento

Una vez que hayas troceado la salida del comando, puedes seleccionar las filas que quieras conservar y pulsar Ctrl+C para copiarlas en el portapapeles. Out-GridView copia los elementos en el portapapeles como datos separados por tabuladores, de modo que puedas pegar fácilmente la información en una hoja de cálculo u otro archivo para procesarla posteriormente.

Además de admitir la salida del portapapeles, el cmdlet Out-GridView admite el filtrado de objetos de fidelidad completa si utilizas su parámetro -PassThru. Para ver un ejemplo de este filtrado de fidelidad completa, consulta la Receta 2.4.

1.26 Programa: Ver y Explorar Objetos Interactivamente

Cuando trabajas con objetos desconocidos en PowerShell, gran parte de tu tiempo lo pasas con los comandos Get-Member y Format-List: navegando por las propiedades, revisando los miembros, etc.

Para la investigación ad hoc, suele ser útil una interfaz gráfica.

Para resolver este problema en, el Ejemplo 1-18 proporciona una vista de árbol interactiva que puedes utilizar para explorar y navegar por los objetos. Por ejemplo, para examinar la estructura de un script tal y como la ve PowerShell (su árbol de sintaxis abstracta):

$ps = { Get-Process -ID $pid }.Ast
Show-Object $ps

Para más información sobre cómo analizar la estructura de los scripts PowerShell, consulta la Receta 10.10.

Ejemplo 1-18. Mostrar-Objeto.ps1
#############################################################################
##
## Show-Object
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Provides a graphical interface to let you explore and navigate an object.


.EXAMPLE

PS > $ps = { Get-Process -ID $pid }.Ast
PS > Show-Object $ps

#>

param(
    ## The object to examine
    [Parameter(ValueFromPipeline = $true)]
    $InputObject
)

Set-StrictMode -Version 3

Add-Type -Assembly System.Windows.Forms

## Figure out the variable name to use when displaying the
## object navigation syntax. To do this, we look through all
## of the variables for the one with the same object identifier.
$rootVariableName = dir variable:\* -Exclude InputObject,Args |
    Where-Object {
        $_.Value -and
        ($_.Value.GetType() -eq $InputObject.GetType()) -and
        ($_.Value.GetHashCode() -eq $InputObject.GetHashCode())
}

## If we got multiple, pick the first
$rootVariableName = $rootVariableName| % Name | Select -First 1

## If we didn't find one, use a default name
if(-not $rootVariableName)
{
    $rootVariableName = "InputObject"
}

## A function to add an object to the display tree
function PopulateNode($node, $object)
{
    ## If we've been asked to add a NULL object, just return
    if(-not $object) { return }

    ## If the object is a collection, then we need to add multiple
    ## children to the node
    if([System.Management.Automation.LanguagePrimitives]::GetEnumerator($object))
    {
        ## Some very rare collections don't support indexing (i.e.: $foo[0]).
        ## In this situation, PowerShell returns the parent object back when you
        ## try to access the [0] property.
        $isOnlyEnumerable = $object.GetHashCode() -eq $object[0].GetHashCode()

        ## Go through all the items
        $count = 0
        foreach($childObjectValue in $object)
        {
            ## Create the new node to add, with the node text of the item and
            ## value, along with its type
            $newChildNode = New-Object Windows.Forms.TreeNode
            $newChildNode.Text = "$($node.Name)[$count] = $childObjectValue"
            $newChildNode.ToolTipText = $childObjectValue.GetType()

            ## Use the node name to keep track of the actual property name
            ## and syntax to access that property.
            ## If we can't use the index operator to access children, add
            ## a special tag that we'll handle specially when displaying
            ## the node names.
            if($isOnlyEnumerable)
            {
                $newChildNode.Name = "@"
            }

            $newChildNode.Name += "[$count]"
            $null = $node.Nodes.Add($newChildNode)

            ## If this node has children or properties, add a placeholder
            ## node underneath so that the node shows a '+' sign to be
            ## expanded.
            AddPlaceholderIfRequired $newChildNode $childObjectValue

            $count++
        }
    }
    else
    {
        ## If the item was not a collection, then go through its
        ## properties
        foreach($child in $object.PSObject.Properties)
        {
            ## Figure out the value of the property, along with
            ## its type.
            $childObject = $child.Value
            $childObjectType = $null
            if($childObject)
            {
                $childObjectType = $childObject.GetType()
            }

            ## Create the new node to add, with the node text of the item and
            ## value, along with its type
            $childNode = New-Object Windows.Forms.TreeNode
            $childNode.Text = $child.Name + " = $childObject"
            $childNode.ToolTipText = $childObjectType
            if([Management.Automation.LanguagePrimitives]::GetEnumerator($childObject))
            {
                $childNode.ToolTipText += "[]"
            }

            $childNode.Name = $child.Name
            $null = $node.Nodes.Add($childNode)

            ## If this node has children or properties, add a placeholder
            ## node underneath so that the node shows a '+' sign to be
            ## expanded.
            AddPlaceholderIfRequired $childNode $childObject
        }
    }
}

## A function to add a placeholder if required to a node.
## If there are any properties or children for this object, make a temporary
## node with the text "..." so that the node shows a '+' sign to be
## expanded.
function AddPlaceholderIfRequired($node, $object)
{
    if(-not $object) { return }

    if([System.Management.Automation.LanguagePrimitives]::GetEnumerator($object) -or
        @($object.PSObject.Properties))
    {
        $null = $node.Nodes.Add( (New-Object Windows.Forms.TreeNode "...") )
    }
}

## A function invoked when a node is selected.
function OnAfterSelect
{
    param($Sender, $TreeViewEventArgs)

    ## Determine the selected node
    $nodeSelected = $Sender.SelectedNode

    ## Walk through its parents, creating the virtual
    ## PowerShell syntax to access this property.
    $nodePath = GetPathForNode $nodeSelected

    ## Now, invoke that PowerShell syntax to retrieve
    ## the value of the property.
    $resultObject = Invoke-Expression $nodePath
    $outputPane.Text = $nodePath

    ## If we got some output, put the object's member
    ## information in the text box.
    if($resultObject)
    {
        $members = Get-Member -InputObject $resultObject | Out-String
        $outputPane.Text += "`n" + $members
    }
}

## A function invoked when the user is about to expand a node
function OnBeforeExpand
{
    param($Sender, $TreeViewCancelEventArgs)

    ## Determine the selected node
    $selectedNode = $TreeViewCancelEventArgs.Node

    ## If it has a child node that is the placeholder, clear
    ## the placeholder node.
    if($selectedNode.FirstNode -and
        ($selectedNode.FirstNode.Text -eq "..."))
    {
        $selectedNode.Nodes.Clear()
    }
    else
    {
        return
    }

    ## Walk through its parents, creating the virtual
    ## PowerShell syntax to access this property.
    $nodePath = GetPathForNode $selectedNode

    ## Now, invoke that PowerShell syntax to retrieve
    ## the value of the property.
    Invoke-Expression "`$resultObject = $nodePath"

    ## And populate the node with the result object.
    PopulateNode $selectedNode $resultObject
}

## A function to handle key presses on the tree view.
## In this case, we capture ^C to copy the path of
## the object property that we're currently viewing.
function OnTreeViewKeyPress
{
    param($Sender, $KeyPressEventArgs)

    ## [Char] 3 = Control-C
    if($KeyPressEventArgs.KeyChar -eq 3)
    {
        $KeyPressEventArgs.Handled = $true

        ## Get the object path, and set it on the clipboard
        $node = $Sender.SelectedNode
        $nodePath = GetPathForNode $node
        [System.Windows.Forms.Clipboard]::SetText($nodePath)

        $form.Close()
    }
    elseif([System.Windows.Forms.Control]::ModifierKeys -eq "Control")
    {
        if($KeyPressEventArgs.KeyChar -eq '+')
        {
            $SCRIPT:currentFontSize++
            UpdateFonts $SCRIPT:currentFontSize

            $KeyPressEventArgs.Handled = $true
        }
        elseif($KeyPressEventArgs.KeyChar -eq '-')
        {
            $SCRIPT:currentFontSize--
            if($SCRIPT:currentFontSize -lt 1) { $SCRIPT:currentFontSize = 1 }
            UpdateFonts $SCRIPT:currentFontSize

            $KeyPressEventArgs.Handled = $true
        }
    }
}

## A function to handle key presses on the form.
## In this case, we handle Ctrl-Plus and Ctrl-Minus
## to adjust font size.
function OnKeyUp
{
    param($Sender, $KeyUpEventArgs)

    if([System.Windows.Forms.Control]::ModifierKeys -eq "Control")
    {
        if($KeyUpEventArgs.KeyCode -in 'Add','OemPlus')
        {
            $SCRIPT:currentFontSize++
            UpdateFonts $SCRIPT:currentFontSize

            $KeyUpEventArgs.Handled = $true
        }
        elseif($KeyUpEventArgs.KeyCode -in 'Subtract','OemMinus')
        {
            $SCRIPT:currentFontSize--
            if($SCRIPT:currentFontSize -lt 1) { $SCRIPT:currentFontSize = 1 }
            UpdateFonts $SCRIPT:currentFontSize

            $KeyUpEventArgs.Handled = $true
        }
        elseif($KeyUpEventArgs.KeyCode -eq 'D0')
        {
            $SCRIPT:currentFontSize = 12
            UpdateFonts $SCRIPT:currentFontSize

            $KeyUpEventArgs.Handled = $true
        }
    }
}

## A function to handle mouse wheel scrolling.
## In this case, we translate Ctrl-Wheel to zoom.
function OnMouseWheel
{
    param($Sender, $MouseEventArgs)

    if(
        ([System.Windows.Forms.Control]::ModifierKeys -eq "Control") -and
        ($MouseEventArgs.Delta -ne 0))
    {
        $SCRIPT:currentFontSize += ($MouseEventArgs.Delta / 120)
        if($SCRIPT:currentFontSize -lt 1) { $SCRIPT:currentFontSize = 1 }

        UpdateFonts $SCRIPT:currentFontSize
        $MouseEventArgs.Handled = $true
    }
}

## A function to walk through the parents of a node,
## creating virtual PowerShell syntax to access this property.
function GetPathForNode
{
    param($Node)

    $nodeElements = @()

    ## Go through all the parents, adding them so that
    ## $nodeElements is in order.
    while($Node)
    {
        $nodeElements = ,$Node + $nodeElements
        $Node = $Node.Parent
    }

    ## Now go through the node elements
    $nodePath = ""
    foreach($Node in $nodeElements)
    {
        $nodeName = $Node.Name

        ## If it was a node that PowerShell is able to enumerate
        ## (but not index), wrap it in the array cast operator.
        if($nodeName.StartsWith('@'))
        {
            $nodeName = $nodeName.Substring(1)
            $nodePath = "@(" + $nodePath + ")"
        }
        elseif($nodeName.StartsWith('['))
        {
            ## If it's a child index, we don't need to
            ## add the dot for property access
        }
        elseif($nodePath)
        {
            ## Otherwise, we're accessing a property. Add a dot.
            $nodePath += "."
        }

        ## Append the node name to the path
        $tempNodePath = $nodePath + $nodeName
        if($nodeName -notmatch '^[$\[\]a-zA-Z0-9]+$')
        {
            $nodePath += "'" + $nodeName + "'"
        }
        else
        {
            $nodePath = $tempNodePath
        }
    }

    ## And return the result
    $nodePath
}

function UpdateFonts
{
    param($fontSize)

    $treeView.Font = New-Object System.Drawing.Font "Consolas",$fontSize
    $outputPane.Font = New-Object System.Drawing.Font "Consolas",$fontSize
}

$SCRIPT:currentFontSize = 12

## Create the TreeView, which will hold our object navigation
## area.
$treeView = New-Object Windows.Forms.TreeView
$treeView.Dock = "Top"
$treeView.Height = 500
$treeView.PathSeparator = "."
$treeView.ShowNodeToolTips = $true
$treeView.Add_AfterSelect( { OnAfterSelect @args } )
$treeView.Add_BeforeExpand( { OnBeforeExpand @args } )
$treeView.Add_KeyPress( { OnTreeViewKeyPress @args } )

## Create the output pane, which will hold our object
## member information.
$outputPane = New-Object System.Windows.Forms.TextBox
$outputPane.Multiline = $true
$outputPane.WordWrap = $false
$outputPane.ScrollBars = "Both"
$outputPane.Dock = "Fill"

## Create the root node, which represents the object
## we are trying to show.
$root = New-Object Windows.Forms.TreeNode
$root.ToolTipText = $InputObject.GetType()
$root.Text = $InputObject
$root.Name = '$' + $rootVariableName
$root.Expand()
$null = $treeView.Nodes.Add($root)

UpdateFonts $currentFontSize

## And populate the initial information into the tree
## view.
PopulateNode $root $InputObject

## Finally, create the main form and show it.
$form = New-Object Windows.Forms.Form
$form.Text = "Browsing " + $root.Text
$form.Width = 1000
$form.Height = 800
$form.Controls.Add($outputPane)
$form.Controls.Add($treeView)
$form.Add_MouseWheel( { OnMouseWheel @args } )
$treeView.Add_KeyUp( { OnKeyUp @args } )
$treeView.Select()
$null = $form.ShowDialog()
$form.Dispose()

Para más información sobre la ejecución de guiones, consulta la Receta 1.2.

1.27 Grabar una transcripción de tu sesión de Shell

Problema

En querrás grabar un registro o transcripción de tu sesión de shell.

Solución

Para grabar una transcripción de tu sesión de shell, ejecuta el comando Start-Transcript. Tiene un parámetro opcional -Path que por defecto elige un nombre de archivo basado en la hora actual del sistema. Por defecto, PowerShell coloca este archivo en el directorio Mis Documentos. Para detener la grabación de la transcripción de tu sistema shell, ejecuta el comando Stop-Transcript.

Debate

Aunque el cmdlet Get-History es útil, no registra la salida producida durante tu sesión PowerShell. Para ello, utiliza el cmdlet Start-Transcript. Además del parámetro Path descrito anteriormente, el cmdlet Start-Transcript también admite parámetros que te permiten controlar cómo interactúa PowerShell con el archivo de salida.

Si no especificas un parámetro -Path, PowerShell genera un nombre de archivo aleatorio por ti. Si quieres procesar este archivo después de detener la transcripción, PowerShell lo añade como nombre de propiedad a la salida de Start-Transcript o Stop-Transcript:

PS > $myTranscript = Start-Transcript
PS > Stop-Transcript
Transcript stopped, output file is D:\Lee\PowerShell_transcript...
PS > $myTranscript | fl * -force

Path   : D:\Lee\PowerShell_transcript.LEE-DESKTOP.kg_Vsm_o.20201217195052.txt
Length : 104

PS > $myTranscript.Path
D:\Lee\PowerShell_transcript.LEE-DESKTOP.kg_Vsm_o.20201217195052.txt

Las transcripciones de PowerShell comienzan con una cabecera de archivo estándar que incluye la hora, el usuario, el nombre de host y otros elementos útiles. Si especificas el parámetro-IncludeInvocationHeader de forma interactiva o mediante una política para todo el sistema, PowerShell también incluye un separador entre los comandos para facilitar el análisis automático.

**********************
PowerShell transcript start
Start time: 20201217190500
Username: ubuntu-20-04\lee
Machine: ubuntu-20-04 (Unix 4.19.128.0)
Host Application: /opt/microsoft/powershell/7/pwsh.dll
Process ID: 1925
OS: Linux 4.19.128-microsoft-standard #1 SMP Tue Jun 23 12:58:10 UTC 2020
(...)
**********************

**********************
Command start time: 20201217190502
**********************
PS /mnt/c/Users/lee> Get-Process

 NPM(K)    PM(M)      WS(M)     CPU(s)      Id  SI ProcessName
 ------    -----      -----     ------      --  -- -----------
      0     0.00       5.26       0.16     984 984 bash
      0     0.00       0.53       0.02       1   0 init
      0     0.00       0.07       0.00     982 982 init
      0     0.00       0.08       0.32     983 982 init
      0     0.00      96.52       0.64    1925 984 pwsh
      0     0.00       3.25       0.00    1873 …73 rsyslogd

**********************
Command start time: 20201217190504
**********************
PS /mnt/c/Users/lee> cat /var/log/powershell.log
(...)

Además de permitirte grabar transcripciones manualmente, PowerShell también te permite establecer una política de sistema para grabarlas automáticamente. Para más información sobre cómo configurarla, consulta la Receta 18.2.

1.28 Ampliar tu Shell con comandos adicionales

Problema

En querrás utilizar cmdlets de PowerShell, proveedores o extensiones basadas en scripts escritos por terceros.

Solución

Si el módulo forma parte de la ruta estándar de módulos de PowerShell, sólo tienes que ejecutar el comando que desees:

Invoke-NewCommand

Si no lo está, utiliza el comando Import-Module para importar comandos de terceros a tu sesión PowerShell.

Para importar un módulo de un directorio concreto:

Import-Module c:\path\to\module

Para importar un módulo desde un archivo concreto (módulo, script o ensamblaje):

Import-Module c:\path\to\module\file.ext

Debate

PowerShell admite dos conjuntos de comandos que habilitan cmdlets y proveedores adicionales: *-Module y *-PsSnapin. Los Snapins eran los paquetes para extensiones en la versión 1 de PowerShell, y se utilizan raramente. Los Snapins sólo admitían extensiones compiladas y tenían onerosos requisitos de instalación.

La versión 2 de PowerShell introdujo módulos que admiten todo lo que admiten los snapins (y más) sin el dolor de instalación asociado. Dicho esto, la versión 2 de PowerShell también requería que recordaras qué módulos contenían qué comandos y que cargaras manualmente esos módulos antes de utilizarlos. Windows incluye ahora miles de comandos en cientos de módulos, lo que hace que depender de la memoria de cada uno sea un enfoque insostenible.

Cualquier versión reciente de PowerShell mejora significativamente la situación al cargar automáticamente los módulos por ti. Internamente, PowerShell mantiene un mapeo de nombres de comandos con el módulo que los contiene. Sólo tienes que empezar a utilizar un comando (que el cmdlet Get-Command puede ayudarte a descubrir), y PowerShell carga automáticamente el módulo adecuado. Si deseas personalizar este comportamiento de carga automática, puedes utilizar la variable de preferencia $PSModuleAutoLoadingPreference.

Cuando PowerShell importa un módulo con un nombre determinado, busca en todos los directorios enumerados en la variable de entorno PSModulePath, buscando el primer módulo que contenga los subdirectorios que coincidan con el nombre que especifiques. Dentro de esos directorios, busca el módulo (*.psd1, *.psm1, y *.dll) con el mismo nombre y lo carga.

Cuando instalas un módulo en tu propio sistema, el lugar más habitual para colocarlo es en el directorio PowerShell\Modules de tu directorio Mis Documentos. En Windows PowerShell, esta ubicación será WindowsPowerShell\Modules. Para que PowerShell busque los módulos en otro directorio, añádelo a tu variable de entorno personal PSModulePath, igual que añadirías un directorio Herramientas a tu ruta personal.

Para más información sobre la gestión de las rutas del sistema, consulta la Receta 16.2.

Si quieres cargar un módulo de un directorio que no está en PSModulePath, puedes proporcionar el nombre completo del directorio y el nombre del módulo al comando Import-Module. Por ejemplo, para un módulo llamado Test, utiliza Import-Module c:\path\to\Test. Al igual que con la carga de módulos por nombre, PowerShell busca en c:\temp\path\to un módulo(*.psd1, *.psm1 o *.dll) llamado Test y lo carga.

Si conoces el archivo específico del módulo que quieres cargar, también puedes especificar la ruta completa a ese módulo.

Si quieres encontrar comandos adicionales, consulta la Receta 1.29.

1.30 Usar comandos desde shells personalizados

Problema

En quieres utilizar los comandos de un producto basado en PowerShell que lanza una versión personalizada de la consola PowerShell, pero en una sesión normal de PowerShell.

Solución

Inicia en la versión personalizada de la consola PowerShell y, a continuación, utiliza los comandos Get-Module y Get-PsSnapin para ver qué módulos y/o snapins adicionales ha cargado.

Debate

Como se describe en la Receta 1.28, los módulos PowerShell y los snapins son las dos formas en que terceros pueden distribuir y añadir comandos PowerShell adicionales. Los productos que proporcionan versiones personalizadas de la consola PowerShell lo hacen lanzando PowerShell con uno de tres parámetros:

  • -PSConsoleFile, para cargar un archivo de consola que proporciona una lista de snapins a cargar.

  • -Commandpara especificar en un comando de arranque inicial (que luego carga un snapin o módulo)

  • -Filepara especificar en un script de arranque inicial (que luego carga un snapin o módulo)

Independientemente de cuál se utilice, puedes examinar el conjunto resultante de extensiones cargadas para ver cuáles puedes importar a tus otras sesiones PowerShell.

Detectar snapins cargados

El comando Get-PsSnapin devuelve todos los snapins cargados en la sesión actual. Siempre devuelve el conjunto de snapins centrales de PowerShell, pero también devolverá cualquier snapin adicional cargado por el entorno personalizado. Por ejemplo, si el nombre de un snapin que reconoces es Product.Feature.Commandspuedes cargarlo en futuras sesiones de PowerShell escribiendo Add-PsSnapin Product.Feature.Commands. Para automatizar esto, añade el comando a tu perfil PowerShell.

Si no estás seguro de qué snapin cargar, también puedes utilizar el comando Get-Command para descubrir qué snapin define un comando concreto:

PS > Get-Command Get-Counter | Select PsSnapin

PSSnapIn
--------
Microsoft.PowerShell.Diagnostics

Detectar módulos cargados

Al igual que el comando Get-PsSnapin, el comando Get-Module devuelve todos los módulos cargados en la sesión actual. Devuelve cualquier módulo que hayas añadido hasta el momento en esa sesión, pero también devolverá cualquier módulo adicional cargado por el entorno personalizado. Por ejemplo, si el nombre de un módulo que reconoces es ProductModulepuedes cargarlo en futuras sesiones PowerShell escribiendo Import-Module ProductModule. Para automatizar esto, añade el comando a tu perfil PowerShell.

Si no estás seguro de qué módulo debes cargar, también puedes utilizar el comando Get-Command para descubrir qué módulo define un comando concreto:

PS > Get-Command Start-BitsTransfer | Select Module

Module
------
BitsTransfer

1.31 Guardar estado entre sesiones

Problema

En querrás guardar el estado o el historial entre sesiones de PowerShell.

Solución

Suscríbete al evento del motor PowerShell.Exiting para que PowerShell invoque un script o bloque de script que guarde cualquier estado que necesites.

Debate

PowerShell proporciona un fácil acceso basado en scripts a una amplia variedad de eventos del sistema, del motor y de otros tipos. Puedes registrarte para recibir notificaciones de estos eventos e incluso procesar automáticamente cualquiera de ellos. En el siguiente ejemplo, nos suscribimos al evento llamado PowerShell.Exiting. PowerShell genera este evento cuando cierras una sesión.

En puedes utilizar este evento para guardar y restaurar estados, variables y cualquier otra cosa que necesites. Aunque el módulo PSReadLine ya guarda automáticamente tu historial de comandos entre sesiones, a efectos de demostración podemos implementar una funcionalidad similar a través del evento PowerShell.Exiting. Colocarías una llamada a Enable-HistoryPersistence en tu perfil(Ejemplo 1-19).

Ejemplo 1-19. Activar-HistoriaPersistencia.ps1
##############################################################################
##
## Enable-HistoryPersistence
##
## From PowerShell Cookbook (O'Reilly)
## by Lee Holmes (http://www.leeholmes.com/guide)
##
##############################################################################

<#

.SYNOPSIS

Reloads any previously saved command history, and registers for the
PowerShell.Exiting engine event to save new history when the shell
exits.

#>

Set-StrictMode -Version 3

## Load our previous history
$GLOBAL:maximumHistoryCount = 32767
$historyFile = (Join-Path (Split-Path $profile) "commandHistory.clixml")
if(Test-Path $historyFile)
{
    Import-CliXml $historyFile | Add-History
}

## Register for the engine shutdown event
$null = Register-EngineEvent -SourceIdentifier `
    ([System.Management.Automation.PsEngineEvent]::Exiting) -Action {

    ## Save our history
    $historyFile = (Join-Path (Split-Path $profile) "commandHistory.clixml")
    $maximumHistoryCount = 1kb

    ## Get the previous history items
    $oldEntries = @()
    if(Test-Path $historyFile)
    {
        $oldEntries = Import-CliXml $historyFile -ErrorAction SilentlyContinue
    }

    ## And merge them with our changes
    $currentEntries = Get-History -Count $maximumHistoryCount
    $additions = Compare-Object $oldEntries $currentEntries `
        -Property CommandLine | Where-Object { $_.SideIndicator -eq "=>" } |
        Foreach-Object { $_.CommandLine }

    $newEntries = $currentEntries | ? { $additions -contains $_.CommandLine }

    ## Keep only unique command lines. First sort by CommandLine in
    ## descending order (so that we keep the newest entries,) and then
    ## re-sort by StartExecutionTime.
    $history = @($oldEntries + $newEntries) |
        Sort -Unique -Descending CommandLine | Sort StartExecutionTime

    ## Finally, keep the last 100
    Remove-Item $historyFile
    $history | Select -Last 100 | Export-CliXml $historyFile
}

Este script podría hacer cualquier cosa, pero en este ejemplo hacemos que guarde nuestro historial de comandos y lo restaure cuando iniciemos PowerShell. ¿Por qué querríamos hacer esto? Bueno, con un búfer de historial rico, podemos encontrar y reutilizar más fácilmente los comandos que hemos ejecutado anteriormente. Para ver dos ejemplos de cómo hacerlo, consulta las Recetas 1.21 y 1.23.

Enable-HistoryPersistence realiza dos acciones principales. Primero, cargamos nuestro historial de comandos almacenado (si existe). Después, registramos una acción automática que se procesará cada vez que el motor genere su evento PowerShell.Exiting. La acción en sí es relativamente sencilla, aunque exportar nuestro nuevo historial requiere un poco de delicadeza. Si tienes varias sesiones abiertas al mismo tiempo, cada una de ellas actualizará el archivo del historial guardado cuando salga. Como no queremos sobrescribir el historial guardado por los otros intérpretes de comandos, primero recargamos el historial desde el disco y lo combinamos con el historial del intérprete de comandos actual.

Una vez que tenemos la lista combinada de líneas de comandos, las ordenamos y elegimos las únicas antes de almacenarlas de nuevo en el archivo.

Para más información sobre cómo trabajar con eventos del motor PowerShell, consulta la Receta 31.2.

Get Libro de cocina PowerShell, 4ª edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.