Implementar subconjunto de shell script

12

Este site teve muitos problemas ao implementar vários idiomas na tag . No entanto, praticamente todos eles eram linguagens esotéricas que ninguém usa. Hora de criar um intérprete para uma linguagem prática que a maioria dos usuários provavelmente já conhece. Sim, é um shell script, caso você tenha problemas para ler o título (não que você tenha). (sim, eu intencionalmente fiz esse desafio, pois estou entediado com linguagens como GolfScript e Befunge ganhando tudo, então coloquei um desafio em que uma linguagem de programação mais prática tem maiores chances de ganhar)

No entanto, o script de shell é uma linguagem relativamente grande, então não vou pedir para você implementá-lo. Em vez disso, vou criar um pequeno subconjunto da funcionalidade de script de shell.

O subconjunto que eu decidi é o seguinte subconjunto:

  • Executando programas (os programas conterão apenas letras, no entanto, mesmo que aspas simples sejam permitidas)
  • Argumentos do programa
  • Aspas simples (aceitando qualquer caractere ASCII imprimível, incluindo espaço em branco, excluindo as aspas simples)
  • Sequências sem aspas (permitindo letras, números e traços ASCII)
  • Tubos
  • Instruções vazias
  • Várias instruções separadas por nova linha
  • À direita / à esquerda / vários espaços

Nesta tarefa, você deve ler a entrada do STDIN e executar todos os comandos solicitados. Você pode assumir com segurança o sistema operacional compatível com POSIX, para que não haja necessidade de portabilidade com o Windows ou algo assim. Você pode assumir com segurança que os programas que não são canalizados para outros programas não serão lidos no STDIN. Você pode assumir com segurança que os comandos existirão. Você pode assumir com segurança que nada mais será usado. Se alguma suposição segura for quebrada, você poderá fazer qualquer coisa. Você pode assumir com segurança no máximo 15 argumentos e linhas abaixo de 512 caracteres (se você precisar de alocação de memória explícita, ou algo assim - eu realmente darei pequenas chances de ganhar para C, mesmo que elas ainda sejam pequenas). Você não precisa limpar os descritores de arquivo.

Você tem permissão para executar programas a qualquer momento - mesmo após receber a linha completa ou após o término do STDIN. Escolha qualquer abordagem que você deseja.

Caixa de teste simples que permite testar seu shell (observe o espaço em branco após o terceiro 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/'

O programa acima deve gerar o seguinte resultado:

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

Você não tem permissão para executar o próprio shell, a menos que não tenha argumentos para o comando (essa exceção foi feita para Perl, que executa o comando no shell quando coloca apenas um argumento system, mas sinta-se à vontade para abusar dessa exceção por outros idiomas também, se você puder fazer isso de uma maneira que salve caracteres), ou o comando executado será o próprio shell. Este é provavelmente o maior problema nesse desafio, pois muitas linguagens possuem systemfunções que executam o shell. Em vez disso, use APIs de linguagem que chamam programas diretamente, como o subprocessmódulo em Python. Esta é uma boa idéia para a segurança de qualquer maneira e, bem, você não gostaria de criar um shell inseguro, gostaria? Isso provavelmente interrompe o PHP, mas existem outras linguagens para escolher de qualquer maneira.

Se você estiver indo para fazer o seu programa em shell script, você não estão autorizados a utilizar eval, sourceou .(como em uma função, não um personagem). Tornaria o desafio muito fácil na minha opinião.

Abuso inteligente de regras permitido. Há muitas coisas que eu não permiti explicitamente, mas tenho quase certeza de que você ainda pode fazer coisas que ainda não fiz. Às vezes, fico surpreso com a maneira como as pessoas interpretam minhas regras. Além disso, lembre-se de que você pode fazer qualquer coisa por qualquer coisa que eu não tenha mencionado. Por exemplo, se eu tentar usar variáveis, você pode limpar o disco rígido (mas não o faça).

O código mais curto vence, pois é um codegolf.

Konrad Borowski
fonte
Pipes ... Por que ele tem que ser tubos ...
JB
1
@JB: Script de shell sem pipelines não é um script de shell na minha opinião, pois o fluxo de código no shell do UNIX é baseado em pipes.
precisa saber é o seguinte
Concordo. Ainda acho que essa é a parte mais dolorosa do desafio a ser implementada.
JB
@JB eu concordo; Eu estou pulando este.
Timtech
4
Eu quis dizer que estou pulando o desafio completamente.
Timtech 4/14

Respostas:

7

Bash (92 bytes)

Aproveitando a mesma brecha que essa resposta , aqui está uma solução muito mais curta:

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
fonte
Isso parece ótimo. Existem algumas otimizações que podem ser feitas (como remover espaços em branco antes *), mas, além disso, fica ótimo :-). Estou surpreso que um novo membro tenha feito uma solução tão boa para um problema difícil.
Konrad Borowski
@xfix Muito obrigado! Gostei muito deste desafio :-)
tecywiz121
10

C (340 bytes)

Não tenho nenhuma experiência em golfe, mas você precisa começar em algum lugar, então aqui vai:

#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;}}

Adicionei quebras de linha para que você não precise rolar, mas não as incluí na minha contagem, pois elas não têm significado semântico. Aqueles após as diretivas do pré-processador são necessários e foram contados.

Versão ungolfed

#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;
  }
}

Recursos

  • Execução paralela: você pode digitar o próximo comando enquanto o anterior ainda está em execução.
  • Continuação de pipes: você pode inserir uma nova linha após um caractere de pipe e continuar o comando na próxima linha.
  • Manuseio correto de palavras / strings adjacentes: as coisas 'ec'ho He'll''o 'worldfuncionam como deveriam. Pode ser que o código tenha sido mais simples sem esse recurso, por isso, gostaria de esclarecer se isso é necessário.

