Compreendendo a substituição do comando Read-a-File do Bash

11

Estou tentando entender como exatamente o Bash trata a seguinte linha:

$(< "$FILE")

De acordo com a página do manual do Bash, isso é equivalente a:

$(cat "$FILE")

e posso seguir a linha de raciocínio dessa segunda linha. O Bash executa expansão variável $FILE, entra na substituição de comando, passa o valor de $FILEpara cat, cat envia o conteúdo de $FILEpara a saída padrão, a substituição do comando termina substituindo a linha inteira pela saída padrão resultante do comando interno, e o Bash tenta executá-lo como um comando simples.

No entanto, para a primeira linha que mencionei acima, entendo como: Bash realiza substituição de variável ativada $FILE, o Bash é aberto $FILEpara leitura na entrada padrão, de alguma forma a entrada padrão é copiada para a saída padrão , a substituição do comando é concluída e o Bash tenta executar o padrão resultante resultado.

Alguém por favor pode me explicar como o conteúdo de $FILEvai de stdin para stdout?

Stanley Yu
fonte

Respostas:

-3

O <não é diretamente um aspecto da substituição do comando bash . É um operador de redirecionamento (como um pipe), que alguns shells permitem sem um comando (o POSIX não especifica esse comportamento).

Talvez seja mais claro com mais espaços:

echo $( < $FILE )

isso é efetivamente * o mesmo que o mais seguro para POSIX

echo $( cat $FILE )

... o que também é eficaz *

echo $( cat < $FILE )

Vamos começar com essa última versão. Isso é executado catsem argumentos, o que significa que ele lerá da entrada padrão. $FILEé redirecionado para a entrada padrão devido ao <, portanto, catcoloca seu conteúdo na saída padrão. A $(command)subscrição então empurra cata saída de argumentos para argumentos para echo.

Em bash(mas não no padrão POSIX), você pode usar <sem um comando. bash(e zshe kshmas não dash) interpretará isso como se cat <, embora sem invocar um novo subprocesso. Como isso é nativo do shell, é mais rápido do que literalmente executar o comando externo cat. * É por isso que digo "efetivamente o mesmo que".

Adam Katz
fonte
Então, no último parágrafo, quando você diz " bashinterpretará isso como cat filename", você quer dizer que esse comportamento é específico para comandar a substituição? Porque, se eu correr < filenamesozinho, o bash não dá certo. Ele não produzirá nada e retornará a um prompt.
Stanley Yu
Um comando ainda é necessário. @cuonglm alterado meu texto original cat < filenamepara cat filenameque eu me oponho e pode reverter.
23415 Adam
1
Um pipe é um tipo de arquivo. O operador shell |cria um canal entre dois subprocessos (ou, com algumas conchas, de um subprocesso para a entrada padrão do shell). O operador shell $(…)cria um canal de um subprocesso para o próprio shell (não para sua entrada padrão). O operador shell <não envolve um canal, apenas abre um arquivo e move o descritor de arquivo para a entrada padrão.
Gilles 'SO- stop be evil'
3
< filenão é o mesmo que cat < file(exceto zshonde é como $READNULLCMD < file). < fileé perfeitamente POSIX e apenas abre filepara leitura e, em seguida, não faz nada (logo filefica próximo). É $(< file)ou `< file`é um operador especial de ksh, zshe bash(e o comportamento não é especificado no POSIX). Veja minha resposta para detalhes.
Stéphane Chazelas
2
Para colocar o comentário de @ StéphaneChazelas sob outra luz: para uma primeira aproximação, $(cmd1) $(cmd2)normalmente será o mesmo que $(cmd1; cmd2). Mas veja o caso onde cmd2está < file. Se dizemos $(cmd1; < file), o arquivo não é lido, mas, com $(cmd1) $(< file), é. Portanto, é incorreto dizer que $(< file)é apenas um caso comum de $(command)com um comando de < file.   $(< …)é um caso especial de substituição de comando e não um uso normal de redirecionamento.
Scott
13

$(<file)(também trabalha com `<file`) é um operador especial do shell Korn copiado por zshe bash. Parece muito com substituição de comando, mas na verdade não é.

Nos shells POSIX, um comando simples é:

< file var1=value1 > file2 cmd 2> file3 args 3> file4

Todas as peças são opcionais, você pode ter apenas redirecionamentos, apenas comandos, apenas atribuições ou combinações.

Se houver redirecionamentos, mas nenhum comando, os redirecionamentos serão executados (para que um > fileabra e trunque file), mas nada acontece. assim

< file

