Melhor maneira de percorrer parcialmente uma string de 25k?

4

Eu tenho 25k caracteres.

Eu gostaria de escrever um script para imprimir ( printf é o mais portátil que me dizem) um número arbitrário de caracteres; percorrendo-os em ordem.

diga:

número do comando

Onde número pode ter qualquer valor de 1-25000 e obter essa saída.

Eu preferiria não ter os dados em um arquivo separado (a solução mais fácil?), e eu preferiria usar somente comandos shell POSIX (para tornar o script o mais portável possível: estou ciente de awk ou perl poderia acabar com isso simplesmente).

Devo armazenar esses dados em uma variável? Ou execute o comando printf completo por meio de um comando cut ( cut -c -$1 )? Ou existe outra solução (melhor?)? Por que posso escolher uma opção em vez de outra opção?

Quais são os outros problemas / ressalvas que estou vendo?

    
por user3082 13.12.2015 / 15:53

5 respostas

2

Você considerou o comando dd ? Ele permite que você pule qualquer número de bytes, em seguida, a saída de qualquer número de bytes.

dd if=infilename bs=1 skip=sk count=ct 2>/dev/null

dd , nome do arquivo de entrada, tamanho do bloco 1, ignorar primeiro sk bytes de entrada arquivo, em seguida, copiar ct bytes para stdout (ou especificar um arquivo com %código%). Redirecionar mensagens de erro para evitar as mensagens de status geralmente imprime no final.

    
por 13.12.2015 / 16:39
1

Provavelmente não é uma boa ideia armazenar dados grandes como uma variável, por razões de portabilidade e confiabilidade. Quanto a uma solução que não seja awk e que também seja POSIX, para que seja mais provável que seja portável, faça uso de sed .

Explicação

Para grandes quantidades de dados, evite armazenar como uma variável. Mesmo que O próprio Bash não impõe um limite, mas o sistema operacional pode fazê-lo

Suponha que você diga "funciona no meu sistema operacional". Mas,

  • Diferentes sistemas operacionais terão limites diferentes
  • Então, se você quer maximizar a portabilidade, por que arriscar seu script trabalhando em um sistema operacional e travando em outro apenas porque eles têm limites diferentes?
  • , para evitar esse problema, não armazenar em uma variável em primeiro lugar

Então, armazenamos em um arquivo. Especificamente, divida sua string em um caractere (ou qualquer outra unidade que desejar), em linhas separadas.

Em seguida, use sed :

  • sed não precisa carregar todo o arquivo enorme, ele funciona linha por linha
  • sed é definido nas especificações POSIX , para cumprir seu requisito% POS_de%, mas ainda POSIX

Além disso, pense nas vantagens de manutenção de código do uso de um arquivo. A atualização de linhas armazenadas em um arquivo pode ser mais fácil do que navegar no código em um script.

Exemplo

Tenha os dados, um caractere (ou qualquer unidade menor que você queira "percorrer") por linha, por exemplo, em um arquivo non-AWK :

a
b
c
d
e

Tenha seu data.lst contém:

#!/bin/bash

stop_number="$1"

sed -n "1,${stop_number}p" data.lst

Então, você testa isso no prompt de comando e vê:

$ ./script.sh 3
a
b
c
  • usa script.sh para imprimir a linha sed até o número especificado por 1 . Escrevemos $stop_number em vez de $stop_number diretamente, para maior clareza
  • $1 é obtido através do parâmetro posicional $stop_number , que é a entrada numérica arbitrária que você queria
  • , então ele passou com êxito por 3 caracteres de $1 , na sequência exibida em data.lst
  • no momento, se você inserir um número maior que o número real de linhas, ele mostrará todas as linhas.
  • no momento, data.lst está no mesmo diretório que data.lst , mas se você não quiser, se realmente tiver em outro lugar, como script.sh , basta ajustá-lo para dizer ~/some/dir/data.lst

Assim, depois de ter seus dados reais em ~/some/dir/data.lst , você mesmo poderá testar esse script.

    
por 13.12.2015 / 17:43
1

Emprestando um pouco de Tom:

#!/bin/sh
skp(){  dd bs="$1" skip=1 count=0; }    # direct seek to target
rd (){  dd bs="$1" skip=0 count=1; }    # single read at target
tail=$(sed -ne'/^don/{=;q;}' <"$0")     # skip script by line#
while   [ 1 -gt "$#" ] && exit          # exit when args exhausted
        exec <&- <"$0" || exit          # exec <"$0" each iteration
do      head -n "$tail" >&3             # only consider the tail
        case ${2+$1}  in                # test args
        (*[1-9]*|-*[!0]*)               # skp() when ${2++} && $1 != 0
          skp "$1";esac 2>&3            # send stderr to dev/null
          rd  "${2-$1}" 2>&3            # else just rd() from head of offset
        echo; shift ${2+"2"}            # append a newline and shift args away
done    3>/dev/null                     # put your data below this

não faça isso em uma variável - coloque em seu arquivo. uma variável de 25k não será divertida para o shell, e seu arquivo pode ser buscado em uma única ação atômica, praticamente . Então, se você quiser imprimir bytes 23843 - 24843, você poderia fazer algo parecido com o acima, e depois chamá-lo com:

myscript 23843 1000

... e primeiro um head irá cair do padrão compartilhado no descritor de arquivo todas as linhas do seu script para que o deslocamento seja definido exatamente na cabeça da sua string de 25k, então o primeiro dd procura esse deslocamento ~ 23k, e o segundo dd irá lê-lo. é a maneira mais simples de fazer isso. o shell é feito para a leitura de caractere por caractere - o read interno de um shell típico, por exemplo, faz um um byte read() em um loop até encontrar uma nova linha - e não para até que isso aconteça. dd fará um read por par de argumentos.

Eu testei assim:

# after a copy to my clipboard
ddscr(){ sh /tmp/ddscr.sh "$@"; }
{ xsel; man man; } > /tmp/ddscr.sh
{ echo show the size; ls -l /tmp/ddscr*
  echo read from the top; ddscr 80
  echo from the middle;   ddscr 15k 160
  echo from the tail;     ddscr 64k | tail -n5
}
show the size
-rw-r--r-- 1 mikeserv mikeserv 37564 Dec 13 11:27 /tmp/ddscr.sh
read from the top
MAN(1)                           Manual pager utils                          MAN
from the middle
lso use manconv(1) directly.
              However, this option allows you to convert several manual pages to  a
              single  encoding  without  having
from the tail
       31st  March  2001  -  present day: Colin Watson <[email protected]> is now
       developing and maintaining man-db.

2.7.5                                2015-11-06                              MAN(1)

... e ...

ddscr 10k 10 20k 10 10250 10
       fi
is  option
le.   If
    
por 13.12.2015 / 19:00
1

Bytes simples

Se a cadeia contiver apenas bytes ASCII e nenhuma nova linha, você poderá usar cut . O comando cut poderia trabalhar com apenas bytes .

$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 1-5
ajgjk

Mas isso falhará assim que a string de corte contiver caracteres de múltiplos bytes:

$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 7-12
géós

Existem 6 bytes (7,8,9,10,11 e 12), mas esses não são 6 caracteres.
E o corte também quebra se houver novas linhas na string.

Caracteres

Para trabalhar com caracteres "multi-byte", precisamos de uma ferramenta que entenda esses caracteres de multibyte, ambos sed e awk do.
O comando sed pode extrair caracteres de uma string:

$ s=5;l=3;echo "ajgjkggéóskm" | sed -E 's/^.{'"$s"'}(.{'"$l"'}).*//'
ggé

Mas a opção -E é uma extensão GNU, por isso precisamos mudar a linha para torná-la compatível com um POSIX sed (somente BRE regex):

$ s=5;l=3;echo "ajgjkggéóskm" | sed 's/^.\{'"$s"'\}\(.\{'"$l"'\}\).*//'
ggé

Se a string não contiver novas linhas. Como sed quebra a entrada é linhas em cada caractere de nova linha.
Isso quebra assim que houver novas linhas:

$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee' | sed 's/^.\{'"$s"'\}\(.\{'"$l"'\}\).*//'
jéw
skm
qwe

