Como encontrar a linha com menos caracteres

21

Estou escrevendo um script de shell, usando qualquer comando geral do UNIX. Eu tenho que recuperar a linha que tem menos caracteres (espaços em branco incluídos). Pode haver até 20 linhas.

Eu sei que posso usar head -$L | tail -1 | wc -m para encontrar a contagem de caracteres da linha L. O problema é que o único método em que posso pensar, usando isso, seria escrever manualmente uma confusão de instruções if, comparando os valores.

Exemplo de dados:

seven/7
4for
8 eight?
five!

retornaria 4for desde que a linha tenha menos caracteres.

No meu caso, se várias linhas tiverem o menor comprimento, uma única deverá ser retornada. Não importa qual é selecionado, contanto que seja do comprimento mínimo. Mas eu não vejo o mal em mostrar as duas maneiras para outros usuários com outras situações.

    
por Matthew D. Scholefield 03.06.2015 / 08:51

17 respostas

13

Um jeito Perl. Observe que, se houver muitas linhas do mesmo comprimento mais curto, essa abordagem só imprimirá uma delas:

perl -lne '$m//=$_; $m=$_ if length()<length($m); END{print $m if $.}' file 

Explicação

  • perl -lne : -n significa "leia o arquivo de entrada linha por linha", -l faz com que as novas linhas finais sejam removidas de cada linha de entrada e uma nova linha seja adicionada a cada chamada de print ; e -e é o script que será aplicado a cada linha.
  • $m//=$_ : defina $m para a linha atual ( $_ ) a menos que $m seja definido. O operador //= está disponível desde o Perl 5.10.0.
  • $m=$_ if length()<length($m) : se o comprimento do valor atual de $m for maior que o comprimento da linha atual, salve a linha atual ( $_ ) como $m .
  • END{print $m if $.} : depois de todas as linhas terem sido processadas, imprima o valor atual de $m , a linha mais curta. O if $. garante que isso só aconteça quando o número da linha ( $. ) for definido, evitando imprimir uma linha vazia para entrada em branco.

Como alternativa, como seu arquivo é pequeno o suficiente para caber na memória, você pode fazer:

perl -e '@K=sort{length($a) <=> length($b)}<>; print "$K[0]"' file 

Explicação

  • @K=sort{length($a) <=> length($b)}<> : <> aqui é uma matriz cujos elementos são as linhas do arquivo. O sort os classificará de acordo com seu tamanho e as linhas ordenadas serão salvas como array @K .
  • print "$K[0]" : imprime o primeiro elemento da matriz @K : a linha mais curta.

Se você quiser imprimir todas linhas mais curtas, você pode usar

perl -e '@K=sort{length($a) <=> length($b)}<>; 
         print grep {length($_)==length($K[0])}@K; ' file 
    
por 03.06.2015 / 11:29
17

Aqui está uma variante de uma solução awk para imprimir a primeira linha mínima encontrada:

awk '
  NR==1 || length<len {len=length; line=$0}
  END {print line}
'

que pode ser simplesmente estendido por uma condição para imprimir todas as linhas mínimas:

awk '
  length==len {line=line ORS $0}
  NR==1 || length<len {len=length; line=$0}
  END {print line}'
'
    
por 03.06.2015 / 09:38
16

com sqlite3 :

sqlite3 <<EOT
CREATE TABLE file(line);
.import "data.txt" file
SELECT line FROM file ORDER BY length(line) LIMIT 1;
EOT
    
por 03.06.2015 / 10:18
11

O Python é bastante conciso, e o código Faz o que diz no estanho:

python -c "import sys; print min(sys.stdin, key=len),"

A vírgula final é obscura, eu admito. Isso evita que a instrução print inclua um quebra de linha adicional. Além disso, você pode escrever isso no Python 3 com suporte a 0 linhas como:

python3 -c "import sys; print(min(sys.stdin, key=len, default='').strip('\n'))"

    
por 04.06.2015 / 13:59
10

Eu sempre adoro soluções com script de shell puro (sem exec!).

