Elmord's Magic Valley

Software, lingüística e rock'n'roll. Às vezes em Português, sometimes in English.

Posts com a tag: unix

Determinando a posição do cursor em um arquivo com o lsof

2013-07-18 23:21 -0300. Tags: comp, unix, mundane, em-portugues

Às vezes executamos comandos do tipo:

cat imagem_grande_que_demora_para_copiar.img >/dev/sdb

e queremos saber o andamento do processo. Quando copiamos os dados de um disco para um arquivo podemos simplesmente olhar o tamanho do arquivo para ter uma idéia do andamento da operação, mas quando o destino é um disco isso não é possível.

É possível, entretanto, olhar a posição do cursor no arquivo. No Unix, todo arquivo aberto tem associado a si um cursor, i.e., a posição a partir da qual operações de leitura e escrita operam por padrão; cada leitura ou escrita no arquivo avança a posição do cursor.

Podemos utilizar um programinha chamado lsof (list open files, pacote lsof no Debian/Ubuntu) para visualizar diversas informações sobre arquivos abertos, dentre elas a posição do cursor. Por padrão, o lsof mostra uma coluna que contém ou a posição do cursor ou o tamanho do arquivo, dependendo do tipo de arquivo; a opção -o (offset) força o lsof a mostrar sempre o cursor. Além disso, por padrão o lsof mostra a posição em hexadecimal se ela ocupar mais de 8 dígitos decimais; a opção -o 0 desabilita esse comportamento. As duas opções podem ser combinadas como -oo0.

Por padrão, todos os arquivos abertos são listados. É possível especificar os nomes dos arquivos a serem listados, ou os nomes (-c nome) ou PIDs (-p pid) dos processos cujos arquivos abertos se deseja ver. Por exemplo:

# cat /dev/sda5 >/dev/sda6 &
[1] 28252
# lsof -oo0 /dev/sda6
COMMAND   PID USER   FD   TYPE DEVICE      OFFSET NODE NAME
cat     28252 root    1w   BLK    8,6 0t254476288 1303 /dev/sda6

Para mais informações, consulte a manpage do lsof.

Appendix A: Do fato de que ninguém deveria usar dd para copiar discos sem uma boa razão

# ls -lah foo
-rw-r--r-- 1 root root 512M Jul 18 23:00 foo
# time cat foo >bar

real    0m21.304s
user    0m0.068s
sys     0m5.212s
# time dd if=foo of=bar
1048576+0 records in
1048576+0 records out
536870912 bytes (537 MB) copied, 39.397 s, 13.6 MB/s

real    0m39.621s
user    0m1.528s
sys     0m25.910s

O motivo para isso é que o dd sempre copia os arquivos usando um tamanho de bloco fixo (indicado pelo parâmetro bs=tamanho, 512 bytes por padrão), enquanto o cat usa um tamanho de bloco "ótimo", o que permite que ele faça a cópia com menos chamadas de sistema (o que se reflete no tempo de sys na saída do comando time). Alternativamente, você pode especificar um tamanho de bloco maior para o dd (e.g., bs=1M), mas isso não apresenta nenhuma vantagem sobre usar o cat, a menos que você queira especificar o tamanho do arquivo também (e.g., no clássico dd if=/dev/zero of=foo bs=1M count=512 (que no entanto também pode ser substituído por um head -c 512M /dev/zero >foo)).

1 comentário / comment

Coisas que você não sabe sobre a glibc

2013-05-29 11:48 -0300. Tags: comp, prog, c, unix, em-portugues

Em algum momento do ano passado, por falta de coisa melhor para fazer, eu me parei a ler o manual da GNU libc. Não cheguei a ir muito longe, mas descobri um bocado de coisas interessantes no processo.

scanf

A scanf é uma das primeiras funções que vemos quando aprendemos C. Por isso mesmo, acabamos vendo só a funcionalidade básica para sobrevivência. Aí achamos que conhecemos a scanf e nunca mais nos preocupamos com ela. Ela possui um bocado de features interessantes, entretanto:

Other I/O

Miscelânea

No más

A glibc tem muita coisa (a versão em PDF do manual tem cerca de mil páginas). Vale a pena dar uma olhada no manual, nem que seja apenas para descobrir que tipo de recursos ela fornece, caso um dia você precise de algum deles.

3 comentários / comments

Tcl pode ser um bom shell

2013-05-23 00:01 -0300. Tags: comp, unix, shell, em-portugues

Para o meu choque e horror, acabei de me dar conta de que Tcl pode ser uma alternativa decente ao shell do Unix. Essa galera me convenceu ainda mais disso. Com meia dúzia de funções auxiliares para fazer piping e redirecionamentos de maneira mais conveniente, essa pode ser uma boa solução.

Por um lado, problem solved. Por outro lado, isso me tira uns 80% da motivação para escrever um shell. Talvez o que seria uma boa é criar uma "extensão" de Tcl que permita o uso da sintaxe convencional do shell para pipes e redirecionamentos. O fato de que o Tcl usa strings para representar todos os tipos de dados inclusive torna trivial o problema de passar dados estruturados entre processos no Unix.

Well, melhor seguir adiante com o resto dos meus planos de dominação mundial.

[P.S.: Eis uma introdução interessante aos poderes do Tcl.]

4 comentários / comments

Twitter via linha de comando

2013-05-07 14:27 -0300. Tags: comp, unix, web, about, em-portugues

Por falta de coisa mais interessante para fazer, e já que RSS não é exatamente a tecnologia da modinha, estou disponibilizando experimentalmente um feed do blog no Twitter. A continuidade do "serviço" está sujeita à existência de usuários. [Update: Pensando melhor, provavelmente todo o mundo que tem interesse em seguir blogs usa RSS. Enfim, o feed está aí, por enquanto.]

A parte interessante da história é que eu descobri um bocado de clientes de linha de comando do Twitter no processo. Dos que eu experimentei, o que melhor me satisfez foi o t. (Eu experimentei mais outros dois clientes: o TTYtter, um cliente interativo pra lá de bizarro, mas com mil features e aparentemente fácil de estender; e o twidge, que aparentemente não suporta UTF-8. Existem dúzias de outros clientes, como uma pesquisa no Google revela.)

Os poderes mágicos do t derivam do fato de ele ser particularmente conveniente de usar em scripts. Um exemplo extraído da documentação:

Favorite the last 10 tweets that mention you

t mentions -n 10 -l | awk '{print $1}' | xargs t favorite

É possível instalar o t pelo RubyGems, através do comando gem install t. Antes de instalá-lo, certifique-se de que você tem instalado o Ruby e o RubyGems (pacotes ruby, ruby-dev e rubygems no Debian/Ubuntu; não ter o ruby-dev é um problema comum).

Uma vez instalado, é necessário executar t authorize, para realizar o processo de registro da aplicação no Twitter e de autorização do acesso da aplicação à sua conta. Você pode executar t sem argumentos para ver uma lista dos comandos disponíveis. Para mais informações, dê uma olhada no README na página do projeto.

(Quem me contou foi essa página.)

4 comentários / comments

Gravando áudio e eliminando ruído com o SoX

2013-04-26 19:52 -0300. Tags: comp, unix, audio, mundane, em-portugues

O SoX (SOund eXchange; pacotes sox e libsox-fmt-all no Debian) é uma biblioteca e um programa de linha de comando que permitem converter entre diversos formatos de arquivo de áudio, opcionalmente aplicando filtros. A sintaxe básica do comando sox é:

sox [opções-globais]
    [opções-de-formato] entrada1
    [[opções-de-formato] entrada2 ...]
    [opções-de-formato] saída
    [filtros ...]

