Implementar subconjunto de script de shell

12

Este sitio tenía muchos problemas relacionados con la implementación de varios idiomas en la etiqueta de . Sin embargo, prácticamente todos ellos eran lenguajes esotéricos que nadie usa. Es hora de hacer un intérprete para un lenguaje práctico que la mayoría de los usuarios aquí probablemente ya conozcan. Sí, es un script de shell, en caso de que tenga problemas para leer el título (no es que tenga). (Sí, hice este desafío intencionalmente, ya que estoy aburrido de que lenguajes como GolfScript y Befunge ganen todo, así que pongo un desafío donde un lenguaje de programación más práctico tiene mayores posibilidades de ganar)

Sin embargo, el script de shell es un lenguaje relativamente grande, por lo que no le pediré que lo implemente. En cambio, voy a hacer un pequeño subconjunto de funcionalidad de script de shell.

El subconjunto que decidí es el siguiente:

  • Ejecución de programas (sin embargo, los programas solo contendrán letras, incluso si se permiten comillas simples)
  • Argumentos del programa
  • Comillas simples (aceptando cualquier carácter ASCII imprimible, incluyendo espacios en blanco, excluyendo comillas simples)
  • Cadenas sin comillas (que permiten letras, números y guiones ASCII)
  • Tubería
  • Declaraciones vacías
  • Múltiples declaraciones separadas por una nueva línea.
  • Espacios finales / iniciales / múltiples

En esta tarea, debe leer la entrada de STDIN y ejecutar cada comando solicitado. Puede asumir de forma segura un sistema operativo compatible con POSIX, por lo que no hay necesidad de portabilidad con Windows, ni nada de eso. Puede asumir con seguridad que los programas que no se canalizan a otros programas no leerán de STDIN. Puede asumir con seguridad que los comandos existirán. Puede asumir con seguridad que no se usará nada más. Si se rompe alguna suposición segura, puede hacer cualquier cosa. Puede suponer con seguridad a lo sumo 15 argumentos y líneas por debajo de 512 caracteres (si necesita una asignación de memoria explícita, o algo así, realmente voy a dar pequeñas posibilidades de ganar para C, incluso si todavía son pequeños). No tiene que limpiar los descriptores de archivo.

Se le permite ejecutar programas en cualquier momento, incluso después de recibir la línea completa o después de que STDIN finalice. Elija cualquier enfoque que desee.

Caso de prueba simple que le permite probar su shell (observe el espacio en blanco de seguimiento después del tercer comando):

echo hello world
printf '%08X\n' 1234567890
'echo'   'Hello,   world!'  

echo heeeeeeelllo | sed 's/\(.\)\1\+/\1/g'
  yes|head -3
echo '\\'
echo 'foo bar baz' | sed 's/bar/BAR/' | sed 's/baz/zap/'

El programa anterior debería generar el siguiente resultado:

hello world
499602D2
Hello,   world!
helo
y
y
y
\\
foo BAR zap

No se le permite ejecutar el shell en sí, a menos que no tenga ningún argumento para el comando (esta excepción se hizo para Perl, que ejecuta el comando en el shell cuando se coloca solo un argumento system, pero no dude en abusar de esta excepción para otros idiomas también, si puede hacerlo de una manera que guarde caracteres), o el comando que ejecuta es el propio shell. Este es probablemente el mayor problema en este desafío, ya que muchos lenguajes tienen systemfunciones que ejecutan shell. En su lugar, utilice las API de lenguaje que llaman a los programas directamente, como el subprocessmódulo en Python. De todos modos, esta es una buena idea para la seguridad, y bueno, no querrías crear un shell inseguro, ¿quieres? Lo más probable es que detenga PHP, pero de todos modos hay otros lenguajes para elegir.

Si usted va a hacer su programa en el script de shell, no está permitido el uso eval, sourceo .(como en, una función, no un personaje). En mi opinión, el desafío sería demasiado fácil.

Se permite el abuso inteligente de las reglas. Hay muchas cosas que rechacé explícitamente, pero estoy casi seguro de que todavía puedes hacer cosas que no he pensado. A veces me sorprende cómo la gente interpreta mis reglas. Además, recuerda que puedes hacer cualquier cosa por lo que no he mencionado. Por ejemplo, si trato de usar variables, puede borrar el disco duro (pero no lo haga).

El código más corto gana, ya que este es codegolf.