Abre filepara leitura, mas nada acontece, pois não há comando. Então o fileé então fechado e é isso. Se $(< file)fosse uma simples substituição de comando , ela se expandiria para nada.

Na especificação POSIX , em $(script), se scriptconsiste apenas em redirecionamentos, produz resultados não especificados . Isso é para permitir esse comportamento especial do shell Korn.

No ksh (testado aqui com ksh93u+), se o script consistir em um e apenas um comando simples (embora os comentários sejam permitidos antes e depois) que consistem apenas em redirecionamentos (sem comando, sem atribuição) e se o primeiro redirecionamento for um stdin (fd 0) única entrada ( <, <<ou <<<) de redireccionamento, de modo que:

  • $(< file)
  • $(0< file)
  • $(<&3)(também $(0>&3), na verdade, o mesmo operador)
  • $(< file > foo 2> $(whatever))

mas não:

  • $(> foo < file)
  • nem $(0<> file)
  • nem $(< file; sleep 1)
  • nem $(< file; < file2)

então

  • todos, exceto o primeiro redirecionamento, são ignorados (são analisados)
  • e ele se expande para o conteúdo do arquivo / heredoc / herestring (ou qualquer outro conteúdo que possa ser lido no descritor de arquivo, se estiver usando coisas do tipo <&3), menos os caracteres de nova linha à direita.

como se estivesse usando, $(cat < file)exceto que

  • a leitura é feita internamente pela casca e não por cat
  • nenhum tubo ou processo extra está envolvido
  • Como conseqüência do exposto acima, como o código interno não é executado em um subshell, qualquer modificação permanece posteriormente (como em $(<${file=foo.txt})ou $(<file$((++n))))
  • erros de leitura (embora não sejam erros ao abrir arquivos ou duplicar descritores de arquivos) são ignorados silenciosamente.

Em zsh, é o mesmo, exceto que esse comportamento especial só é acionado quando há apenas um redirecionamento de entrada de arquivo ( <fileou 0< file, não <&3, <<<here, < a < b...)

No entanto, exceto ao emular outras conchas, em:

< file
<&3
<<< here...

é quando há apenas redirecionamentos de entrada sem comandos, fora da substituição de comando, zshexecuta o $READNULLCMD(um pager por padrão) e quando há redirecionamentos de entrada e saída, o $NULLCMD( catpor padrão), portanto, mesmo que $(<&3)não seja reconhecido como especial operador, ele ainda funcionará como se kshestivesse chamando um pager para fazê-lo (esse pager agindo como se catseu stdout fosse um canal).

No entanto, enquanto ksh's $(< a < b)se expandiria para o conteúdo a, em zsh, ele se expande para o conteúdo do ae b(ou apenas bse a multiosopção for desativada), $(< a > b)iria copiar apara be expandir a nada, etc.

bash tem um operador semelhante, mas com algumas diferenças:

  • comentários são permitidos antes, mas não depois:

    echo "$(
       # getting the content of file
       < file)"
    

    funciona mas:

    echo "$(< file
       # getting the content of file
    )"
    

    expande para nada.

  • como em zsh, apenas um arquivo stdin redirecionamento, embora não há nenhuma volta queda a um $READNULLCMD, por isso $(<&3), $(< a < b)não executar os redirecionamentos mas expandir para nada.

  • por algum motivo, embora bashnão seja invocado cat, ele ainda bifurca um processo que alimenta o conteúdo do arquivo através de um canal, tornando-o muito menos otimizador do que em outros shells. É como um local $(cat < file)onde catseria construído cat.
  • Como conseqüência do exposto acima, qualquer alteração feita no interior é perdida posteriormente (no $(<${file=foo.txt})mencionado acima, por exemplo, essa $fileatribuição é perdida posteriormente).

Em bash, IFS= read -rd '' var < file (também funciona zsh) é uma maneira mais eficaz de ler o conteúdo de um arquivo de texto em uma variável. Ele também tem o benefício de preservar os caracteres de nova linha à direita. Veja também $mapfile[file]em zsh(no zsh/mapfilemódulo e apenas para arquivos regulares) que também funciona com arquivos binários.

Observe que as variantes baseadas em pdksh kshtêm algumas variações em comparação com o ksh93. De interesse, em mksh(uma dessas conchas derivadas de pdksh), em

var=$(<<'EOF'
That's multi-line
test with *all* sorts of "special"
characters
EOF
)

é otimizado, pois o conteúdo do documento here (sem os caracteres finais) é expandido sem que um arquivo ou canal temporário seja usado, como é o caso dos documentos here, o que o torna uma sintaxe eficaz de cotação de várias linhas.

