¿Qué tan complejo se puede escribir un programa en puro Bash? [cerrado]

17

Después de una investigación muy rápida, parece que Bash es un lenguaje completo de Turing .

Me pregunto, ¿por qué Bash se usa casi exclusivamente para escribir scripts relativamente simples? Dado que un shell Bash viene con Linux, puede ejecutar scripts de shell sin ningún intérprete o compilador externo, como se requiere para otros lenguajes informáticos populares. Esta es una gran ventaja, que podría compensar la mediocridad del lenguaje en algunos casos.

Entonces, ¿hay un límite en la complejidad de estos programas? ¿Se utiliza puro Bash para escribir programas complejos? ¿Es posible escribir, digamos, un archivo compresor / descompresor en puro Bash? Un compilador? ¿Un simple videojuego?

¿Se usa tan escasamente solo porque solo hay herramientas de depuración muy limitadas?

Bregalad
fuente
2
La shsecuencia de comandos configureque se utiliza como parte del proceso de compilación para una gran cantidad de paquetes un * x no es "relativamente simple".
user4556274
@ user4556274 No lo es, pero generalmente tampoco está escrito a mano, sino a partir de un amplio conjunto de m4macros.
Kusalananda
2
Hay un ensamblador x86 en Bash, así que sí, Bash se usa ocasionalmente para escribir programas complejos. ¿Por qué la gente no hace eso más a menudo? Posiblemente porque el intérprete también es lento, malo y propenso a errores "interesantes" (ver fi Shellshock ). Además, los scripts de Bash tienden a ser exponencialmente más difíciles de mantener con el tamaño. Mira el ensamblador de arriba; ¿puede decir desde la fuente si sigue la sintaxis de AT&T o Intel?
Satō Katsura
configureLos guiones también son lentos, hacen un montón de trabajo inútil y han sido objeto de algunas críticas divertidas. Por supuesto, el shell se puede usar para programas grandes, pero, de nuevo, la gente también ha hecho computadoras con Game of Life y Minecraft de Conway , y también hay lenguajes de programación como Brainf ** k y Hexagony . Aparentemente, a algunas personas les gusta construir algo a partir de átomos realmente pequeños y confusos. Incluso puedes vender juegos con esa idea ...
ilkkachu
Entonces, ¿es esta pregunta responsable o no? Lo ponen en espera y dicen que no tiene respuesta, pero aún así obtengo algunas respuestas excelentes. Sería bueno ser coherente, ya que soy nuevo en este SE, para poder dirigirme a qué tipo de preguntas son y no deseables en este SE.
Bregalad

Respuestas:

30

parece que Bash es un lenguaje completo de Turing

El concepto de integridad de Turing está completamente separado de muchos otros conceptos útiles en un lenguaje para la programación en general : usabilidad, expresividad, comprensibilidad, velocidad, etc.

Si Turing-completo eran todo lo que necesitábamos, no tendríamos ningún lenguaje de programación en absoluto , ni siquiera lenguaje ensamblador . Los programadores informáticos simplemente escribirían el código de la máquina , ya que nuestras CPU también están completas en Turing.

¿Por qué se usa Bash casi exclusivamente para escribir scripts relativamente simples?

