Scripting 101: Introducción a PowerShell

Abstract

Scripting con PowerShell

¿Qué es scripting?

Por scripting entendemos la práctica de escribir pequeños programas destinados a automatizar tareas repetitivas. A diferencia de una aplicación completa, un script suele ser breve, orientado a ejecutar comandos del sistema, manipular archivos o coordinar herramientas ya existentes. Su valor está en reducir pasos manuales y hacer procesos repetibles y confiables.

En este curso usaremos PowerShell 7+ como lenguaje de scripting único en Windows, macOS y Linux. Esta decisión simplifica el material, reduce el ruido y te permite concentrarte en lo importante: construir y automatizar con un mismo conjunto de herramientas en cualquier sistema.

¿Por qué elegimos PowerShell?

Elegir un solo lenguaje para todo el curso reduce la carga cognitiva y facilita el mantenimiento del material: solo tenemos que actualizar ejemplos y explicaciones de PowerShell.

  • Multiplataforma: PowerShell 7+ funciona en Windows, macOS y Linux. Un único lenguaje para todos los ejemplos y ejercicios del curso.
  • “Más poderoso” en este contexto: la pipeline pasa objetos 1  fuertemente tipados (no solo texto), lo que facilita filtrar, ordenar y transformar sin recurrir a múltiples utilidades externas.
  • Más simple: muchas tareas comunes (JSON, XML, archivos, procesos) ya vienen resueltas con cmdlets 2 y tipos .NET, reduciendo dependencias y evitando trabajo manual de conexión (parsear texto, encadenar utilidades externas, etc.).
  • Enfoque y mantenimiento: usar un solo lenguaje simplifica explicaciones y actualizaciones del material: mantenemos una sola ruta de instalación, sintaxis y ejemplos coherentes.

Un vistazo a la ventaja “orientada a objetos”

En PowerShell, lo que fluye por la tubería son objetos. Por ejemplo, listar procesos, filtrar por memoria y proyectar columnas legibles (no es necesario que entiendas todo el comando):

Pipeline con objetos (ejemplo)
Get-Process |
    Where-Object { $_.WorkingSet64 -gt 200MB } |
    Sort-Object -Property WorkingSet64 -Descending |
    Select-Object -First 20 -Property Name, Id, @{
        Name       = 'RAM(MB)';
        Expression = { [math]::Round($_.WorkingSet64 / 1MB) }
    } |
    Format-Table -AutoSize

No hace falta encadenar múltiples herramientas para parsear texto: trabajas con propiedades y tipos directamente.

¿Y Bash?

Podríamos lograr esto mismo en Bash, pero el resultado suele ser más complejo y dependiente de utilidades externas no nativas de Bash (ps, awk, head). Además, sus flags pueden variar entre distribuciones Linux y macOS/BSD.

En contraste, el código en PowerShell tiende a ser más expresivo y legible: opera directamente sobre objetos y propiedades en lugar de cadenas de texto. Además, los cmdlets siguen la convención verbo-sustantivo ( Get-Process , Sort-Object ), lo que los hace más autoexplicativos y fáciles de recordar que comandos abreviados como ps o awk .

Pipeline con texto (ejemplo)
ps -eo pid,comm,rss --sort=-rss | \
    awk 'NR==1 {print "Name\tId\tRAM(MB)"; next} 
        {printf "%s\t%s\t%d\n", $2, $1, $3/1024}' | \
    head -20

Importante

PowerShell no es un reemplazo absoluto de Bash. Ambos son entornos de scripting potentes con filosofías distintas. La elección entre uno u otro depende siempre del contexto y de las necesidades específicas. En este curso priorizamos PowerShell por su carácter multiplataforma y expresividad, pero Bash sigue siendo la herramienta estándar en muchos entornos Unix.

Estructura base del “workspace”

Comencemos creando una organización básica para los proyectos del curso. Esta carpeta puede compartirse entre varios proyectos; si decides no usarla, solo tendrás que adaptar las rutas en los comandos posteriores.