Ser portátil para todas as versões de ksh, zshe bash, o melhor é limitar-se a $(<file)evitar comentários e ter em mente que as modificações nas variáveis ​​feitas dentro podem ou não ser preservadas.

Stéphane Chazelas
fonte
É correto que $(<)seja um operador nos nomes de arquivos? Está <em $(<)um operador de redirecionamento, ou não é um operador por si só, e deve fazer parte de todo o operador $(<)?
Tim
@ Tim, não importa muito como você deseja chamá-los. $(<file)destina-se a expandir o conteúdo de filemaneira semelhante à que $(cat < file)seria. Como isso é feito varia de shell para shell, que é descrito detalhadamente na resposta. Se desejar, você pode dizer que é um operador especial que é acionado quando o que parece uma substituição de comando (sintaticamente) contém o que parece um único redirecionamento stdin (sintaticamente), mas novamente com advertências e variações dependendo do shell, conforme listado aqui .
Stéphane Chazelas
@ StéphaneChazelas: fascinante, como sempre; Eu marquei isso. Então, n<&me n>&mfaz a mesma coisa? Eu não sabia disso, mas acho que não é tão surpreendente.
Scott
@ Scott, sim, os dois fazem um dup(m, n). Eu posso ver algumas evidências do ksh86 usando o stdio e outras fdopen(fd, "r" or "w"), então pode ter sido importante. Mas usar stdio em um shell faz pouco sentido, então não espero que você encontre nenhum shell moderno onde isso faça diferença. Uma diferença é que >&né dup(n, 1)(abreviação de 1>&n), enquanto <&né dup(n, 0)(abreviação de 0<&n).
Stéphane Chazelas
Direita. Exceto, é claro, a forma de dois argumentos da chamada de duplicação do descritor de arquivo é chamada dup2(); dup()usa apenas um argumento e, como open(), usa o descritor de arquivo disponível mais baixo. (Hoje eu aprendi que há uma dup3()função .)
Scott
8

Como bashele é feito internamente para você, expandiu o nome do arquivo e atribuiu o arquivo ao arquivo como saída padrão, como se você fosse fazer $(cat < filename). É um recurso bash, talvez você precise procurar no bashcódigo fonte para saber exatamente como ele funciona.

Aqui está a função para lidar com esse recurso (do bashcódigo-fonte, arquivo builtins/evalstring.c):

/* Handle a $( < file ) command substitution.  This expands the filename,
   returning errors as appropriate, then just cats the file to the standard
   output. */
static int
cat_file (r)
     REDIRECT *r;
{
  char *fn;
  int fd, rval;

  if (r->instruction != r_input_direction)
    return -1;

  /* Get the filename. */
  if (posixly_correct && !interactive_shell)
    disallow_filename_globbing++;
  fn = redirection_expand (r->redirectee.filename);
  if (posixly_correct && !interactive_shell)
    disallow_filename_globbing--;

  if (fn == 0)
    {
      redirection_error (r, AMBIGUOUS_REDIRECT);
      return -1;
    }

  fd = open(fn, O_RDONLY);
  if (fd < 0)
    {
      file_error (fn);
      free (fn);
      return -1;
    }

  rval = zcatfd (fd, 1, fn);

  free (fn);
  close (fd);

  return (rval);
}

Uma nota que $(<filename)não é exatamente equivalente a $(cat filename); o último falhará se o nome do arquivo começar com um traço -.

$(<filename)foi originalmente de kshe foi adicionado a bashpartir de Bash-2.02.

cuonglm
fonte
1
cat filenamefalhará se o nome do arquivo começar com um traço porque cat aceita opções. Você pode contornar isso na maioria dos sistemas modernos com cat -- filename.
Adam Katz
-1

Pense na substituição de comando como executando um comando como de costume e descartando a saída no ponto em que você está executando o comando.

A saída dos comandos pode ser usada como argumento para outro comando, para definir uma variável e até para gerar a lista de argumentos em um loop for.

foo=$(echo "bar")irá definir o valor da variável $foocomo bar; a saída do comando echo bar.

Substituição de Comando

iyrin
fonte
1
Eu acredito que é bastante claro pela pergunta que o OP entende o básico da substituição de comando; a pergunta é sobre o caso especial de $(< file), e ele não precisa de um tutorial sobre o caso geral. Se você está dizendo que esse $(< file)é apenas um caso comum de $(command)comando < file, então você está dizendo a mesma coisa que Adam Katz está dizendo e ambos estão errados.
Scott