Los scripts de shell grandes y complejos, como los configurescripts generados por GNU Autoconf, son atípicos por muchas razones:

  1. Hasta hace relativamente poco, no podía contar con un shell compatible con POSIX en todas partes .

    Muchos sistemas, particularmente los más antiguos, técnicamente tienen un shell compatible con POSIX en algún lugar del sistema, pero puede que no esté en una ubicación predecible como /bin/sh. Si está escribiendo un script de shell y tiene que ejecutarse en muchos sistemas diferentes, ¿cómo se escribe la línea shebang ? Una opción es seguir adelante y usar /bin/sh, pero elija restringirse al dialecto de shell Bourne anterior a POSIX en caso de que se ejecute en dicho sistema.

    Los proyectiles Bourne anteriores a POSIX ni siquiera tienen aritmética incorporada; tienes que llamar expro bchacer eso.

    Incluso con un shell POSIX, se está perdiendo matrices asociativas y otras características que esperamos encontrar en los lenguajes de script Unix desde que Perl se hizo popular a principios de la década de 1990 .

    Ese hecho histórico significa que existe una tradición de décadas de ignorar muchas de las poderosas características de los modernos intérpretes de script de la familia Bourne puramente porque no se puede contar con tenerlos en todas partes.

    De hecho, esto todavía continúa hasta el día de hoy: Bash no obtuvo matrices asociativas hasta la versión 4 , pero es posible que se sorprenda de cuántos sistemas aún en uso se basan en Bash 3. Apple todavía envía Bash 3 con macOS en 2017, aparentemente para razones de licenciamiento , y los servidores Unix / Linux a menudo se ejecutan prácticamente sin producción durante mucho tiempo, por lo que es posible que tenga un sistema antiguo estable que aún ejecute Bash 3, como una caja CentOS 5. Si tiene tales sistemas en su entorno, no puede usar matrices asociativas en scripts de shell que tienen que ejecutarse en ellos.

    Si su respuesta a ese problema es que solo escribe scripts de shell para sistemas "modernos", entonces debe hacer frente al hecho de que el último punto de referencia común para la mayoría de los shells de Unix es el estándar de shell POSIX , que en gran medida no ha cambiado desde que fue introducido en 1989. Hay muchos shells diferentes basados ​​en ese estándar, pero todos han divergido en diferentes grados de ese estándar. Para tomar las matrices asociativas, de nuevo, bash, zsh, y ksh93todos tienen esa característica, pero hay múltiples incompatibilidades de implementación. Su elección, entonces, es usar solo Bash, o solo usar Zsh, o solo usar ksh93.

    Si su respuesta a ese problema es "así que simplemente instale Bash 4" o ksh93, o lo que sea, entonces ¿por qué no "simplemente" instalar Perl o Python o Ruby en su lugar? Eso es inaceptable en muchos casos; los valores predeterminados importan.

  2. Ninguno de los lenguajes de scripting shell de la familia Bourne admite módulos .

    Lo más cercano a un sistema de módulos en un script de shell es el .comando, también conocido sourceen las variantes de shell Bourne más modernas, que falla en varios niveles en relación con un sistema de módulo adecuado, el más básico de los cuales es el espacio de nombres .

    Independientemente del lenguaje de programación, la comprensión humana comienza a marcarse cuando cualquier archivo individual en un programa general más grande excede unos pocos miles de líneas. La razón por la que estructuramos programas grandes en muchos archivos es para que podamos abstraer su contenido a una oración o dos como máximo. El archivo A es el analizador de línea de comandos, el archivo B es la bomba de E / S de red, el archivo C es el calce entre la biblioteca Z y el resto del programa, etc. Cuando su único método para ensamblar muchos archivos en un solo programa es la inclusión textual , usted pone un límite a qué tan grandes pueden crecer razonablemente sus programas.

    A modo de comparación, sería como si el lenguaje de programación C no tuviera un vinculador, solo #includedeclaraciones. Tal dialecto C-lite no necesitaría palabras clave como externo static. Esas características existen para permitir la modularidad.

  3. POSIX no define una forma de abarcar variables a una sola función de script de shell, mucho menos a un archivo.

    Esto efectivamente hace que todas las variables sean globales , lo que nuevamente perjudica la modularidad y la capacidad de componer.

    Hay soluciones para esto en los shells posteriores a POSIX, ciertamente en bash, ksh93y zshal menos, pero eso solo lo lleva de vuelta al punto 1 anterior.

    Puede ver el efecto de esto en las guías de estilo en la escritura de macros de Autoconf de GNU, donde recomiendan prefijar nombres de variables con el nombre de la macro en sí, lo que lleva a nombres de variables muy largos con el único fin de reducir la posibilidad de colisión a un nivel aceptablemente cercano cero.

    Incluso C es mejor en este puntaje, por una milla. La mayoría de los programas de C no solo se escriben principalmente con variables locales de función, sino que también admite el alcance de bloques, permitiendo que múltiples bloques dentro de una sola función reutilicen nombres de variables sin contaminación cruzada.

  4. Los lenguajes de programación de Shell no tienen una biblioteca estándar.

    Es posible argumentar que la biblioteca estándar de un lenguaje de scripting de shell es el contenido de PATH, pero eso solo dice que para hacer algo de consecuencia, un script de shell debe llamar a otro programa completo, probablemente uno escrito en un lenguaje más poderoso para empezar con.

    Tampoco existe un archivo ampliamente utilizado de bibliotecas de utilidades de shell como con el CPAN de Perl . Sin una gran biblioteca disponible de código de utilidad de terceros, un programador debe escribir más código a mano, para que sea menos productivo.

    Incluso ignorando el hecho de que la mayoría de los scripts de shell dependen de programas externos típicamente escritos en C para hacer algo útil, existe la sobrecarga de todas esas cadenas de llamadas pipe()fork()exec(). Ese patrón es bastante eficiente en Unix, en comparación con IPC y el lanzamiento de procesos en otros sistemas operativos, pero aquí reemplaza efectivamente lo que haría con una llamada de subrutina en otro lenguaje de secuencias de comandos, que aún es mucho más eficiente. Eso pone un límite serio al límite superior de la velocidad de ejecución del script de shell.

  5. Los scripts de Shell tienen poca capacidad integrada para aumentar su rendimiento a través de la ejecución paralela.

    Los shells Bourne tienen &, waity las tuberías para esto, pero eso es en gran medida útil para componer múltiples programas, no para lograr el paralelismo de CPU o E / S. No es probable que pueda vincular los núcleos o saturar una matriz RAID únicamente con scripts de shell, y si lo hace, probablemente podría lograr un rendimiento mucho mayor en otros idiomas.

    Las tuberías en particular son formas débiles de aumentar el rendimiento a través de la ejecución paralela. Solo permite que dos programas se ejecuten en paralelo, y uno de los dos probablemente estará bloqueado en E / S hacia o desde el otro en cualquier momento dado.

    Hay formas de evitar esto en los últimos días, como xargs -Py GNUparallel , pero esto simplemente se traslada al punto 4 anterior.

    Sin una capacidad incorporada para aprovechar al máximo los sistemas multiprocesador, los scripts de shell siempre serán más lentos que un programa bien escrito en un lenguaje que pueda usar todos los procesadores del sistema. Para configurevolver a tomar el ejemplo del script GNU Autoconf , duplicar el número de núcleos en el sistema hará poco para mejorar la velocidad a la que se ejecuta.

  6. Los lenguajes de scripting de shell no tienen punteros o referencias .

    Esto le impide hacer muchas cosas fácilmente en otros lenguajes de programación.

    Por un lado, la incapacidad de referirse indirectamente a otra estructura de datos en la memoria del programa significa que está limitado a las estructuras de datos integradas . Su shell puede tener matrices asociativas , pero ¿cómo se implementan? Hay varias posibilidades, cada una con diferentes compensaciones: los árboles rojo-negros , los árboles AVL y las tablas hash son los más comunes, pero hay otros. Si necesita un conjunto diferente de compensaciones, está atascado, porque sin referencias, no tiene una forma de transferir manualmente muchos tipos de estructuras de datos avanzadas. Estás atrapado con lo que te dieron.

    O bien, puede ser que necesite una estructura de datos que ni siquiera tenga una alternativa adecuada integrada en su intérprete de script de shell, como un gráfico acíclico dirigido , que podría necesitar para modelar un gráfico de dependencia . He estado programando durante décadas, y la única forma en que puedo pensar en hacer eso en un script de shell sería abusar del sistema de archivos , utilizando enlaces simbólicos como referencias falsas. Ese es el tipo de solución que obtienes cuando confías simplemente en la integridad de Turing, que no te dice nada acerca de si la solución es elegante, rápida o fácil de entender.

    Las estructuras de datos avanzadas son solo un uso para punteros y referencias. Hay montones de otras aplicaciones para ellos , que simplemente no se pueden hacer fácilmente en un lenguaje de scripting shell de la familia Bourne.