Problemas conhecidos

  • Metade dos descritores de arquivo nunca é fechada, os processos filhos nunca são colhidos. A longo prazo, isso provavelmente causará algum tipo de esgotamento de recursos.
  • Se um programa tenta ler a entrada, o comportamento é indefinido, pois meu shell lê a entrada da mesma fonte ao mesmo tempo.
  • Qualquer coisa pode acontecer se a execvpchamada falhar, por exemplo, devido a um nome de programa digitado incorretamente. Então nós temos dois processos brincando de concha simultaneamente.
  • Caracteres especiais '|' e quebra de linha mantêm seu significado especial dentro de seqüências de caracteres entre aspas. Isso viola os requisitos, por isso estou investigando maneiras de corrigir isso. Corrigido, a um custo de cerca de 11 bytes.

Outras notas

  • Obviamente, a coisa não inclui um único cabeçalho, portanto depende de declarações implícitas de todas as funções usadas. Dependendo das convenções de chamada, isso pode ou não ser um problema.
  • Inicialmente eu tive um bug onde echo 'foo bar baz' | sed 's/bar/BAR/' | sed 's/baz/zap/'travou. Aparentemente, o problema era o canal de gravação não fechado, então tive que adicionar esse comando close, que aumentou meu tamanho de código em 10 bytes. Talvez existam sistemas em que essa situação não ocorra, portanto meu código pode ser classificado com 10 bytes a menos. Eu não sei.
  • Graças às dicas de golfe C , em particular nenhum tipo de retorno para o operador principal , manuseio de EOF e operador ternário , o último a apontar que ?:pode ter sido aninhado ,sem (…).
MvG
fonte
Você pode mover int c, m, f[3];fora main, para evitar declarar tipos. Para variáveis ​​globais, você não precisa declarar int. Mas, geralmente, solução interessante.
Konrad Borowski
diversão com garfo () no windows. heh
Isso não está funcionando para mim. Comandos sem um pipe emitem duas vezes e yes|head -3continuam indo para sempre e o shell sai após cada comando. Estou usando a versão 4.6.3 do gcc (Ubuntu / Linaro 4.6.3-1ubuntu5) sem nenhuma opção.
Dennis
@ Dennis: Obrigado pelo relatório. Uso incorreto do operador ternário. Eu deveria ter executado testes de unidade antes de colar, mas tinha tanta certeza ... Corrigido agora, ao custo de mais um byte.
MvG
Funciona bem agora. Eu acho que você pode evitar mais 4 bytes: 2 definindo a macro #define B break;case(o break;antes defaultse torna )B-1:) e 2 substituindo case'\n'e case'\'') por case 10e case 39.
Dennis
3

bash (+ tela) 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'

Produzirá 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
fonte
Isso chama bater no meu sistema, que eu não acho que é permitido
tecywiz121
Claro, mas depois pergunta lendo re-, eu acho que isso não quebrar qualquer regra (Nenhum sistema, nenhum argumento, nenhuma eval, origem ou ponto ...)
F. Hauri
Sim, mas de uma maneira interessante: Usando uma sessão desanexada e invisível para fazer todo o trabalho, antes de sair, despeje todo o histórico no console inicial.
F. Hauri
Eu estou bem com esse abuso de regra. É inteligente o suficiente na minha opinião - e a pergunta permite um abuso inteligente de regras. +1 de mim.
precisa saber é o seguinte
1

Fator (208 caracteres)

Como as regras não proíbem a transferência do trabalho para terceiros ( http://www.compileonline.com/execute_bash_online.php ), eis uma solução:

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 ;

Você pode escrever o programa como uma linha única ainda mais curta no repl ( 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
fonte
Acho que não deveria ter permitido o abuso de regras. Oh certo, eu fiz. +1 de mim - eu nunca pensaria nisso.
21978 Konrad Borowski
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 shell faz algumas coisas estúpidas. Inicie um shell interativo perl shell.ple tente:

  • lsimprime em uma coluna, porque a saída padrão não é um terminal. O shell redireciona a saída padrão para um pipe e lê a partir do pipe.
  • perl -E 'say "hi"; sleep 1' espera 1 segundo para dizer oi, porque o shell atrasa a saída.
  • ddlê 0 bytes, a menos que seja o primeiro comando desse shell. O shell redireciona a entrada padrão de um tubo vazio, para cada pipeline após o primeiro.
  • perl -e '$0 = screamer; print "A" x 1000000' | dd of=/dev/null termina com sucesso.
  • perl -e '$0 = screamer; print "A" x 1000000' | cat | dd of=/dev/null pendura a concha!
    • Bug # 1: O shell espera estupidamente o primeiro comando antes de iniciar o terceiro comando no mesmo pipeline. Quando os tubos estão cheios, o invólucro entra em conflito. Aqui, o shell não inicia o dd até que o gritador saia, mas o gritador espera pelo gato, e o gato aguarda o shell. Se você matar um gritador (talvez com pkill -f screameroutro shell), o shell continuará.
  • perl -e 'fork and exit; $0 = sleeper; sleep' pendura a concha!
    • Bug # 2: O shell aguarda o último comando em um pipeline para fechar o tubo de saída. Se o comando sair sem fechar o tubo, o shell continuará aguardando. Se você matar dorminhoco, o shell recomeça.
  • 'echo $((2+3))'executa o comando em / bin / sh. Esse é o comportamento do exec e do sistema do Perl com um argumento, mas apenas se o argumento contiver caracteres especiais.

Versão ungolfed

#!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
fonte