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?
fuente
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".m4
macros.configure
Los 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 ...Respuestas:
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.
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
obc
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
, yksh93
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 usarksh93
.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 conocidosource
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 comoextern
ostatic
. 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
yzsh
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.
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 susconfigure
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á.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.
"Pure" Bash, sin ninguna llamada a las cosas en el
PATH
? El compresor probablemente sea factible usandoecho
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 llamandood
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 elPATH
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.
Aquí está Tetris con cáscara . Otros juegos de este tipo están disponibles, si vas a buscar.
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
yset -x
, que juntos son suficientes para depurar una gran cantidad de problemas.fuente
&
puede ejecutar procesos en paralelo. Puedewait
para 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.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.
fuente
Algunas razones para no usar scripts de shell para programas grandes, justo fuera de mi cabeza:
mkdir
ogrep
internamente.zsh
tiene algo de soporte. Esto también se debe a que la interfaz para programas externos se basa principalmente en texto y\0
se utiliza como separador.bash -c ...
ossh -c ...
)fuente