El script se construye en 5 versiones incrementales. Cada una añade un concepto y se puede ejecutar y probar sola.
Preparación: carpeta de pruebas
mkdir -p ~/proyectos/organizador/caos && cd ~/proyectos/organizador
git init
touch caos/foto.JPG caos/informe.pdf caos/cancion.mp3 caos/video.mp4 \
caos/backup.tar.gz caos/script.py caos/README caos/notas.txt
mkdir caos/subcarpeta
touch crea archivos vacíos, y para el script solo importa el nombre. Un desorden fácil de regenerar permite probar muchas veces: si algo sale mal, rm -rf caos y se vuelve a crear.
Versión 1: recibir y validar el argumento
Conceptos: argumentos ($1, $#, $0), if, códigos de salida.
#!/usr/bin/env bash
# organizar.sh - Ordena los archivos de un directorio en carpetas por tipo
# Uso: ./organizar.sh <directorio>
if [ $# -ne 1 ]; then
echo "Uso: $0 <directorio>" >&2
exit 1
fi
directorio="$1"
if [ ! -d "$directorio" ]; then
echo "Error: '$directorio' no es un directorio válido" >&2
exit 1
fi
echo "Voy a organizar: $directorio"
| Pieza | Qué significa |
|---|---|
$# |
Cantidad de argumentos recibidos |
$1 |
Primer argumento. $0 es el nombre del script |
-ne |
“not equal” para números. También -eq, -lt, -gt |
[ ... ] |
Es un comando (type [ lo demuestra). Los espacios dentro son obligatorios |
-d |
Verdadero si es un directorio. Otros: -f archivo, -e existe |
! |
Niega la condición |
>&2 |
Manda el mensaje a stderr |
exit 1 |
Termina con código de error (0 es éxito) |
chmod +x organizar.sh
./organizar.sh # Uso: ./organizar.sh <directorio>
./organizar.sh /noexiste # Error
./organizar.sh caos # Voy a organizar: caos
echo $? # 0
Validar al inicio y salir (fail fast) es mejor que dejar que el script falle más adelante con un error confuso.
git add . && git commit -m "v1: validación de argumentos"
Versión 2: recorrer los archivos
Conceptos: bucle for, comodines (globbing), comillas, basename.
Reemplaza el último echo por:
for ruta in "$directorio"/*; do
[ -f "$ruta" ] || continue
nombre=$(basename "$ruta")
echo "Encontré: $nombre"
done
| Pieza | Qué significa |
|---|---|
"$directorio"/* |
La shell expande * a la lista de archivos antes de correr el bucle |
for ruta in ... |
Repite el bloque una vez por cada elemento |
[ -f "$ruta" ] || continue |
Si no es archivo regular, salta a la siguiente vuelta. Así se ignoran las carpetas |
$(comando) |
Sustitución de comandos: ejecuta y reemplaza por su salida |
basename |
Quita la ruta: caos/foto.JPG pasa a foto.JPG |
Puntos que sorprenden:
- Las comillas dobles alrededor de
"$ruta"son obligatorias. Sin ellas, un archivo llamadomi foto.pngse partiría en dos palabras. Se comprueba contouch "caos/mi foto.png"y quitando las comillas. - El
*no incluye archivos ocultos (los que empiezan con.), lo cual aquí conviene. - Si el directorio está vacío,
*queda literal y-ffalla. Elcontinuetambién protege ese caso.
git commit -am "v2: recorrer archivos"
Versión 3: obtener la extensión
Conceptos: expansión de parámetros, [[ ]], minúsculas.
Dentro del bucle, después de nombre=...:
if [[ "$nombre" == *.* ]]; then
extension="${nombre##*.}"
extension="${extension,,}"
else
extension=""
fi
echo "$nombre -> extensión: '$extension'"
| Pieza | Qué significa |
|---|---|
[[ ... ]] |
Versión mejorada de [ ], exclusiva de bash. Permite patrones con *. Por esto el shebang no es sh |
*.* |
Patrón: “contiene al menos un punto” |
${nombre##*.} |
Borra el prefijo más largo que termine en . y deja solo la extensión |
${extension,,} |
Convierte a minúsculas, para que JPG y jpg se traten igual |
Para entender ##, conviene probarlo en la terminal:
f="backup.tar.gz"
echo ${f##*.} # gz (## quita lo más largo)
echo ${f#*.} # tar.gz (# quita lo más corto)
echo ${f%.*} # backup.tar (% quita desde el final)
Caso borde: README no tiene punto, así que queda con extensión vacía. Por eso existe el else.
git commit -am "v3: extraer extensión"
Versión 4: decidir la categoría
Conceptos: funciones, case, echo como “valor de retorno”.
Antes del bucle (las funciones se definen antes de usarse):
categoria() {
case "$1" in
jpg|jpeg|png|gif|bmp|svg|webp) echo "Imagenes" ;;
pdf|doc|docx|txt|odt|md|xlsx|csv|pptx) echo "Documentos" ;;
mp3|wav|flac|ogg) echo "Audio" ;;
mp4|mkv|avi|mov) echo "Videos" ;;
zip|tar|gz|rar|7z) echo "Comprimidos" ;;
sh|py|js|c|html|css|java) echo "Codigo" ;;
*) echo "Otros" ;;
esac
}
En el bucle, el echo de depuración se reemplaza por:
carpeta=$(categoria "$extension")
echo "$nombre -> $carpeta"
| Pieza | Qué significa |
|---|---|
categoria() { ... } |
Define una función. Se llama como un comando |
$1 dentro de la función |
Es el argumento de la función, no del script |
case ... esac |
Un if/elif múltiple para comparar contra patrones |
a|b|c) |
El | significa “o” |
;; |
Fin de cada caso |
*) |
Caso por defecto, lo que no coincidió |
$(categoria ...) |
Captura lo que la función imprime con echo |
En bash las funciones no “retornan” textos: imprimen, y quien las llama captura la salida. El return solo devuelve códigos numéricos de éxito o error.
Esta versión todavía no mueve nada. Es buena práctica ver qué haría el script antes de dejar que modifique archivos.
git commit -am "v4: clasificación por categoría"
Versión 5: mover los archivos
Conceptos: mkdir -p, mv, seguridad ante colisiones, aritmética.
Antes del bucle van los contadores:
movidos=0
omitidos=0
Y el echo de depuración se reemplaza por el bloque final:
carpeta=$(categoria "$extension")
destino="$directorio/$carpeta"
mkdir -p "$destino"
if [ -e "$destino/$nombre" ]; then
echo " OMITIDO: $nombre (ya existe en $carpeta/)"
omitidos=$((omitidos + 1))
continue
fi
mv "$ruta" "$destino/"
echo " $nombre -> $carpeta/"
movidos=$((movidos + 1))
done
echo "Listo: $movidos movidos, $omitidos omitidos."
| Pieza | Qué significa |
|---|---|
mkdir -p |
Crea la carpeta si no existe, sin error si ya existe |
mv origen destino/ |
Mueve. La / final indica “esto es una carpeta” |
-e |
Verdadero si la ruta existe |
$((a + 1)) |
Aritmética entera. Sin $(( )), bash trataría + como texto |
mvsobrescribe en silencio. Sin la verificación con-e, mover un segundoinforme.pdfdestruiría el primero sin avisar.
touch caos/informe.pdf # un duplicado
./organizar.sh caos # OMITIDO: informe.pdf
La lección de fondo: un script que modifica archivos debe pensar primero en qué puede salir mal.
git commit -am "v5: mover archivos con protección"
Script final
#!/usr/bin/env bash
# organizar.sh - Ordena los archivos de un directorio en carpetas por tipo
# Uso: ./organizar.sh <directorio>
if [ $# -ne 1 ]; then
echo "Uso: $0 <directorio>" >&2
exit 1
fi
directorio="$1"
if [ ! -d "$directorio" ]; then
echo "Error: '$directorio' no es un directorio válido" >&2
exit 1
fi
categoria() {
case "$1" in
jpg|jpeg|png|gif|bmp|svg|webp) echo "Imagenes" ;;
pdf|doc|docx|txt|odt|md|xlsx|csv|pptx) echo "Documentos" ;;
mp3|wav|flac|ogg) echo "Audio" ;;
mp4|mkv|avi|mov) echo "Videos" ;;
zip|tar|gz|rar|7z) echo "Comprimidos" ;;
sh|py|js|c|html|css|java) echo "Codigo" ;;
*) echo "Otros" ;;
esac
}
movidos=0
omitidos=0
for ruta in "$directorio"/*; do
[ -f "$ruta" ] || continue
nombre=$(basename "$ruta")
if [[ "$nombre" == *.* ]]; then
extension="${nombre##*.}"
extension="${extension,,}"
else
extension=""
fi
carpeta=$(categoria "$extension")
destino="$directorio/$carpeta"
mkdir -p "$destino"
if [ -e "$destino/$nombre" ]; then
echo " OMITIDO: $nombre (ya existe en $carpeta/)"
omitidos=$((omitidos + 1))
continue
fi
mv "$ruta" "$destino/"
echo " $nombre -> $carpeta/"
movidos=$((movidos + 1))
done
echo "Listo: $movidos movidos, $omitidos omitidos."
Resultado esperado sobre la carpeta caos:
caos/
├── Audio/ cancion.mp3
├── Codigo/ script.py
├── Comprimidos/ backup.tar.gz
├── Documentos/ informe.pdf, notas.txt
├── Imagenes/ foto.JPG
├── Otros/ README
├── Videos/ video.mp4
└── subcarpeta/ (no se toca)
Depurar y verificar
- Probar siempre sobre una copia o sobre
caos/, nunca sobre la carpeta personal en la primera ejecución. bash -x ./organizar.sh caosimprime cada línea que se ejecuta con sus variables ya expandidas. Es la mejor herramienta para entender qué hace un script.- Para verificar el resultado:
tree caosofind caos -type f | sort.
Retos para profundizar
- Modo simulación: con
--dry-runsolo muestra qué haría, sin mover nada. Obliga a procesar opciones y a separar decidir de actuar. - Renombrar duplicados en lugar de omitirlos (
informe_1.pdf,informe_2.pdf). Requiere un buclewhile. - Resumen final con la cantidad de archivos por categoría.
- Deshacer: que el script guarde con
>>un registro de cada movimiento y otro script lo revierta. - Evitar que el script se mueva a sí mismo si se ejecuta dentro de su propia carpeta. Pista:
[ "$ruta" -ef "$0" ].