Eu resolvi a resposta sed
pouco depois de postar essa pergunta; ninguém mais usou sed
até agora, então aqui está:
sed '$!N;/^\(.*\)\n$/d;P;D'
Um pouco de brincadeira com o problema mais geral (que sobre a exclusão de linhas em conjuntos de três? Ou quatro ou cinco?) forneceu a seguinte solução extensível:
sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n$/d;P;D' temp
Extensão para remover triplas de linhas:
sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\n$/d;P;D' temp
Ou para remover quadras de linhas:
sed -e ':top' -e '$!{/\n.*\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\n\n$/d;P;D' temp
sed
tem uma vantagem adicional sobre a maioria das outras opções, que é a capacidade de operar verdadeiramente em um fluxo, sem precisar armazenar mais memória do que o número real de linhas a serem verificadas para duplicatas.
Como cuonglm apontou nos comentários , a definição do código de idioma para C é necessária para evitar falhas na remoção adequada de linhas que contenham caracteres de múltiplos bytes. Então os comandos acima se tornam:
LC_ALL=C sed '$!N;/^\(.*\)\n$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n/!{N;b top' -e '};};/^\(.*\)\n$/d;P;D' temp
LC_ALL=C sed -e ':top' -e '$!{/\n.*\n/!{N;b top' -e '};};/^\(.*\)\n\n$/d;P;D' temp
# Etc.