Podría seguir y seguir, pero creo que estás entendiendo el punto aquí. En pocas palabras, hay muchos lenguajes de programación más potentes para sistemas de tipo Unix.

Esta es una gran ventaja, que podría compensar la mediocridad del lenguaje en algunos casos.

Claro, y es precisamente por eso que GNU Autoconf utiliza un subconjunto restringido deliberadamente de la familia Bourne de lenguajes de script de shell para sus configuresalidas de script: para que sus configurescripts se ejecuten prácticamente en todas partes.

Probablemente no encontrará un grupo más grande de creyentes en la utilidad de escribir en un dialecto de shell Bourne altamente portátil que los desarrolladores de GNU Autoconf, sin embargo, su propia creación está escrita principalmente en Perl, más algunos m4, y solo un poco de shell guión; solo el resultado de Autoconf es un script de shell Bourne puro. Si eso no plantea la cuestión de cuán útil es el concepto "Bourne en todas partes", no sé qué lo hará.

Entonces, ¿hay un límite en la complejidad de estos programas?

Técnicamente hablando, no, como sugiere su observación de integridad de Turing.

Pero eso no es lo mismo que decir que los scripts de shell arbitrariamente grandes son agradables de escribir, fáciles de depurar o rápidos de ejecutar.

¿Es posible escribir, digamos, un archivo compresor / descompresor en puro bash?