Por exemplo, para gravar do microfone (usando ALSA) em um arquivo WAV:

sox -t alsa default -t wav blabla.wav

(Use Control-C para terminar a gravação. Tecnicamente o -t wav pode ser omitido, já que o sox é capaz de deduzir o formato do arquivo pela extensão.)

Um par de filtros particularmente interessante é o noiseprof/noisered, que permitem eliminar ou reduzir ruído constante de fundo. Isso é feito em duas etapas. Primeiro, executa-se o sox com o filtro noiseprof [profile.txt] sobre uma "amostra de silêncio", i.e., um trecho de áudio que consista apenas do ruído de fundo, de maneira a produzir um profile de ruído. Você pode capturar o "silêncio" do microfone ou de algum outro arquivo que consista apenas de silêncio (a opção --null pode ser usada no lugar do arquivo de saída, já que estamos interessados apenas no profile de ruído):

sox -t alsa default --null noiseprof profile.txt

sox algum-arquivo-que-consista-apenas-de-silêncio.wav --null noiseprof profile.txt

Alternativamente, você pode selecionar um trecho de um arquivo com o filtro trim início [duração] e usá-lo como fonte de silêncio:

# Seleciona o intervalo de de 1s até 2.5s. Aqui usamos '-t alsa default' como
# saída para podermos ouvir se o trecho selecionado de fato corresponde a "silêncio".

sox entrada.wav -t alsa default trim 1 1.5 noiseprof profile.txt

Se o nome do arquivo de profile for omitido, o sox escreve o profile na stdout.

Gerado o profile de ruído, podemos usar o filtro noisered [profile.txt [quantidade]] para remover o ruído do arquivo completo. quantidade é um número entre 0 e 1 indicando a quantidade de ruído que deve ser removida. Quanto maior o número, mais ruído será removido – e mais não-ruído também. Experimente com números pequenos (e.g., 0, 0.05, 0.1, etc.) primeiro.

sox entrada.wav saída.wav noisered profile.txt 0.05

Se você tem um microfone problemático, você pode querer guardar o arquivo de profile para usos futuros (assumindo que o padrão de ruído produzido seja sempre o mesmo).

Se o arquivo de entrada para o noisered não for especificado ou for -, o sox lê o profile da stdin. Assim, você pode combinar o profiling e a redução em um pipeline:

sox entrada.wav --null trim 0 1 noiseprof | sox entrada.wav saída.wav noisered - 0.05

Para mais informações, consulte a manpage do sox.

2 comentários / comments

Gerando códigos de barras no GNU/Linux

2013-03-14 18:42 -0300. Tags: comp, unix, em-portugues

O GNU Barcode (pacote barcode no Debian) é uma biblioteca e um utilitário de linha de comando capaz de gerar códigos de barras de diversos tipos nos formatos PS e PCL. O programa possui diversas opções, que permitem controlar o tamanho e a posição dos códigos na página gerada, entre outras coisas.

Por exemplo, o comando:

barcode -c -e i25 -b 00123456 >codigo.ps

gera um PS com uma página contendo um código de barras sem checksum (-c) do tipo "interleaved 2-of-5" (-e i25), codificando a string 00123456. (Não vou nem dizer para que alguém geraria um código desses.)

Para converter o PS para um PNG, você pode usar o GIMP, ou o comando convert do ImageMagick (pacote imagemagick):

convert -trim codigo.ps codigo.png

Use it well.

2 comentários / comments

Convertendo arquivos com o LibreOffice pela linha de comando

2013-03-08 02:04 -0300. Tags: comp, unix, mundane, em-portugues

Acabei de descobrir que é possível converter entre os formatos de documento suportados pelo LibreOffice/OpenOffice pela linha de comando. A sintaxe é:

libreoffice --invisible --convert-to extensão[:nome-do-filtro]
            [--outdir diretório-de-saída] arquivo...

Mais de um arquivo pode ser especificado. Se o diretório de saída for omitido, o diretório atual é utilizado. --invisible faz com que a interface gráfica não seja carregada. Por exemplo, para converter uma apresentação em ODP para PDF, você pode usar o comando:

libreoffice --convert-to pdf apresentacao-feliz.odp

Quem me contou foi esse cara (e a manpage do libreoffice). Também existe um programa chamado unoconv, mas não cheguei a experimentá-lo.

Comentários / Comments

Migrando para o XKB

2013-02-23 21:48 -0300. Tags: comp, unix, kbd, em-portugues

(Se você não quer saber de preliminares, clique aqui.)

Depois de muita enrolação, decidi migrar meu keymap do xmodmap para o XKB. Sobrevivi à experiência.

O objetivo deste post é relatar parte do que eu aprendi no processo. Para uma referência mais ou menos completa da configuração do XKB, você pode consultar o mui excelente An Unreliable Guide to XKB Configuration, ou algum outro dos links listados no final do post sobre o xmodmap. Neste post, pretendo cobrir o básico necessário para migrar do xmodmap para o XKB (talvez um pouco mais do que o básico, mas enfim).

Little rant

O XKB é um pequeno monstro. Ele foi feito para resolver todos os problemas que ninguém teve antes dele. Por exemplo: existe uma seção do keymap, xkb_geometry, cujo objetivo é descrever a disposição física das teclas no teclado. Afinal, algum programa pode querer desenhar o teclado na tela (o comando xkbprint faz isso), ou saber que tecla fica do lado de qual. Nunca se sabe. Existem pelo menos três maneiras de descrever um layout pré-pronto (XkbKeymap, XkbRules, ou descrevendo componente por componente do keymap), e pelo menos três meios de ativar uma configuração (setxkbmap, xkbcomp ou pelo arquivo de configuração do X). E como todo programa no Unix, o XKB usa um arquivo de configuração com sua própria sintaxe idiossincrática, com seu próprio parser que emite mensagens de erro extremamente prestativas, como esta diante da falta de um ; na linha 21:

syntax error: line 23 of /root/.xkb/default
last scanned symbol is: modifier_map
Errors encountered in /root/.xkb/default; not compiled.

Alguém podia voltar no tempo e colocar um leitor de S-expressions na libc original.

Dito isso, muito da complexidade do XKB pode ser ignorada para tarefas mais simples de edição de keymap (leia-se: tudo o que era possível com o xmodmap). Além disso, o fato de o povo do X ter conseguido implementar as loucuras do XKB sem quebrar compatibilidade com os programas mais antigos é digno de três estrelinhas (pelo que eu entendo o próprio xmodmap continuou funcionando intacto depois da adição do XKB ao X, ainda que com ocasionais comportamentos estranhos).

But that's enough talk. Have at you!

Conceitos básicos

Antes de mais nada, gostaria de esclarecer que XKB não é um programa, e sim uma infraestrutura de configuração de keymap. O XKB é composto por uma extensão do servidor X e um conjunto de funções da biblioteca Xlib que torna as funcionalidades do XKB acessíveis aos clientes X. Além disso, o X vem com alguns programas utilitários, tais como xkbcomp, setxkbmap e xkbprint, que utilizam a infraestrutura do XKB para ler e manipular o layout. (O xmodmap, por outro lado, é um programa que utiliza a o "core protocol" do X para manipular o teclado; a função do xmodmap é vagamente similar à do xkbcomp e setxkbmap.)

Outra diferença entre o xmodmap/core-protocol e o XKB é que no XKB não existe (aparentemente) a noção de alterar o keybinding de teclas individuais do keymap; é necessário sempre carregar um keymap completo. Por outro lado, os arquivos de configuração do XKB suportam uma diretiva include, que nos permite incluir um keymap pré-pronto em um arquivo de configuração e especificar apenas o que queremos de diferente do default.

