Como remover a última linha de todos os arquivos de um diretório?

17

Eu tenho muitos arquivos de texto em um diretório e quero remover a última linha de todos os arquivos no diretório.

Como eu posso fazer isso?

ThehalfarisedPheonix
fonte
6
O que você tentou? unix.stackexchange.com/help/how-to-ask : "Compartilhar sua pesquisa ajuda a todos. Conte-nos o que você encontrou e por que ela não atendeu às suas necessidades. Isso demonstra que você gastou um tempo para tentar ajudar a si mesmo , evita a reiteração de respostas óbvias e, acima de tudo, ajuda a obter uma resposta mais específica e relevante! "
Patrick
Por que o pensamento de alguém acidentalmente aplicar isso para / etc têm uma qualidade muito especial de indução cringe :)
rackandboneman

Respostas:

4

Se você tiver acesso vim, poderá usar:

for file in ./*
do
  if [ -f "${file}" ]
  then
    vim -c '$d' -c "wq" "${file}"
  fi
done
R Sahu
fonte
16

Você pode usar este bom oneliner se tiver o GNU sed .

 sed -i '$ d' ./*

Ele removerá a última linha de cada arquivo não oculto no diretório atual. Mudar -ipara GNU sedsignifica operar no lugar e '$ d'comandar a sedexclusão da última linha (que $significa última e que dsignifica excluir).

StefanR
fonte
3
Isso gerará erros (e não fará nada) se a pasta contiver algo que não seja um arquivo comum, por exemplo, outra pasta ...
Najib Idrissi
1
@StefanR Você está usando o -ique é um GNUism, então isso é discutível, mas eu perderia minha barba antiga se deixasse de ressaltar que algumas versões mais antigas seddo não permitem que você coloque espaço em branco entre o $e o d(ou, em geral, entre o padrão e o comando).
Zwol 31/01/19
1
@zwol Como escrevi, isso resultará em um erro , não em um aviso, e o sed desistirá assim que alcançar esse arquivo (pelo menos com a versão do sed que eu tenho). Os próximos arquivos não serão processados. Jogar fora as mensagens de erro seria uma péssima idéia, pois você nem sabia que isso havia acontecido! Com o zsh, você pode usar o *(.)glob para arquivos regulares, não sei sobre outros shells.
Najib Idrissi
@NajibIdrissi Hmm, você está certo. Isso me surpreende; Eu esperava que ele se queixasse do diretório, mas depois vá para o próximo arquivo na linha de comando. Na verdade, acho que vou relatar isso como um bug.
Zwol 31/01
@don_crissti Também tenho o GNU sed v4.3 ... Não sei o que dizer, acabei de testar novamente. gist.github.com/nidrissi/66fad6be334234f5dbb41c539d84d61e
Najib Idrissi
11

As outras respostas têm problemas se o diretório contiver algo diferente de um arquivo comum ou um arquivo com espaços / novas linhas no nome do arquivo. Aqui está algo que funciona independentemente:

find "$dir" -type f -exec sed -i '$d' '{}' '+'
  • find "$dir" -type f: encontre os arquivos no diretório $dir
    • -type f que são arquivos regulares;
    • -exec execute o comando em cada arquivo encontrado
    • sed -i: edite os arquivos no local;
    • '$d': delete ( d) o último ($ ) linha.
    • '+': diz ao find para continuar adicionando argumentos sed(um pouco mais eficiente do que executar o comando para cada arquivo separadamente, graças a @zwol).

Se você não desejar descer para subdiretórios, poderá adicionar o argumento -maxdepth 1a find.

Najib Idrissi
fonte
1
Hmm, mas ao contrário das outras respostas, isso desce em subdiretórios. (Além disso, com as versões atuais do findpresente é mais eficiente escrito find $dir -type f -exec sed -i '$d' '{}' '+'.)
Zwol
@ zwol Obrigado, eu adicionei isso na resposta.
Najib Idrissi
-print0não está presente no comando completo, por que colocá-lo na explicação?
Ruslan
1
Além disso, -depth 0não funciona (findutils 4.4.2), deveria ser -maxdepth 1.
Ruslan
@Ruslan Eu tive uma primeira versão em que usei xargs, mas depois me lembrei -exec.
Najib Idrissi
9

Usar o GNU sed -i '$d'significa ler o arquivo completo e fazer uma cópia dele sem a última linha, enquanto seria muito mais eficiente truncar o arquivo no local (pelo menos para arquivos grandes).

Com o GNU truncate, você pode fazer:

for file in ./*; do
  [ -f "$file" ] &&
    length=$(tail -n 1 "$file" | wc -c) &&
    [ "$length" -gt 0 ] &&
    truncate -s "-$length" "$file"
done

Se os arquivos forem relativamente pequenos, isso provavelmente seria menos eficiente, pois ele executa vários comandos por arquivo.

Observe que, para arquivos que contenham bytes extras após o último caractere de nova linha (após a última linha) ou em outras palavras, se eles tiverem uma última linha não delimitada , dependendo da tailimplementação, tail -n 1retornará apenas esses bytes extras (como GNU tail), ou a última linha (delimitada corretamente) e esses bytes extras.

Stéphane Chazelas
fonte
você precisa de um |wc -cna tailchamada? (ou a ${#length})
Jeff Schaller
@JeffSchaller. Opa wc -c foi realmente planejado. ${#length}não funcionaria, pois conta caracteres, não bytes, e $(...)removeria o caractere de nova linha posterior ${#...}, sendo desativado em um, mesmo que todos os caracteres fossem de byte único.
Stéphane Chazelas
6

Uma abordagem mais portátil:

for f in ./*
do
test -f "$f" && ed -s "$f" <<\IN
d
w
q
IN
done

Eu não acho que isso precisa qualquer explicação ... exceto, talvez, que neste caso dé o mesmo que $duma vez edpor seleciona padrão a última linha.
Isso não procurará recursivamente e não processará arquivos ocultos (também conhecidos como dotfiles).
Se você também deseja editá-los, consulte Como combinar * com arquivos ocultos em um diretório

don_crissti
fonte
Agradável! Se você mudar [[]]para [], será totalmente compatível com POSIX. ( [[ ... ]]é um Bashism).
Curinga
@Wildcard - graças, alteradas (embora [[não seja um Bashismo )
don_crissti
Um não-POSIX-ism, devo dizer. :)
Curinga
3

Uma linha compatível com POSIX para todos os arquivos que começam recursivamente no diretório atual, incluindo arquivos de ponto:

find . -type f -exec sh -c 'for f; do printf "\$d\nx\n" | ex "$f"; done' sh {} +

Apenas para .txtarquivos, de forma não recursiva:

find . -path '*/*/*' -prune -o -type f -name '*.txt' -exec sh -c 'for f; do printf "\$d\nx\n" | ex "$f"; done' sh {} +

Veja também:

Curinga
fonte