"Pure" Bash, sin ninguna llamada a las cosas en el PATH? El compresor probablemente sea factible usando echosecuencias de escape hexagonales, pero sería bastante doloroso hacerlo. El descompresor puede ser imposible de escribir de esa manera debido a la incapacidad de manejar datos binarios en shell . Terminaría llamando ody traduciendo datos binarios a formato de texto, la forma nativa de manejo de datos de Shell.

Una vez que comience a hablar sobre el uso de scripts de shell de la manera prevista, como pegamento para conducir otros programas en el PATH, las puertas se abren, porque ahora está limitado solo a lo que se puede hacer en otros lenguajes de programación, es decir no tienen límites en absoluto. Un script de shell que obtiene todo su poder llamando a otros programas en el PATHno se ejecuta tan rápido como los programas monolíticos escritos en lenguajes más potentes, pero sí se ejecuta.

Y ese es el punto. Si necesita que un programa se ejecute rápido, o si necesita ser poderoso por derecho propio en lugar de tomar prestado el poder de otros, no lo escriba en shell.

¿Un simple videojuego?

Aquí está Tetris con cáscara . Otros juegos de este tipo están disponibles, si vas a buscar.

solo hay herramientas de depuración muy limitadas

Pondría el soporte de la herramienta de depuración en el puesto 20 en la lista de características necesarias para soportar la programación en general. Muchos programadores confían mucho más en la printf()depuración que los depuradores adecuados, independientemente del idioma.

En shell, tiene echoy set -x, que juntos son suficientes para depurar una gran cantidad de problemas.

Warren Young
fuente
2
"Los scripts de Shell tienen poca capacidad incorporada para realizar ejecuciones paralelas". En mi opinión, el shell tiene un mejor soporte para el procesamiento paralelo que la mayoría de los otros idiomas. Con un solo carácter &puede ejecutar procesos en paralelo. Puede waitpara que los procesos secundarios se completen. Puede configurar tuberías y redes de tuberías más complejas utilizando tuberías con nombre. Lo que es más importante, es simple hacer el procesamiento paralelo de la manera correcta, con muy poco código repetitivo y evitando los riesgos y dificultades del subprocesamiento múltiple de memoria compartida.
Sam Watkins
@SamWatkins: He actualizado el punto 5 anterior para responder a su respuesta. Si bien yo también soy fanático de la transmisión de mensajes entre procesos separados como una forma de evitar muchos de los problemas inherentes al paralelismo de memoria compartida, el punto que estaba haciendo aquí es sobre el aumento del rendimiento, no sobre la capacidad de compilación y demás, y eso a menudo requiere paralelismo de memoria compartida.
Warren Young
Los scripts de Shell son buenos para la creación de prototipos, pero eventualmente un proyecto debe pasar a un lenguaje de programación adecuado, luego idealmente a un lenguaje compilado. Luego, en casos extremos, el ensamblaje, como verías con el proyecto FFmpeg. Cmake es un buen ejemplo de lo que debería suceder con Autotools: está escrito en C y no requiere Perl, Texinfo o M4. Es un poco vergonzoso realmente que Autotools todavía dependa tanto de los scripts de shell después de 30 años wikipedia.org/wiki/GNU_Build_System#Criticism
Steven Penny
9

Podemos caminar o nadar en cualquier lugar, entonces, ¿por qué nos molestamos con bicicletas, automóviles, trenes, botes, aviones y otros vehículos? Claro, caminar o nadar puede ser agotador, pero hay una gran ventaja en no necesitar ningún equipo adicional.

Por un lado, aunque bash es Turing completo, no es bueno para manipular datos que no sean enteros (no demasiado grandes), cadenas, matrices (unidimensionales) de cadenas y mapas finitos de cadenas a cadenas. Cualquier otro tipo de datos necesita una codificación molesta, lo que dificulta la escritura del programa y, a menudo, impondría un rendimiento que no es lo suficientemente bueno en la práctica. Por ejemplo, las operaciones de punto flotante en bash son difíciles y lentas.

Además, bash tiene muy pocas formas de interactuar con su entorno. Puede ejecutar procesos, puede realizar algunos tipos simples de acceso a archivos (a través de la redirección), y eso es todo. Bash también tiene un cliente de red del lado del cliente. Bash puede emitir bytes nulos con bastante facilidad ( printf \\0) pero no puede analizar bytes nulos en su entrada, lo que lo hace inadecuado para leer datos binarios. Bash no puede hacer otras cosas directamente: tiene que llamar a programas externos para eso. Y eso está bien: ¡los shells están diseñados para el propósito principal de ejecutar programas externos! Los shells son el lenguaje adhesivo para combinar programas juntos. Pero si está ejecutando un programa externo, eso significa que ese programa tiene que estar disponible, y luego reduce la ventaja de portabilidad:)