#!/bin/bash
min=
is_empty_input="yes"

while IFS= read -r a; do
    if [ -z "$min" -a "$is_empty_input" = "yes" ] || [ "${#a}" -lt "${#min}" ]; then
        min="$a"
    fi
    is_empty_input="no"
done

if [ -n "$a" ]; then
    if [ "$is_empty_input" = "yes" ]; then
        min="$a"
        is_empty_input="no"
    else
        [ "${#a}" -lt "${#min}" ] && min="$a"
    fi
fi

[ "$is_empty_input" = "no" ] && printf '%s\n' "$min"

Nota :

Existe um problema com bytes NUL na entrada. Então, printf "ababcd\ncd\n" | bash this_script imprime %code% em vez de %code% .

    
por 03.06.2015 / 09:46
8

Aqui está uma solução zsh pura (imprime todas as linhas com o comprimento mínimo, a partir de file ):

IFS=$'\n'; print -l ${(M)$(<file):#${~${(o@)$(<file)//?/?}[1]}}

Exemplo de entrada:

seven/7
4for
8 eight?
five!
four

A saída é:

4for
four

Eu acho que precisa de uma breve explicação: -)

Primeiro, definimos o separador de campo interno como nova linha:

IFS=$'\n';

Até aí tudo bem, agora a parte difícil. print usa o sinalizador -l para imprimir o resultado separado por novas linhas em vez de espaços.

Agora começamos por dentro:

$(<file)

O arquivo é lido linha por linha e tratado como array. Então:

${(o@)...//?/?}

O sinalizador o diz que o resultado deve ser ordenado em ordem crescente, o @ significa tratar o resultado como array também. A parte por trás ( //?/? ) é uma substituição e substitui todos os caracteres por ? . Agora:

${~...[1]}

Nós pegamos o primeiro elemento da matriz [1] , que é o mais curto, no seu caso agora é ???? .

${(M)$(<file):#...}

A correspondência é executada em cada elemento da matriz separadamente, e os elementos da matriz sem correspondência são removidos ( M ). Cada elemento que corresponde a ???? (4 caracteres) permanece na matriz. Então, os elementos restantes são os que têm 4 caracteres (os mais curtos).

Editar: Se você precisar de apenas uma das linhas mais curtas, essa versão modificada imprimirá a primeira:

IFS=$'\n'; print -l ${${(M)$(<file):#${~${(o@)$(<file)//?/?}[1]}}[1]}
    
por 03.06.2015 / 10:04
7
tr -c \n 1 <testfile |   #first transform every [^\n] char to a 1
grep -nF ''           |   #next get line numbers
paste -d: - testfile  |   #then paste it together with itself
sort  -t: -nk2,2          #then sort on second field

... e o vencedor é ... linha 2, parece.

2:1111:4for
4:11111:five!
1:1111111:seven/7
3:11111111:8 eight?

Mas o problema é que cada linha deve mais que dobrar de tamanho para que funcione - então LINE_MAX é reduzido pela metade. A causa é que ele está usando - o que, uma base 1? - para representar o comprimento da linha. Uma abordagem semelhante - e talvez mais arrumada - pode ser compactar essa informação no fluxo. A primeira ideia ao longo daquelas linhas que me ocorre é que eu devo unexpand it:

tr -c \n \  <testfile    |   #transform all [^\n] to <space>
unexpand -t10             |   #squeeze every series of 10 to one tab
grep -nF ''               |   #and get the line numbers
sed    's/:/!d;=;:/;h;:big    #sed compares sequential lines
$P;$!N; /\(:[^ ]*\)\( *\)\n.*.*/!D     #newest line is shorter or...
        g;/:./!q;b big'   |   #not; quit input entirely for blank line
sed -f - -e q testfile        #print only first occurrence of shortest line

Isso imprime ...

2
4for

Outro, apenas sed :

sed -n '/^\n/D;s/\(.\)\(\n.*\)*//g
$p;h;   s// /g;G;x;n;//!g;H;s// /g
G;      s/^\( *\)\(\n  *\)\{0,1\}\n//
D'      <infile >outfile

A sintaxe é compatível com os padrões - mas isso não é garantia de que qualquer sed antigo manipulará o \(reference-group\)\{counts\} corretamente - muitos não.

Basicamente, aplica-se o mesmo regexp à entrada repetidamente - o que pode ser muito benéfico quando é hora de compilá-los. Esse padrão é:

\(.\)\(\n.*\)*

Que corresponde cadeias diferentes de maneiras diferentes. Por exemplo:

string1\nstring2\nstring3

... corresponde a s em e '' a string nula em .

1\nstring2\nstring3

... corresponde a 1 em e \nstring2\nstring3 em

\nstring2\nstring3

... corresponde a \n em e '' a string nula em . Isso seria problemático se houvesse alguma chance de um \n ewline ocorrer na cabeça do espaço de padrão - mas os comandos /^\n/D e //!g são usados para evitar isso. Eu usei [^\n] , mas outras necessidades para este pequeno script tornaram a portabilidade uma preocupação e eu não fiquei satisfeito com as muitas maneiras em que isso é muitas vezes mal interpretado. Além disso, . é mais rápido.

\nstring2
string1

... corresponde \n e s novamente em e ambos recebem a sequência '' null em . Linhas vazias não combinam de todo.

Quando o padrão é aplicado g lobally , os dois vieses - o viés padrão mais à esquerda e o menor \n ewline - são contrabalançados para efetuar um salto . Alguns exemplos:

s/\(.\)\(\n.*\)*/:/g
s/\(.\)\(\n.*\)*/:/g
s/\(.\)\(\n.*\)*/: /g
s/\(.\)\(\n.*\)*/ :/g

... se todas forem aplicadas (não em sucessão) à seguinte string ...

string1\nstring2

... irá transformá-lo para ...

s:t:r:i:n:g:1:\nstring2
s:t:r:i:n:g:\nstring21:
s:t:r:i:n:g:1: 
 : : : : : : :\nstring2

Basicamente eu uso o regexp para sempre manipular somente a primeira linha em qualquer padrão de espaço ao qual eu aplico. Isso permite que eu faça malabarismos com duas versões diferentes de uma linha retida de correspondência mais curta até agora e a linha mais recente sem recorrer a loops de teste - cada substituição aplicada lida com todo o espaço de padrão de uma só vez.

As diferentes versões são necessárias para comparações literais de string / string - então deve haver uma versão de cada linha onde todos os caracteres sejam iguais. Mas, claro, se um ou outro deve ser realmente a linha mais curta que ocorre na entrada, então a linha impressa para a saída provavelmente deve ser a versão original da linha - não a que eu tenha higienizado / homogeneizado para comparação. E então eu preciso de duas versões de cada uma.

É lamentável que outra necessidade seja a troca de buffer para lidar com o mesmo - mas pelo menos nenhum buffer excede mais do que as quatro linhas necessárias para se manter atualizado - e talvez não seja terrível.

De qualquer forma, para cada ciclo, a primeira coisa que acontece é uma transformação na linha lembrada - porque a única cópia realmente salva é o original literal - em ...

^               \nremembered line$

... e depois a linha de entrada n ext sobrescreve qualquer buffer antigo. Se não contiver pelo menos um único caractere, ele será efetivamente ignorado. Seria muito mais fácil apenas q uit na primeira linha em branco, mas, bem, meus dados de teste tinham muitos deles e eu queria lidar com vários parágrafos.

E se ele contiver um caractere, sua versão literal será anexada à linha lembrada e sua versão de comparação espaçada será posicionada no início do espaço padrão, assim:

^   \n               \nremembered line\nnew$

Por último, uma substituição é aplicada a esse espaço padrão:

s/^\( *\)\(\n  *\)\{0,1\}\n//

Portanto, se a nova linha puder caber dentro do espaço necessário para conter a linha lembrada com pelo menos um caractere de sobra, as duas primeiras linhas serão substituídas, senão somente a primeira.

Independentemente do resultado, a primeira linha no espaço padrão é sempre D eletida no final do ciclo antes de iniciar novamente. Isso significa que, se a nova linha for menor que a última, a string ...

new

... é enviado de volta para a primeira substituição no ciclo, que sempre tira apenas do primeiro caractere de nova linha - e, portanto, permanece inteiro. Mas se não for então a string ...

remembered line\nnew

... começará o próximo ciclo, e a primeira substituição removerá a string ...

\nnew

... todas as vezes.

Na última linha, a linha lembrada é impressa como padrão, e assim, para os dados de exemplo fornecidos, ela imprime:

4for

Mas, sério, use tr .

    
por 03.06.2015 / 13:09
6

Tente:

awk '{ print length, $0 }' testfile | sort -n | cut -d" " -f2- | head -1

A idéia é usar awk para imprimir o comprimento de cada linha primeiro. Isto irá aparecer como:

echo "This is a line of text" | awk '{print length, $0}'
22 This is a line of text

Em seguida, use a contagem de caracteres para classificar as linhas por sort , cut para se livrar da contagem e head para manter a primeira linha (aquela com menos caracteres). É claro que você pode usar tail para obter a linha com o maior número de caracteres nesse caso.

(Esta foi adotada de esta resposta )

    
por 03.06.2015 / 09:03
5

Com o POSIX awk:

awk 'FNR==1{l=$0;next};length<length(l){l=$0};END{print l}' file
    
por 03.06.2015 / 09:05
3

Tomando emprestadas algumas das ideias de @ mikeserv:

< testfile sed 'h;s/./:/g;s/.*/expr length "&"/e;G;s/\n/\t/' | \
sort -n | \
sed -n '1s/^[0-9]+*\t//p'

O primeiro sed faz o seguinte:

  • h salva a linha original no buffer de espera
  • Substitua todos os caracteres na linha por : - isso é para remover qualquer perigo de injeção de código
  • Substitua a linha inteira por expr length "whole line" - esta é uma expressão de shell que pode ser avaliada
  • O comando e para s é uma extensão do GNU sed para avaliar o espaço padrão e colocar o resultado de volta no espaço padrão.
  • G acrescenta uma nova linha e o conteúdo do espaço de espera (a linha original) ao espaço padrão
  • o% final s substitui a nova linha por uma guia

O número de caracteres agora é um número no início de cada linha, portanto, sort -n é classificado por comprimento de linha.

O% final sed , em seguida, remove todas, exceto a primeira linha (mais curta) e o comprimento da linha, e imprime o resultado.

    
por 04.06.2015 / 21:17
3

Ocorreu-me que a coisa toda é possível em uma expressão sed . Não é bonito:

$ sed '1h;s/.*/&\n&/;G;:l;s/\n[^\n]\([^\n]*\)\n[^\n]/\n\n/;tl;/\n\n/{s/\n.*//;x};${x;p};d' testfile
4for
$ 

Quebrando isso:

1h            # save line 1 in the hold buffer (shortest line so far)
s/.*/&\n&/    # duplicate the line with a newline in between
G             # append newline+hold buffer to current line
:l            # loop start
s/\n[^\n]\([^\n]*\)\n[^\n]/\n\n/
              # attempt to remove 1 char both from current line and shortest line
tl            # jump back to l if the above substitution succeeded
/\n\n/{       # matches if current line is shorter
  s/\n.*//    # remove all but original line
  x           # save new shortest line in hold buffer
}
${            # at last line
  x           # get shortest line from hold buffer
  p           # print it
}
d             # don't print any other lines

O BSD sed no OS X é um pouco mais exigente com as novas linhas. Esta versão funciona para as versões BSD e GNU do sed:

$ sed -e '1h;G;s/\([^\n]*\)\(\n\)\(.*\)//;:l' -e 's/\(\n\)[^\n]\([^\n]*\n\)[^\n]//;tl' -e '/\n\n/{s/\n.*//;x;};${x;p;};d' testfile
4for
$

Note que esta é mais uma resposta "porque é possível" do que uma tentativa séria de fornecer uma resposta de melhor prática. Eu acho que isso significa que eu tenho jogado muito código-colf

    
por 05.06.2015 / 00:16
2

Outra solução perl: armazena as linhas em um hash-of-arrays, sendo a chave hash o comprimento da linha. Em seguida, imprima as linhas com a chave mínima.

perl -MList::Util=min -ne '
    push @{$lines{ length() }}, $_;
} END {
    print @{$lines{ min keys %lines }};
' sample 
4for
    
por 04.06.2015 / 18:18
2

Para obter apenas a primeira linha mais curta:

f=file; sed -n "/^$(sed 's/./1/g' $f | sort -ns | sed 's/././g;q')$/{p;q}" $f

Para obter todos os lances mais curtos, altere apenas {p;q} para p

Outro método (um tanto incomum) é ter sort do tipo real por tamanho . É relativamente lento, mesmo com linhas curtas, e se torna drasticamente mais lento à medida que o comprimento da linha aumenta.
No entanto, acho interessante a ideia de ordenar por sobreposição de chaves . Estou postando no caso de outros também acharem interessante / informativo.

Como funciona:
Ordenar por variantes de comprimento da mesma chave - key 1 que abrange toda a linha
Cada variante de chave sucessiva incrementa o tamanho da chave em um caractere, até o comprimento da linha mais longa do arquivo (determinada por wc -L )

Para obter apenas a primeira linha mais curta (classificada):

f=file; sort -t'
f=file.in; 
l=$(<"$f" wc -L)
k=$(seq -f "-k1.%0.0f" $l -1 1) 
sort -st'
f=file; sed -n "/^$(sed 's/./1/g' $f | sort -ns | sed 's/././g;q')$/{p;q}" $f
' $k "$f" | head -n1
' $(seq -f "-k1.%0.0f" $(<"$f" wc -L) -1 1) "$f" | head -n1

que é o mesmo que:

f=file; sort -t'
f=file.in; 
l=$(<"$f" wc -L)
k=$(seq -f "-k1.%0.0f" $l -1 1) 
sort -st'%pre%' $k "$f" | head -n1
' $(seq -f "-k1.%0.0f" $(<"$f" wc -L) -1 1) "$f" | head -n1
    
por 04.06.2015 / 17:34
2

Supondo que linhas em branco não sejam consideradas a linha mais curta e que linhas em branco possam existir, o seguinte AWK puro funcionará:

awk '
    {
        len   = length;
        a[$0] = len
    }
    !len { next }
    !min { min = len }
    len < min { min = len }
    END {
        for (i in a)
            if (min == a[i])
                print i
    }
' infile.txt
    
por 05.06.2015 / 12:54
2

Que tal usar a classificação?

awk '{ print length($0) "\t" $0 }' input.txt | sort -n | head -n 1 | cut -f2-
    
por 09.06.2015 / 01:28
1

Com o GNU awk

gawk '
    {
         a[length]=$0
    };
    END
    {
        PROCINFO["sorted_in"]="@ind_num_asc";
        for (i in a)
        {
            print a[i]; 
            exit
        }
    }
    ' file
  • Leia cada linha em uma matriz indexada por comprimento de linha.

  • Defina PROCINFO["sorted_in"] como @ind_num_asc para forçar array digitalização a ser ordenada pelo índice da matriz, ordenada numericamente

  • A configuração de PROCINFO na maneira acima força a linha com o menor comprimento a ser escolhido primeiro na travessia da matriz. Então, imprima o primeiro elemento da matriz e saia

Isso tem a desvantagem de ser um nlogn , enquanto algumas das outras abordagens são n no tempo

    
por 25.04.2019 / 01:29
1

Método de ferramentas de shell de nível médio, sem sed ou awk :

f=inputfile
head -n $(xargs -d '\n' -L 1 -I % sh -c 'exec echo "%" | wc -c' < $f | 
          cat -n | sort -n -k 2 | head -1 | cut -f 1)  $f | tail -1
    
por 25.04.2019 / 08:21