Módulo 4: Permisos, procesos y entorno

7. Configurar tu shell: archivos rc, alias y dotfiles

Descripción

Al terminar esta lección vas a poder explicar qué archivo lee tu shell y en qué momento —.bashrc frente a .bash_profile, .zshrc frente a .zprofile— para diagnosticar por qué "mi variable funciona en una terminal pero no en otra"; hacer persistente un cambio de PATH en vez de perderlo cada vez que cierras la ventana; crear alias y funciones a partir de comandos que ya repites a diario; y dejar tu configuración protegida contra dos fugas silenciosas: un secreto que queda guardado para siempre en el historial de comandos, y un secreto que queda guardado para siempre en el historial de un repositorio de git.

Esto no es cosmética de terminal. La primera semana en cualquier trabajo con una laptop nueva, o la primera vez que configuras un servidor desde cero, vas a pasar por el mismo ritual: instalar herramientas, exportar variables, definir atajos. Sin entender qué archivo hace qué, ese ritual se vuelve una lista de comandos copiados de un tutorial que "a veces funcionan" — y el día que abras una pestaña nueva de terminal y tu herramienta recién instalada dé command not found, no vas a saber si el problema es el PATH, el archivo equivocado, o algo más profundo.

Conexión con el módulo: la lección anterior te enseñó qué es el PATH y cómo diagnosticar cuál binario corre realmente cuando hay dos versiones instaladas — pero todo lo que hiciste ahí vivía solo en la sesión de terminal abierta; en cuanto la cerrabas, desaparecía. Hoy resuelves exactamente ese problema: dónde vive la configuración que sobrevive al cierre de la terminal, y cómo construirla de forma profesional. La siguiente lección es el proyecto final del módulo, y una de las tres fallas que vas a reparar ahí —un PATH mal armado— la vas a dejar arreglada de manera permanente usando precisamente lo que aprendes hoy.

Cada terminal que abres reconstruye tu entorno de trabajo desde cero

Piensa en un turno en una cafetería con dos rutinas distintas. Cuando alguien abre el local al inicio del día, hace una lista larga: prender las máquinas, contar la caja, revisar el inventario. Cuando un barista que ya estaba trabajando simplemente vuelve al mostrador después de una pausa, no repite esa lista completa — solo se lava las manos y se pone el delantal otra vez. Es la misma persona, el mismo local, pero dos rutinas de arranque completamente distintas según cómo "entró" a trabajar en ese momento.

Tu shell hace exactamente eso cada vez que se abre, y usa dos preguntas independientes para decidir qué rutina de arranque ejecutar:

  • ¿Es una shell de login? — equivalente a "abrir el local". Ocurre cuando inicias sesión de verdad: al conectarte por SSH a un servidor, o al abrir una terminal en macOS (Terminal.app e iTerm2 arrancan shells de login por defecto). Lee la lista larga de configuración: variables de entorno, PATH, todo lo que necesita existir antes de que puedas trabajar.
  • ¿Es una shell interactiva? — significa que hay un humano tecleando y esperando un prompt, sin importar si fue login o no. Una pestaña nueva dentro de tmux, o escribir bash o zsh sueltos dentro de otra shell ya abierta, son shells interactivas no de login: "volver al mostrador", no "abrir el local".

Estas dos preguntas son independientes entre sí, y cada combinación dispara un archivo distinto:

ShellLogin (arranque completo)Interactiva no-login (mostrador)
bash/etc/profile, luego el primero que exista entre ~/.bash_profile, ~/.bash_login, ~/.profile~/.bashrc
zsh~/.zshenv~/.zprofile~/.zshrc~/.zlogin~/.zshenv~/.zshrc

Fíjate en la diferencia clave: en bash, una shell de login no lee .bashrc automáticamente — son dos rutas separadas que no se tocan solas. En zsh, en cambio, toda shell interactiva lee .zshrc sin importar si también es de login, así que una terminal de macOS (login e interactiva) termina leyendo .zprofile y .zshrc, en ese orden. Esa asimetría entre bash y zsh es la razón técnica exacta detrás de "esto funcionaba en mi otra terminal": si pusiste algo solo en .bash_profile, cualquier shell no-login (una pestaña nueva en muchos emuladores de Linux, un script) nunca lo va a ver.

