Módulo 2: Trabajar con archivos y texto

3. Copiar, mover y borrar sin destruir nada

Descripción

Al terminar esta lección vas a poder copiar archivos y carpetas completas con cp, mover y renombrar con mv entendiendo por qué en Unix son literalmente el mismo comando, y borrar con rm, rm -r y rm -rf aplicando un criterio de seguridad explícito en lugar de escribir el comando y rezar. También vas a saber qué hacer en el peor momento posible: cuando ya apretaste Enter y el archivo que acabas de borrar era el que no tenías respaldado.

Esta es la lección donde la terminal deja de ser inofensiva. Hasta ahora creaste estructura (mkdir, touch) y no hay mucho que romper creando carpetas vacías. Reorganizar un proyecto, limpiar una carpeta de compilación antes de un despliegue, o preparar un directorio de entrega para un cliente son tareas de todos los días en cualquier trabajo con terminal — y las tres involucran mover o borrar cosas que sí importan. El costo de un descuido aquí no es un mensaje de error: es un archivo que ya no existe en ningún lado.

Conexión con el módulo: la lección anterior te dio las herramientas para crear estructura (mkdir -p, touch, expansión de llaves). Esta lección te da las herramientas para reorganizarla y limpiarla — exactamente lo que vas a necesitar en el proyecto final del módulo, donde vas a auditar un directorio ajeno y dejarlo ordenado.

Una ruta es una etiqueta, no el archivo

En un depósito grande, cada caja tiene una etiqueta pegada con su ubicación: "estante 4, fila B". Si un empleado mueve la caja del estante 4 al estante 7, no destruye la caja ni fabrica una nueva — despega la etiqueta vieja y pega una nueva. El contenido nunca se tocó. Y si alguien arranca la etiqueta sin mover la caja, la caja sigue ahí físicamente, pero nadie que solo tenga la lista de etiquetas la va a volver a encontrar.

Eso es casi exactamente lo que pasa con tus archivos. Un nombre de archivo no es el archivo: es una entrada en un directorio que apunta a dónde vive el contenido real en el disco. Con esa idea, los tres comandos de esta lección dejan de ser tres cosas sueltas para memorizar y se vuelven una sola idea aplicada tres veces:

  • cp (copy) crea contenido nuevo con una etiqueta nueva. Ahora hay dos cajas.
  • mv (move) cambia la etiqueta de una caja existente — o la mueve de estante. El contenido nunca se duplica.
  • rm (remove) arranca la etiqueta. Si ninguna otra etiqueta apunta a esa caja, el espacio queda disponible para que el sistema lo reutilice — pero nadie te va a devolver el contenido llamándolo por su nombre viejo.

Esa última frase es la que le da forma a toda la lección: en la terminal no hay papelera por defecto. Cuando borras, borras.

Ejemplo trabajado: preparar un respaldo, renombrar una entrega y limpiar después

Supón que tienes este proyecto para un cliente, construido con lo que aprendiste en la lección anterior:

mkdir -p client-a/src client-a/exports
touch client-a/notes.txt client-a/src/app.py
touch client-a/exports/report-draft.pdf client-a/exports/report-final.pdf
ls -R client-a

Qué esperar:

client-a:
exports		notes.txt	src

client-a/exports:
report-draft.pdf	report-final.pdf

client-a/src:
app.py

Antes de hacer un cambio riesgoso (por ejemplo, reorganizar exports/), haces una copia completa de respaldo con cp -r (-r de recursive: sin ella, cp se niega a copiar un directorio):

cp -r client-a client-a-backup
ls -R client-a-backup

Qué esperar: la misma estructura completa, ahora bajo client-a-backup/exports/, notes.txt y src/app.py copiados byte a byte. Como client-a-backup no existía todavía, cp simplemente lo crea como una copia completa; en esto, macOS y Linux se comportan igual.

cp no sobrescribe a ciegas por defecto en la mayoría de las configuraciones interactivas, pero la forma explícita de estar seguro es -i (interactive):

