Como posso usar um arquivo em um comando e redirecionar a saída para o mesmo arquivo sem truncá-lo?


96

Basicamente, quero pegar como texto de entrada de um arquivo, remover uma linha desse arquivo e enviar a saída de volta para o mesmo arquivo. Algo nessa linha se isso torna mais claro.

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

entretanto, quando faço isso, acabo com um arquivo em branco. Alguma ideia?


Respostas:


84

Você não pode fazer isso porque o bash processa os redirecionamentos primeiro e, em seguida, executa o comando. Então, no momento em que grep olha para file_name, ele já está vazio. Você pode usar um arquivo temporário.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

assim, considere usar mktemppara criar o tmpfile, mas observe que não é POSIX.


47
A razão pela qual você não pode fazer isso: o bash processa os redirecionamentos primeiro e, em seguida, executa o comando. Então, no momento em que grep olha para file_name, ele já está vazio.
glenn jackman

1
@glennjackman: por "redirecionamento de processos você quer dizer que no caso de> abrir o arquivo e apagá-lo e no caso de >> apenas abrir"?
Razvan

2
sim, mas é importante observar que nesta situação o >redirecionamento abrirá o arquivo e o truncará antes que o shell seja iniciado grep.
glenn jackman

1
Veja minha resposta se você não quiser usar um arquivo temporário, mas por favor, não vote a favor deste comentário.
Zack Morris

Em vez disso, a resposta usando o spongecomando deve ser aceita.
vlz

95

Use uma esponja para este tipo de tarefas. É parte do moreutils.

Tente este comando:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
Obrigado pela resposta. Como uma adição possivelmente útil, se você estiver usando homebrew no Mac, pode usar brew install moreutils.
Anthony Panozzo

2
Ou sudo apt-get install moreutilsem sistemas baseados em Debian.
Jonah de

3
Droga! Obrigado por me apresentar ao moreutils =) alguns programas legais lá!
netigger

muito obrigado, moreutils pelo resgate! esponja como um chefe!
aqquadro de

3
Aviso, "esponja" é destrutiva, portanto, se houver um erro em seu comando, você pode apagar seu arquivo de entrada (como fiz na primeira vez que tentei a esponja). Certifique-se de que seu comando funcione e / ou o arquivo de entrada esteja sob controle de versão se você estiver tentando iterar para fazer o comando funcionar.
user107172

18

Em vez disso, use sed:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
iirc -ié uma extensão GNU apenas, apenas observando.
c00kiemon5ter

3
No * BSD (e, portanto, também no OSX) você pode dizer -i ''que a extensão não é estritamente obrigatória, mas a -iopção requer algum argumento.
tripleee

13

tente este simples

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Seu arquivo não ficará em branco desta vez :) e sua saída também será impressa em seu terminal.


1
Gosto desta solução! E se você não quiser que seja impresso no terminal, você ainda pode redirecionar a saída para /dev/nullou locais semelhantes.
Frozn

4
Isso limpa o conteúdo do arquivo aqui também. Isso é devido a uma diferença GNU / BSD? Estou no macOS ...
ssc

7

Você não pode usar o operador de redirecionamento ( >ou >>) para o mesmo arquivo, porque ele tem uma precedência mais alta e irá criar / truncar o arquivo antes mesmo de o comando ser invocado. Para evitar isso, você deve usar as ferramentas adequadas, tais como tee, sponge, sed -iou qualquer outra ferramenta que pode escrever resultados para o arquivo (por exemplo sort file -o file).