O programa principal de controle do XKB é o xkbcomp. A função principal desse programa é ler um arquivo texto descrevendo um keymap e compilá-lo para um formato binário que o X é capaz de digerir. A sintaxe básica é:

xkbcomp [opções] origem destino

Porém, o xkbcomp é uma criatura bastante flexível. Você pode especificar como destino algo como :0, instruindo o xkbcomp a compilar o keymap e ativá-lo no display :0, sem necessidade de criar um arquivo binário intermediário. Além disso, você pode especificar :0 como origem para fazer um dump do keymap atual para um arquivo texto especificado como destino (você pode usar - para imprimir para a stdout). O dump do xkbcomp é enorme (1844 linhas na minha máquina; arquivos de keymap na prática usam includes, o que os torna muito menores), mas é útil para vermos quais são as diretivas de configuração possíveis e qual é a sintaxe de cada uma.

A função primária do setxkbmap é pôr em efeito um keymap pré-pronto. A sintaxe básica é:

setxkbmap [opções] [layout [variant [option...]]]

Onde layout, variant e option são uma das três maneiras de descrever um keymap no X (XkbRules). Exemplos:

setxkbmap br abnt2                 # Layout ABNT-2
setxkbmap us intl                  # Layout US internacional (com acentos)
setxkbmap br abnt2 ctrl:swapcaps   # Layout ABNT-2 com Ctrl e CapsLock invertidos
setxkbmap br abnt2 ctrl:nocaps     # Layout ABNT-2, CapsLock se comporta como Ctrl

Como mencionado anteriormente, você pode atribuir keymaps diferentes a teclados diferentes usando a opção -device id, onde id é o id do dispositivo tal como exibido pelo comando xinput list.

O esquema de rules é muito bonito quando queremos usar um keymap pronto, mas não quando queremos fazer modificações ao keymap. Nesse caso, teremos que criar nosso próprio arquivo de keymap. Como não é possível aplicar um keymap parcial sobre o keymap existente com o XKB, e não queremos ter que descrever todo o keymap só para alterar meia dúzia de teclas, o ideal é usar includes. Felizmente, o setxkbmap suporta uma opção -print, que imprime um keymap do XKB que descreve o keymap atual (ou um especificado na linha de comando) em termos de includes:

# setxkbmap -print -option "" br abnt2
xkb_keymap {
        xkb_keycodes  { include "evdev+aliases(qwerty)" };
        xkb_types     { include "complete"      };
        xkb_compat    { include "complete"      };
        xkb_symbols   { include "pc+br(abnt2)+inet(evdev)"      };
        xkb_geometry  { include "pc(pc105)"     };
};

(O -option "" serve para resetar as options do XKB (tais como ctrl:nocaps e ctrl:swapcaps). Por padrão, o setxkbmap usa as opções ativas atualmente, mesmo que elas não sejam especificadas na linha de comando. Você também pode executar simplesmente setxkbmap -print, sem especificar um keymap, para usar o keymap atual (ou pelo menos o que o setxkbmap pensa que é o keymap atual).)

Eis um keymap completo! Se você salvar esse keymap em um arquivo, você pode usar o comando xkbcomp arquivo :0 para pôr o keymap em efeito.

Componentes e includes

Como podemos ver na saída do setxkbmap -print, um keymap é composto por cinco componentes:

Basicamente a única parte que nos interessa do keymap é a xkb_symbols; podemos ignorar todo o resto e simplesmente copiar a configuração emitida pelo setxkbmap.

A diretiva include instrui o xkbcomp a procurar os componentes especificados em seu caminho de pesquisa. Por padrão, o xkbcomp procurará no diretório /usr/share/X11/xkb. Dentro desse diretório você encontrará subdiretórios para cada componente (keycodes, symbols, etc.). O xkbcomp procurará no subdiretório apropriado dependendo da seção em que a diretiva include ocorrer.

Cada um desses arquivos pode conter mais de uma versão do mesmo componente. Uma diretiva da forma include "br(abnt2)" indica o componente chamado abnt2 dentro do arquivo br no diretório apropriado. Também é possível incluir múltiplos componentes, usando a sintaxe include "componente1+componente2".

Você pode adicionar outros diretórios ao caminho de pesquisa do xkbcomp usando a opção -Idiretório (sem espaço após o -I). O xkbcomp esperará encontrar os componentes a incluir dentro dos subdiretórios apropriados no diretório especificado (e.g., diretório/symbols, etc.).

Símbolos

Vamos ao que interessa: fazer com o XKB o que a essas alturas já teríamos feito com o xmodmap. Eis um arquivo de keymap de exemplo:

xkb_keymap {
    xkb_keycodes  { include "evdev+aliases(qwerty)" };
    xkb_types     { include "complete"      };
    xkb_compat    { include "complete"      };
    xkb_symbols   {
         include "pc+br(abnt2)+inet(evdev)"
         key <LatA> { [ a, A, aacute, Aacute ] };
         key <AE02> { [ 3, numbersign, threesuperior ] };
         key <CAPS> { [ Control_L ] };
         modifier_map Control { <CAPS> };
    };
    xkb_geometry  { include "pc(pc105)"     };
};

O trecho em negrito são os comandos que adicionamos; o resto foi gerado pelo comando setxkbmap -print -option "" br abnt2. Note que não vai ponto-e-vírgula após o include.

O principal comando da seção xkb_symbols é o key. A sintaxe básica é:

key <nome-da-tecla> { [ level1, level2, level3, level4 ] };

Onde <nome-da-tecla> é o nome simbólico da tecla definido na seção xkb_keycodes (uma maneira "prática" de descobrir os nomes das teclas é olhar a seção xkb_symbols do keymap atual com o comando xkbcomp :0 - | less), e level1~level4 são (basicamente) os símbolos emitidos pela tecla pura, Shift+tecla, AltGr+tecla e AltGr+Shift+tecla. Você pode especificar menos que quatro níveis, se desejar.

O comando modifier_map associa teclas a modificadores, e tem basicamente a mesma função do comando add no xmodmap, com a diferença de que no XKB ele funciona direito. A sintaxe é:

modifier_map ModifierName { <nome-da-tecla-1>, <nome-da-tecla-2>, ... };

Onde ModifierName é algo como Control, Shift, Lock, Mod1, etc. (i.e., os mesmos modifiers do xmodmap), e <nome-da-tecla-1> é um nome simbólico de tecla, como <CAPS> ou <LCTL> (left Control) ou <RALT> (right Alt).

É possível especificar mais de um comando modifier_map com o mesmo modificador (o que é ótimo, pois o arquivo em que estamos dando include contém também suas próprias definições de modifier_map). Aparentemente não é possível desassociar um modificador de uma tecla (i.e., aparentemente não existe algo equiparável aos comandos remove e clear do xmodmap), mas o XKB parece ser esperto o suficiente para limpar uma tecla automaticamente quando ela é redefinida pelo comando key (i.e., não é necessário fazer os malabarismos de clear e add que tínhamos que fazer com o xmodmap para remapear Control e CapsLock).

Você pode salvar esse keymap e executar xkbcomp arquivo :0 para pô-lo em vigor.

Simples, hã? Se tudo o que você queria era fazer com o XKB o que você fazia com o xmodmap, você já tem toda a informação de que precisa. (Exceto o comando pointer do xmodmap, cujo equivalente no XKB, se houver, eu não conheço.)

Levels e Groups

