Proyectos · Bash y la terminal · Intermedio

Organizador de descargas

Un script que ordena la carpeta de descargas en subcarpetas por tipo (imágenes, documentos, música…), con modo simulación para ver qué haría antes de mover nada.

  • 5 pasos
  • 1 hora aproximadamente
  • 53 líneas de código

Lo que vas a construir

La carpeta de descargas suele ser un cajón desastre. Vas a escribir un script de Bash que la ordena en subcarpetas según el tipo de cada archivo. Por el camino aprenderás a recorrer archivos con nombres raros (con espacios), a decidir con case y a añadir un modo simulación, que es lo primero que debería tener cualquier script que mueve o borra cosas.

Lo que vas a practicar

  • Recorrer archivos con for y comodines
  • Quitar partes de un texto: ${nombre##*.}
  • Elegir con case
  • Mover sin sobrescribir y simular
  • Arrays asociativos y comprobaciones de seguridad

Cómo ejecutarlo: Guarda el script como organizar.sh, dale permiso de ejecución con chmod +x organizar.sh y pruébalo primero en una carpeta de prueba: mkdir prueba && touch prueba/{foto.jpg,factura.pdf,cancion.mp3}, y luego ./organizar.sh --simular prueba. En Windows funciona con Git Bash o WSL.

Así funciona al final
$ ./organizar.sh --simular
(Simulación: no se ha movido nada)
Resumen de /home/lucia/Descargas:
  Imagenes: 2
  Documentos: 3
  Musica: 1
  Videos: 1
  Comprimidos: 1
  Otros: 1
Total: 9 archivos

Antes de empezar, conviene haber hecho estas lecciones

Paso 1 de 5

Recorrer los archivos

"$carpeta"/* se convierte en la lista de lo que hay en la carpeta. Las comillas son importantes: sin ellas, un archivo como «mi archivo.docx» se partiría en dos.

[ -f "$archivo" ] || continue salta lo que no sea un archivo (las carpetas) y ${nombre##*.} quita todo hasta el último punto, dejando la extensión.

organizar.sh
#!/bin/bash
carpeta="$HOME/Descargas"

for archivo in "$carpeta"/*; do
  [ -f "$archivo" ] || continue          # solo archivos, no carpetas
  nombre=$(basename "$archivo")
  extension="${nombre##*.}"              # lo que hay tras el último punto
  echo "$nombre → $extension"
done
Al ejecutarlo
cancion.mp3 → mp3
captura.PNG → PNG
copia.zip → zip
factura.pdf → pdf
foto.jpg → jpg
instalador.exe → exe
mi archivo.docx → docx
notas.txt → txt
video.mp4 → mp4

Paso 2 de 5

Una categoría para cada extensión

La función categoria_de decide con case, que compara un valor con varios patrones: jpg|jpeg|png significa «cualquiera de estos» y * es el caso por defecto.

${1,,} pasa la extensión a minúsculas, para que PNG y png se traten igual. local hace que la variable solo exista dentro de la función.

organizar.shLo nuevo, resaltado
#!/bin/bash
carpeta="$HOME/Descargas"

categoria_de() {
  local extension="${1,,}"               # en minúsculas: PNG y png son lo mismo
  case "$extension" in
    jpg|jpeg|png|gif|webp|heic) echo "Imagenes" ;;
    pdf|doc|docx|odt|txt|xlsx|pptx) echo "Documentos" ;;
    mp3|wav|flac|m4a|ogg) echo "Musica" ;;
    mp4|mkv|mov|avi|webm) echo "Videos" ;;
    zip|rar|7z|tar|gz) echo "Comprimidos" ;;
    *) echo "Otros" ;;
  esac
}

for archivo in "$carpeta"/*; do
  [ -f "$archivo" ] || continue
  nombre=$(basename "$archivo")
  echo "$nombre → $(categoria_de "${nombre##*.}")"
done
Al ejecutarlo
cancion.mp3 → Musica
captura.PNG → Imagenes
copia.zip → Comprimidos
factura.pdf → Documentos
foto.jpg → Imagenes
instalador.exe → Otros
mi archivo.docx → Documentos
notas.txt → Documentos
video.mp4 → Videos

Paso 3 de 5

Mover… o simularlo

mkdir -p crea la carpeta de destino si no existe (y no da error si ya existe) y mv -n mueve el archivo sin sobrescribir nunca uno con el mismo nombre.

Antes de mover nada de verdad, conviene ver qué pasaría: con --simular, el script solo dice lo que haría. Pruébalo siempre así primero.

organizar.shLo nuevo, resaltado
#!/bin/bash
carpeta="$HOME/Descargas"
simular=false
[ "${1:-}" = "--simular" ] && simular=true

categoria_de() {
  local extension="${1,,}"
  case "$extension" in
    jpg|jpeg|png|gif|webp|heic) echo "Imagenes" ;;
    pdf|doc|docx|odt|txt|xlsx|pptx) echo "Documentos" ;;
    mp3|wav|flac|m4a|ogg) echo "Musica" ;;
    mp4|mkv|mov|avi|webm) echo "Videos" ;;
    zip|rar|7z|tar|gz) echo "Comprimidos" ;;
    *) echo "Otros" ;;
  esac
}

for archivo in "$carpeta"/*; do
  [ -f "$archivo" ] || continue
  nombre=$(basename "$archivo")
  destino="$carpeta/$(categoria_de "${nombre##*.}")"
  if $simular; then
    echo "Movería $nombre a $destino"
  else
    mkdir -p "$destino"
    mv -n "$archivo" "$destino/"          # -n: nunca sobrescribir un archivo que ya esté
    echo "Movido $nombre a $destino"
  fi
done
Al ejecutarlo
$ ./organizar.sh --simular
Movería cancion.mp3 a /home/lucia/Descargas/Musica
Movería captura.PNG a /home/lucia/Descargas/Imagenes
Movería copia.zip a /home/lucia/Descargas/Comprimidos
Movería factura.pdf a /home/lucia/Descargas/Documentos
Movería foto.jpg a /home/lucia/Descargas/Imagenes
Movería instalador.exe a /home/lucia/Descargas/Otros
Movería mi archivo.docx a /home/lucia/Descargas/Documentos
Movería notas.txt a /home/lucia/Descargas/Documentos
Movería video.mp4 a /home/lucia/Descargas/Videos

Paso 4 de 5

Un resumen al final

En vez de una línea por archivo, ahora se cuentan por categoría en un array asociativo (declare -A), que usa textos como claves: cuenta[Imagenes].

${cuenta[$categoria]:-0} da 0 si la categoría aún no tiene ninguno. Al final se muestran las categorías en un orden fijo, porque Bash no garantiza el orden de las claves.

organizar.shLo nuevo, resaltado
#!/bin/bash
carpeta="$HOME/Descargas"
simular=false
[ "${1:-}" = "--simular" ] && simular=true

categoria_de() {
  local extension="${1,,}"
  case "$extension" in
    jpg|jpeg|png|gif|webp|heic) echo "Imagenes" ;;
    pdf|doc|docx|odt|txt|xlsx|pptx) echo "Documentos" ;;
    mp3|wav|flac|m4a|ogg) echo "Musica" ;;
    mp4|mkv|mov|avi|webm) echo "Videos" ;;
    zip|rar|7z|tar|gz) echo "Comprimidos" ;;
    *) echo "Otros" ;;
  esac
}

declare -A cuenta                         # array asociativo: categoría → número de archivos
total=0

for archivo in "$carpeta"/*; do
  [ -f "$archivo" ] || continue
  nombre=$(basename "$archivo")
  categoria=$(categoria_de "${nombre##*.}")
  cuenta[$categoria]=$(( ${cuenta[$categoria]:-0} + 1 ))
  total=$(( total + 1 ))
  if ! $simular; then
    mkdir -p "$carpeta/$categoria"
    mv -n "$archivo" "$carpeta/$categoria/"
  fi
done

echo "Resumen:"
for categoria in Imagenes Documentos Musica Videos Comprimidos Otros; do
  [ -n "${cuenta[$categoria]:-}" ] && echo "  $categoria: ${cuenta[$categoria]}"
done
echo "Total: $total archivos"
Al ejecutarlo
$ ./organizar.sh --simular
Resumen:
  Imagenes: 2
  Documentos: 3
  Musica: 1
  Videos: 1
  Comprimidos: 1
  Otros: 1
Total: 9 archivos

Paso 5 de 5

Un script en el que confiar

set -euo pipefail detiene el script ante cualquier error o variable sin definir, y shopt -s nullglob evita que, con la carpeta vacía, * se tome como un nombre de archivo. La carpeta se puede pasar como argumento (${1:-…} pone la de descargas si no se indica) y se comprueba que exista.

Los archivos sin extensión se dejan donde están, y un aviso deja claro cuándo el resultado es solo una simulación.

organizar.shLo nuevo, resaltado
#!/bin/bash
# Uso: organizar.sh [--simular] [carpeta]   (por defecto, ~/Descargas)
set -euo pipefail
shopt -s nullglob                         # si la carpeta está vacía, el bucle no da vueltas

simular=false
if [ "${1:-}" = "--simular" ]; then
  simular=true
  shift
fi
carpeta="${1:-$HOME/Descargas}"

if [ ! -d "$carpeta" ]; then
  echo "No existe la carpeta: $carpeta" >&2
  exit 1
fi

categoria_de() {
  local extension="${1,,}"
  case "$extension" in
    jpg|jpeg|png|gif|webp|heic) echo "Imagenes" ;;
    pdf|doc|docx|odt|txt|xlsx|pptx) echo "Documentos" ;;
    mp3|wav|flac|m4a|ogg) echo "Musica" ;;
    mp4|mkv|mov|avi|webm) echo "Videos" ;;
    zip|rar|7z|tar|gz) echo "Comprimidos" ;;
    *) echo "Otros" ;;
  esac
}

declare -A cuenta
total=0

for archivo in "$carpeta"/*; do
  [ -f "$archivo" ] || continue
  nombre=$(basename "$archivo")
  [[ "$nombre" == *.* ]] || continue      # sin extensión: se deja donde está
  categoria=$(categoria_de "${nombre##*.}")
  cuenta[$categoria]=$(( ${cuenta[$categoria]:-0} + 1 ))
  total=$(( total + 1 ))
  if ! $simular; then
    mkdir -p "$carpeta/$categoria"
    mv -n "$archivo" "$carpeta/$categoria/"
  fi
done

$simular && echo "(Simulación: no se ha movido nada)"
echo "Resumen de $carpeta:"
for categoria in Imagenes Documentos Musica Videos Comprimidos Otros; do
  if [ -n "${cuenta[$categoria]:-}" ]; then
    echo "  $categoria: ${cuenta[$categoria]}"
  fi
done
echo "Total: $total archivos"
Al ejecutarlo
$ ./organizar.sh --simular
(Simulación: no se ha movido nada)
Resumen de /home/lucia/Descargas:
  Imagenes: 2
  Documentos: 3
  Musica: 1
  Videos: 1
  Comprimidos: 1
  Otros: 1
Total: 9 archivos

Retos para seguir

Has terminado el proyecto. Si quieres ir más allá, prueba a añadirle esto por tu cuenta:

  1. Si ya existe un archivo con el mismo nombre en el destino, renombra el nuevo añadiendo un número.
  2. Mueve a la carpeta «Antiguos» los archivos de más de 90 días (pista: find -mtime +90).
  3. Programa el script con cron para que ordene la carpeta cada noche.

Conceptos clave

Repasa la ruta de Bash y la terminal o elige otro proyecto.