Bash no tiene ningún tipo de característica que facilite la escritura de programas robustos, aparte de set -e. No tiene tipos (útiles), espacios de nombres, módulos o estructuras de datos anidados. Los errores son la dificultad número uno en la programación; Si bien la facilidad de escribir programas libres de errores no siempre es el factor decisivo para elegir un idioma, bash se encuentra mal en ese aspecto. Bash también se clasifica mal en rendimiento cuando se hacen otras cosas además de combinar programas juntos.

Durante mucho tiempo, bash no se ejecutó en Windows, e incluso hoy no está presente en una instalación predeterminada de Windows, y no se ejecuta de forma nativa (incluso en WSL) en el sentido de que no tiene interfaces para Características nativas de Windows. Bash no se ejecuta en iOS y no está instalado de forma predeterminada en Android. Entonces, a menos que esté escribiendo una aplicación solo para Unix, bash no es portátil en absoluto.

Requerir un compilador no es un problema para la portabilidad. El compilador se ejecuta en la máquina de los desarrolladores. Requerir un intérprete o bibliotecas de terceros puede ser un problema, pero en Linux es un problema resuelto a través de paquetes de distribución, y en Windows, Android e iOS, las personas generalmente agrupan componentes de terceros en su paquete de aplicación. Por lo tanto, el tipo de preocupaciones de portabilidad que tiene en mente no son preocupaciones prácticas para las aplicaciones habituales.

Mi respuesta se aplica a proyectiles que no sean bash. Algunos detalles varían de un shell a otro, pero la idea general es la misma.

Gilles 'SO- deja de ser malvado'
fuente
1
Creo que el mito de la portabilidad se ha hablado con bastante frecuencia, no estoy seguro de que usaría ese elemento en particular como negativo, ya que también se aplica a la mayoría de los otros idiomas, incluido Java. Incluso PHP que se ejecuta en un servidor de Windows frente a un servidor * nix tiene algunas pequeñas diferencias que siempre debe tener en cuenta, si es lo suficientemente tonto como para ejecutar algo en un servidor de Windows, es decir. Muchas cosas no se ejecutan en Android o iOS, así que tampoco estoy seguro de cómo podría ser un comentario válido.
Lizardx
7

Algunas razones para no usar scripts de shell para programas grandes, justo fuera de mi cabeza:

  • La mayoría de las funciones se realizan bifurcando comandos externos, lo cual es lento. En contraste, los lenguajes de programación como Perl pueden hacer el equivalente mkdiro grepinternamente.
  • No hay una manera fácil de acceder a las bibliotecas C, o hacer llamadas directas al sistema, lo que significa que, por ejemplo, el videojuego sería difícil de crear
  • Los lenguajes de programación adecuados tienen un mejor soporte para estructuras de datos complejas. Aunque Bash tiene matrices y matrices asociativas, no me gustaría pensar en una lista vinculada o un árbol.
  • El shell está hecho para procesar comandos que se hacen si es texto. Los datos binarios (es decir, las variables que contienen bytes NUL (bytes con valor cero)) son difíciles de manejar. Depende un poco del caparazón, zshtiene algo de soporte. Esto también se debe a que la interfaz para programas externos se basa principalmente en texto y \0se utiliza como separador.
  • También debido a los comandos externos, la separación entre el código y los datos es un poco difícil. Sea testigo de todos los problemas que existen al citar datos en otro shell (es decir, cuando se ejecuta bash -c ...o ssh -c ...)
ilkkachu
fuente
Este es el conjunto de negativos más preciso para mí, como alguien que hace muchos scripts de bash grandes, estos serían más o menos lo que enumeraría también como negativos. Sin embargo, una cosa que he encontrado es que Bash en realidad no es mucho más lento que otros lenguajes compilados al comparar funcionalidades similares. Tengo la sospecha de que si intentara escribir algunas de las cosas más complicadas que tengo en bash en python, la diferencia de velocidad no haría que el monstruoso trabajo involucrado valiera la pena. Sin embargo, Bash solo me pareció demasiado limitado, pero Bash + gawk funciona bien, gawk es casi real.
Lizardx