Introdução
Algum tempo atrás escrevi um post falando sobre coprocessos [1], mas por falta de tempo não tinha fornecido nenhum exemplo elaborado sobre o assunto. Então, hoje, estarei postando um calculadora usando os conceitos de coprocessos. Essa calculadora é bem parecida com a pcalc [4] que basicamente realiza operações aritméticas com conversão de base implícita.
dcalc
A calculadora se chama dcalc e foi baseada na pcalc. A dcalc está escrita em shell script e utiliza o bc como coprocesso para fazer o trabalho árduo, ou seja, o cálculo do valor das expressões . No mais esse script é apenas um “front-end” para o bc, sendo que seu trabalho é manipular uma expressão de entrada e fornecer para o bc a expressão modificada.
O código
Antes de tudo irei postar o código e a seguir apresentarei os exemplos.
#!/bin/bash # [dcalc] # Calculadora com conversão de bases embutida. # # [Autor] # Marcos Paulo Ferreira (Daemonio) # undefinido gmail com # https://daemoniolabs.wordpress.com # -* UTF-8 *- # # [Uso] # $ chmod +x dcalc # $ ./dcalc '20 * ( (0xBFFFFA - 0xBFFFFB) / 5 ) + 0y1001011 - 0o1' # 70(10) : 70 # 70(2) : 0y1000110 # 70(8) : 0o106 # 70(16) : 0x46 # # + Versão 1.0, by daemonio @ Sun May 20 12:37:36 BRT 2012 # # + Versão 1.1, by daemonio @ Wed May 23 23:31:53 BRT 2012 # - Retirada da função recparser. Agora o parse é # iterativo. # - Opção -c # - Melhorada no código e nos comentários. # # # Variáveis do script # # Versão do script. VERSAO='1.1' # Número de digitos após casa decimal SCALE=3 # Valor '1' para mostrar variáveis # internas do script. DEBUG=0 # Formato limpo, sem formatação da saída (mostrar a saída # somente na base 10). O padrão é 0 (com formatação). # A opção -c altera o valor dessa flag para 1. CLEANFORMAT=0 # Correção de grau para radiano. Esse # valor corresponde a pi/180. PI180='.0174532925' # Expressão modificada depois que todos os # valores em outra bases foram convertidos. NOVAEXPRESSAO= # Recebe resultado final do bc. RESPBC= # # Funções # # Ajuda function show_help { echo 'dcalc '$VERSAO' - by daemonio' echo '[uso] dcalc [-c] <expressao>' echo '-c : Saida limpa, sem formatacao (base 10).' echo 'OBS:' echo ' 0y = denota numero na base binaria.' echo ' 0x = denota numero na base hexadecimal.' echo ' 0o = denota numero na base octal.' echo ' sen/cos = recebem parametro em graus.' echo ' sqrt() = tira raiz quadrada.' exit 1 } # Debug: Mostra a mensagem do parâmetro somente # se a variável DEBUG foi definida como '1'. function debug { [ "$DEBUG" = '1' ] && echo "$*" } # Transforma base 10 para a base N. # A base N vem no segundo parâmetro, e o # número em si vem no primeiro. function base10tobaseN { local num=$1 local base=$2 local format= if [ "$base" = 2 ] then # Valor negativo em complemento de 2. # Aqui transformamos para unsigned # com a ajuda de printf. num=$(printf '%u\n' "$num") echo "obase=2; $num; obase=A" >&4 read -u 5; echo $REPLY else [ "$base" = 8 ] && format='%o\n' [ "$base" = 16 ] && format='%X\n' printf "$format" "$num" fi } # Função de parser. Essa função converte os números em outras # bases para a base dez (transforma 0y111 em 7, por exemplo). # Ela também modifica as funções trigonométricas, seno e cosseno, # adicionando o fator de correção grau-radiano. function parsentrada { local t= local num= # O '#' indica que temos um número em outra base # na entrada. local exp=$(echo "scale=$SCALE; $1" | sed 's/\b0/#&/g') # Troca sen() por s() e cos() por c() e ainda # acrescenta o fator de correção grau-radiano. if [[ "$exp" =~ [sc] ]] then exp=$(echo "$exp" | sed "s/sen *(/s($PI180 * /g") exp=$(echo "$exp" | sed "s/cos *(/c($PI180 * /g") fi # Não há o que substituir, então simplesmente # retorna a expressão. if ! [[ "$exp" =~ '#' ]] then echo "$exp" return fi # Daqui em diante temos números em outras bases. Com # isso devemos substitui-los pelos seus correspondentes # na base 10. while [ "$exp" ] do if [ "${exp:0:1}" = '#' ] then # Recebe o caracter após o # '#'. Pode ser, y, x ou o. t=${exp:2:1} ; # Transforma base 2 em base 10 if [ "$t" = 'y' ] then echo "${exp:3}" | sed "s/\(^[0-1]*\).*/ibase=2; \1; ibase=A/" >&4 read -u 5; echo $REPLY # Transforma base 16 em base 10 elif [ "$t" = 'x' ] then num=$(echo "${exp:3}" | sed 's/\(^[0-9a-fA-F]*\).*/\1/; y/abcdef/ABCDEF/') printf '%u\n' 0x${num} # Transforma base 8 em base 10 elif [ "$t" = 'o' ] then num=$(echo "${exp:3}" | sed 's/\(^[0-8]*\).*/\1/') printf '%u\n' 0${num} # Número na base 10 que começa com zero. else num=$(echo "${exp:1}" | sed 's/\(^[0-9]*\).*/\1/') echo $num fi # Atualiza a expressão, retirando o número em outra base. exp=$(echo "$exp" | sed 's/^[#xyo0-9a-fA-F]*//') fi # Coloca na saída tudo antes do primeiro '#'. echo "$exp" | sed 's/^\([^#]*\).*/\1/' # Atualiza a expressão. Agora ela aponta # para o próximo '#', ou seja, o próximo # número a ser convertido. exp=$(echo "$exp" | sed 's/^.[^#]*//') done } # Formata a saída para algumas bases. function formatarsaidabases { echo -n "$1(10) : "; echo "$1" ; echo -n "$1(2) : 0y"; base10tobaseN $1 2 echo -n "$1(8) : 0o"; base10tobaseN $1 8 echo -n "$1(16) : 0x"; base10tobaseN $1 16 } # # MAIN # # Testa se passou opção -c [ "$1" = "-c" ] && CLEANFORMAT=1 && shift # Parâmetros insuficientes. [ -z "$1" ] && show_help # Cria coprocesso. # Veja que: # Descritor 4: Entrada do bc. # Descritor 5: Saída do bc. coproc bc -l exec 4>&${COPROC[1]} exec 5<&${COPROC[0]} # Debug: Expressão vinda da linha de comando debug "[+] Expressao linha de comando: $*" # Obtém a nova expressão já com os valores em # outras bases convertidos para a base 10. NOVAEXPRESSAO=$(parsentrada "$*" | tr -d '\n'; echo) # Debug: Mostra a saída do parser (entrada do bc). debug "[+] Entrada bc: $NOVAEXPRESSAO" # Envia a expressão completa para o bc. echo "$NOVAEXPRESSAO" >&4 ; read -u 5 RESPBC # Debug: Mostra a saída do bc (resultado total). debug "[+] Saida bc: $RESPBC" # Formata a saída somente se temos um número inteiro. if [[ "$RESPBC" =~ \..*[1-9] ]] then # Saída para float. echo "$RESPBC" elif [ "$CLEANFORMAT" = "0" ] then # Saída para inteiro formatado. RESPBC="$(echo "$RESPBC" | sed '/\..*/s///')" formatarsaidabases "$RESPBC" else # Saída para inteiro sem formato. RESPBC="$(echo "$RESPBC" | sed '/\..*/s///')" echo "$RESPBC" fi #EOF
Exemplos de Uso
A conversão implícita de base é feita através de prefixos. Esses prefixos são:
+---------+-------------+ |Prefixo | Base | +---------+-------------+ | 0x | Hexadecimal | +---------+-------------+ | 0y | Binária | +---------+-------------+ | 0o | Octal | +---------+-------------+
Sabendo disso agora fica fácil utilizar essa calculadora.
$ chmod +x dcalc $ ./dcalc '20 * (0y1101101)' 2180(10) : 2180 2180(2) : 0y100010000100 2180(8) : 0o4204 2180(16) : 0x884
O número 0y1101101 nada mais é que o 109 na base 2. Assim o resultado será 20*109 = 2180 na base 10. Veja que, quando o resultado é inteiro, a calculadora mostra o resultado nas 4 bases mais usuais: decimal, octal, binária e hexadecimal.
$ ./dcalc '-17' -17(10) : -17 -17(2) : 0y1111111111111111111111111111111111111111111111111111111111101111 -17(8) : 0o1777777777777777777757 -17(16) : 0xFFFFFFFFFFFFFFEF $ ./dcalc '-20 - 0y1010' -30(10) : -30 -30(2) : 0y1111111111111111111111111111111111111111111111111111111111100010 -30(8) : 0o1777777777777777777742 -30(16) : 0xFFFFFFFFFFFFFFE2
Para números negativos, a saída já sai em complemento de dois (o mesmo não ocorre para o bc/dc).
Podemos também usar expressões mais complexas:
$ ./dcalc '(0xFFAA + 0y111110)*(0o5557 + 0y1100) - 0xBABA' 192491966(10) : 192491966 192491966(2) : 0y1011011110010011000110111110 192491966(8) : 0o1336230676 192491966(16) : 0xB7931BE
dcalc é muito útil quando estamos debugando algum programa e necessitamos calcular offsets de memória. Imagine um buffer que se inicia no endereço 0xbfffffa4 e tem tamanho 64 bytes. Então o fim desse buffer está no endereço:
$ ./dcalc '0xBFFFFFA4 - 64' 3221225316(10) : 3221225316 3221225316(2) : 0y10111111111111111111111101100100 3221225316(8) : 0o27777777544 3221225316(16) : 0xBFFFFF64
O endereço final é dado nas 4 bases, mas o mais útil é na base hex, cujo valor deu: 0xBFFFFF64.
Algumas funções matemáticas também estão implementadas, graças ao bc. São elas:
+---------+---------------------------------+ | Função | Descrição | +---------+---------------------------------+ | sen(x) | Calcula seno de x (em graus) | +---------+---------------------------------+ | cos(x) | Calcula cosseno de x (em graus) | +---------+---------------------------------+ |sqrt(x) | Calcula raiz quadrada de x | +---------+---------------------------------+ | e(x) | Função exponencial | +---------+---------------------------------+
Exemplos usando essas funções matemáticas:
$ ./dcalc 'sen(30)' .500
Veja que o parâmetro é dado em graus, o oposto do bc, cujo parâmetro deve ser em radianos.
$ ./dcalc '(sen(0x1E)/cos(0x1E)) * 1/sen(0y11110)' 1.154
Esse exemplo calculou a secante de 30, pois 0x1E = 0y11110 = 30.
$ ./dcalc '(5 + sqrt(1))/2 + (5 - sqrt(1))/2' 5(10) : 5 5(2) : 0y101 5(8) : 0o5 5(16) : 0x5
O exemplo acima calculou a soma das raízes da equação de segundo grau: x^2 – 5x + 6 [3].
Detalhes sobre o código
1) Coprocessos
Logo no início do script é declarado um coprocesso usando o bc. Isso significa que o bc estará “conectado” ao script e poderá receber dados pelo descritor 4 e retornar dados em 5 (veja mais sobre exec em [2]).
Basicamente onde temos >&4 e read -u 5 é quando estamos interagindo com o coprocesso. Operações de conversões e aritméticas são feitas usando esse coprocesso.
2) Parser
A função parsentrada recebe a expressão de entrada e substitui os valores 0[xyo][0-9] por seu correspondente na base 10. Os números da forma 0[xyo][0-9] são substituídos por #[xyo][0-9], sendo o caractere especial ‘#’ usado como indicador. Dentro do loop while dessa função, há um conjunto de if’s que testam a base do número em questão, e após conversão desse número na base 10, o resultado é mostrado na penúltima linha da função.
Para entender como essa função foi elaborada, veja como ela manipula a seguinte entrada:
$ ./dcalc '20 * (0y1010 + 0xFACA) - 50' parsentrada exp = 20 * (#0y1010 + #0xFACA) - 50 ) |_____| saída = 20 * ( exp = #0y1010 + #0xFACA) - 50 ) |______| if $t = 'y' --> saída = 10 exp = + #0xFACA) - 50 ) |_| saída = + exp = #0xFACA) - 50 ) |_____| if $t = 'x' --> saída = 64202 exp = ) - 50) |_____| saída = ) - 50 Ao final, a saída completa será: 20 * ( 10 + 64202 ) - 50
A variável NOVAEXPRESSAO recebe a saída acima só que executando um tr para retirar os ‘\n’ desnecessários. No final teremos:
NOVAEXPRESSAO="20 * ( 10 + 64202 ) - 50" O valor de NOVAEXPRESSAO é passado para o coprocesso bc, e o resultado é: echo "$NOVAEXPRESSAO" >&4; read -u 5 RESPC RESPC=1284190 # Resultado final
Se o resultado for inteiro e a opção -c não foi passada, a saída será formatada, ou seja, o resultado será mostrado nas 4 bases. Se o resultado tem casas decimais ou a opção -c foi usada, a saída será sem formatação.
Setando a variável DEBUG com valor 1, podemos ver a expressão final e o resultado dela:
$ ./dcalc -c '20 * (0y1010 + 0xFACA) - 50' [+] Expressao linha de comando: 20 * (0y1010 + 0xFACA) - 50 # expressão de entrada [+] Entrada bc: scale=3; 20 * (10 + 64202) - 50 # expressão final [+] Saida bc: 1284190 # resultado da expressão 1284190
3) Conversão com printf
As conversões entre as bases 10, 16 e 8 é realizada usando printf, um comando built-in do bash. O problema do printf é que as operações são limitadas em 64 bits, isso é, qualquer número a ser convertido maior que 2^64, resulta num erro de overflow.
4) TODO
Se você achou esse script útil, fique livre em usá-lo e modificá-lo. Qualquer alteração no código que você julgue útil, poste aqui nos comentários.
Referências
[1] Entendendo Coprocessos (coproc) Do Bash, by Daemonio (Acessado em: Maio/2012)
https://daemoniolabs.wordpress.com/2011/07/19/entendendo-coprocessos-coproc-do-bash/
[2] Manipulando Arquivos Descritores No Shell, by Daemonio (Acessado em: Maio/2012)
https://daemoniolabs.wordpress.com/2012/02/24/manipulando-arquivos-descritores-no-shell/
[3] Raiz de uma Equação Completa do 2º grau, by Marcos Noé (Acessado em: Maio/2012)
http://www.brasilescola.com/matematica/raiz-uma-equacao-2-grau.htm
[4] pcalc, by vipier (Acessado em: Maio/2012)
http://sourceforge.net/projects/pcalc/
Post editado em: 25/05/2012 (post modificado e nova versão do script)