Apuntes DAM
Volver al inicio

Alta masiva de usuarios a partir de un CSV

Ejercicio de BashDifícilUnos 70 minutos

Prepara el alta de los alumnos y profesores de un curso a partir de un CSV: genera cada login sin tildes ni eñes, resuelve las coincidencias con un número, valida los grupos y escribe las órdenes useradd y chage en modo simulación. Arrays, expansión de parámetros y funciones.

  • while IFS=';' read
  • Arrays indexados y asociativos
  • Expansión ${var//patrón/}
  • ${var,,} y ${var:0:1}
  • Funciones con local
  • read -a y <<<

Enunciado

Al empezar el curso, el administrador del instituto tiene que crear las cuentas de los alumnos nuevos en el servidor Linux. Secretaría le pasa un CSV con nombre, apellidos y grupo, y hacerlo a mano con useradd uno por uno no es una opción.

El login sigue la norma del centro: la inicial del nombre, el primer apellido y la inicial del segundo, en minúsculas y sin tildes ni eñes (Lucía Martín Ruiz → lmartinr). Si ya existe, se le añade un número (lmartinr2, lmartinr3…). Hay que tener en cuenta tanto las cuentas que ya hay en el sistema como las que se van creando en el propio fichero.

Un script que crea usuarios es peligroso de probar, así que primero se ejecuta en modo simulación: en lugar de crear nada, escribe las órdenes que ejecutaría. Así se pueden revisar antes de lanzarlas de verdad.

Qué tiene que hacer el programa

  1. La primera línea es GRUPOS grupo1,grupo2,… (si no, escribe La primera línea debe ser GRUPOS grupo1,grupo2… y termina). La segunda es EXISTEN cuenta1,cuenta2,… con las cuentas que ya hay en el sistema, que puede estar vacía tras la palabra (si no es esa línea, Falta la línea EXISTEN y termina).
  2. Cada línea siguiente es nombre;apellido1;apellido2;grupo (el segundo apellido puede estar vacío). Las líneas vacías se ignoran. Si falta el nombre, el primer apellido o el grupo, o hay más campos, escribe # Línea N: faltan o sobran campos; si el grupo no está en la lista, # Línea N: grupo desconocido G (N es el número de línea de la entrada).
  3. Login: inicial del nombre + primer apellido + inicial del segundo apellido, sin tildes (á é í ó ú ü), con la ñ como n, en minúsculas y quitando todo lo que no sea una letra de la a a la z. Si coincide con una cuenta existente o ya creada, se prueba añadiendo 2, 3… hasta encontrar uno libre.
  4. Por cada alta escribe useradd -m -s /bin/bash -g grupo -c "Nombre Apellido1 Apellido2" login (el comentario con el nombre completo tal como viene, sin segundo apellido si está vacío) y chage -d 0 login (para obligar a cambiar la contraseña en el primer inicio de sesión).
  5. Al final, # Altas: N (grupo1 n1, grupo2 n2, …) con todos los grupos en el orden de la línea GRUPOS.

Entrada

Línea 1: GRUPOS lista. Línea 2: EXISTEN lista. Resto: nombre;apellido1;apellido2;grupo.

Ejemplos de ejecución

Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.

Alumnos y profesores nuevos

Entrada

GRUPOS alumnos,profesores,administracion
EXISTEN root,ana,lmartinr
Lucía;Martín;Ruiz;alumnos
Luis;Martín;Rodríguez;alumnos
Íñigo;Núñez;Ibáñez;profesores
María José;O'Donnell;;alumnos
Pedro;Gil;López;conserjes
Sofía;Ruiz

Salida por consola

useradd -m -s /bin/bash -g alumnos -c "Lucía Martín Ruiz" lmartinr2
chage -d 0 lmartinr2
useradd -m -s /bin/bash -g alumnos -c "Luis Martín Rodríguez" lmartinr3
chage -d 0 lmartinr3
useradd -m -s /bin/bash -g profesores -c "Íñigo Núñez Ibáñez" inunezi
chage -d 0 inunezi
useradd -m -s /bin/bash -g alumnos -c "María José O'Donnell" modonnell
chage -d 0 modonnell
# Línea 7: grupo desconocido conserjes
# Línea 8: faltan o sobran campos
# Altas: 4 (alumnos 3, profesores 1, administracion 0)

Logins repetidos dentro del fichero

Entrada

GRUPOS aula1
EXISTEN
Ana;García;Pérez;aula1
Antonio;García;Pons;aula1
Alba;García;Puig;aula1

Ana;García;Pérez;aula1

Salida por consola

useradd -m -s /bin/bash -g aula1 -c "Ana García Pérez" agarciap
chage -d 0 agarciap
useradd -m -s /bin/bash -g aula1 -c "Antonio García Pons" agarciap2
chage -d 0 agarciap2
useradd -m -s /bin/bash -g aula1 -c "Alba García Puig" agarciap3
chage -d 0 agarciap3
useradd -m -s /bin/bash -g aula1 -c "Ana García Pérez" agarciap4
chage -d 0 agarciap4
# Altas: 4 (aula1 4)

Guía paso a paso

Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.

1. Partir líneas con IFS

IFS=';' read -r a b c d resto reparte los campos de una línea separada por punto y coma. La variable resto recoge lo que sobre: si no está vacía, la línea tenía campos de más. Para una lista separada por comas, IFS=',' read -r -a array <<< "$lista" la convierte en un array.

2. Quitar tildes sin depender del idioma del sistema

tr no maneja bien los caracteres de varios bytes de UTF-8. Con dos arrays paralelos y la sustitución ${s//patrón/reemplazo} se cambia cada letra con tilde por la suya, y después ${s,,} pasa a minúsculas.

bash
local con=(á é í ó ú ü ñ Á É Í Ó Ú Ü Ñ) sin=(a e i o u u n a e i o u u n)
for i in "${!con[@]}"; do
  s=${s//${con[$i]}/${sin[$i]}}
done
s=${s,,}
echo "${s//[^a-z]/}"
3. Subcadenas

${s:0:1} es el primer carácter… o el primer byte: con LC_ALL=C, la Í de Íñigo ocupa dos bytes y ${s:0:1} la partiría por la mitad. Normaliza primero el nombre y los apellidos y toma después las iniciales. Con ${ape2:+ $ape2} añades el segundo apellido con un espacio delante solo si no está vacío.

4. Logins únicos

Guarda todas las cuentas ocupadas en un array asociativo (declare -A existe). Mientras el login candidato esté ocupado, prueba con el número siguiente; cuando encuentres uno libre, márcalo como ocupado antes de seguir.

5. Contar por grupo y escribir el resumen

Otro array asociativo cuenta las altas de cada grupo. Para el resumen, recorre el array de grupos en orden y concatena con resumen+="${resumen:+, }$g n": la coma solo aparece si ya había algo.

Resuélvelo aquí

El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.

🖥️BashAlta masiva de usuarios a partir de un CSVDifícil

Ejemplo

Entrada (lo que se escribe por teclado)
GRUPOS alumnos,profesores,administracion
EXISTEN root,ana,lmartinr
Lucía;Martín;Ruiz;alumnos
Luis;Martín;Rodríguez;alumnos
Íñigo;Núñez;Ibáñez;profesores
María José;O'Donnell;;alumnos
Pedro;Gil;López;conserjes
Sofía;Ruiz
Salida esperada
useradd -m -s /bin/bash -g alumnos -c "Lucía Martín Ruiz" lmartinr2
chage -d 0 lmartinr2
useradd -m -s /bin/bash -g alumnos -c "Luis Martín Rodríguez" lmartinr3
chage -d 0 lmartinr3
useradd -m -s /bin/bash -g profesores -c "Íñigo Núñez Ibáñez" inunezi
chage -d 0 inunezi
useradd -m -s /bin/bash -g alumnos -c "María José O'Donnell" modonnell
chage -d 0 modonnell
# Línea 7: grupo desconocido conserjes
# Línea 8: faltan o sobran campos
# Altas: 4 (alumnos 3, profesores 1, administracion 0)
⏳
Test oculto #3
⏳
Test oculto #4
⏳
Test oculto #5
0/5 tests pasados · pulsa un test para ver su entrada y su salida esperada

Solución explicada

Ver la solución completa
bash
1#!/bin/bash
2# Genera las órdenes para dar de alta usuarios a partir de un CSV, sin ejecutarlas (modo simulación)
3export LC_ALL=C
4
5# «Lucía Martín Ruiz» → «lmartinr»: sin tildes ni eñes, en minúsculas y solo letras
6normalizar() {
7  local s=$1 i
8  local con=(á é í ó ú ü ñ Á É Í Ó Ú Ü Ñ) sin=(a e i o u u n a e i o u u n)
9  for i in "${!con[@]}"; do
10    s=${s//${con[$i]}/${sin[$i]}}
11  done
12  s=${s,,}
13  echo "${s//[^a-z]/}"
14}
15
16read -r palabra lista_grupos
17if [ "$palabra" != "GRUPOS" ] || [ -z "$lista_grupos" ]; then
18  echo "La primera línea debe ser GRUPOS grupo1,grupo2…"
19  exit 0
20fi
21IFS=',' read -r -a grupos <<< "$lista_grupos"
22
23declare -A existe por_grupo
24read -r palabra lista_existen
25if [ "$palabra" = "EXISTEN" ]; then
26  IFS=',' read -r -a cuentas <<< "$lista_existen"
27  for c in "${cuentas[@]}"; do existe[$c]=1; done
28  numero=2
29else
30  echo "Falta la línea EXISTEN"
31  exit 0
32fi
33
34altas=0
35while IFS=';' read -r nombre ape1 ape2 grupo resto; do
36  numero=$((numero + 1))
37  [ -z "$nombre$ape1$ape2$grupo" ] && continue
38  if [ -z "$nombre" ] || [ -z "$ape1" ] || [ -z "$grupo" ] || [ -n "$resto" ]; then
39    echo "# Línea $numero: faltan o sobran campos"
40    continue
41  fi
42  valido=0
43  for g in "${grupos[@]}"; do [ "$g" = "$grupo" ] && valido=1; done
44  if [ $valido -eq 0 ]; then
45    echo "# Línea $numero: grupo desconocido $grupo"
46    continue
47  fi
48
49  # Primero se normaliza y después se toma la inicial: con LC_ALL=C, ${s:0:1} es un byte, y la Í ocupa dos
50  n=$(normalizar "$nombre")
51  a1=$(normalizar "$ape1")
52  a2=$(normalizar "$ape2")
53  base="${n:0:1}$a1${a2:0:1}"
54  login=$base
55  n=2
56  # Si el login ya existe (en el sistema o en este mismo fichero), se le añade un número
57  while [ -n "${existe[$login]}" ]; do
58    login="$base$n"
59    n=$((n + 1))
60  done
61  existe[$login]=1
62  completo="$nombre $ape1${ape2:+ $ape2}"
63  echo "useradd -m -s /bin/bash -g $grupo -c \"$completo\" $login"
64  echo "chage -d 0 $login"
65  por_grupo[$grupo]=$(( ${por_grupo[$grupo]:-0} + 1 ))
66  altas=$((altas + 1))
67done
68
69resumen=""
70for g in "${grupos[@]}"; do
71  resumen+="${resumen:+, }$g ${por_grupo[$g]:-0}"
72done
73echo "# Altas: $altas ($resumen)"

El script lee la configuración (grupos y cuentas existentes) y después recorre los usuarios con un único bucle while read, validando cada línea antes de generar nada. Los errores se escriben como comentarios (#), así la salida completa se puede guardar en un fichero y ejecutarse tal cual cuando esté revisada.

La normalización se hace con expansiones de parámetros de Bash, que trabajan con las cadenas de bytes exactas de cada letra: funciona igual con cualquier configuración de idioma, cosa que no garantizan tr ni iconv con TRANSLIT.

El array asociativo existe es el conjunto de logins ocupados: incluye los del sistema y los que el propio script va asignando. Sin él, dos alumnos con el mismo login en el fichero tendrían la misma cuenta.

Modo simulación primero, ejecución después: es una práctica de administración responsable para cualquier script que cree, borre o modifique cuentas. chage -d 0 obliga a cambiar la contraseña inicial, que se asignaría aparte (por ejemplo, con chpasswd).

Para ir más allá

  • Añade un parámetro --ejecutar que, además de escribirlas, ejecute las órdenes (y compruebe con id que cada usuario se ha creado).
  • Genera una contraseña aleatoria para cada alta con openssl rand -base64 12 y guarda el listado en un fichero con permisos 600.
  • Lee las cuentas existentes del sistema real con getent passwd | cut -d: -f1 en lugar de la línea EXISTEN.

Dónde se explica