La práctica estándar en bash para evitar ese problema es hacer que .bash_profile cargue .bashrc explícitamente, así el contenido vive en un solo lugar sin importar cómo arrancó la shell:

# dentro de ~/.bash_profile
if [ -f ~/.bashrc ]; then
  source ~/.bashrc
fi

Ejemplo trabajado

Vas a instalar una herramienta cuyo binario vive en ~/bin, y quieres que cualquier terminal nueva la encuentre sin que tengas que exportar el PATH a mano cada vez.

Primero, dos comandos para saber en qué shell estás parado y si es de login:

echo $SHELL
echo $0

Qué esperar:

$ echo $SHELL
/bin/zsh

$ echo $0
-zsh

$SHELL te dice cuál es tu shell por defecto asignada en el sistema (no necesariamente la que corre ahora mismo, si escribiste otra a mano). $0 es el dato interesante: el guion inicial (-zsh, no zsh) es la convención con la que bash y zsh marcan una shell de login — sin el guion, sabrías que es una shell interactiva no-login. Con eso confirmado, sabes que en macOS con zsh de login vas a leer .zprofile y luego .zshrc.

Ahora, agrega el PATH al archivo correcto — .zshrc, porque es el que toda sesión interactiva de zsh lee sin excepción, sea o no de login — y aplícalo sin cerrar la terminal:

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
echo $PATH

Qué esperar:

$ echo 'export PATH="$HOME/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
$ echo $PATH
/Users/dev/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

source ~/.zshrc (equivalente corto: . ~/.zshrc) relee el archivo dentro de la shell actual, sin abrir un proceso nuevo — por eso el cambio aparece de inmediato en esta misma ventana. Abrir una terminal nueva habría logrado lo mismo, porque también lee .zshrc desde cero, pero con una diferencia real: perderías cualquier variable o directorio de trabajo que hubieras armado a mano en esa sesión y que no vive en ningún archivo. source aplica el archivo sin descartar el resto del estado de la shell actual.

Si tu shell es bash en vez de zsh, el mismo cambio va en .bashrc (asumiendo que tu .bash_profile ya lo carga como se mostró arriba):

echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Alias, funciones y por qué a veces necesitas la segunda

Un alias es una sustitución de texto: le dices a la shell "cuando escriba esto, en realidad ejecuta esto otro". Sirve perfecto para comandos que repites idénticos, tecla por tecla:

alias gs='git status'
alias ll='ls -lah'
alias ..='cd ..'

Pero un alias no acepta lógica ni argumentos posicionales de verdad — es texto pegado antes de lo que escribas después. En cuanto necesitas que el comando haga algo con un valor que le pasas, necesitas una función:

mkcd() {
  mkdir -p "$1" && cd "$1"
}

mkcd proyecto-nuevo crea la carpeta y entra a ella en un solo paso; $1 es el primer argumento que le pasaste, algo que un alias no puede referenciar de forma confiable. La regla práctica: si el comando es fijo, alias; si necesita recibir un dato y decidir qué hacer con él, función. Ambos van en el mismo archivo que ya identificaste arriba (.zshrc o .bashrc), porque son configuración de shell interactiva, igual que el PATH.

El historial: herramienta poderosa, fuga silenciosa

history lista los comandos que ya ejecutaste, numerados. !42 vuelve a correr el comando número 42 sin retipearlo. Y Ctrl+R abre una búsqueda incremental hacia atrás: empiezas a teclear un fragmento y la shell te muestra, en vivo, el comando más reciente que lo contiene — presiona Ctrl+R otra vez para seguir buscando hacia atrás si el primer resultado no es el que buscabas.

El problema es que todo lo que escribes en la línea de comandos, sin excepción, queda guardado en un archivo en texto plano (~/.bash_history o ~/.zsh_history) — incluido un export API_KEY=sk_live_... que tecleaste una sola vez sin pensarlo. La lección anterior te enseñó a mantener secretos fuera del código con un archivo .env en permisos 600; el historial es una fuga distinta, que ese cuidado no cubre, porque el secreto nunca tocó un archivo — lo escribiste directamente en el prompt.