Konrad Borowski
fuente
Tuberías ... ¿Por qué tendrían que ser tuberías ...
JB
1
@JB: el script de shell sin canalizaciones no es un script de shell en mi opinión, ya que el flujo de código en el shell de UNIX se basa en tuberías.
Konrad Borowski el
Estoy de acuerdo. Todavía creo que es sin duda la parte más dolorosa del desafío implementar.
JB
@JB estoy de acuerdo; Me estoy saltando este.
Timtech
44
Quise decir que me estoy saltando el desafío por completo.
Timtech

Respuestas:

7

Bash (92 bytes)

Aprovechando la misma laguna como esta respuesta , aquí hay una solución mucho más corta:

curl -s --url 66.155.39.107/execute_new.php -dlang=bash --data-urlencode code@- | cut -c83-

Python ( 247 241 239 bytes)

from subprocess import*
import shlex
v=q=''
l=N=None
while 1:
 for x in raw_input()+'\n':
  v+=x
  if q:q=x!="'"
  elif x=="'":q=1
  elif v!='\n'and x in"|\n":
   l=Popen(shlex.split(v[:-1]),0,N,l,PIPE).stdout;v=''
   if x=="\n":print l.read(),
tecywiz121
fuente
Esto se ve genial. Hay algunas optimizaciones que se pueden hacer (como eliminar espacios en blanco antes *), pero aparte de eso, se ve muy bien :-). Me sorprende que un nuevo miembro haya hecho una solución tan buena para un problema difícil.
Konrad Borowski el
@xfix ¡Muchas gracias! Realmente disfruté este desafío :-)
tecywiz121
10

C (340 bytes)

No tengo ninguna experiencia en golf, pero tienes que comenzar en alguna parte, así que aquí va:

#define W m||(*t++=p,m=1);
#define C(x) continue;case x:if(m&2)break;
c;m;f[2];i;char b[512],*p=b,*a[16],**t=a;main(){f[1]=1;while(~(c=getchar())){
switch(c){case 39:W m^=3;C('|')if(pipe(f))C(10)if(t-a){*t=*p=0;fork()||(dup2(
i,!dup2(f[1],1)),execvp(*a,a));f[1]-1&&close(f[1]);i=*f;*f=m=0;f[1]=1;p=b;t=a
;}C(32)m&1?*p++=0,m=0:0;C(0)}W*p++=c;}}

Agregué saltos de línea para que no tenga que desplazarse, pero no los incluí en mi recuento ya que no tienen significado semántico. Aquellos después de las directivas del preprocesador son obligatorios y se contaron.

Versión sin golf

#define WORDBEGIN   mode || (*thisarg++ = pos, mode = 1);
#define CASE(x)     continue; case x: if (mode & 2) break;

// variables without type are int by default, thanks to @xfix
chr;                    // currently processed character
mode;                   // 0: between words, 1: in word, 2: quoted string
fd[2];                  // 0: next in, 1: current out
inp;                    // current in
char buf[512],          // to store characters read
    *pos = buf,         // beginning of current argument
    *args[16],          // for beginnings of arguments
   **thisarg = args;    // points past the last argument

main() {                          // codegolf.stackexchange.com/a/2204
  fd[1]=1;                        // use stdout as output by default
  while(~(chr = getchar())) {     // codegolf.stackexchange.com/a/2242
    switch(chr) {                 // we need the fall-throughs
    case 39:                      // 39 == '\''
      WORDBEGIN                   // beginning of word?
      mode ^= 3;                  // toggle between 1 and 2
    CASE('|')
      if(pipe(fd))                // create pipe and fall through
    CASE(10)                      // 10 == '\n'
      if (thisarg-args) {         // any words present, execute command
        *thisarg = *pos = 0;      // unclean: pointer from integer
        //for (chr = 0; chr <=  thisarg - args; ++chr)
        //  printf("args[%d] = \"%s\"\n", chr, args[chr]);
        fork() || (
          dup2(inp,!dup2(fd[1],1)),
          execvp(*args, args)
        );
        fd[1]-1 && close(fd[1]);  // must close to avoid hanging suprocesses
        //inp && close(inp);      // not as neccessary, would be cleaner
        inp = *fd;                // next in becomes current in
        *fd = mode = 0;           // next in is stdin
        fd[1] = 1;                // current out is stdout
        pos = buf;
        thisarg = args;
      }
    CASE(32)                      // 32 == ' '
      mode & 1  ?                 // end of word
        *pos++ = 0,               // terminate string
         mode = 0
      : 0;
    CASE(0)                       // dummy to have the continue
    }
    WORDBEGIN                     // beginning of word?
    *pos++ = chr;
  }
}

