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 configure
scripts generados por GNU Autoconf, son atípicos por muchas razones:
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 expr
o bc
hacer 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 ksh93
todos 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.
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 source
en 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 #include
declaraciones. Tal dialecto C-lite no necesitaría palabras clave como extern
o static
. Esas características existen para permitir la modularidad.
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
, ksh93
y zsh
al 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.
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.
Los scripts de Shell tienen poca capacidad integrada para aumentar su rendimiento a través de la ejecución paralela.
Los shells Bourne tienen &
, wait
y 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 -P
y 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 configure
volver 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.
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 configure
salidas de script: para que sus configure
scripts 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 echo
secuencias 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 od
y 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 PATH
no 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 echo
y set -x
, que juntos son suficientes para depurar una gran cantidad de problemas.
sh
secuencia de comandosconfigure
que se utiliza como parte del proceso de compilación para una gran cantidad de paquetes un * x no es "relativamente simple".