Scripting 101: Introducción a PowerShell
Scripting con PowerShell
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):
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
Get-Process
), lo que los hace más autoexplicativos y fáciles de recordar que
comandos abreviados como Sort-Object
Sort-Object
o ps
ps
.
awk
awk
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
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).
$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
- Asignación con
$nombre = 'valor'
$nombre = 'valor'
$
$
-
Join-Path
Join-Path
\
\
/
/
-
New-Item -ItemType Directory -Path ...
New-Item -ItemType Directory -Path ...
-Force
-Force
- Pipelines en PowerShell: el operador
|
|
| Out-Null
| Out-Null
Alternativas
También se puede suprimir la salida con
o
(comando) >$null
(comando) >$null
. En este apunte
preferimos $null = (comando)
$null = (comando)
por ser explícito y fácil
de leer.
| Out-Null
| Out-Null
New-Item -ItemType Directory -Path $scriptsPath -Force >$null
$null = New-Item -ItemType Directory -Path $scriptsPath -Force
Pipelines: mini-ejemplo
# 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
Get-ChildItem $dibs
FileSystemInfo
con propiedades comoName
Name
PSIsContainer
PSIsContainer
-
Where-Object -FilterScript { $PSItem.PSIsContainer }
Where-Object -FilterScript { $PSItem.PSIsContainer }
$PSItem
$PSItem
-
Select-Object -Property Name
Select-Object -Property Name
Name
Name
Puedes pensarlo como una “tabla” de objetos: cada fila es un
archivo/carpeta y cada columna una propiedad.
filtra filas según una condición sobre el Where-Object
Where-Object
actual, y
$PSItem
$PSItem
elige qué columnas mostrar. Al
operar con objetos (y no con strings), el pipeline es más expresivo y menos
propenso a errores.
Select-Object
Select-Object
Aliases
cat
cat
en vez de
Get-Content
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.
# Buscar los aliases que apuntan a un comando específico
Get-Alias -Definition Set-Location
En este caso
(que cambia el directorio
actual) tiene 2 aliases disponibles:
Set-Location
Set-Location
CommandType Name Version Source
----------- ---- ------- ------
Alias chdir -> Set-Location
Alias sl -> Set-Location
README.md
básico
Primer script: generar un
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.
README.md
con PowerShell #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/.
"@
-
#Requires -Version 7.0
#Requires -Version 7.0
-
[CmdletBinding()]
[CmdletBinding()]
-Verbose
-Verbose
Write-Verbose
Write-Verbose
-Verbose
-Verbose
-
[OutputType([string])]
[OutputType([string])]
- El bloque
param(...)
param(...)
[Atributo1][Atributo2][Tipo] $Parametro
[Atributo1][Atributo2][Tipo] $Parametro
-
[Parameter(Mandatory)]
[Parameter(Mandatory)]
-
[ValidateNotNullOrEmpty()]
[ValidateNotNullOrEmpty()]
$null
$null
-
[string]
[string]
-
- Usa un here-string para devolver texto multilínea listo para escribir a archivo. La sintaxis con
@" ... "@
@" ... "@
$Name
$Name
$(Get-Date ...)
$(Get-Date ...)
@' ... '@
@' ... '@
# Desde dibs/scripts:
.\New-Readme.ps1 -Name 'Utility Scripts - DIBS' -Verbose |
Set-Content -Path README.md -Encoding UTF8 -Force
-
Set-Location -Path $scriptsPath
Set-Location -Path $scriptsPath
dibs/scripts
, donde vive el script. -
.\New-Readme.ps1 -Name '...'
.\New-Readme.ps1 -Name '...'
-Name
-Name
-
-Verbose
-Verbose
[CmdletBinding()]
[CmdletBinding()]
- El resultado (texto del README) se envía por pipeline a
Set-Content
Set-Content
-
Set-Content -Path README.md -Encoding UTF8 -Force
Set-Content -Path README.md -Encoding UTF8 -Force
README.md
con codificación UTF-8.
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
(vía
-WhatIf
-WhatIf
). Así evitamos efectos
no deseados y ganamos confianza en el script.
SupportsShouldProcess
SupportsShouldProcess
-WhatIf
) #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
}
-
SupportsShouldProcess
SupportsShouldProcess
-WhatIf
-WhatIf
-Confirm
-Confirm
ConfirmImpact = 'Medium'
ConfirmImpact = 'Medium'
$ConfirmPreference
$ConfirmPreference
-
Low
Low
-
Medium
Medium
-
High
High
Medium
comunica que el script modifica el sistema de forma relevante (crea carpetas y archivos), pero sin ser una acción destructiva. -
-
$PSCmdlet
$PSCmdlet
[CmdletBinding()]
[CmdletBinding()]
$PSCmdlet.ShouldProcess()
$PSCmdlet.ShouldProcess()
$PSCmdlet.SessionState
$PSCmdlet.SessionState
- La variable
$target
$target
$PSCmdlet.GetUnresolvedProviderPathFromPSPath(...)
$PSCmdlet.GetUnresolvedProviderPathFromPSPath(...)
~
en una ruta absoluta válida del sistema de archivos.- El operador
??
??
$Path
$Path
$Name
$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(...)
$PSCmdlet.ShouldProcess(...)
ShouldProcess
ShouldProcess
-WhatIf
-WhatIf
-Confirm
-Confirm
-
$PSScriptRoot
$PSScriptRoot
-
& $helperPath -Name $Name -Verbose:$VerbosePreference | Set-Content ...
& $helperPath -Name $Name -Verbose:$VerbosePreference | Set-Content ...
-
&
&
$helperPath
$helperPath
-
-Verbose:$VerbosePreference
-Verbose:$VerbosePreference
-Verbose
-Verbose
$VerbosePreference
$VerbosePreference
:$VerbosePreference
:$VerbosePreference
$true
$true
-Verbose
-Verbose
-
# Desde dibs:
.\scripts\Initialize-Project.ps1 -Name "Test" -Path "test" -Verbose -WhatIf
-WhatIf
-WhatIf
What if: Performing the operation "Initialize project and create README.md" on target "C:\path\to\dibs\test".
...
Notas
- 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
-
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
Get-Process
New-Item
New-Item
-
Puedes ajustar
#Requires
#Requires
- Microsoft define una lista de verbos recomendados en Approved Verbs for Windows PowerShell Commands . Volver
-
&
&
.
.