cp -i client-a/notes.txt client-a-backup/notes.txt

Qué esperar:

overwrite client-a-backup/notes.txt? (y/n [n])

Si respondes n (o solo presionas Enter, que toma n por defecto), la copia vieja queda intacta. Es la misma lógica que vas a usar en mv y en rm con -i: antes de destruir algo, el comando te muestra exactamente qué va a pisar y espera tu confirmación.

Con el respaldo ya hecho, renombras la carpeta de exportación una vez que el cliente aprobó la versión final:

mv client-a/exports client-a/deliverables
ls client-a

Qué esperar:

deliverables	notes.txt	src

exports ya no existe como nombre; deliverables contiene exactamente los mismos dos archivos, con las mismas fechas de modificación. No se copió ni un byte — mv solo reescribió la entrada del directorio. Por eso es instantáneo sin importar si esa carpeta pesa un kilobyte o cien gigabytes: mientras origen y destino estén en el mismo sistema de archivos (el mismo disco), mover es puro cambio de etiqueta. Solo cuando mueves algo entre discos distintos — tu disco interno y una memoria USB, por ejemplo — mv tiene que copiar el contenido de verdad y luego borrar el original, y ahí sí el tiempo depende del tamaño.

Por último, con el respaldo verificado y ya no necesario, lo eliminas. Antes de borrar, listas exactamente lo que vas a borrar — la primera de las tres reglas de seguridad que ves en detalle más abajo:

ls client-a-backup
rm -r client-a-backup
ls client-a-backup

Qué esperar: el primer ls muestra el contenido completo del respaldo — tu última oportunidad de confirmar que es lo que crees que es. rm -r no imprime nada si funciona (silencio es éxito en la terminal). El segundo ls confirma que ya no existe:

ls: client-a-backup: No such file or directory

Sin confirmación, sin papelera, sin deshacer. rm -r hizo exactamente lo que le pediste.

La barra final en cp: el mismo comando, dos resultados distintos

Hay un detalle de cp -r que sorprende a casi todo el mundo la primera vez, y que depende de tu sistema operativo. Ya viste que si el destino no existe todavía, cp -r origen destino simplemente crea destino como copia completa — ahí no hay ambigüedad, y da igual macOS o Linux.

El problema aparece cuando el destino ya existe como carpeta. Ahí importa si el origen termina en / o no:

Comando (destino ya existe)macOS / BSDLinux / GNU
cp -r deliverables client-a-backup (sin barra)Anida: client-a-backup/deliverables/...Anida: client-a-backup/deliverables/...
cp -r deliverables/ client-a-backup (con barra)Vuelca el contenido directo: client-a-backup/report-draft.pdfIgual que sin la barra: client-a-backup/deliverables/...

El manual de cp en macOS lo dice de forma literal: "If the source_file ends in a /, the contents of the directory are copied rather than the directory itself." GNU cp (el de cualquier Linux, incluido WSL2 con Ubuntu) ignora esa barra por completo: con o sin ella, el resultado es idéntico.

En la práctica, esto significa que un comando que probaste en tu Mac y funcionó puede comportarse distinto en el servidor Linux al que te conectas por SSH — con la misma línea, letra por letra. La forma de no depender de la memoria es siempre correr un ls después de copiar y verificar que el árbol quedó como esperabas, en vez de asumir. Si de verdad necesitas que el comportamiento sea idéntico en ambos sistemas, la forma portable de decir "quiero el contenido, no la carpeta" es agregar un punto: cp -r deliverables/. client-a-backup/.

Las tres reglas antes de escribir "rm -rf"

rm no pregunta, no tiene papelera y no tiene deshacer — a diferencia de arrastrar un archivo a la papelera en una interfaz gráfica, aquí no hay una segunda oportunidad integrada. Eso no significa que sea impredecible: significa que la seguridad depende de un hábito tuyo, no del programa. Son tres reglas, y las tres cuestan tres segundos aplicarlas.