Este puede ser tu primer contacto con PowerShell. Haremos un repaso mínimo de sintaxis pensando en que ya conoces conceptos generales (condicionales, bucles, funciones, objetos).

Desde la terminal de PowerShell
$dibs         = 'dibs'           # raíz de proyectos del curso
$scripts      = 'scripts'        # scripts de terminal
$scriptsPath  = Join-Path $dibs $scripts

New-Item -ItemType Directory -Path $scriptsPath -Force | Out-Null

¿Qué acabamos de hacer?

  • Asignación con $nombre = 'valor' . Las variables comienzan con $ y pueden contener strings, números u objetos.
  • Join-Path compone rutas de forma portable (evita preocuparte por \ vs / ).
  • New-Item -ItemType Directory -Path ... crea carpetas. Usar -Force permite repetir el comando sin error si ya existen, lo cual es importante para respetar el principio de idempotencia.
  • Pipelines en PowerShell: el operador | pasa objetos al siguiente comando. Aquí usamos | Out-Null para descartar la salida y mantener la terminal limpia.

Alternativas

También se puede suprimir la salida con (comando) >$null o $null = (comando) . En este apunte preferimos | Out-Null por ser explícito y fácil de leer.

Otras formas de suprimir salida
New-Item -ItemType Directory -Path $scriptsPath -Force >$null
$null = New-Item -ItemType Directory -Path $scriptsPath -Force

Pipelines: mini-ejemplo

Encadenar comandos con objetos
# Lista objetos de tipo FileSystemInfo y luego filtra por carpetas
Get-ChildItem -Path $dibs |
  Where-Object -FilterScript { $PSItem.PSIsContainer } |
  Select-Object -Property Name

En este ejemplo cada comando del pipeline recibe y devuelve objetos, no texto plano:

  • Get-ChildItem $dibs produce objetos FileSystemInfo con propiedades como Name y PSIsContainer .
  • Where-Object -FilterScript { $PSItem.PSIsContainer } filtra y deja pasar solo carpetas. Fuera de un contexto de pipeline o de un bloque que reciba desde el pipeline, $PSItem no tiene significado.
  • Select-Object -Property Name proyecta la propiedad Name para mostrar únicamente el nombre de cada carpeta.

Puedes pensarlo como una “tabla” de objetos: cada fila es un archivo/carpeta y cada columna una propiedad. Where-Object filtra filas según una condición sobre el $PSItem actual, y Select-Object elige qué columnas mostrar. Al operar con objetos (y no con strings), el pipeline es más expresivo y menos propenso a errores.

Aliases

PowerShell incluye alias que hacen que la experiencia sea más parecida a Bash o CMD (por ejemplo, cat en vez de Get-Content ). Son útiles para escribir rápido en la terminal, pero no los usaremos en este apunte ya que en scripts y documentación las formas completas son más claras y fáciles de leer.
Inspeccionando aliases de Set-Location
# Buscar los aliases que apuntan a un comando específico
Get-Alias -Definition Set-Location

En este caso Set-Location (que cambia el directorio actual) tiene 2 aliases disponibles:

Output
CommandType     Name                  Version    Source
-----------     ----                  -------    ------
Alias           chdir -> Set-Location
Alias           sl    -> Set-Location

Primer script: generar un README.md básico

Hasta ahora hemos trabajado ejecutando comandos de forma directa en la terminal. El siguiente paso es encapsular esa lógica en un script sencillo y reutilizable, guardado dentro de dibs/scripts. Nuestro ejemplo genera el contenido inicial de un README.md para un proyecto. Incluye parámetros obligatorios y una opción de verbosity para mostrar mensajes adicionales durante la ejecución. Te recomiendo editar y mantener estos scripts en un entorno cómodo como VS Code, que ofrece resaltado de sintaxis, integración con terminal y depuración básica.

Generar README.md con PowerShell
scripts/New-Readme.ps1
#Requires -Version 7.0
[CmdletBinding()]
[OutputType([string])]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Name
)

Write-Verbose "Creating README.md for project '$Name'"

return @"
# $Name

Project initialized on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss').