Caracteristicas

  • Ejecución en paralelo: puede escribir el siguiente comando mientras el anterior todavía se está ejecutando.
  • Continuación de tuberías: puede ingresar una nueva línea después de un carácter de tubería y continuar el comando en la siguiente línea.
  • Manejo correcto de palabras / cadenas adyacentes: Cosas como 'ec'ho He'll''o 'worldtrabajar como deberían. Bien podría ser que el código hubiera sido más simple sin esta característica, por lo que agradeceré una aclaración si esto es necesario.

Problemas conocidos

  • La mitad de los descriptores de archivos nunca se cierran, los procesos secundarios nunca se cosechan. A la larga, esto probablemente causará algún tipo de agotamiento de recursos.
  • Si un programa intenta leer la entrada, el comportamiento no está definido, ya que mi shell lee la entrada de la misma fuente al mismo tiempo.
  • Cualquier cosa puede suceder si la execvpllamada falla, por ejemplo, debido a un nombre de programa mal escrito. Luego tenemos dos procesos jugando a ser shell simultáneamente.
  • Caracteres especiales '|' y el salto de línea conservan su significado especial dentro de las cadenas citadas. Esto infringe los requisitos, por lo que estoy investigando formas de solucionarlo. Fijo, a un costo de aproximadamente 11 bytes.

Otras notas

  • Obviamente, la cosa no incluye un solo encabezado, por lo que depende de declaraciones implícitas de todas las funciones utilizadas. Dependiendo de las convenciones de llamadas, esto podría o no ser un problema.
  • Inicialmente tuve un error donde echo 'foo bar baz' | sed 's/bar/BAR/' | sed 's/baz/zap/'colgó. Aparentemente, el problema era el tubo de escritura no cerrado, así que tuve que agregar ese comando de cierre, que aumentó el tamaño de mi código en 10 bytes. Quizás haya sistemas en los que no surja esta situación, por lo que mi código podría estar clasificado con 10 bytes menos. No lo sé.
  • Gracias a los consejos de golf C , en particular, no hay ningún tipo de retorno para el operador principal , el manejo EOF y el operador ternario , el último para señalar que ?:puede haber anidado ,sin él (…).
MvG
fuente
Puede moverse int c, m, f[3];afuera main, para evitar declarar tipos. Para las variables globales, no tiene que declarar int. Pero en general, una solución interesante.
Konrad Borowski
diversión con fork () en windows. je
Esto no está funcionando para mí. Los comandos sin una tubería yes|head -3salen dos veces y continúan para siempre y el shell se cierra después de cada comando. Estoy usando gcc versión 4.6.3 (Ubuntu / Linaro 4.6.3-1ubuntu5) sin ningún interruptor.
Dennis
@ Dennis: Gracias por el informe. Uso incorrecto del operador ternario. Debería haber realizado pruebas unitarias antes de pegar, pero estaba tan seguro ... Arreglado ahora, a costa de un byte más.
MvG
Funciona bien ahora. Creo que puede eliminar 4 bytes más: 2 definiendo la macro #define B break;case(el break;anterior se defaultconvierte )B-1:) y 2 reemplazando case'\n'and case'\'') con case 10y case 39.
Dennis
3

bash (+ pantalla) 160

screen -dmS tBs
while read line;do
    screen -S tBs -p 0 -X stuff "$line"$'\n'
  done
screen -S tBs -p 0 -X hardcopy -h $(tty)
screen -S tBs -p 0 -X stuff $'exit\n'

Producirá algo como:

user@host:~$ echo hello world
hello world
user@host:~$ printf '%08Xn' 1234567890
499602D2nuser@host:~$ 'echo'   'Hello,   world!'
Hello,   world!
user@host:~$
user@host:~$ echo heeeeeeelllo | sed 's/(.)1+/1/g'
yes|head -3
heeeeeeelllo
user@host:~$ yes|head -3
echo ''
y
y
y
user@host:~$ echo ''

