Usando a cabeça e a cauda para pegar diferentes conjuntos de linhas e salvar no mesmo arquivo

7

Então, isso é para o dever de casa, mas eu não vou fazer a pergunta específica de lição de casa.

Eu preciso usar cabeça e cauda para pegar diferentes conjuntos de linhas de um arquivo. Então, como linhas 6-11 e linhas 19-24 e salve-os para outro arquivo. Eu sei que posso fazer isso usando o append como

head -11 file|tail -6 > file1; head -24 file| tail -6 >> file1. 

Mas eu não acho que devemos fazer isso.
Existe uma maneira específica de combinar os comandos head e tail e salvar no arquivo?

    
por user2709291 27.01.2015 / 20:29

5 respostas

8

Você pode fazer isso com head sozinho e aritmética básica, se você agrupar comandos com { ... ; } usando uma construção como

{ head -n ...; head -n ...; ...; } < input_file > output_file

onde todos os comandos compartilham a mesma entrada (obrigado @mikeserv ). Obter linhas 6-11 e linhas 19-24 é equivalente a:

head -n 5 >/dev/null  # dump the first 5 lines to '/dev/null' then
head -n 6             # print the next 6 lines (i.e. from 6 to 11) then
head -n 7 >/dev/null  # dump the next 7 lines to '/dev/null' ( from 12 to 18)
head -n 6             # then print the next 6 lines (19 up to 24)

Então, basicamente, você executaria:

{ head -n 5 >/dev/null; head -n 6; head -n 7 >/dev/null; head -n 6; } < input_file > output_file
    
por 27.01.2015 / 22:29
4

Você pode usar a construção de agrupamento { … } para aplicar o operador de redirecionamento a um comando composto.

{ head -n 11 file | tail -n 6; head -n 24 file | tail -n 6; } >file1

Em vez de duplicar as primeiras linhas M + N e manter apenas o último N, você pode pular as primeiras M linhas e duplicar as próximas N. Isso é mensuravelmente mais rápido em arquivos grandes . Tenha em atenção que o argumento +N de tail não é o número de linhas a saltar, mas uma mais - é o número da primeira linha a imprimir com linhas numeradas de 1.

{ tail -n +6 file | head -n 6; tail -n +19 file | head -n 6; } >file1

De qualquer forma, o arquivo de saída é aberto apenas uma vez, mas o arquivo de entrada é percorrido uma vez para cada fragmento a ser extraído. Que tal agrupar as entradas?

{ tail -n +6 | head -n 6; tail -n +14 | head -n 6; } <file >file1

Em geral, isso não funciona. (Pode funcionar em alguns sistemas, pelo menos quando a entrada é um arquivo regular.) Por quê? Por causa do buffer de entrada . A maioria dos programas, incluindo tail , não lê seus bytes de entrada por byte, mas alguns kilobytes de cada vez, porque é mais rápido. Portanto, tail lê alguns kilobytes, pula um pouco no início, passa um pouco mais para head e para - mas o que é lido é lido e não está disponível para o próximo comando.

Outra abordagem é usar head canalizado para /dev/null para pular linhas.

{ head -n 5 >/dev/null; head -n 6; head -n 7 >/dev/null; head -n 6; } <file >file1

Mais uma vez, não há garantia de que isso funcione devido ao buffering. Acontece que funciona com o comando head do GNU coreutils (aquele encontrado em sistemas Linux não embutidos), quando a entrada é de um arquivo regular. Isso porque, uma vez que essa implementação de head tenha lido o que deseja, define a posição do arquivo como o primeiro byte que não saiu. Isso não funciona se a entrada for um pipe.

Uma maneira mais simples de imprimir várias seqüências de linhas de um arquivo é chamar uma ferramenta mais genérica, como sed ou awk . (Isso pode ser mais lento, mas só é importante para arquivos extremamente grandes).

sed -n -e '6,11p' -e '19,24p' <file >file1
sed -e '1,5d' -e '12,18d' -e '24q' <file >file1
awk '6<=NR && NR<=11 || 19<=NR && NR<=24' <file >file1
awk 'NR==6, NR==11; NR==19, NR==24' <file >file1
    
por 28.01.2015 / 01:34
1

Eu sei que você disse que precisa usar cabeça e cauda, mas sed é definitivamente a ferramenta mais simples para o trabalho aqui.

$ cat foo
a 1 1
a 2 1
b 1 1
a 3 1
c 3 1
c 3 1
$ sed -ne '2,4p;6p' foo
a 2 1
b 1 1
a 3 1
c 3 1

Você pode até mesmo construir os blocos em uma string com algum outro processo e executá-lo através do sed.

$ a="2,4p;6p"
$ sed -ne $a foo
a 2 1
b 1 1
a 3 1
c 3 1

-n nega a saída, em seguida, você especifica intervalos para imprimir com p, com o primeiro e o último número do intervalo separados por uma vírgula.

Assim sendo, você pode fazer o agrupamento de comandos que @don_crissti sugeriu, ou percorrer o arquivo algumas vezes com a cabeça / cauda pegando um pedaço de linha cada vez que você passar.

$ head -4 foo | tail -3; head -6 foo | tail -1
a 2 1
b 1 1
a 3 1
c 3 1

Quanto mais linhas em um arquivo e mais blocos você tiver, mais eficiente será o sed.

    
por 28.01.2015 / 01:29
1