Regla 1 — Lista con ls exactamente lo que vas a borrar, antes de borrarlo. Si vas a correr rm -r client-a-backup, corre primero ls client-a-backup (o ls -R si quieres ver todo el árbol) y lee el resultado. Si vas a borrar con un patrón, corre primero ls con ese mismo patrón: ls *.tmp antes de rm *.tmp. El objetivo no es memorizar la regla — es hacer que el patrón coincida en tu cabeza con lo que realmente hay en el disco antes de que sea irreversible.

Regla 2 — No uses comodines a ciegas. Un comodín (*, como en rm -r build/*) lo expande la shell antes de que rm vea nada — para cuando rm recibe el comando, ya no hay ningún *, hay una lista literal de nombres de archivo. Si esa expansión no es la que imaginabas (porque estabas en el directorio equivocado, porque el patrón coincidió con más de lo que pensabas), rm va a borrar exactamente esa lista sin quejarse. GNU rm en Linux trae una protección para el caso extremo: por defecto rechaza borrar recursivamente si el argumento literal es /, con el mensaje it is dangerous to operate recursively on '/'. Pero esa protección no te salva de rm -rf /* — ahí la shell ya expandió /* en una lista de carpetas (/bin /etc /home /usr ...) antes de que rm la viera, así que nunca recibe un / literal y la protección no se activa. La defensa real no es el software: es correr ls con ese mismo patrón primero.

Regla 3 — Nunca dejes una variable sin comillas dentro de un rm, y verifica que no esté vacía antes de usarla. Estas son dos protecciones distintas, no una sola:

  • Comillas, para que un valor con espacios no se parta en varios argumentos. Sin comillas, rm -rf $file con file="final report.txt" intenta borrar dos cosas: un archivo o carpeta llamado final y otro llamado report.txt — casi nunca lo que querías.
  • Verificar que no esté vacía, porque las comillas por sí solas no te salvan de una variable sin definir. En enero de 2015, el instalador de Steam para Linux tenía una línea parecida a rm -rf "$STEAMROOT/"*. Cuando por un bug anterior $STEAMROOT quedaba vacío, esa línea — con comillas y todo — se convertía en rm -rf "/"*, y borraba todo lo que el usuario tenía permiso de borrar en su cuenta, empezando desde la raíz del sistema. Las comillas evitaron el problema de los espacios; no evitaron el problema de la variable vacía.
# MAL: si $BUILD_DIR nunca se definió, esto intenta borrar "/*"
rm -rf $BUILD_DIR/*

# BIEN: comillas contra espacios, y una verificación explícita contra vacío
if [ -z "$BUILD_DIR" ]; then
  echo "Error: BUILD_DIR no está definida. Abortando." >&2
  exit 1
fi
rm -rf "$BUILD_DIR"/*

(La forma corta y más elegante de esa misma verificación, con set -euo pipefail y trap, la vas a ver cuando escribas tus propios scripts más adelante en la guía — por ahora, el if explícito alcanza y se entiende sin trucos.)

Papelera desde la terminal: trash-cli (y sus límites reales)

Si el riesgo de rm te incomoda, existe una alternativa real: mandar los archivos a una papelera en vez de borrarlos de forma permanente. En Linux, trash-cli (se instala con pip install trash-cli) agrega los comandos trash-put (envía a la papelera en vez de borrar), trash-list (qué hay adentro) y trash-restore (recuperar algo). En macOS, la alternativa equivalente se llama trash (brew install trash) y usa la papelera nativa del Finder, la misma que ves si abres una ventana gráfica.

trash-put old-report.txt      # Linux, con trash-cli instalado
trash old-report.txt          # macOS, con trash instalado

Dicho esto, hay que ser honesto sobre cuánto se usan en la práctica: casi nadie las tiene instaladas en un servidor remoto, y ninguna viene preinstalada por defecto. En una máquina que administras tú mismo y usas todos los días, puede valer la pena. En un servidor al que te conectas por SSH para una tarea puntual, vas a encontrarte con el rm de siempre, sin red de seguridad — que es exactamente por qué las tres reglas de la sección anterior importan más que cualquier herramienta que instales.

Si ya borraste algo importante

Si el archivo ya no está y no tienes copia, hay una secuencia que maximiza tus chances, de la más a la menos probable de funcionar:

  1. Deja de escribir en ese disco. No crees archivos nuevos, no instales nada, no sigas trabajando en esa carpeta. Cuando rm borra un archivo, el espacio que ocupaba queda marcado como disponible pero los datos no se sobrescriben en el instante — cualquier escritura nueva puede reutilizar justo esos bloques y destruir la única chance real de recuperación.
  2. Busca una copia que ya tenías. Un respaldo con Time Machine (macOS), una instantánea del sistema de archivos, una sincronización con la nube, o simplemente un git status si el archivo estaba versionado. Esto resuelve el problema en el 90% de los casos reales y es infinitamente más confiable que cualquier herramienta de recuperación.
  3. Si un programa todavía tiene el archivo abierto, en Linux puedes copiar su contenido desde /proc/<pid>/fd/ antes de que ese proceso lo cierre — mientras algún proceso lo mantiene abierto, los datos siguen vivos aunque el nombre ya haya desaparecido.
  4. Como último recurso, existen herramientas de recuperación a nivel de disco (extundelete, testdisk, photorec), pero no garantizan nada: dependen del sistema de archivos, de si el espacio ya se reutilizó, y en discos SSD modernos el propio disco puede borrar físicamente los bloques marcados como libres (TRIM) mucho antes de que lo notes.

La conclusión honesta es la misma que en la sección anterior: la protección real no llega después del borrado. Llega antes, con las tres reglas y con un respaldo que ya existía cuando lo necesitaste.

Errores comunes

Pensar que mv copia y después borra, siempre. Es el malentendido conceptual más común de esta lección, y es razonable llegar a él porque mv sí hace eso — pero solo cuando origen y destino están en discos distintos. Dentro del mismo sistema de archivos, mv nunca copia ni un byte: solo reescribe una entrada de directorio, por eso es instantáneo sin importar el tamaño. Cómo se detecta el malentendido: si esperas que mover una carpeta de 20 GB tarde un rato y termina en el acto, tu modelo mental estaba mal, no el comando. Cómo se corrige: recuerda la etiqueta y la caja — mv cambia la etiqueta; solo copia contenido de verdad cuando la caja tiene que cruzar a un estante que está en otro edificio (otro disco).

Olvidar -r al copiar o borrar una carpeta. cp carpeta destino y rm carpeta fallan con mensajes como cp: carpeta is a directory (not copied) o rm: carpeta: is a directory. No es un bug: cp y rm exigen el flag recursivo a propósito, como un freno deliberado antes de una operación que puede tocar cientos de archivos de golpe. Cómo se detecta: el mensaje de error lo dice explícitamente ("is a directory"). Cómo se corrige: agregar -r (o -R) — pero antes de agregarlo sin pensar, es el momento exacto de aplicar la Regla 1 y confirmar con ls que esa carpeta es realmente la que quieres tocar.

Confiar en el comodín sin confirmar en qué carpeta estás parado. El desastre clásico es correr rm -rf * pensando que estás dentro de build/ cuando en realidad un cd anterior falló silenciosamente (por ejemplo, escribiste mal el nombre) y seguís en la carpeta del proyecto completo. rm no tiene forma de saber que te equivocaste de lugar — solo ve el patrón expandido y actúa. Cómo se detecta: revisando pwd y ls antes del rm, no después. Cómo se corrige: el hábito de encadenar pwd && ls (o directamente ls con el mismo patrón del rm) inmediatamente antes de cualquier borrado con comodín, sin excepciones — es la misma Regla 1, aplicada al lugar y no solo al contenido.

Ejercicios

Ejercicio 1. Tienes esta carpeta:

reports/
├── summary.txt
└── charts/
    └── q1.png

Corres este comando dos veces seguidas, sin borrar nada entre medio:

cp -r reports archive
cp -r reports archive

¿Qué contiene archive después de la segunda vez? Escribe el árbol resultante.

Ver solución

Después del primer comando, archive no existía, así que se crea como copia completa: archive/summary.txt y archive/charts/q1.png.

Después del segundo comando, archive ya existe como carpeta — y el origen (reports, sin barra final) no la tiene. Eso significa que se anida: cp copia reports completo dentro de archive. El árbol final es:

archive/
├── summary.txt
├── charts/
│   └── q1.png
└── reports/
    ├── summary.txt
    └── charts/
        └── q1.png

Por qué funciona así: cuando el destino ya existe como directorio y el origen no termina en /, tanto macOS como Linux anidan el origen completo dentro del destino — es el único de los dos casos de la tabla de la lección donde ambos sistemas coinciden sin condiciones.

Ejercicio 2. Encuentras esta línea en un script de limpieza que alguien más escribió:

rm -rf $TEMP_DIR/*

a) ¿Qué pasa si la variable $TEMP_DIR nunca se definió (por ejemplo, por un error de tipeo más arriba en el script)?

b) Reescribe la línea aplicando la Regla 3 completa.

Ver solución

a) Si $TEMP_DIR está vacía, la shell la sustituye por nada antes de correr el comando. $TEMP_DIR/* se convierte en /*, así que el comando real que se ejecuta es rm -rf /* — que borra recursivamente todo lo que el proceso tiene permiso de borrar a partir de la raíz. Es exactamente el mismo mecanismo del incidente de Steam de 2015.

b)

if [ -z "$TEMP_DIR" ]; then
  echo "Error: TEMP_DIR no está definida. Abortando." >&2
  exit 1
fi
rm -rf "$TEMP_DIR"/*

Por qué funciona: la verificación con -z detiene el script antes de que rm vea nada si la variable está vacía, y las comillas alrededor de "$TEMP_DIR" evitan que un valor con espacios se parta en varios argumentos. Son dos protecciones distintas y hacen falta las dos.

Ejercicio 3. Tienes client-a-draft/ (varios archivos, ya aprobado por el cliente) y client-a-old-exports/ (una carpeta vieja que ya no necesitas). Escribe, en orden, los comandos para: (1) renombrar client-a-draft a client-a-final, y (2) borrar client-a-old-exports aplicando la Regla 1. Después, explica en una frase por qué el paso 1 termina al instante sin importar cuántos gigabytes pese la carpeta.

Ver solución
mv client-a-draft client-a-final

ls client-a-old-exports
rm -r client-a-old-exports

Por qué el renombrado es instantáneo: client-a-draft y client-a-final están en el mismo sistema de archivos (el mismo disco), así que mv no copia ningún byte de contenido — solo cambia la entrada de directorio que apunta al mismo contenido de siempre. El tamaño de la carpeta nunca entra en juego porque nunca se lee ni se reescribe ese contenido.

Resumen y siguiente paso

Antes de seguir, deberías poder copiar una carpeta completa con cp -r, predecir si va a anidarse o volcarse según si el destino ya existe y tu sistema operativo, renombrar y mover con mv explicando por qué es instantáneo dentro del mismo disco, y borrar con rm -r o rm -rf habiendo listado antes exactamente qué ibas a borrar.

Ya sabes crear estructura y ya sabes reorganizarla y destruirla con criterio. Lo que todavía no tienes es una forma de leer lo que hay adentro de un archivo sin abrir un editor — y eso importa porque en la próxima lección vas a encontrarte con archivos que no caben en una sola pantalla, algo que ningún comando de esta lección resuelve. cat, less, head y tail son exactamente esa pieza que falta.

Recursos