Learn more about READMEs at https://www.makeareadme.com/.
"@

¿Qué acabamos de hacer?

  • #Requires -Version 7.0 : asegura que el script se ejecute con PowerShell 7+. 3
  • [CmdletBinding()] convierte el script en un cmdlet avanzado . Esto agrega características extra, como permitir el uso de -Verbose para mostrar mensajes de diagnóstico opcionales al ejecutar el script. Luego, Write-Verbose emite esos mensajes solo si se invoca con -Verbose .
  • [OutputType([string])] declara que el script devuelve una cadena. Esto no cambia la ejecución, pero sirve como documentación y ayuda a herramientas de análisis y autocompletado a entender mejor el resultado esperado (puedes pensarlo como los type-hints de Python).
  • El bloque param(...) define los parámetros del script. La sintaxis [Atributo1][Atributo2][Tipo] $Parametro aplica metadatos y validaciones sobre cada parámetro:
    • [Parameter(Mandatory)] indica que el argumento es obligatorio y debe proporcionarse al invocar la función.
    • [ValidateNotNullOrEmpty()] asegura que no se acepte un valor vacío o $null .
    • [string] define el tipo estático del parámetro, lo que mejora la validación y el autocompletado.
  • Usa un here-string para devolver texto multilínea listo para escribir a archivo. La sintaxis con @" ... "@ define una cadena que preserva saltos de línea e interpolación de variables y expresiones (por ejemplo, $Name o $(Get-Date ...) ). También existe la variante @' ... '@ que no expande variables, tratándolas como texto literal. 4
Desde la terminal de PowerShell
# Desde dibs/scripts:
.\New-Readme.ps1 -Name 'Utility Scripts - DIBS' -Verbose | 
    Set-Content -Path README.md -Encoding UTF8 -Force

¿Qué acabamos de hacer?

  • Set-Location -Path $scriptsPath cambia el directorio actual a dibs/scripts, donde vive el script.
  • .\New-Readme.ps1 -Name '...' ejecuta el script en la carpeta actual. El parámetro -Name es obligatorio.
  • -Verbose muestra mensajes de diagnóstico (habilitado por [CmdletBinding()] dentro del script).
  • El resultado (texto del README) se envía por pipeline a Set-Content , que escribe el archivo en disco.
  • Set-Content -Path README.md -Encoding UTF8 -Force crea o sobrescribe README.md con codificación UTF-8.

Nota

Por simplicidad omitiremos la documentación formal de los scripts, pero en un entorno real conviene incluir comentarios y ayuda integrados. Revisa Comment-Based Help .

Patrón de ensayo seguro (simular antes de ejecutar)

Antes de modificar archivos o crear estructuras, es buena práctica hacer un “ensayo” que muestre qué se haría sin hacerlo realmente. En PowerShell esto se logra con -WhatIf (vía SupportsShouldProcess ). Así evitamos efectos no deseados y ganamos confianza en el script.

Initialize-Project (con -WhatIf)
scripts/Initialize-Project.ps1
#Requires -Version 7.0
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
param(
    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string] $Name,

    [string] $Path
)

# Si no se pasa -Path, usar el nombre del proyecto como carpeta en el cwd
$target = $PSCmdlet.GetUnresolvedProviderPathFromPSPath($Path ?? $Name)

if ($PSCmdlet.ShouldProcess($target, 'Initialize project and create README.md')) {
    New-Item -Path $target -ItemType Directory -Force | Out-Null

    $helperPath = Join-Path -Path $PSScriptRoot -ChildPath 'New-Readme.ps1'
    & $helperPath -Name $Name -Verbose:$VerbosePreference |
        Set-Content -Path (
            Join-Path -Path $target -ChildPath 'README.md') -Encoding UTF8 -Force
}