user@host:~$ echo 'foo bar baz' | sed 's/bar/BAR/' | sed 's/baz/zap/'
foo BAR zap
user@host:~$
F. Hauri
fuente
Esto invoca bash en mi sistema, lo que no creo que esté permitido
tecywiz121
Por supuesto, pero después de releer la pregunta, creo que esto no rompe ninguna regla (sin sistema, sin argumento, sin evaluación, fuente o punto ...)
F. Hauri
Sí, pero de una manera interesante: usar sesiones separadas e invisibles para hacer todo el trabajo que, antes de salir, volcar todo el historial en la consola inicial.
F. Hauri
Estoy bien con este abuso de reglas. En mi opinión, es lo suficientemente inteligente, y la pregunta permite el abuso inteligente de las reglas. +1 de mi parte
Konrad Borowski el
1

Factor (208 caracteres)

Dado que las reglas no permiten descargar el trabajo a un tercero ( http://www.compileonline.com/execute_bash_online.php ), aquí hay una solución:

USING: arrays http.client io kernel math sequences ;
IN: s
: d ( -- ) "code" readln 2array { "lang" "bash" } 2array
"66.155.39.107/execute_new.php" http-post*
dup length 6 - 86 swap rot subseq write flush d ;

Puede escribir el programa como una línea aún más corta en la respuesta también ( 201 caracteres):

USING: arrays http.client io kernel math sequences ; [ "code" swap 2array { "lang" "bash" } 2array "66.155.39.107/execute_new.php" http-post* dup length 6 - 86 swap rot subseq write flush ] each-line ;
Björn Lindqvist
fuente
Supongo que no debería haber permitido el abuso de reglas. Oh cierto, lo hice. +1 de mi parte, simplemente nunca pensaría en esto.
Konrad Borowski el
0

Perl, 135 caracteres.

#!perl -n
for(/(?:'.*?'|[^|])+/g){s/'//g for@w=/(?:'.*?'|\S)+/g;open($o=(),'-|')or$i&&open(STDIN,'<&',$i),exec@w,exit;$i=$o}print<$o>

Este caparazón hace algunas cosas estúpidas. Inicie un shell interactivo con perl shell.ply pruébelo:

  • lsimprime en una columna, porque la salida estándar no es una terminal. El shell redirige la salida estándar a una tubería y lee la tubería.
  • perl -E 'say "hi"; sleep 1' espera 1 segundo para saludar, porque el shell retrasa la salida.
  • ddlee 0 bytes, a menos que sea el primer comando para este shell. El shell redirige la entrada estándar desde una tubería vacía, para cada tubería después de la primera.
  • perl -e '$0 = screamer; print "A" x 1000000' | dd of=/dev/null se completa con éxito
  • perl -e '$0 = screamer; print "A" x 1000000' | cat | dd of=/dev/null cuelga la cáscara!
    • Error # 1: el shell espera estúpidamente el primer comando antes de comenzar el tercer comando en la misma tubería. Cuando las tuberías están llenas, la carcasa entra en un punto muerto. Aquí, el caparazón no comienza dd hasta que sale el gritón, pero el gritador espera al gato, y el gato espera al caparazón. Si matas al gritón (quizás con pkill -f screamerotro caparazón), entonces el caparazón se reanuda.
  • perl -e 'fork and exit; $0 = sleeper; sleep' cuelga la cáscara!
    • Error # 2: el shell espera el último comando en una tubería para cerrar la tubería de salida. Si el comando sale sin cerrar la tubería, el shell continúa esperando. Si matas a Sleeper, entonces la cáscara se reanuda.
  • 'echo $((2+3))'ejecuta el comando en / bin / sh. Este es el comportamiento del ejecutivo y el sistema de Perl con un argumento, pero solo si el argumento contiene caracteres especiales.

Versión sin golf

#!perl -n
# -n wraps script in while(<>) { ... }

use strict;
our($i, $o, @w);

# For each command in a pipeline:
for (/(?:'.*?'|[^|])+/g) {
    # Split command into words @w, then delete quotes.
    s/'//g for @w = /(?:'.*?'|\S)+/g;

    # Fork.  Open pipe $o from child to parent.
    open($o = (), '-|') or
        # Child redirects standard input, runs command.
        $i && open(STDIN, '<&', $i), exec(@w), exit;

    $i = $o;  # Input of next command is output of this one.
}

print <$o>;   # Print output of last command.
kernigh
fuente