Se você fizer um dump do keymap atual com xkbcomp :0 -, verá que a sintaxe que ele usa para o comando key é um pouco mais verbosa:

key <AC01> {
    type= "FOUR_LEVEL_ALPHABETIC",
    symbols[Group1]= [ a, A, aacute, Aacute ]
};

Comecemos pelo fim. A sintaxe symbols[Group1] = [ ... ] indica que os quatro símbolos estão sendo associados aos quatro níveis do grupo Group1 da tecla. Basicamente, o nível de uma tecla é afetado pelas teclas Shift, AltGr e CapsLock. É por isso que, em versões modernas do X, a tecla AltGr é associada ao símbolo ISO_Level3_Shift, como discutido no post sobre o xmodmap.

Um grupo, por sua vez, é uma maneira de atribuir diversos conjuntos de símbolos para uma mesma tecla. Por exemplo, se você costuma escrever em russo, você pode ter um layout ABNT-2 no grupo Group1 e um layout russo no grupo Group2, e atribuir uma tecla para alternar entre os grupos. A vantagem de se usar, por exemplo, dois grupos de quatro níveis ao invés de um grupo de oito níveis, é que cada grupo pode ser definido em um arquivo separado. Se você usar uma diretiva do tipo:

include "pc+br(abnt2)+us(intl):2"

você está instruindo o xkbcomp a incluir o layout br(abnt2) como o primeiro grupo e o layout us(intl) como o segundo*. Você pode então definir uma tecla para alternar entre os dois grupos: o símbolo Mode_switch alterna para o próximo grupo enquanto a tecla é pressionada, e ISO_Next_Group, ISO_Prev_Group e outros alternam permanentemente o grupo atual (como se fosse um CapsLock para grupos).

Ao invés de usar a sintaxe symbols[Group1] = [ ... ], symbols[Group2] = [ ... ], você pode simplesmente especificar mais de um grupo envolvido em [ ... ] dentro do comando key:

key <AC01> { [ a, A, aacute, Aacute ], [ ae, AE, aring, Aring ] };

Agora voltemos ao começo. type= "FOUR_LEVEL_ALPHABETIC" diz que o tipo da tecla A é FOUR_LEVEL_ALPHABETIC. Se nós olharmos o conteúdo da seção xkb_types do dump do xkbcomp, encontraremos a seguinte definição:

type "FOUR_LEVEL_ALPHABETIC" {
    modifiers= Shift+Lock+LevelThree;
    map[Shift]= Level2;
    map[Lock]= Level2;
    map[LevelThree]= Level3;
    map[Shift+LevelThree]= Level4;
    map[Lock+LevelThree]= Level4;
    map[Shift+Lock+LevelThree]= Level3;
    level_name[Level1]= "Base";
    level_name[Level2]= "Shift";
    level_name[Level3]= "Alt Base";
    level_name[Level4]= "Shift Alt";
};

O que essa definição nos diz é que uma tecla do tipo FOUR_LEVEL_ALPHABETIC é afetada pelos modificadores Shift, Lock e LevelThree (i.e., AltGr), e que diferentes combinações desses modificadores ativam diferentes níveis da tecla. As teclas de pontuação, por outro lado, são do tipo FOUR_LEVEL. A definição de FOUR_LEVEL é similar à de FOUR_LEVEL_ALPHABETIC, mas não inclui o Lock entre os modificadores que afetam a tecla. As teclas do keypad numérico, por sua vez, são do tipo FOUR_LEVEL_KEYPAD, que é análogo ao FOUR_LEVEL_ALPHABETIC, mas usa o modificador NumLock ao invés de Lock para escolher os níveis superiores. Há diversos outros tipos de teclas, definidos de maneira similar.

End of Game

Por hoje ficamos por aqui. O XKB possui inúmeras outras features, sobre as quais talvez eu venha escrever no futuro. Para mais informações, consulte os links já referidos, e dê uma olhada na saída do xkbcomp :0 -.

____

* Três estrelinhas para o primeiro que achar essa informação documentada em algum lugar; eu descobri olhando o /usr/share/X11/xkb/rules/xorg, chutando o significado do :n e testando. By the way, aparentemente simplesmente não existe documentação oficial, que dirá uma especificação, da sintaxe desses arquivos. Para mais informações, consulte o código-fonte da libxkbfile. (E não, a gramática não é especificada por um arquivo yacc da vida. Vire-se no parser escrito à mão em C, se sua paciência for capaz.)

2 comentários / comments

Múltiplos keymaps no X

2013-02-09 03:50 -0200. Tags: comp, unix, mundane, kbd, em-portugues

(O script desenvolvido neste post pode ser encontrado aqui.)

Em um episódio anterior deste blog, vimos como usar o xmodmap para alterar o keymap do X. Também vimos que embora o xmodmap seja muito mais simples de usar do que o XKB, ele está semi-obsoleto e não lida muito bem com certos recursos mais modernos do XKB. Recentemente eu descobri mais um: o xmodmap não funciona direito com múltiplos teclados (e.g., um teclado builtin de notebook e um teclado externo). Especificamente:

  1. O xmodmap altera os layouts de todos os teclados conectados; não é possível atribuir um layout US para um teclado e um ABNT-2 para outro, por exemplo;
  2. O xmodmap altera apenas os layouts dos teclados conectados; se você ligar um novo teclado, ele assumirá o layout default, e você terá que rodar o xmodmap novamente para afetá-lo.

Sendo assim, temos dois problemas a resolver: detectar quando um teclado é conectado (o que independe de usarmos xmodmap ou XKB), e atribuir um layout a ele (o que exige o XKB se quisermos usar um layout específico para cada teclado; se quisermos apenas propagar o layout corrente para o novo teclado, podemos usar o xmodmap).

Detectando o teclado

A maneira mais palatável que eu encontrei de detectar o teclado* é monitorando o arquivo de log do X (/var/log/Xorg.n.log, onde n é o número do display). As linhas relevantes do arquivo têm a forma:

[ 10848.216] (II) XINPUT: Adding extended input device "GASIA GASIA USB KB Pro" (type: KEYBOARD)

Podemos fazer um script que monitora esse arquivo com o comando tail -f, filtra apenas os dados que nos interessam, e fica em um laço aguardando que novas linhas sejam escritas no arquivo:

#!/bin/bash

[[ $DISPLAY == :* ]] || echo "$0: Oops, remote display?" >&2
display="${DISPLAY##*:}"
display="${display%%.*}"

logfile="/var/log/Xorg.$display.log"

tail -n +1 -f "$logfile" |
    sed -une 's/.*XINPUT: Adding extended input device "\([^"]*\)" (type: \([^)]*\)).*/\2\t\1/p' |
    while IFS=$'\t' read type name; do

        echo "Dispositivo detectado: $name"
        # comandos...

    done

Salve esse script em algum local apropriado (e.g., ~/bin/xinput-monitor) e dê-lhe permissão de execução (chmod +x ~/bin/xinput-monitor). Você pode executá-lo como está para testar se os dispositivos estão sendo realmente detectados (experimente conectar um teclado externo com o script rodando).

A opção -n +1 faz com que o tail imprima o conteúdo do arquivo desde a primeira linha antes de começar a monitorá-lo; isso faz com que o script "detecte" mesmo os dispositivos que já estavam conectados antes de ele rodar. Se você não desejar esse comportamento, pode trocar o -n +1 por -n 0.

Se tudo o que você quer é rodar o xmodmap (ou qualquer outro comando) quando um dispositivo é conectado, basta colocar o comando no trecho indicado no while. Feito isso, basta pôr o script para rodar na inicialização de seu ambiente gráfico (colocando uma linha do tipo ~/bin/xinput-monitor & no seu ~/.xinitrc ou ~/.xsession, ou em algum arquivo de configuração do seu ambiente gráfico favorito).