La defensa estándar en bash es la variable HISTCONTROL:

# en ~/.bashrc
export HISTCONTROL=ignoreboth

ignoreboth combina dos comportamientos: ignorespace (cualquier comando que empiece con un espacio en blanco no se guarda en el historial) e ignoredups (no repite líneas idénticas consecutivas). Con eso activo, basta con anteponer un espacio a un comando sensible:

 export STRIPE_KEY=sk_live_abc123

para que esa línea nunca llegue al archivo de historial. En zsh, el equivalente es la opción setopt HIST_IGNORE_SPACE dentro de .zshrc. Es un hábito, no un reflejo automático — tienes que acordarte del espacio en el momento — pero es la diferencia entre un secreto que vive treinta segundos en tu memoria y uno que vive para siempre en un archivo de texto sin cifrar.

Dotfiles versionados: tu configuración como código, no como suerte

"Dotfile" es simplemente cualquier archivo de configuración cuyo nombre empieza con un punto —.zshrc, .bashrc, .gitconfig, .vimrc— y que por convención Unix queda oculto en los listados normales. La práctica profesional es tratarlos como código: versionados en un repositorio de git propio, no como archivos sueltos que existen solo en tu laptop y que perderías por completo si el disco falla.

Un patrón simple y ampliamente usado es el repositorio bare con un alias dedicado, en vez de symlinks manuales:

git clone --bare <url-de-tu-repo> "$HOME/.dotfiles"
alias dotfiles='/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME'
dotfiles config --local status.showUntrackedFiles no
dotfiles checkout

Con eso, dotfiles add ~/.zshrc y dotfiles commit versionan tu configuración exactamente como cualquier proyecto, sin que git confunda tu $HOME con un repositorio normal. El beneficio real: si cambias de laptop, o si tu equipo estandariza el entorno de desarrollo, tu configuración completa se reproduce con un clone en vez de reconstruirla de memoria — y cada cambio queda en un historial de commits que puedes revisar y revertir.

Honestidad sobre oh-my-zsh y frameworks similares

Frameworks como oh-my-zsh empaquetan temas de prompt, decenas de plugins comunitarios (autocompletado de git, docker, kubectl) y resaltado de sintaxis, listos para activar sin escribir nada tú mismo. Para alguien que recién está armando su .zshrc, es un punto de partida razonable: defaults sensatos, comunidad enorme, cero configuración manual.

El costo real es tiempo de arranque: cada plugin y cada tema que activas es código adicional que se ejecuta antes de mostrarte el prompt, así que una instalación con varios plugins activos (git, docker, kubectl, resaltado de sintaxis, autosugerencias) se siente notoriamente más lenta al abrir una terminal nueva que un .zshrc sin framework con las mismas tres o cuatro líneas que realmente usas. No confíes en una cifra de memoria — mídelo tú mismo en tu propia máquina:

for i in $(seq 1 5); do time zsh -i -c exit; done

Qué esperar: cinco líneas con un tiempo real cada una; si ese número te sorprende comparado con lo que esperarías de una shell "vacía", ahí tienes tu respuesta concreta, no una cifra ajena. Si el arranque se siente lento, la respuesta correcta no es "acostumbrarte" — es podar los plugins que no usas, o evaluar administradores de plugins más livianos (Zinit o Antidote, que difieren la carga de plugins hasta después de mostrar el prompt) que dan funcionalidad equivalente con menos trabajo por arranque. No hay una elección incorrecta aquí — hay un costo que conviene medir en tu propia máquina antes de aceptarlo.

Errores comunes

Editar el archivo equivocado para el tipo de shell que realmente tienes (conceptual). Qué pasa: agregas un export a .bash_profile, confirmas que funciona en tu terminal, y luego el mismo cambio "desaparece" en una pestaña nueva, en una sesión SSH, o dentro de un script. Por qué ocurre: .bash_profile solo lo lee una shell de login; cualquier shell interactiva no-login (muchas pestañas nuevas en Linux, un bash escrito a mano) lee únicamente .bashrc, y nunca toca .bash_profile. Son dos archivos con audiencias distintas, no dos nombres para lo mismo. Cómo detectarlo: corre echo $0 en la sesión donde falla — si no tiene el guion inicial (bash en vez de -bash), confirmas que no es una shell de login, así que .bash_profile nunca se leyó ahí. Cómo corregirlo: pon la configuración en .bashrc, y haz que .bash_profile la cargue con source ~/.bashrc para que ambos casos queden cubiertos sin duplicar contenido.

