Shell um liner para preceder um arquivo
Pergunta
Isto é provavelmente uma solução complexa.
Estou procurando um operador simples como ">>", mas para prefixar.
Receio que não exista.vou ter que fazer algo do tipo
mv myfile tmp cat myheader tmp > myfile
Algo mais inteligente?
Solução
O hackear abaixo estava uma resposta rápida e improvisada que funcionou e recebeu muitos votos positivos.Então, à medida que a pergunta se tornou mais popular e o tempo passou, pessoas indignadas começaram a relatar que ela meio que funcionava, mas coisas estranhas poderiam acontecer, ou simplesmente não funcionava, então foi furiosamente rejeitada por um tempo.Tão divertido.
A solução explora a implementação exata de descritores de arquivo em seu sistema e, como a implementação varia significativamente entre nixes, seu sucesso depende inteiramente do sistema, definitivamente não é portátil e não deve ser usado para nada, mesmo que vagamente importante.
Agora, com tudo isso resolvido, a resposta foi:
Criando outro descritor de arquivo para o arquivo (exec 3<> yourfile
) daí escrevendo para isso (>&3
) parece superar o dilema de leitura/gravação no mesmo arquivo.Funciona para mim em arquivos de 600K com awk.No entanto, tentar o mesmo truque usando 'cat' falha.
Passando o prefixo como uma variável para awk (-v TEXT="$text"
) supera o problema de aspas literais que impede esse truque com 'sed'.
#!/bin/bash
text="Hello world
What's up?"
exec 3<> yourfile && awk -v TEXT="$text" 'BEGIN {print TEXT}{print}' yourfile >&3
Outras dicas
Isso ainda usa um arquivo temporário, mas pelo menos está em uma linha:
echo "text" | cat - yourfile > /tmp/out && mv /tmp/out yourfile
echo '0a
your text here
.
w' | ed some_file
ed é o Editor Padrão! http://www.gnu.org/fun/jokes/ed.msg.html
João Mee:não é garantido que seu método funcione e provavelmente falhará se você anexar mais de 4.096 bytes de material (pelo menos é o que acontece com o gnu awk, mas suponho que outras implementações terão restrições semelhantes).Nesse caso, ele não apenas falhará, mas também entrará em um loop infinito onde lerá sua própria saída, fazendo com que o arquivo cresça até que todo o espaço disponível seja preenchido.
Experimente você mesmo:
exec 3<>myfile && awk 'BEGIN{for(i=1;i<=1100;i++)print i}{print}' myfile >&3
(aviso:mate-o depois de um tempo ou ele preencherá o sistema de arquivos)
Além disso, é muito perigoso editar arquivos dessa maneira e é um péssimo conselho, pois se algo acontecer enquanto o arquivo estiver sendo editado (travamento, disco cheio), é quase certo que você ficará com o arquivo em um estado inconsistente.
Não é possível sem um arquivo temporário, mas aqui vai um oneliner
{ echo foo; cat oldfile; } > newfile && mv newfile oldfile
Você pode usar outras ferramentas, como ed ou perl, para fazer isso sem arquivos temporários.
Pode ser interessante notar que muitas vezes é um Boa ideia para gerar com segurança o arquivo temporário usando um utilitário como mktemp, pelo menos se o script for executado com privilégios de root.Você poderia, por exemplo, fazer o seguinte (novamente no bash):
(tmpfile=`mktemp` && { echo "prepended text" | cat - yourfile > $tmpfile && mv $tmpfile yourfile; } )
Se precisar disso nos computadores que você controla, instale o pacote "moreutils" e use "sponge".Então você pode fazer:
cat header myfile | sponge myfile
Usando um heredoc bash você pode evitar a necessidade de um arquivo tmp:
cat <<-EOF > myfile
$(echo this is prepended)
$(cat myfile)
EOF
Isso funciona porque $(cat myfile) é avaliado quando o script bash é avaliado, antes que o cat com redirecionamento seja executado.
assumindo que o arquivo que você deseja editar seja my.txt
$cat my.txt
this is the regular file
E o arquivo que você deseja acrescentar é o cabeçalho
$ cat header
this is the header
Certifique-se de ter uma linha final em branco no arquivo de cabeçalho.
Agora você pode anexá-lo com
$cat header <(cat my.txt) > my.txt
Você acaba com
$ cat my.txt
this is the header
this is the regular file
Pelo que eu sei, isso só funciona em 'bash'.
Quando você começar a tentar fazer coisas que se tornam difíceis no shell-script, sugiro fortemente reescrever o script em uma linguagem de script "adequada" (Python/Perl/Ruby/etc)
Quanto a acrescentar uma linha a um arquivo, não é possível fazer isso via piping, como quando você faz algo como cat blah.txt | grep something > blah.txt
, ele inadvertidamente deixa o arquivo em branco.Existe um pequeno comando utilitário chamado sponge
você pode instalar (você faz cat blah.txt | grep something | sponge blah.txt
e armazena em buffer o conteúdo do arquivo e, em seguida, grava-o no arquivo).É semelhante a um arquivo temporário, mas você não precisa fazer isso explicitamente.mas eu diria que é um requisito "pior" do que, digamos, Perl.
Pode haver uma maneira de fazer isso via awk ou similar, mas se você precisar usar shell-script, acho que um arquivo temporário é de longe a maneira mais fácil (/ única?).
EDITAR:Isto está quebrado.Ver Comportamento estranho ao anexar um arquivo com cat e tee
A solução alternativa para o problema de substituição é usar tee
:
cat header main | tee main > /dev/null
Como sugere Daniel Velkov, use camiseta.
Para mim, essa é uma solução simples e inteligente:
{ echo foo; cat bar; } | tee bar > /dev/null
Aquele que eu uso.Este permite que você especifique a ordem, caracteres extras, etc. da maneira que desejar:
echo -e "TEXTFIRSt\n$(< header)\n$(< my.txt)" > my.txt
P.S:só que não funciona se os arquivos contiverem texto com barra invertida, porque ele é interpretado como caracteres de escape
Principalmente para diversão/shell golf, mas
ex -c '0r myheader|x' myfile
resolverá o problema e não há pipelines ou redirecionamentos.É claro que o vi/ex não é realmente para uso não interativo, então o vi irá piscar brevemente.
Por que não simplesmente usar o comando ed (como já sugerido por fluuffle aqui)?
ed lê o arquivo inteiro na memória e executa automaticamente uma edição do arquivo no local!
Então, se o seu arquivo não for tão grande assim...
# cf. "Editing files with the ed text editor from scripts.",
# http://wiki.bash-hackers.org/doku.php?id=howto:edit-ed
prepend() {
printf '%s\n' H 1i "${1}" . wq | ed -s "${2}"
}
echo 'Hello, world!' > myfile
prepend 'line to prepend' myfile
Outra solução alternativa seria usar identificadores de arquivos abertos, conforme sugerido por Jürgen Hötzel em Redirecionar a saída de sed 's/c/d/' myFile para myFile
echo cat > manipulate.txt
exec 3<manipulate.txt
# Prevent open file from being truncated:
rm manipulate.txt
sed 's/cat/dog/' <&3 > manipulate.txt
Tudo isso poderia ser colocado em uma única linha, é claro.
Uma variante da solução do cb0 para "sem arquivo temporário" para preceder o texto fixo:
echo "text to prepend" | cat - file_to_be_modified | ( cat > file_to_be_modified )
Novamente, isso depende da execução do sub-shell - o (..) - para evitar que o gato se recuse a ter o mesmo arquivo para entrada e saída.
Observação:Gostei desta solução.No entanto, no meu Mac, o arquivo original foi perdido (pensei que não deveria, mas aconteceu).Isso poderia ser corrigido escrevendo sua solução como:eco "texto para prender" | CAT - FILE_TO_BE_MODIFICADO | gato> tmp_file;mv tmp_file file_to_be_modified
Aqui está o que descobri:
echo -e "header \n$(cat file)" >file
sed -i -e '1rmyheader' -e '1{h;d}' -e '2{x;G}' myfile
AVISO:isso precisa de um pouco mais de trabalho para atender às necessidades do OP.
Deveria haver uma maneira de fazer a abordagem sed de @shixilun funcionar, apesar de suas dúvidas.Deve haver um comando bash para escapar de espaços em branco ao ler um arquivo em uma string substituta sed (por exemplosubstitua caracteres de nova linha por '
'.Comandos de shell vis
e cat
pode lidar com caracteres não imprimíveis, mas não com espaços em branco, portanto isso não resolverá o problema do OP:
sed -i -e "1s/^/$(cat file_with_header.txt)/" file_to_be_prepended.txt
falha devido às novas linhas brutas no script substituto, que precisam ser anexadas com um caractere de continuação de linha () e talvez seguidas por um &, para manter o shell e o sed felizes, como esta resposta SO
sed
tem um limite de tamanho de 40K para comandos de substituição de pesquisa não globais (sem /g após o padrão), portanto, provavelmente evitaria os assustadores problemas de saturação de buffer do awk sobre os quais o anônimo alertou.
sed -i -e "1s/^/new first line\n/" old_file.txt
Com $(comando) você pode escrever a saída de um comando em uma variável.Então fiz isso em três comandos em uma linha e nenhum arquivo temporário.
originalContent=$(cat targetfile) && echo "text to prepend" > targetfile && echo "$originalContent" >> targetfile
Se você tiver um arquivo grande (algumas centenas de kilobytes no meu caso) e acesso ao python, isso é muito mais rápido do que cat
soluções para tubos:
python -c 'f = "filename"; t = open(f).read(); open(f, "w").write("text to prepend " + t)'
Uma solução com printf
:
new_line='the line you want to add'
target_file='/file you/want to/write to'
printf "%s\n$(cat ${target_file})" "${new_line}" > "${target_file}"
Você também pode fazer:
printf "${new_line}\n$(cat ${target_file})" > "${target_file}"
Mas nesse caso você tem que ter certeza de que não há nenhum %
em qualquer lugar, incluindo o conteúdo do arquivo de destino, pois isso pode ser interpretado e atrapalhar seus resultados.
Você pode usar a linha de comando perl:
perl -i -0777 -pe 's/^/my_header/' tmp
Onde -i criará uma substituição em linha do arquivo e -0777 ingerirá o arquivo inteiro e fará ^ corresponderá apenas o início.-pe imprimirá todas as linhas
Ou se my_header for um arquivo:
perl -i -0777 -pe 's/^/`cat my_header`/e' tmp
Onde o /e permitirá uma avaliação do código na substituição.
current=`cat my_file` && echo 'my_string' > my_file && echo $current >> my_file
onde "my_file" é o arquivo ao qual preceder "my_string".
Estou gostando @fluffe's Ed abordagem o melhor.Afinal, as opções de linha de comando de qualquer ferramenta versus os comandos do editor com script são essencialmente a mesma coisa aqui;não ver a "limpeza" de uma solução de editor com script sendo menor ou algo assim.
Aqui está minha frase anexada a .git/hooks/prepare-commit-msg
para preceder um in-repo .gitmessage
arquivo para confirmar mensagens:
echo -e "1r $PWD/.gitmessage\n.\nw" | ed -s "$1"
Exemplo .gitmessage
:
# Commit message formatting samples:
# runlevels: boot +consolekit -zfs-fuse
#
estou fazendo 1r
em vez de 0r
, porque isso deixará a linha vazia pronta para gravação na parte superior do arquivo do modelo original.Não coloque uma linha vazia no topo do seu .gitmessage
então, você terminará com duas linhas vazias. -s
suprime a saída de informações de diagnóstico de ed.
Em relação a passar por isso, eu descoberto que para os vim-buffs também é bom ter:
[core]
editor = vim -c ':normal gg'
variáveis, certo?
NEWFILE=$(echo deb http://mirror.csesoc.unsw.edu.au/ubuntu/ $(lsb_release -cs) main universe restricted multiverse && cat /etc/apt/sources.list)
echo "$NEWFILE" | sudo tee /etc/apt/sources.list
Acho que esta é a variação mais limpa de ed:
cat myheader | { echo '0a'; cat ; echo -e ".\nw";} | ed myfile
Como uma função:
function prepend() { { echo '0a'; cat ; echo -e ".\nw";} | ed $1; }
cat myheader | prepend myfile
Se você estiver criando scripts no BASH, na verdade, você pode simplesmente emitir:
cat - yourfile /tmp/out && mv /tmp/out yourfile
Na verdade, isso está no exemplo complexo que você postou em sua própria pergunta.
IMHO, não existe uma solução shell (e nunca será uma) que funcione de forma consistente e confiável, independentemente do tamanho dos dois arquivos myheader
e myfile
.A razão é que se você quiser fazer isso sem recorrer a um arquivo temporário (e sem permitir que o shell recorra silenciosamente a um arquivo temporário, por exemplo,através de construções como exec 3<>myfile
, tubulação para tee
, etc.
A solução "real" que você está procurando precisa mexer no sistema de arquivos e, portanto, não está disponível no espaço do usuário e depende da plataforma:você está pedindo para modificar o ponteiro do sistema de arquivos em uso por myfile
para o valor atual do ponteiro do sistema de arquivos para myheader
e substitua no sistema de arquivos o EOF
de myheader
com um link encadeado para o endereço do sistema de arquivos atual apontado por myfile
.Isso não é trivial e obviamente não pode ser feito por um não-superusuário, e provavelmente também não pelo superusuário...Brinque com inodes, etc.
Você pode mais ou menos fingir isso usando dispositivos de loop.Veja por exemplo este tópico SO.