Portar una configuración de fish a nushell: dos errores que solo aparecen en una shell de verdad
Llevo años usando fish como shell diaria, con una configuración gestionada por chezmoi que cubre la preparación del PATH, integraciones de herramientas (starship, zoxide, atuin, mise), un par de docenas de abreviaturas y un puñado de funciones propias. Nushell estaba al lado como un esqueleto sin configurar, instalado pero nunca usado de verdad. Esta semana lo puse a la par.
El objetivo no es reemplazar fish. Recurro a cada shell para cosas distintas: fish sigue siendo mi shell interactiva por defecto, y nu sale cuando quiero pipelines de datos estructurados en lugar de manipular texto. Lo que quería era que lo cotidiano se sintiera idéntico independientemente de en cuál esté sentado, de modo que cambiar entre ellas nunca me cueste un segundo pensamiento. Mi prompt de starship muestra qué shell está activa, así que la única diferencia visible es una etiqueta, no un cambio en la memoria muscular.
La parte mecánica fue más o menos como cabría esperar: nu no tiene una convención de autocarga conf.d como fish, así que la configuración se reparte en un puñado de archivos incluidos explícitamente en lugar de un archivo por asunto. Algunas cosas se traducen directamente (alias/def para el abbr de fish). Algunas cosas no se traducen en absoluto y necesitaron un mecanismo completamente distinto, sobre todo cd: la versión de fish salta de forma difusa vía zoxide e imprime un listado después, y nu simplemente no tiene forma de sobrescribir cd y aun así llamar al builtin real una vez que lo has ensombrecido. La solución ahí fue dejar de intentar sobrescribir cd y enganchar $env.config.hooks.env_change.PWD en su lugar, que se dispara ante cualquier cambio de directorio (cd, z, cd -) en vez de una función sobrescrita concreta. Probablemente sea el mejor diseño de todos modos.
Nada de eso es la parte interesante, sin embargo. La parte interesante es lo que solo apareció una vez que dejé de probar con nu -c "..." y empecé a probar en una sesión interactiva de verdad.
Lección uno: nu -c no carga config.nu
Había estado verificando cada cambio con comandos como:
nu -c "print (which ll | length)"
Esto funciona bien cuando pasas los flags explícitos --config/--env-config, que es lo que había estado haciendo durante la construcción inicial. Pero un nu -c "..." a secas, sin esos flags, solo carga env.nu. No carga config.nu por defecto. env.nu establece variables de entorno ($env.EDITOR, entradas del PATH), así que esas comprobaciones seguían pasando. config.nu es donde viven los comandos propios, los alias y el prompt, así que cualquier cosa que dependiera de él quedó sin probar en silencio todo el tiempo que creí que lo estaba probando.
El modo de fallo cuando esto entró en producción fue exactamente lo que cabría suponer: ll no encontrado, prompt equivocado, ningún comando propio en absoluto. En cuanto cambié a nu -i (modo interactivo) para la verificación, ambos errores restantes que siguen aparecieron de inmediato.
Lección dos: nu resuelve las rutas de configuración por SO, no solo por la especificación XDG
Fish siempre lee ~/.config/fish, punto, en cualquier plataforma. Nushell no funciona así. En macOS, en ausencia de un XDG_CONFIG_HOME explícito, nu usa por defecto ~/Library/Application Support/nushell/ para su configuración, no ~/.config/nushell/. Mi configuración de chezmoi despliega todo en ~/.config/nushell (la ubicación convencional de XDG, coincidiendo con fish, coincidiendo con el resto de los dotfiles), así que nu estaba leyendo en silencio una configuración estándar completamente distinta, autogenerada, que llevaba ahí desde la primera vez que nu se ejecutó sin encontrar una de verdad.
Dos formas de arreglar esto: exportar XDG_CONFIG_HOME a nivel de sesión para que toda herramienta del sistema que entienda XDG siga la convención, o redirigir de forma acotada solo nu. Opté por el arreglo acotado, un enlace simbólico gestionado por chezmoi:
~/Library/Application Support/nushell -> ~/.config/nushell
solo para darwin, ya que ~/Library no existe en Linux y nu ya hace ahí lo convencional de XDG sin ayuda. Un efecto secundario agradable: el data-dir y el config-dir de nu resultaron ser la misma ruta en macOS, así que este único enlace simbólico también cubre el directorio vendor/autoload que nu usa para las completaciones cargadas automáticamente, no solo config.nu en sí.
Lección tres: las closures resuelven los nombres en tiempo de parseo, no en tiempo de llamada
Esta fue más sutil. config.nu asigna a $env.config.hooks.env_change.PWD una closure que llama a un comando propio, y por separado asigna al completador externo una closure que llama a un ayudante has-cmd. Ambos comandos vienen de archivos importados con use más abajo en el mismo script. Mi suposición era que una closure, al ser un valor asignado en tiempo de ejecución, resolvería los nombres dentro de su cuerpo cuando la closure realmente se ejecuta, es decir, ámbito dinámico. Eso es incorrecto para nu: las referencias a nombres de comando dentro de un literal de closure se resuelven en el punto en que la closure se parsea, usando lo que esté léxicamente en ámbito en ese punto del archivo. No importa que la closure no se invoque hasta mucho después, bien pasada la ejecución de las importaciones. Si la sentencia use viene después del literal de closure en el código fuente, la closure no puede verla.
El error es inequívoco en cuanto lo encuentras en una sesión de verdad:
Error: nu::shell::external_command
× External command failed
Command `on-directory-change` not found
help: A command with that name exists in module `commands`. Try importing it with `use`
El arreglo fue mecánico una vez diagnosticado: mover las sentencias use al principio del todo de config.nu, antes de cualquier cosa que pudiera referenciarlas en el cuerpo de una closure.
Conclusión
Cada una de estas tres lecciones se reduce a lo mismo: la semántica de carga de configuración de una shell no es algo que puedas verificar del todo ejecutando fragmentos con -c. Tienes que cargar la cosa de la forma en que lo haría un terminal de verdad. Había probado este port con bastante minuciosidad durante la construcción, y aun así entregué una configuración que falló en su primer uso real, dos veces, por razones que no tenían nada que ver con el trabajo de traducción de fish a nu en sí y todo que ver con suposiciones sobre cómo nu mismo carga y da ámbito a las cosas.