Processando um único arquivo como entrada e saída em pipes

Processando um único arquivo como entrada e saída em pipes

Boa noite,

Gostaria de filtrar o conteúdo de um arquivo com alguns comandos canalizados e depois gravar o resultado novamente no mesmo arquivo. Eu sei, não posso fazer isso do jeito que escrevi. Aguentar …

Este é o script bash que tenho.

grep '^[a-zA-Z.:]' "$filepath" \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$filepath"

Então pensei que poderia ter sucesso usando a substituição de processos. Eu então escrevi:

grep '^[a-zA-Z.:]' < <(cat "$filepath") | …

Isso também não resolveu nada. Eu esperava que a substituição do processo “salvasse” o conteúdo do meu arquivo de entrada em algum lugar, como em um arquivo temporário. Parece que também não entendi a substituição de processos.

Eu li tópicos sobre edição "in-place", mas esses artigos destacaram opções especiais de alguns binários como sed -iou sort -omas preciso de uma solução geral (quero dizer, ela deve se adequar a qualquer comando canalizado).

Então, primeiro, por que o 'tubo padrão' não pode fazer isso, o que está acontecendo por baixo? :/E como devo resolver meu problema? Alguém poderia por favorexplicareu, o que é isso tudo?

Obrigado.

Responder1

Como foi mencionado, a esponja demaisutilsé ótimo. Eu uso este script para emular para evitar a dependência de moreutils:

#!/bin/sh -e
#Soak up input and tee it to arguments
st=0; tmpf=
tmpf="`mktemp`" && exec 3<>"$tmpf" || st="$?"
rm -f "$tmpf" #remove it even if exec failed; noop if mktemp failed
[ "$st" = 0 ] || exit "$st"
cat >&3
</dev/fd/3 tee "$@" >/dev/null

Você pode usá-lo assim:

grep '^[a-zA-Z.:]' "$filepath" \
| sed -r '/^(rm|cd)/d' \
| uniq -u | sponge "$filepath" 

Você não pode fazer isso com o redirecionamento de saída simples porque os redirecionamentos ocorrem antes dos comandos serem iniciados e um redirecionamento de saída trunca o arquivo de saída.

Em outras palavras, no momento em que o grep (o primeiro comando simples do pipeline) é iniciado, o último redirecionamento já truncou o arquivo de entrada/saída.

Na verdade, não existem utilitários padrão do UNIX que façam a verdadeira edição no local, até onde eu sei. sed -iapenas o emula com um arquivo temporário. Acho que o motivo é que a verdadeira filtragem local pode facilmente corromper o arquivo se uma etapa do pipeline falhar.

No que diz respeito ao que está acontecendo por baixo - ambos |usam <()pipes do sistema que passam IO, um buffer por vez. O mecanismo não cria arquivos temporários (de qualquer maneira, não arquivos reais (sistema de arquivos)) e tenta evitar manter toda a entrada na memória de cada vez.

Responder2

Se você deseja entrada e saída para o mesmo arquivo, você pode tentaresponja. Como afirma sua descrição:

sponge reads standard input and writes it out to the specified file. 
Unlike a shell redirect, sponge soaks up all its input before writing 
the output file. This allows constructing pipelines that read from and 
write to the same file.

Então você pode ter algo como sed '...' file | grep '...' | sponge [-a] filereceber informações dearquivoe saída para o mesmoarquivo.


Por outro lado, usar arquivos temporários também é uma ótima maneira de trabalhar com o mesmo arquivo para entrada e saída. Você pode inicializar seus arquivos temporários da seguinte maneira:

tempfile=`mktemp tempFile.XXXX` # You can replace "tempFile" with any name you want

Isso cria um arquivo temporário chamado "tempFile" no diretório onde este script é executado, com a extensão "XXXX" onde os x são substituídos por uma combinação do número do processo atual e letras aleatórias (por exemplo, tempFile.AVm7).

Agora você pode modificar seu pipe (ou qualquer comando canalizado) da seguinte maneira:

grep '^[a-zA-Z.:]' "$filepath" \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$tempfile"

Após o filtro, você pode mover o arquivo temporário para o arquivo original da seguinte maneira:

mv "$tempfile" "$filepath"

Isso elimina seu arquivo temporário e você permanece com o arquivo original filtrado. Mas, às vezes, você pode acabar criando muitos arquivos temporários que talvez não precise e que não foram destruídos, por isso é uma boa ideia limpar seu diretório excluindo todos os arquivos temporários após o término do script, se você não precisar mais deles . Você pode escrever uma rotina para isso da seguinte maneira:

remove_temp_files() {
    rm `find . -name "tempFile.????"`
}

Depois você pode simplesmente chamar sua rotina remove_temp_filesno final do seu script, eliminando todo e qualquer arquivo temporário que foi criado no formato descrito acima.

Responder3

UsandoAqui-DocumentoeSubstituição de comandoé o caminho padrão a seguir neste caso:

grep '^[a-zA-Z.:]' <<IN \
    | sed -r '/^(rm|cd)/d' \
    | uniq -u \
    > "$filepath"
$(cat -- "$filepath")
IN

Para outras questões, elas foram explicadas em muitas questões anteriores:

informação relacionada