A saída é de fato 3 caracteres começando com a primeira (1), mas para cada linha.

Chars e novas linhas.

A única outra ferramenta disponível é o awk. Que faz ter uma especificação POSIX . Usando as Funções de string disponíveis no AWK :

$ s=6;l=4;echo "ajgjkggéóskm" | awk -v m="$s" -v n="$l" '{print substr($0,m,n)}'
ggéó

Mas isso também quebra novas linhas:

$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee'  | awk -v m="$s" -v n="$l" '{print substr($0,m,n)}'
ajé
ósk
ðqw

No entanto, podemos especificar um caractere que não deve estar em uso na string, como o RS (separador de registro): o byte nulo (\ 0). Isso proíbe as strings que contêm NULs (\ 0), um problema muito raro.

Por favor note que estou não falando sobre a string vazia: '' , pois isso fará com que o awk use uma "linha vazia" como separador de registro.

Para fazer isso, usarei uma capacidade de bash (nem todas as shells poderiam fazer isso) de escrever um zero byte como este: $'main "$@"' . Para outras camadas, a solução deve ser diferente.
Se o AWK estiver configurado com esse RS, ele receberá toda a entrada como um registro.

$ s=1;l=3;echo $'ajéw\nóskmæß\nðqwee'  |
awk -v RS=$'
#!/bin/bash
main(){
    while [ $# -gt 0 ]; do
        s=${1//[^0-9]/}; s="$((${s?Missing start of text.}+0))"
        l=${2//[^0-9]/}; s="$((${l?Missing start of text.}+0))"
        echo "from $s read $l characters"
        shift 2
        readchars "$s" "$l"
    done
}

readchars(){
    awk -v RS=$'
$ ./script.sh 35 12
from 35 read 12 characters
mæßð
aéóskmæ
' -v sstr="$1" -v lstr="$2" ' {printf("%s\n",substr($0,sstr,lstr))} ' <<-\_safe_place_for_string_ aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð aéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð _safe_place_for_string_ } main "$@"
' -v m="$s" -v n="$l" '{print substr($0,m,n)}' ajé

Não há mais interferência de nova linha. Bem, precisamos usar printf para evitar alguns problemas com a impressão de novas linhas. Com isso, poderíamos construir um script.
Algumas notas no script, como não é tão normal:

  • O script inicia a execução na última linha: _safe_place_for_string_ . Isso garante que todo o script tenha sido lido por bash e que ambas as funções definidas tenham sido analisadas.
  • A (s) linha (s) entre os dois _safe_place_for_string_ deve ser preenchida com qualquer texto que você precise incluir no arquivo.
  • O último } deve estar no início de uma linha, não ter texto adicional depois dele (nem mesmo espaços) e começar no início da linha (ou depois de um caractere de tabulação).
  • Depois disso, deve haver o fechamento da função main "$@" e o início da execução: \n

O script:

$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 1-5
ajgjk

Chame o script como:

$ echo "ajgjkggéóskmæßðasgbmdéóskmæßðushghsvéóskmæßð" | cut -b 7-12
géós

Note que a primeira "nova linha" vem de dentro da string. A última "nova linha" foi adicionada pelo ./script.sh 35 12 17 12 no printf, você pode removê-la, se necessário.

Ou até mesmo como while , o %code% interno processará as chamadas repetidas. Lembre-se de colocar seu texto no script para obter a saída esperada.

    
por 14.12.2015 / 01:22
0

Se você realmente quiser incluir programas e dados no mesmo arquivo, a melhor maneira é usar perl . Não sei por que você acha que não é portátil: é padrão em qualquer distribuição Unix que você venha a encontrar (incluindo Linux e OS X); você não vai encontrá-lo padrão no Windows, mas você não encontrará bash lá também.

#!/usr/bin/env perl
print substr(<DATA>, $ARGV[0], $ARGV[1]), "\n";

__DATA__
Just add all your text after 
the __DATA__ line... no fuss, no quoting, 
no tricks

Por exemplo, suponha que você nomeie-o selective_print e queira imprimir 30 caracteres a partir de 10:

% selective_print 10 30
    
por 13.12.2015 / 22:24