Asumir que un alias funciona igual dentro de un script que en la terminal (conceptual). Qué pasa: defines alias ll='ls -lah', lo usas todo el día sin problema, y luego escribes un script que empieza con ll en la primera línea — y al correrlo con ./script.sh falla con command not found: ll. Por qué ocurre: los alias, por diseño, solo se expanden en shells interactivas — un script ejecutado como archivo corre en una shell no-interactiva, que ni siquiera carga .bashrc o .zshrc por defecto, así que el alias nunca llegó a existir en ese contexto. Cómo detectarlo: si un comando que "siempre funciona" falla específicamente dentro de un script o de un cron job, sospecha primero de un alias o una función definida solo en tu archivo interactivo. Cómo corregirlo: dentro de scripts, escribe el comando completo (ls -lah, no ll) — los alias son para ti tecleando, no para código que se ejecuta sin ti.

Guardar un secreto en texto plano dentro de un dotfile versionado (práctico y de seguridad). Qué pasa: pones export STRIPE_KEY=sk_live_abc123 directo en tu .zshrc, y ese archivo vive en tu repositorio de dotfiles que subiste a GitHub para tenerlo respaldado. Meses después borras esa línea, pero el secreto sigue expuesto. Por qué ocurre: git no olvida — el commit donde agregaste la línea con la clave sigue existiendo en el historial del repositorio aunque la borres en un commit posterior; cualquiera con acceso al repositorio (o a su historial público) puede recuperar esa versión anterior. Cómo detectarlo: revisa con git log -p si algún commit de tu repo de dotfiles alguna vez tuvo una clave, token o contraseña en texto plano, sin importar si ya no está en la versión actual del archivo. Cómo corregirlo: nunca pongas el valor real de un secreto en un archivo que vas a versionar — exporta la variable desde un archivo separado que no entra al repositorio (agrégalo a .gitignore) y haz que tu .zshrc o .bashrc lo cargue con source; si un secreto ya quedó expuesto en el historial de git, considéralo comprometido y rótalo, porque borrarlo del archivo no lo borra del historial.

Ejercicios

1. Un compañero te dice: "agregué export EDITOR=vim a mi ~/.bash_profile, funciona perfecto en mi terminal de macOS, pero cuando me conecto a nuestro servidor Ubuntu por SSH y abro una pestaña nueva de tmux dentro de esa misma sesión SSH, $EDITOR aparece vacío." Corre echo $0 dentro de esa pestaña de tmux y obtiene bash (sin guion inicial). Explica qué está pasando y qué archivo debería usar en su lugar.

Ver solución

La conexión SSH original sí fue una shell de login (por eso .bash_profile se leyó y $EDITOR funcionó ahí), pero una pestaña nueva dentro de tmux, abierta ya con la sesión en marcha, es una shell interactiva no de login — el $0 sin guion lo confirma. Esa shell nunca lee .bash_profile; solo lee .bashrc. La corrección es mover (o duplicar mediante source) el export EDITOR=vim a ~/.bashrc, e idealmente hacer que .bash_profile cargue .bashrc con source ~/.bashrc para que el valor quede disponible sin importar cómo arrancó cada shell. Esto funciona porque login e interactivo son dos preguntas independientes que bash resuelve leyendo archivos distintos, y una pestaña de tmux responde "sí" a la segunda pero "no" a la primera.

2. Escribe una función de shell llamada backup que reciba la ruta de un archivo como argumento y cree una copia con el sufijo .bak en el mismo directorio (por ejemplo, backup notes.txt debería crear notes.txt.bak). Explica por qué esto tiene que ser una función y no un alias.

Ver solución
backup() {
  cp "$1" "$1.bak"
}

