Alta masiva de usuarios a partir de un CSV
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
- La primera línea es
GRUPOS grupo1,grupo2,…(si no, escribeLa primera línea debe ser GRUPOS grupo1,grupo2…y termina). La segunda esEXISTEN 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 EXISTENy termina). - 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). - 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.
- 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) ychage -d 0 login(para obligar a cambiar la contraseña en el primer inicio de sesión). - 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.
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.
Ejemplo
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
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)
Solución explicada
Ver la solución completa
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
--ejecutarque, además de escribirlas, ejecute las órdenes (y compruebe conidque cada usuario se ha creado). - Genera una contraseña aleatoria para cada alta con
openssl rand -base64 12y guarda el listado en un fichero con permisos 600. - Lee las cuentas existentes del sistema real con
getent passwd | cut -d: -f1en lugar de la línea EXISTEN.