Basicamente, redirecionar a entrada para o mesmo arquivo original não faz sentido e você deve usar editores apropriados no local para isso, por exemplo, editor Ex (parte do Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

Onde:

  • '+cmd'/ -c- executa qualquer comando Ex / Vim
  • g/pattern/d- remove as linhas que correspondem a um padrão usando global ( help :g)
  • -s- modo silencioso ( man ex)
  • -c wq- executar :writee :quitcomandos

Você pode usar sedpara atingir o mesmo (como já demonstrado em outras respostas), no entanto in-place ( -i) é a extensão FreeBSD não-padrão (pode funcionar de forma diferente entre Unix / Linux) e, basicamente, é uma s tream ed itor, não um editor de arquivos . Veja: O modo Ex tem alguma utilidade prática?


6

Uma alternativa de linha - defina o conteúdo do arquivo como variável:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

Uma vez que esta pergunta é o principal resultado nos motores de busca, aqui está um one-liner baseado em https://serverfault.com/a/547331 que usa um subshell em vez de sponge(que geralmente não faz parte de uma instalação vanilla como o OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

O caso geral é:

echo "$(cat file_name)" > file_name

Editar, a solução acima tem algumas ressalvas:

  • printf '%s' <string>deve ser usado em vez de echo <string>para que os arquivos que contenham -nnão causem comportamento indesejado.
  • A substituição de comandos remove novas linhas ( este é um bug / recurso de shells como o bash ), então devemos anexar um caractere pós-fixado como xà saída e removê-lo do lado de fora via expansão de parâmetro de uma variável temporária como ${v%x}.
  • O uso de uma variável temporária $vpisa o valor de qualquer variável existente $vno ambiente shell atual, portanto, devemos aninhar a expressão inteira entre parênteses para preservar o valor anterior.
  • Outro bug / recurso de shells como o bash é que a substituição do comando remove caracteres não imprimíveis, como nullna saída. Eu verifiquei isso chamando dd if=/dev/zero bs=1 count=1 >> file_namee visualizando em hexadecimal com cat file_name | xxd -p. Mas echo $(cat file_name) | xxd -pestá despojado. Portanto, esta resposta não deve ser usada em arquivos binários ou qualquer coisa que use caracteres não imprimíveis, como Lynch apontou .

A solução geral (albiet ligeiramente mais lento, mais memória intensiva e ainda removendo caracteres não imprimíveis) é:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Teste em https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Deve imprimir:

hello
world

Enquanto chamando cat file_uniquely_named.txt > file_uniquely_named.txto shell atual:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Imprime uma string vazia.

Não testei isso em arquivos grandes (provavelmente com mais de 2 ou 4 GB).

Peguei emprestada essa resposta de Hart Simha e kos .


2
Claro que não funcionará com arquivos grandes. Essa não pode ser uma boa solução ou funcionar o tempo todo. O que está acontecendo é que o bash executa primeiro o comando e depois carrega o stdout de cate o coloca como primeiro argumento para echo. É claro que as variáveis ​​não imprimíveis não sairão corretamente e corromperão os dados. Não tente redirecionar um arquivo para ele mesmo, simplesmente não pode ser bom.
Lynch

1

Também existe ed(como alternativa a sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

Você pode fazer isso usando a substituição de processo .

É um pouco um hack, já que o bash abre todos os canais de forma assíncrona e temos que contornar isso usando sleepYMMV.

No seu exemplo:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) cria um arquivo temporário que recebe a saída do grep
  • sleep 1 demora por um segundo para dar tempo ao grep para analisar o arquivo de entrada
  • finalmente cat > file_nameescreve a saída

1

Você pode usar slurp com POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

Exemplo


1
Talvez deva ser apontado que "slurp" significa "ler o arquivo inteiro na memória". Se você tiver um arquivo de entrada grande, talvez queira evitar isso.
tripleee

1

Isso é muito possível, você só precisa ter certeza de que, no momento de escrever a saída, está gravando em um arquivo diferente. Isso pode ser feito removendo o arquivo depois de abrir um descritor de arquivo nele, mas antes de gravá-lo:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

Ou linha por linha, para entender melhor:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

Ainda é uma coisa arriscada de se fazer, porque se COMMAND não funcionar corretamente, você perderá o conteúdo do arquivo. Isso pode ser atenuado restaurando o arquivo se COMMAND retornar um código de saída diferente de zero:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

Também podemos definir uma função shell para torná-la mais fácil de usar:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Exemplo:

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Além disso, observe que isso manterá uma cópia completa do arquivo original (até que o terceiro descritor de arquivo seja fechado). Se você estiver usando Linux e o arquivo no qual está processando for muito grande para caber duas vezes no disco, você pode verificar este script que canalizará o arquivo para o comando especificado bloco a bloco enquanto desaloca o já processado blocos. Como sempre, leia os avisos na página de uso.


0

Tente isto

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

Uma breve explicação ou mesmo comentários podem ser úteis.
Rich

Acho que funciona porque a extrapolação de string é executada antes do operador de redirecionamento, mas não sei exatamente
Виктор Пупкин

0

O seguinte realizará a mesma coisa sponge, sem exigir moreutils:

    shuf --output=file --random-source=/dev/zero 

A --random-source=/dev/zeroparte funciona como um truque shufsem fazer qualquer embaralhamento, portanto, armazenará em buffer sua entrada sem alterá-la.

No entanto, é verdade que usar um arquivo temporário é melhor, por motivos de desempenho. Então, aqui está uma função que escrevi que fará isso para você de uma forma generalizada:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.