Tiene que ser una función porque necesita recibir un argumento real ($1, la ruta que le pasas) y usarlo dos veces dentro de una lógica —una vez tal cual, otra vez con el sufijo agregado—. Un alias es sustitución de texto fija: alias backup='cp' solo antepondría cp a lo que escribas después, sin ninguna forma confiable de repetir ese argumento con un sufijo distinto en la misma línea. Esto funciona porque las funciones sí reciben parámetros posicionales completos ($1, $2, etc.) mientras que los alias no tienen ese mecanismo.

3. Sin querer, tecleaste directamente en la terminal export DB_PASSWORD=hunter2 para probar una conexión rápida, sin el espacio inicial que hubiera evitado que quedara guardado. ¿Qué configuración deberías tener activa para que esto no vuelva a pasar, y qué harías con esta línea específica que ya quedó en el historial?

Ver solución

Para el futuro, la variable HISTCONTROL=ignoreboth (bash) o setopt HIST_IGNORE_SPACE (zsh), agregada en .bashrc o .zshrc, hace que cualquier comando que empiece con un espacio en blanco se excluya del historial — el hábito correcto de ahí en adelante es anteponer un espacio a cualquier comando con un secreto. Para la línea que ya quedó guardada, no basta con "no volver a hacerlo": el hunter2 ya está en texto plano en ~/.bash_history o ~/.zsh_history, así que conviene tratar esa contraseña como potencialmente vista por cualquiera con acceso a ese archivo o a esa máquina y rotarla (cambiarla por una nueva), además de poder borrar esa línea puntual del historial con history -d <número> en bash. Esto funciona porque HISTCONTROL previene el problema hacia adelante, pero no limpia retroactivamente un archivo que ya escribió el dato en disco — la única defensa real contra un secreto ya expuesto es rotarlo.

4. Explica, en tus propias palabras, por qué versionar tus dotfiles en un repositorio de git te obliga a tener más cuidado con los secretos que cuando esos mismos archivos solo vivían sueltos en tu $HOME.

Ver solución

Cuando un dotfile solo vive en tu disco, un secreto adentro es riesgoso pero está limitado a esa máquina y desaparece si editas o borras el archivo. En cuanto ese mismo archivo entra a un repositorio de git —sobre todo si lo subes a un servicio remoto como GitHub—, cualquier commit donde el secreto aparezca queda preservado permanentemente en el historial del repositorio, accesible con git log -p incluso después de que borres la línea en un commit posterior, y potencialmente visible para cualquiera con acceso al repositorio (colaboradores, o el público entero si es un repo público). Esto funciona porque git está diseñado para nunca perder versiones anteriores de un archivo — esa es exactamente su utilidad para código, pero es la razón exacta por la que un secreto nunca debe entrar a un commit, ni siquiera "por un momento" con intención de borrarlo después.

Resumen y siguiente paso

Hoy viste que cada terminal que abres reconstruye tu entorno leyendo un archivo distinto según dos preguntas independientes —¿es de login?, ¿es interactiva?— y que esa distinción es la causa técnica exacta detrás de "mi variable funciona en una terminal y no en otra". Aprendiste a hacer persistente un cambio de PATH en el archivo correcto, a elegir entre alias y función según si necesitas argumentos, a usar source para aplicar cambios sin cerrar tu sesión, a proteger tu historial de secretos con HISTCONTROL, y a tratar tus dotfiles como código versionado en vez de configuración frágil que solo existe en una laptop.

Antes de avanzar deberías poder: explicar de memoria la diferencia entre .bash_profile/.bashrc y entre .zprofile/.zshrc; agregar una línea a tu PATH que sobreviva al cierre de la terminal; escribir un alias y una función y justificar cuándo usar cada uno; y explicar por qué un secreto tecleado directamente en la terminal, o guardado en un dotfile versionado, es una fuga distinta a la de un archivo .env sin protección.

Lo que aprendiste hoy es exactamente la herramienta que te falta para cerrar el módulo: la siguiente lección es un proyecto donde vas a provocar y reparar tres fallas reales, y una de ellas —un command not found causado por un PATH mal armado— la vas a dejar arreglada de forma permanente en tu archivo rc, con evidencia documentada, en vez de arreglarla solo "para esta sesión" y volver a romperla la próxima vez que abras la terminal.

Recursos