Com sed , você pode fazer:

sed '24q;1,5d;12,18d' <infile >outfile

... Possivelmente, uma solução mais eficiente poderia ser obtida com head . Don já demonstrou como isso pode funcionar muito bem, mas também estou brincando com ele. Algo que você pode fazer para lidar com esse caso específico:

for   n in 5 6 7 6
do    head -n"$n" >&"$((1+n%2))"
done  <infile >outfile 2>/dev/null

... que chamaria head 4 vezes gravando outfile ou /dev/null dependendo se o valor da iteração para $n é um número par ou ímpar.

Para casos mais gerais, juntei tudo isso de outras coisas que eu já tinha:

somehead()( 
### call it like:
### somehead -[repeat] [-][numlines]* <infile >outfile
    set -e -- "${1#-}" "$@"                             #-e for arg validation
    r=; cd -- "${TMP:-/tmp}"                            #go to tmp
    dd bs=4096 of="$$$$" <&4 2>&3 &                     #dd <in >tmpfile &bg
    until [ -s "$$$$" ]; do :; done                     #wait while tmpfile empty
    exec <"$$$$" 4<&-;   rm "$$$$"                      #<tmpfile; rm tmpfile
    [ "$3${1}0" -ne "$3${2#?}0" ]          ||           #validate args - chk $1
            shift "$(((r=-${1:--1})||1))"; shift        #shift 1||2
    while [ "$(((r+=(_n=1))-1))" -ne 0 ]   &&           #while ! $rptmax &&
          IFS= read -r l                   &&           #      ! EOF     &&
          printf "%.$(($1>0?${#l}+1:0))s" "$l           #      ? printf  do
";  do    for n do [ "${n#-}" -gt 0 ]      || exit      #args all -[nums>0]
          head "-n$((${n#-}-_n))" >&"$((n>(_n=0)?1:3))" #head -n?$1 >?[+-]
    done; done                                          #done and done
)   4<&0 3>/dev/null                                    #4<for dd 3>for head

Isso pode fazer o que você gosta:

 seq 100 | somehead -1 -5 6 -7 6

... que imprime ...

6
7
8
9
10
11
19
20
21
22
23
24

Ele espera que seu primeiro argumento seja uma contagem de repetições prefixada com um - ou, na sua falta, apenas um - . Se uma contagem for fornecida, ela repetirá o padrão de linha fornecido nos seguintes argumentos quantas vezes forem especificadas e parará assim que tiver feito isso.

Para cada arg que segue, ele interpretará um inteiro negativo para indicar uma contagem de linhas que deve ser escrita em /dev/null e um inteiro positivo para indicar uma contagem de linhas que deve ser escrita em stdout .

Portanto, no exemplo acima, as primeiras 5 linhas são impressas para /dev/null , as próximas 6 para stdout , as próximas 7 para /dev/null novamente e as próximas 6 novamente para stdout . Tendo atingido o último dos seus args e totalmente percorrido a contagem de repetição -1 , ele é encerrado. Se o primeiro argumento tivesse sido -2 , teria repetido o processo mais uma vez, ou se - pelo tempo que pudesse.

Para cada ciclo de arg, o loop while é processado uma vez. Na parte superior de cada loop, a primeira linha de stdin é lida na variável $l da shell. Isso é necessário porque while head </dev/null; do :; done será repetido indefinidamente - head indica em seu retorno quando atingiu o final do arquivo. Portanto, a verificação em relação a EOF é dedicada a read e printf escreverá $l mais uma nova linha para stdout apenas se o segundo argumento for um inteiro positivo.

A verificação de read dificulta um pouco o loop, porque imediatamente após outro loop é chamado - um loop for que itera sobre args 2-$# como representado em $n para cada iteração de seu loop while pai. Isso significa que, para cada iteração, o primeiro argumento deve ser decrementado em um do valor especificado na linha de comando, mas todos os outros devem manter seus valores originais e, portanto, o valor do $_n marker var é subtraído de cada um. sempre contém um valor maior que 0 para o primeiro argumento.

Isso constitui o loop principal da função, mas a maior parte do código está no topo e destina-se a permitir que a função limpe até mesmo um pipe como entrada. Isso funciona primeiro chamando um dd em segundo plano para copiar seu arquivo tmp na saída em blocos de 4k por peça. A função então configura um loop de espera - que quase nunca deve completar um único ciclo completo - apenas para assegurar que dd tenha feito pelo menos uma única gravação no arquivo antes que a função substitua seu stdin por um descritor de arquivo ligado a o tmpfile e depois desassocia imediatamente o arquivo com rm . Isso permite que a função processe o fluxo de forma confiável sem exigir traps ou de outra forma para limpeza - assim que a função liberar sua declaração no fd, o arquivo tmp deixará de existir porque seu único link do sistema de arquivos já foi removido.

    
por 29.01.2015 / 03:34
0

Use uma função bash como esta:

seq 1 30 > input.txt
f(){ head $1 input.txt | tail $2 >> output.txt ;}; f -11 -2; f -24 -3
cat output.txt
10
11
22
23
24

Isso é um pouco exagerado nesse caso, mas se os filtros ficarem maiores, isso pode se tornar um benefício.

    
por 28.01.2015 / 09:47

Tags