Note que nem todos os dispositivos listados são teclados (nem mesmo todos os dispositivos com type: KEYBOARD são teclados); se você não quiser executar o xmodmap uma vez para cada um desses dispositivos (o que em tese é inofensivo, mas nunca se sabe), você pode usar um if ou case para ignorar os dispositivos que não lhe interessam. Por exemplo:

    ...
    while IFS=$'\t' read type name; do
        echo "Dispositivo detectado: $name"

        case "$name" in
            'Power Button'|'Video Bus'|'Integrated Webcam'|*'Synaptics'*)
                # Não faz nada.
                ;;
            *)
                # Os comandos que desejamos executar quando um teclado é conectado.
                xmodmap ~/.xmodmaprc
                xset r rate 250
                xkbset exp =m
                ;;
        esac
    done

Setando um layout específico para cada teclado

Para atribuir um layout individual para cada teclado conectado, teremos que abandonar o bom[citation needed] e velho xmodmap e entrar no maravilhoso[dubious – discuss] mundo do XKB. Felizmente, usar um layout pronto com o XKB é bem simples: basta executar um comando como:

# Layout ABNT-2.
setxkbmap -device device_id br abnt2

# Layout US internacional (com acentos).
setxkbmap -device device_id us intl

A opção -device device_id indica qual teclado o comando deve afetar. Se ela não for especificada, o comando afeta todos os teclados.

O grande problema é determinar o device_id de cada teclado; ele pode variar dependendo dos dispositivos presentes e da ordem em que o X os encontra. Aparentemente o id não aparece no arquivo de log do X. A solução é usar o xinput (que vem no pacote xinput no Debian, Ubuntu e companhia), um comando que permite ver e modificar diversas configurações de dispositivos de entrada no X (tais como os dez mil parâmetros que controlam velocidade, aceleração, gestures e outras firulas de touchpads), entre outras coisas. O comando xinput list produz uma listagem dos dispositivos presentes e respectivos ids, com a seguinte cara:

# xinput list
⎡ Virtual core pointer                          \tid=2\t[master pointer  (3)]
⎜   ↳ Virtual core XTEST pointer                \tid=4\t[slave  pointer  (2)]
⎜   ↳ SynPS/2 Synaptics TouchPad                \tid=13\t[slave  pointer  (2)]
⎜   ↳ GASIA GASIA USB KB Pro                    \tid=10\t[slave  pointer  (2)]
⎣ Virtual core keyboard                         \tid=3\t[master keyboard (2)]
    ↳ Virtual core XTEST keyboard               \tid=5\t[slave  keyboard (3)]
    ↳ Power Button                              \tid=6\t[slave  keyboard (3)]
    ↳ Video Bus                                 \tid=7\t[slave  keyboard (3)]
    ↳ Power Button                              \tid=8\t[slave  keyboard (3)]
    ↳ Integrated Webcam                         \tid=11\t[slave  keyboard (3)]
    ↳ AT Translated Set 2 keyboard              \tid=12\t[slave  keyboard (3)]
    ↳ GASIA GASIA USB KB Pro                    \tid=9\t[slave  keyboard (3)]

(Os \t representam TABs.)

Um problema visível nessa saída é que pode haver mais de um dispositivo com o mesmo nome (temos dois Power Button e dois GASIA GASIA USB KB Pro (sendo que um deles é um mouse (!))). Como chamar o setxkbmap sobre um não-teclado parece não ter efeito algum, podemos simplesmente tentar os dois ids e nos darmos por satisfeitos.

O primeiro passo é obter os ids a partir dos nomes. Para isso, podemos recorrer ao sed:

        device_ids="$(xinput list | sed -n "s|.*↳ $name *\tid=\([^\t]*\)\t.*|\1|p")"

Com os ids à mão, basta executar o setxkbmap para cada id, usando os parâmetros apropriados dependendo do nome do dispositivo detectado (altere as cláusulas do case para "configurar" o script para os seus teclados e layouts):

        for id in $device_ids; do
            case "$name" in
                'AT Translated Set 2 keyboard') setxkbmap -device "$id" us intl ;;
                'Some weird keyboard')          setxkbmap -device "$id" is ;;
                *)                              setxkbmap -device "$id" br abnt2 ;;
            esac
        done

'Tis over

E era isso. Eis uma versão completa do script. Ficou faltando resolver o problema de portar um xmodmaprc personalizado (caso você tenha um) para o XKB, mas isso fica para um próximo post.

(Este foi o 100-ésimo post do blog, por sinal.)

____

* Outra maneira freqüentemente citada pelo povo da Internet é alterar as configurações do udev para rodar um script quando o teclado é conectado. Além de exigir direitos de root, o script vai ter que descobrir o nome que o X atribui ao teclado por vodu, vai rodar como root (a menos que você tome a precaução de fazer o script trocar para o usuário "dono" da sessão X, o que implica descobrir quem é o dono), e você vai ter que tomar precauções para o caso de haver mais de um display X rodando (por exemplo, se você estiver usando a função "trocar de usuário" de certos ambientes gráficos). Just say no.

Comentários / Comments

The shell is completely wrong

2013-01-04 13:56 -0200. Tags: comp, prog, bash, unix, wrong, rant, em-portugues

Já faz algum tempo que eu estou para escrever uma série de posts sobre como os ambientes computacionais modernos estão completamente errados. Este não é um dos posts previstos. Ao contrário da maior parte das idéias que eu pretendia/pretendo expor na série, este post trata de um tópico dentro do âmbito do tranqüilamente implementável, sem envolver mudanças de paradigma. Considere este o zero-ésimo post da série. Considere este também o proto-manifesto de um shell que eu posso vir a escrever ou não durante as férias. Comentários são bem-vindos.