¿Qué acabamos de hacer?

  • SupportsShouldProcess habilita -WhatIf y -Confirm , permitiendo simular u obligar confirmación antes de actuar. Además, con ConfirmImpact = 'Medium' indicamos la “gravedad” de la operación: PowerShell solo pedirá confirmación automática si la preferencia del usuario ( $ConfirmPreference ) es igual o más baja.
    • Low → operaciones triviales, casi nunca piden confirmación.
    • Medium → operaciones que crean o modifican archivos (nuestro caso: inicializar un proyecto).
    • High → operaciones destructivas, como borrar datos críticos.
    Usar Medium comunica que el script modifica el sistema de forma relevante (crea carpetas y archivos), pero sin ser una acción destructiva.
  • $PSCmdlet es una variable automática disponible en funciones avanzadas ( [CmdletBinding()] ). Representa la instancia actual del cmdlet en ejecución y permite acceder a información y utilidades internas, como $PSCmdlet.ShouldProcess() o $PSCmdlet.SessionState .
  • La variable $target se calcula con $PSCmdlet.GetUnresolvedProviderPathFromPSPath(...) , un método de PowerShell que convierte una ruta relativa o con ~ en una ruta absoluta válida del sistema de archivos.
    • El operador ?? devuelve $Path si fue pasado como argumento, o en su defecto el valor de $Name .
    • No es necesario pasar un directorio base explícito: PowerShell resuelve la ruta relativa con respecto al directorio actual y la expande a una forma absoluta, incluso si la carpeta aún no existe.
  • $PSCmdlet.ShouldProcess(...) consulta si la acción debe ejecutarse. ShouldProcess habilita soportar -WhatIf y -Confirm , permitiendo simular o pedir confirmación antes de modificar el sistema.
  • $PSScriptRoot contiene la ruta absoluta de la carpeta donde se encuentra el script actual. Es útil para construir rutas relativas seguras (por ejemplo, a otros scripts o recursos en el mismo directorio), en lugar de depender del directorio desde el que se ejecuta PowerShell.
  • & $helperPath -Name $Name -Verbose:$VerbosePreference | Set-Content ... ejecuta otro script y redirige su salida a un archivo.
    • & es el call operator: permite invocar el script cuya ruta está almacenada en $helperPath . 5
    • -Verbose:$VerbosePreference asegura que la preferencia -Verbose se respete también en el script secundario. $VerbosePreference es una variable automática de PowerShell que guarda si la sesión actual está en modo detallado. Al usar la sintaxis :$VerbosePreference , transmitimos explícitamente el valor booleano (por ejemplo, $true si se ejecutó con -Verbose ). Sin esto, el script llamado ignoraría el estado de verbosidad y siempre trabajaría en modo silencioso.
Desde la terminal de PowerShell
# Desde dibs:
.\scripts\Initialize-Project.ps1 -Name "Test" -Path "test" -Verbose -WhatIf
Salida con -WhatIf
What if: Performing the operation "Initialize project and create README.md" on target "C:\path\to\dibs\test".

...

Notas

  1. A diferencia de los shells tradicionales, PowerShell trata todo como objetos de .NET. Esto habilita capacidades como composición, herencia, manejo de excepciones y tipado más estricto, lo que acerca la experiencia a la de un lenguaje de programación completo. Además, permite cierto nivel de compatibilidad con C#, lo que facilita integrar scripts con bibliotecas y funcionalidades del ecosistema .NET para resolver tareas más complejas. Volver
  2. Un cmdlet es un comando ligero de PowerShell diseñado para realizar una única tarea bien definida. Suelen seguir la convención Verbo-Sustantivo (por ejemplo, Get-Process o New-Item ), lo que los hace más expresivos y fáciles de entender que muchos comandos en Bash. A diferencia de los ejecutables tradicionales, los cmdlets devuelven objetos .NET, lo que permite combinarlos en pipelines potentes y consistentes. Volver
  3. Puedes ajustar #Requires a otra versión, pero no garantizamos compatibilidad con PowerShell anterior. Volver
  4. Microsoft define una lista de verbos recomendados en Approved Verbs for Windows PowerShell Commands . Volver
  5. & ejecuta un script en un nuevo contexto, sin compartir variables locales. En cambio, . (dot-sourcing) carga el script en el contexto actual, permitiendo que sus funciones y variables permanezcan disponibles después de la ejecución. Volver