Inteiro com zeros à esquerda (portáteis)?

3

É um "recurso" do shell que um número com um zero à esquerda é interpretado como um número octal:

$ echo "$((00100))"
64

Mas não há como desautorizar esse "recurso" em muitos shells, então, torna-se difícil forçar a interpretação de uma seqüência de dígitos como um número decimal (ou outra base).

Quando há apenas um número para converter, existem vários programas externos que podem fazer o recorte:

expr "00100" + 0 
echo "00100" | sed 's/^0*//'
echo "00100" | grep -o '[^0].*$'
echo "00100" | awk '{print int($0)}'
echo "00100" | perl -pe '$_=int."\n";'

Mas leva algum tempo para executá-los cada vez que são necessários. Acumule o uso de tais ferramentas externas em muitas chamadas e o atraso se torna bastante grande. Apenas para medir o atraso causado, repita as chamadas acima de 1000 vezes e você receberá (em segundos):

expr      1.934
sed       3.450
grep      3.775
awk       5.291
perl      5.064

Claro (exceto expr) a maioria das ferramentas pode processar um arquivo com 1000 linhas em:

sed  file 0.004
grep file 0.003
awk  file 0.007
perl file 0.006

Se todos os valores individuais de 1000 estiverem disponíveis no mesmo ponto no tempo.
Isso não poderia ser o caso. Então, o que ainda resta a ser respondido é:

Existe uma maneira nativa (para o shell) de extrair um número inteiro que seja mais rápido do que chamar ferramentas externas para cada número inteiro individual (não uma lista em um arquivo)?

Cada chamada se acumula e o atraso se torna importante.

O processamento se torna mais envolvido se o número também tiver um sinal principal e você quiser rejeitar números inválidos.

    
por Isaac 08.08.2018 / 13:25

3 respostas

2

Note que enquanto $((010)) é requerido pelo POSIX para expandir para 8, vários shells não o fazem por padrão (ou apenas em alguns contextos) a menos que em um modo de conformidade como esse é um recurso você geralmente não quer.

Com zsh , isso é controlado pela opção octalzeroes (desativada por padrão, exceto na emulação sh / ksh).

$ zsh -c 'echo $((010))'
10
$ zsh -o octalzeroes -c 'echo $((010))'
8
$ (exec -a sh zsh -c 'echo "$((010))"')
8

Em mksh , isso é controlado pela opção posix (desativada por padrão):

$ mksh -c 'echo "$((010))"'
10
$ mksh -o posix -c 'echo "$((010))"'
8

No bash, não há opção para desativá-lo, mas você pode usar a sintaxe $((10#010)) ksh para forçar a interpretação em decimal (também funciona em ksh e zsh), embora em bash e mksh -o posix , $((10#-010)) não funciona (tratado como 10#0 - 010 , como você pode ver na expansão de $((-10#-010)) yielding -8 ), você precisa de $((-10#010)) (ou $((- 10#010)) para compatibilidade com zsh que reclama -10 sendo uma base inválida).

$ bash -c 'echo "$((10#010))"'
10

Com ksh93 , compare:

$ ksh93 -c 'echo "$((010))"'
8
$ ksh93 -c '((a = 010)); echo "$a"'
8

com:

$ ksh93 -c 'a=010; echo "$((a))"'
10
$ ksh93 -c 'printf "%d\n" 010'
10
$ ksh93 -c 'let a=010; echo "$a"'
10
$ ksh93 -c 'echo "$((010e0))"'
10
$ ksh93 -o letoctal -c 'let a=010; echo "$a"'
8

Então, pelo menos, se você estiver codificando especificamente para algum desses shells, existem maneiras de contornar essa "má funcionalidade".

Mas nada disso ajudaria ao escrever um script POSIX portátil; nesse caso, você deseja remover os zeros à esquerda, conforme mostrado.

    
por 08.08.2018 / 16:18
0

Algo semelhante pode ser feito em uma linha com:

$ a=-00100; a=${a%"${a#[+-]}"}${a#"${a%%[!0+-]*}"}; a=${a:-0}
$ echo "$a"
-100

Leva apenas 0,0482 para 1000 repetições, 100 vezes menos do que usando um programa externo.

É baseado em duas expansões de dois parâmetros:

  1. Extraia o sinal:
    • ${a#[+-]} remove o primeiro caractere, desde que seja um sinal.
    • ${a%"${a#[+-]}"} mantém o primeiro sinal, desde que seja um sinal.
  2. Remova todos os sinais e / ou zeros iniciais:
    • ${a%%[!0+-]*} remove começando em qualquer (não 0 ou + ou -) até o final.
    • ${a#"${a%%[!0+-]*}"} remove o acima, ou seja, todos os zeros e sinais iniciais.

Isso escolhe um sinal e remove todos os zeros à esquerda. No entanto, permite (sem erro):

  1. Vários sinais principais.
  2. Qualquer caractere após os sinais e zeros iniciais.
  3. Um número "fora do intervalo" (muito grande).

Se esses testes forem necessários, continue lendo.

O número de sinais pode ser testado com:

signs=${a%%[!+-]*} 
[ ${#signs} -gt 1 ] && echo "$0: Invalid number $a: Too many signs"

O tipo de caracteres permitido pode ser verificado com:

num=${a#"${a%%[!0+-]*}"}

any=${num%%[!0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ@_]*}
[ "$any" != "$num" ] && echo "$0: Invalid number $a"

hex=${num%%[!0123456789abcdefABCDEF]*}
[ "$hex" != "$num" ] && echo "$0: Invalid hexadecimal number $a"

dec=${num%%[!0123456789]*}
[ "$dec" != "$num" ] && echo "$0: Invalid decimal number $a"

E, finalmente, podemos aproveitar a capacidade de printf de imprimir um aviso para números "fora do intervalo" (somente para bases que o printf entende):

printf '%d' $sign$dec >/dev/null                            # for a decimal number
printf '%d' "${sign}0x$hex" >/dev/null                      # for hex numbers

Sim, todos os printf usam %d , não é um erro de digitação.

E, sim, todos os itens acima funcionam na maioria dos shells que possuem printf .

    
por 08.08.2018 / 13:25
0

Aqui está o seu exemplo x1000 no meu sistema:

$ cat shell.sh
#!/bin/dash
q=1
while [ "$q" -le 1000 ]
do
  z=-00100
  z=${z%"${z#[+-]}"}${z#"${z%%[!0+-]*}"}
  z=${z:-0}
  echo "$z"
  q=$((q + 1))
done

Resultado:

$ time ./shell.sh >/dev/null
real    0m0.047s

Agora, discordo do exemplo sed. Eu vejo um exemplo com um arquivo, mas eu sou Não vejo uma razão clara para usar um arquivo não é aceitável. também o exemplo com pipe é problemático porque um pipe não é necessário - nem está chamando sed 1000 vezes. se você simplesmente não pode usar um arquivo por qualquer motivo - um heredoc seria bem:

cat > sed.sh <<alfa
sed 's/^0*//' <<bravo
$(yes 00100 | head -1000)
bravo
alfa

Resultado:

$ time ./sed.sh >/dev/null
real    0m0.047s

Então, no meu sistema, é exatamente a mesma velocidade sem o barulho.

    
por 09.08.2018 / 05:20