[Foto de John McCarthy com a legenda 'Programming: You're doing it completely wrong']

It's a fucking programming language

Although most users think of the shell as an interactive command interpreter, it is really a programming language in which each statement runs a command. Because it must satisfy both the interactive and programming aspects of command execution, it is a strange language, shaped as much by history as by design.

The Unix Programming Environment

Embora desde os primórdios o shell do Unix tenha sido reconhecido como uma linguagem de programação, as pessoas tendem a não encará-lo como uma "linguagem de verdade"; o shell serve para rodar "scripts", ao invés de "programas". Não só os usuários de shells têm essa visão, mas também os próprios desenvolvedores de shells: embora bash, zsh e companhia tenham acumulado diversas features ao longo dos anos, faltam nos shells features básicas que se espera encontrar em qualquer linguagem de programação razoável. Por exemplo, extensões do bash em relação ao Bourne shell (sh) original incluem:

  • Variáveis array (que só podem ser arrays de string);
  • Arrays associativos, i.e., arrays indexados por strings;
  • Um comando mapfile para ler o conteúdo de um arquivo para dentro de um array (mas não mapeia coisa nenhuma: alterações no array não se refletem no arquivo);
  • Sintaxe para permitir fall-through das cláusulas de um case (i.e., permitir o comportamento de um case sem break em C);

E no entanto:

  • Arrays só podem conter strings; não é possível criar estruturas de dados aninhadas;
  • Arrays não são elementos de primeira classe: não é possível passar um array como argumento para uma função, ou retornar um array; by the way...
  • Não existe um mecanismo decente para retornar valores de uma função; o jeito é usar uma variável global, ou imprimir o valor a retornar para a stdout e ler usando a sintaxe $(função args...), que cria um subprocesso só para executar a função e captura a saída.

Além disso, a sintaxe e a semântica de certas features são bastante facão. Isso em parte se deve ao fato de que o bash tentou acrescentar features novas sem quebrar compatibilidade com a sintaxe do sh, mas também por falta de princípios de design decentes. Um mecanismo para retornar valores de funções já seria um bom começo, e ajudaria a limpar outros aspectos do shell, já que seria possível forncecer mais funcionalidades através de funções, ao invés de usar sintaxe abstrusa (por exemplo, uma função lowercase string, ao invés da nova novidade, a substituição ${varname,,pattern}, onde pattern é opcional e indica quais caracteres devem ser transformados (sim, o padrão casa com cada caractere; se o padrão tiver mais de um caractere ele não casa com nada); ou uma função substr string start length, ao invés da substituição ${varname:start:length}, cujo uso de : conflita com a substituição ${varname:-string}, o que impede que start comece com -).

Se tanto desenvolvedores quanto usuários do shell tendem a concordar que o shell não foi feito para ser uma "linguagem de verdade", poder-se-ia argumentar que eu que sou o perdido e que o propósito do shell é de fato ser uma linguagem "de brincadeira". Mas que sentido faz isso? Para que termos uma linguagem de programação pela metade, se podemos ter uma linguagem mais decente adicionando umas poucas funcionalidades básicas?

É possível argumentar alguns motivos técnicos para a resistência a estruturas de dados. Um deles é que o mecanismo de invocação de programas é totalmente baseado em strings: não é possível chamar um programa externo passando um array como argumento, por exemplo, e isso não é culpa do shell, e sim da maneira como programas são chamados no Unix. (O que por sinal é lamentável; seria ótimo poder passar estruturas complexas entre os programas. Voltaremos a esse assunto mais adiante.) Isso não é um problema insuperável; só porque comandos externos não podem receber dados estruturados não quer dizer que não possamos passar dados estruturados internamente entre as funções do shell. O caso em que o usuário tenta chamar um comando externo com um array exige um tratamento especial (transformar o array em string, ou em múltiplos argumentos, ou emitir um erro), mas isso não é motivo para eliminar estruturas de dados complexas da linguagem.

(Outro possível motivo é medinho de pôr um garbage collector dentro do shell. Há quem ache ainda hoje que garbage collection é coisa do demônio. Mas dada a popularidade de linguagens garbage-collected hoje em dia (Java, Perl, Python, Ruby, C#, etc.) e dada a existência de bibliotecas de garbage collection para C, esse também não é um motivo forte.)

Shells alternativos

Houve e há diversos shells que tentam escapar da tradição do Bourne shell. Um deles (que serviu de inspiração para outros) é o rc, o shell do Plan 9, que possui versões para Unix. O rc tem uma sintaxe um pouco mais limpa (ou não) do que o sh, mas não apresenta grandes avanços em termos de features. Uma diferença não diretamente relacionada com o shell é que no Plan 9 o exit status de um programa é uma string, e não um inteiro entre 0 e 255 como no Unix, o que possibilita usar o exit status como meio de retornar valores de funções. Porém, o shell não apresenta nenhum recurso sintático para executar uma função e substituir a chamada pelo exit status.

Inspirado no rc surgiu o es, um shell com funções de primeira classe, variáveis com escopo léxico, closures e exceptions. Uma característica interessante do es é que boa parte dos internals do shell são expostos ao usuário. Por exemplo, o operador pipe (|) é apenas açúcar sintático para uma chamada a uma função %pipe, que pode ser substituída pelo usuário de modo a modificar o comportamento do pipeline (um exemplo dado no artigo linkado é calcular o tempo de execução de cada programa da pipeline). O es possui listas/arrays, mas não permite listas aninhadas. (O motivo oferecido é fazer com que passagem de arrays para comandos externos e para funções tenham a mesma semântica; porém, dado que o shell tem que lidar com a possibilidade de o usuário tentar passar uma função para um programa externo, esse argumento não é tão convincente. Não sei o que o shell faz nessa situação; pelo artigo eu suponho que ele passe o corpo da função como uma string, e de fato parece que o shell armazena as funções internamente como strings.) O es também não possui outros tipos de estruturas de dados complexas, embora seja possível implementá-las (ainda que de maneira convoluta) através de closures. O es também permite retornar valores complexos a partir de funções, com uma sintaxe para chamar a função e recuperar o valor. Um ponto levantado no artigo é que esse mecanismo de retorno e o mecanismo de exceptions não interage bem com comandos externos: um script externo não tem como retornar valores ou propagar uma exception para o script que o chamou. Voltaremos a esse tópico mais adiante.

Um shell posterior inspirado no rc e no es é o shell do Inferno. Esse shell não tem grandes novidades comparado com o es (embora o fato de ele rodar no Inferno lhe confira alguns poderes mágicos, como por exemplo realizar comunicação pela rede atavés do sistema de arquivos, e embora ele tenha alguns módulos interessantes, tais como uma interface gráfica). Entretanto, um ponto que chama a atenção é a sintaxe: comandos como if, for e while não são tratados de maneira especial sintaticamente. Ao invés disso, eles são comandos normais que recebem blocos de código como argumentos. A idéia é similar ao método each do Ruby: ao invés de se usar uma estrutura de controle especial para iterar sobre os itens de uma lista, chama-se um método que recebe um bloco de código e o chama sobre cada elemento:

# Ruby.
a = [1, 2, 3, 4, 5]
a.each {|i|
    puts i
}

Com isso, o usuário pode definir novas estruturas de controle que se comportam de maneira similar às estruturas padrão da linguagem.

Outros shells alternativos incluem o fish, um shell com diversos recursos interativos, mas (na minha humilde opinião) sem grandes avanços em termos de programação, e o xmlsh, um shell escrito em Java cujo objetivo é permitir a manipulação de estruturas de dados baseadas em XML, e que conspira em última instância substituir a comunicação baseada em texto dos programas atuais do Unix por um modelo de comunicação estruturada baseada em XML. (Voltaremos a esse assunto mais adiante (ainda).)

O que eu faria diferente

Um shell tem dois casos de uso distintos: como um interpretador de comandos interativo, e como um interpretador de programas. O fato de que o shell deve ser conveniente de usar interativamente se reflete na sintaxe de sua linguagem: strings literais geralmente não precisam ser colocadas entre aspas; valores literais são mais freqüentes do que variáveis, e portanto nomes por si sós representam strings (o caso mais freqüente), e variáveis são indicadas com um símbolo especial ($); execução de comandos externos usa uma sintaxe simples; há uma sintaxe conveniente para gerar listas de arquivos (*.txt, ao invés de uma chamada de função), combinar entrada e saída de comandos (|), redirecionar entrada e saída para arquivos, etc. Essa moldagem da sintaxe ao redor do uso interativo limita as possibilidades sintáticas das features voltadas à programabilidade (e.g., sintaxe para chamadas de função, estruturas de dados, operações aritméticas).

Conveniências lingüísticas à parte, a minha opinião é de que o núcleo do shell deva ser a linguagem de programação em si, e não as facilidades de uso interativo. Coisas como edição de linha de comando, histórico e completamento automático de nomes de arquivos e comandos não deveriam ser internas ao shell; ao invés disso, a linguagem do shell deveria ser suficientemente capaz para que essas coisas todas pudessem ser implementadas como scripts.

Em termos de features da linguagem, é possível tornar o shell mais hábil sem quebrar (muito) a compatibilidade com sh e bash. O primeiro passo é criar um mecanismo para permitir retornar valores de funções sem criar um subshell. Para isso, é necessário definir um comando para retornar valores, e uma sintaxe para chamar uma função e recuperar o valor. Eu usaria reply valor (já que return N já está em uso, para sair da função com um exit status N) e $[função args...] para isso. (Atualmente $[...] causa avaliação aritmética em bash. A sintaxe $[...] é considerada obsoleta, em favor da sintaxe do padrão POSIX, $((...)).)

O segundo passo é tornar arrays elementos de primeira classe, permitindo passá-los para funções e retorná-los, e permitindo armazená-los onde quer que se possa armazenar um valor (por exemplo, dentro de outros arrays). Acaba-se com a noção de "array variable" do bash: uma variável contém um array, não é um array. $array não retorna mais o primeiro valor do array, e sim o array inteiro. É possível passar um array literal para uma função:

função arg1 (1 2 3) arg3

Convém criar uma sintaxe para arrays associativos, possivelmente %(chave valor chave valor ...). Também convém usar um operador diferente para indexar arrays associativos e não-associativos, já que em bash, índices de arrays não-associativos sofrem avaliação aritmética:

i=0
echo ${array_comum[i]}   # Elemento da i-ésima posição
echo ${array_assoc{i}}   # Elemento cuja chave é a string "i"

(Outra alternativa seria nunca fazer avaliação aritmética sem que o programador mande explicitamente, mas isso não só quebra a compatibilidade como é inconveniente na prática.)

Word splitting implícita sobre a os valores de variáveis fora de aspas teria a morte horrível que merece. Pathname expansion sobre valores de variáveis teria as três mortes horríveis que merece.

Em bash, os elementos de uma pipeline são executados em subshells (com exceção do primeiro (ou do último se a opção lastpipe estiver ativa, outra novidade do bash 4)), o que significa que uma função usada em um pipeline não tem como alterar os valores das variáveis globais, pois as alterações que fizer serão isoladas em seu subprocesso, o que freqüemente é inconveniente. Por exemplo, código desse tipo não funciona em bash (embora seja possível contornar o problema em muitos casos):

files=()
find . -name '*.txt' | while read file; do
    files+=("$file")
done

echo "${files[@]}"   # Array continua vazio

Uma feature desejável seria que todos os itens de uma pipeline que possam ser executados internamente ao shell o sejam. Uma solução é, ao invés de criar novos processos, criar threads, e simular os redirecionamentos de entrada e saída do pipe internamente (e.g., se um comando função1 | função2 é executado, o shell executa cada função em uma thread, e faz mágica internamente para dar à função1 a ilusão de que o que ela imprime para a stdout vai para a stdout (file descriptor 1), quando no entanto os dados vão parar em um outro file descriptor usado para fazer a comunicação entre as funções (ou mesmo em uma string, evitando chamadas de sistema para leitura e escrita em file descriptors)). O mesmo mecanismo pode ser usado para executar $(função args...) sem gerar um subprocesso.

Oops! Mas o ponto de inventar a sintaxe $[...] e reply ... era justamente evitar criar um subprocesso! Uma diferença, entretanto, é que a sintaxe $(...) só serve para retornar strings (pela sua definição), enquanto $[...] serve também para retornar estruturas de dados complexas.

Dado o paralelo entre $(...) e as pipelines, ocorre a idéia interessante de termos um equivalente do pipeline para $[...], i.e., um pipeline capaz de passar dados estruturados. Com isso, poderíamos escrever "generators", a la Python e companhia. Algo do tipo:

generate_factorials() {
    local i=1 val=1
    while :; do
        yield $val # Entrega um valor para a próxima função; aqui usamos um valor simples,
                   # mas poderíamos passar qualquer estrutura de dados.
        ((i++, val*=i))
    done
}

consume() {
    local val
    while val=$[take]; do
        echo "Recebi $val"
    done
}

generate_factorials ^ consume   # Sintaxe hipotética para o "pipeline de objetos"

Hmmrgh, agora temos dois conceitos parecidíssimos mas diferentes no shell. Quem sabe se ao invés de usar um tipo especial de pipeline, usássemos o mesmo mecanismo de pipeline para as duas coisas? Conceitualmente o comando yield então escreveria o valor para a fake stdout, e take o leria da fake stdin, enquanto internamente o shell transferiria o objeto de uma função para a outra. Da mesma forma, por consistência, o comando reply escreveria o valor de retorno para a fake stdout, e $[...] o leria da fake stdin. (Não temos como unificar $(...) e $[...] porque a semântica das duas expressões é diferente: uma retorna uma string, não importa o que o subcomando faça, enquanto a outra pode retornar qualquer tipo de valor. $[...] é análogo a um take, enquanto $(...) é análogo a um read.)

A questão é: se yield escreve na fake stdout, o que é que ele escreve? A princípio, não precisaria escrever nada "real": contanto que haja um take na outra ponta, poderíamos transferir o valor do yield para o take por mágica internamente, e apenas conceitualmente escrever no file descriptor correspondente à pipeline. Porém, se ela escrevesse alguma coisa, e essa coisa representasse o valor a ser transferido, as duas pontas do pipeline não precisariam mais estar no mesmo processo! Quer dizer, da mesma forma que o nosso pipeline comum sabe distinguir se as duas pontas estão em processos diferentes ou não, e usa file descriptors de verdade ou fake stdin/stdout dependendo do caso, o nosso pipeline de objetos também poderia fazer o mesmo. Se as duas pontas estão no mesmo processo, transferimos o objeto puro e simples. Mas se as duas pontas estão em processos distintos, podemos serializar o objeto de um lado e des-serializar do outro, de modo que é possível passar uma stream de objetos para um script externo de maneira transparente. Da mesma maneira, $[...] poderia funcionar com scripts externos, lendo o valor de retorno serializado da stdout do script e des-serializando-o novamente. Assim, resolvemos parte do problema mencionado no artigo do shell es: conseguimos fazer nossos valores complexos conviverem com o mundo texto-puro do Unix.

Em parte: falta dar um jeito de podermos passar argumentos complexos para os programas externos. A princípio poderíamos passar uma representação serializada dos argumentos. Porém, precisamos arranjar um meio de permitir ao programa chamado distinguir o array (1 2 3) da string composta pelos sete caracteres (1 2 3). Uma idéia seria sempre passar as strings com aspas em volta. Mas não podemos fazer isso porque não sabemos se o programa chamado é um script ou não, e portanto não sabemos se ele está preparado para entender as aspas (e.g., o comando ls não entende a opção "-la", com aspas em volta). Somos obrigados a passar as strings literalmente. Uma solução é passar uma variável de ambiente para o programa dizendo o tipo de cada argumento; se o subprocesso for um script, ele saberá interpretar a variável e distinguir strings de representações serializadas, e se for um outro programa qualquer, ele não dará bola para a variável*, interpretará as strings como strings, e as representações serializadas como strings também; não faz sentido passar outros objetos para não-scripts, de qualquer forma.

A semântica da passagem por serialização pode ser um pouco diferente da passagem direta dentro de um mesmo processo. Se as estruturas de dados são mutáveis, as mudanças em um processo diferente não se refletirão (a princípio) no processo original (pois o subprocesso tem uma cópia, não uma referência ao objeto original). Porém, isso não há de ser problema.

Um problema com passar valores pela stdout é que o valor de uma chamada de função não é ignorado por padrão. Isto é, enquanto na maior parte das linguagens de programação o valor de retorno de uma função é descartado por padrão se não for usado, no nosso caso a chamada imprime seu valor de retorno para a stdout, e para descartá-lo temos que tomar alguma ação explícita (um >/dev/null, por exemplo). Não sei até que ponto isso é um problema.

It's text all way down (and up)

A idéia de passar objetos estruturados entre os programas não é novidade. A grande glória das pipelines é permitir que combinemos diversos programas simples de modo a realizar uma tarefa mais complexa. No Unix, grande parte dos programas lêem e emitem texto em um formato simples (campos separados por algum caractere como TAB ou :, um registro por linha). A universalidade desse formato e a grande quantidade de ferramentas para manipulá-lo permite que combinemos programas que não foram necessariamente feitos para se comunicar um com o outro. Por exemplo, se quiséssemos contar quantos usuários locais usam cada shell, poderíamos usar um comando do tipo:

# cat /etc/passwd | cut -d: -f7 | sort | uniq -c | sort -rn
     17 /bin/sh
      5 /bin/false
      2 /bin/bash
      1 /usr/sbin/nologin
      1 /bin/sync

No entanto, texto puro por vezes deixa um pouco a desejar. E se quisermos trabalhar com dados hierárquicos, ao invés de tabelas? E se quisermos usar o separador dentro de um dos campos? Esse último problema é freqüente em programas que manipulam nomes de arquivo. No Unix, um nome de arquivo pode conter qualquer caractere, com exceção do ASCII NUL (a.k.a. \0) e da /, que é usada para separar nomes de diretórios. Isso significa que nomes de arquivo podem conter espaços, asteriscos, tabs e quebras de linha, entre outros, o que atrapalha a vida de muitos scripts. Por exemplo, se você usar um comando do tipo:

find / -size +4G >lista-de-arquivos-maiores-que-4-giga.txt

você corre o risco de uma alma perversa ter criado um arquivo com um \n nome, o que vai fazer com que esse nome pareça duas entradas distintas na lista de arquivos. A maior parte das ferramentas não é preparada para lidar com terminadores de linha diferentes de \n (embora diversas ferramentas do GNU tenham opções para lidar com \0). E se ao invés de passar texto puro entre os programas pudéssemos passar dados estruturados, em um formato compreendido por todas as ferramentas?

Como disse, essa idéia não é novidade. Talvez o projeto mais famoso a explorar essa possibilidade no Unix seja o TermKit, um projeto que objetiva liberar o shell do mundo do texto puro e dos emuladores de terminal. As ferramentas do TermKit se comunicam usando JSON e headers baseados em HTTP. A idéia é que, ao invés de transmitir bytes brutos, os dados que entram e saem dos processos carreguem consigo uma indicação de seu tipo (no header), de modo que as ferramentas saibam como lidar com o conteúdo que recebem. O TermKit foca na parte de interação com o usuário, e não provê um shell completo (programável).

Há uma thread longa sobre o assunto nos fóruns do xkcd.

Outro projeto nesse sentido é o xmlsh mencionado na última seção, que utiliza XML para a comunicação entre os processos.

No mundo não-Unix, um programa interessante é o PowerShell do Windows. No caso do PowerShell, os comandos realmente passam objetos entre si (e não representações serializadas). Isso é feito com uma pequena "trapaça": os comandos capazes de lidar com objetos não executam em processos externos, mas sim são instâncias de "cmdlets", objetos que fornecem uma interface para o shell, e que são instanciados dentro do próprio shell. Um exemplo retirado do artigo é o comando:

get-childitem | sort-object extension | select extension | where { $_.extension.length -eq 4 }

get-childitem é uma versão generalizada do ls, que funciona sobre diretórios e outras estruturas hierárquicas. sort-object recebe uma stream de objetos como entrada pelo pipeline, e os ordena pelo campo passado como argumento. select é similar ao cut do Unix, mas trabalha sobre objetos. where é um comando de filtro que recebe como argumento um predicado, i.e., uma função que é executada sobre cada elemento da entrada e decide se o elemento permanece na tabela final ou não.

O que eu acharia realmente ideal seria passar objetos entre processos, não apenas como um mecanismo interno do shell. Isso era possível na Lisp Machine, primariamente porque todos processos eram executados no mesmo espaço de endereçamento (i.e., os processos na verdade são threads). (Além disso, o fato de todos os programas do sistema terem uma linguagem comum (Lisp) garante que os objetos serão entendidos por todos os programas.) Permitir isso em um ambiente com processos isolados é uma tarefa mais complicada.

Mas estamos divergindo do ponto original e começando a sair do âmbito do tranqüilamente implementável. Por enquanto eu só quero criar um shell decentemente programável. Revolucionar a maneira como os programas se comunicam é tópico para outros posts.

O que mais eu faria diferente

Até agora adicionamos features de maneira semi-compatível com o sh. Mas podemos aproveitar a oportunidade para fazer uma limpeza geral no shell. Poucos são os fãs da sintaxe do sh [citation needed], então creio que poucos se objetarão terminantemente a um shell com sintaxe diferente.

Uma coisa que eu gostaria é de seguir o exemplo do shell do Inferno e usar comandos comuns e blocos de código ao invés de estruturas de controle com sintaxe especial. Assim, o usuário poderia definir estruturas de controle novas sem necessidade de alterar a sintaxe do shell. Exemplo hipotético:

function foreach (list body) {
    local i=0
    local length=$[length $list]

    while { $i < $length } {
        $body ${list[i]}
        i += 1
    }
}

# Exemplo de uso. (Sintaxe de bloco roubada do Ruby.)
foreach (*.txt) {|file|
    echo "Eis o arquivo $file:"
    cat $file
}

Tenho outras idéias para um shell, mas acho que esse post já está ficando longo demais. Talvez eu escreva sobre isso em outra ocasião (ou se alguém demonstrar interesse). Por ora ficamos por aqui.

Conclusão

Os pontos principais deste post são:

  • O shell é uma linguagem de programação; não existe um bom motivo para ele ser uma linguagem de programação pela metade;
  • É possível adicionar capacidades que favoreçam a programabilidade do shell sem prejudicar seu uso interativo (ainda que isso force o shell a usar uma sintaxe estranha para algumas coisas, de modo a acomodar mais naturalmente o uso interativo);
  • Todos são a favor da moção.

_____

* Mas o subprocesso pode passar a variável intacta para outros processos, o que eventualmente pode fazer a variável cair em outro shell, que ficará confuso achando que a variável se refere aos argumentos que recebeu. Uma solução é incluir no valor da variável o PID do processo a quem a variável se destina. Mas ainda há o risco de o processo fazer um exec() e executar um shell com o mesmo PID atual, ou de um PID se repetir. Hmmrrgh...

1 comentário / comment

Main menu

Posts recentes

Comentários recentes

Tags

em-portugues (213) comp (137) prog (68) in-english (50) life (47) pldesign (35) unix (34) lang (32) random (28) about (27) mind (25) lisp (23) mundane (22) fenius (20) ramble (17) web (17) img (13) rant (12) hel (12) privacy (10) scheme (10) freedom (8) copyright (7) bash (7) music (7) academia (7) lash (7) esperanto (7) home (6) mestrado (6) shell (6) conlang (5) emacs (5) misc (5) latex (4) editor (4) book (4) php (4) worldly (4) android (4) politics (4) etymology (4) wrong (3) security (3) tour-de-scheme (3) kbd (3) c (3) film (3) network (3) cook (2) poem (2) physics (2) wm (2) treta (2) philosophy (2) comic (2) lows (2) llvm (2) perl (1) en-esperanto (1) audio (1) old-chinese (1) kindle (1) german (1) pointless (1) translation (1)

Elsewhere

Quod vide


Copyright © 2010-2020 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.