Elmord's Magic Valley

Software, lingüística e rock'n'roll. Sometimes in English.

The shell is completely wrong

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

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

Reading and splitting

2012-12-31 13:39 -0200. Tags: comp, prog, bash

Durante os últimos 354 anos eu estive splitando linhas em bash assim:

line="root:x:0:0:root:/root:/bin/bash"

IFS=":"
set -- $line
echo "$1"               # root
echo "$7"               # /bin/bash

# ou:
IFS=":"
fields=($line)
echo "${fields[0]}"     # root
echo "${fields[6]}"     # /bin/bash

Turns out que faz 354 anos que eu estou fazendo isso errado.

Acontece que expansão de parâmetros (i.e., $var) fora de aspas duplas não só causa word splitting (que é o que queremos), mas também causa pathname expansion (substituição de *, ?, ~ e afins pelos nomes de arquivo que casam com o padrão):

cd /
line='a*:b*:c*'
IFS=":"
fields=($line)

echo "${fields[0]}"     # a*
echo "${fields[1]}"     # bin
echo "${fields[2]}"     # boot
echo "${fields[3]}"     # c*

Esse problema nunca me ocorreu na prática, mas me dei conta disso depois de escrever o post sobre manipulação de strings em bash. O pior é que eu já sabia há muito tempo que variáveis fora das aspas sofrem pathname expansion, mas acho que adquiri o hábito de splitar strings assim antes de saber disso. Uma solução correta é usar o comando IFS=delimitadores read -a arrayname (que lê uma linha com campos separados por delimitadores e coloca os pedaços na variável arrayname) em conjunto com o operador <<<string (que alimenta a stdin do comando com a string):

line='a*:b*:c*'
IFS=":" read -a fields <<<"$line"

echo "${fields[0]}"     # a*
echo "${fields[1]}"     # b*
echo "${fields[2]}"     # c*

De brinde agora é possível incluir : em um campo usando a seqüência \:, já que o read entende o \ como um indicador de que o próximo caractere deve ser interpretado literalmente (a menos que a opção -r (de raw) seja usada).

O último exemplo de splitting do post anterior (que procura o nome do shell de um usuário no /etc/passwd) ficaria assim:

#!/bin/bash
user="$1"

IFS=":"
while read -a campos line; do
    if [ "${campos[0]}" = "$user" ]; then
        echo "${campos[6]}"
    fi
done </etc/passwd

Outra feature interessante do read é a opção -d, que permite especificar um caractere diferente de \n como terminador de linha. Isso é útil, por exemplo, em conjunto com o find -print0, que imprime os nomes dos arquivos separados pelo caractere \0 (ASCII NUL) ao invés de \n:

find . -name '*.bkp' -print0 | while IFS= read -d $'\0' file; do
    echo "Arquivo $file será renomeado."
    mv "$file" "${file%.bkp}~"
done

Isso garante que o script vai funcionar mesmo que o nome de algum arquivo contenha quebras de linha, o que faz com que o script não seja vulnerável ao usuário malandro que criar um arquivo chamado foo\n/bin/bash\nbar.bkp em seu home.

4 comentários

Manipulando strings em bash

2012-12-27 02:49 -0200. Tags: comp, prog, bash

Seguindo uma sugestão indireta, eis um post sobre as funções básicas de manipulação de strings do bash.

Antes de mais nada, gostaria de citar uma frase do manual do INTERCAL que me parece apropriada a respeito do bash:

Please be kind to our operators: they may not be very intelligent, but they're all we've got.

Dito isto, vamos ao ponto.

Parameter substitution

Como toda linguagem normal, bash possui variáveis.

person="Elmord"
echo "$person's Magic Valley"

A sintaxe $nome invoca o que se chama de parameter expansion. 'Parâmetro' é um termo mais geral que 'variável' em bash, e inclui variáveis, argumentos recebidos por um script ou função ($1, $2, ...), e alguns valores especiais mantidos pelo bash (e.g., $$ (PID do shell), $* (todos os argumentos do script/função em uma string só), etc.). Em bash, todos os parâmetros são strings (ou quase: $*, $@ e aparentados são meio indecisos sobre se são strings ou arrays de strings).

$nome pode ser escrito como ${nome}. Isso é útil quando se deseja isolar o nome do parâmetro de uma string adjacente que poderia ser interpretada como parte do nome:

animal="Raposa"
echo "${animal}s não dão dinheiro."

Além disso, essa sintaxe estendida permite especificar modificações sobre o valor a ser retornado pela expansão. Entre as modificações mais usadas estão as remoções de prefixos e sufixos:

(Mnemônico: # fica à esquerda (prefixo) de $ no teclado, % à direita (sufixo) de $.)

Por exemplo:

file="arquivo.feliz.html"
echo "$file"         # arquivo.feliz.html
echo "${file%.*}"    # arquivo.feliz (remove o menor sufixo do tipo .*, i.e., do último ponto em diante)
echo "${file##*.}"   # html (remove o maior prefixo do tipo *., i.e., do começo até o último ponto)
echo "${file%%.*}"   # arquivo (remove tudo do primeiro ponto em diante)
echo "${file#*.}"    # feliz.html (remove apenas até o primeiro ponto)

Exemplo levemente mais elaborado:

path="foo/bar/baz/quux/hack"
echo "${path%/*}"          # foo/bar/baz/quux
echo "${path%/*/*}"        # foo/bar/baz
echo "${path%/*/*/*}"      # foo/bar
echo "${path%/*/*/*/*}"    # foo
echo "${path%/*/*/*/*/*}"  # foo/bar/baz/quux/hack

No primeiro echo, removemos apenas o último componente do caminho. No segundo, removemos o menor sufixo que casa com /*/*, i.e., que consiste de uma barra, seguida por qualquer coisa (inclusive nada), seguido por outra barra, seguida por qualquer coisa (inclusive nada), o que resulta na eliminação dos dois últimos componentes do caminho. No último comando, o pattern não casa com nada (não há cinco barras na string), e portanto o resultado é o valor de path intacto. (Note que isso funciona porque estamos removendo o menor sufixo, com o operador % simples. Se usássemos o operador %% (i.e., ${path%%/*}, etc.) ficaríamos apenas com foo em todos os casos exceto o último.)

E se ao invés de remover os dois últimos componentes, quiséssemos ficar com apenas os dois últimos componentes? Infelizmente não existe um operador para remover o "segundo maior prefixo" que case com */. Uma maneira de conseguir isso é fazer a operação em dois passos:

path="foo/bar/baz/quux/hack"
prefix="${path%/*/*}"      # coloca "foo/bar/baz" em prefix
echo "${path#"$prefix"}"   # remove "foo/bar/baz" do começo de $path, deixando "/quux/hack"; ou
echo "${path#"$prefix/"}"  # remove "foo/bar/baz/" do começo de $path, deixando "quux/hack"

Ou, de maneira mais abreviada (e talvez menos legível):

echo "${path#"${path%/*/*}"}"

Note as aspas ao redor da sub-expressão (${path#"prefix"}). Elas são necessárias porque o valor de $prefix poderia conter caracteres como *, que seriam interpretados como wildcards se não houvesse aspas.

Os wildcards que podem aparecer nas patterns são os mesmos que podem ser usados com nomes de arquivos: * (zero ou mais caracteres quaisquer), ? (um caractere qualquer), [abcde] (qualquer um dos caracteres listados; ranges do tipo A-Z, bem como classes de caracteres do tipo [:digit:], podem ser incluídos na lista), [^abcde] (qualquer caractere não listado). Se a opção shopt -s extglob estiver ativa, patterns estendidas também são aceitas, mas isso é assunto para outro post.

Outro tipo de modificação útil são as substituições de substrings:

A pattern de substituição sempre casa com a maior ocorrência (i.e., se x="foo.bar.baz.quux.hack", ${x/.*./BOOM} expande para fooBOOMhack). Aparentemente não há um meio de substituir o menor prefixo/sufixo. Go figure.

Outra modificação útil é o slicing: ${nome:início:tamanho} seleciona tamanho caracteres de $nome, começando pelo início-ésimo (contando do zero). início e tamanho são expressões aritméticas; portanto, nomes de variáveis podem ser usados sem $, e operações aritméticas podem ser realizadas diretamente, sem necessidade de usar a sintaxe usual para expansão aritmética ($((expressão))):

x="foobarbazquux"
echo "${x:6:3}"      # baz
n=6
echo "${x:n:n/2}"    # baz

Se tamanho for omitido, o trecho de início até o final da string é usado. Se início for negativo, conta-se a partir do final da string:

echo "${x:(-4):1}"   # q
echo "${x:(-4)}"     # quux

(Note que o - deve ser separado do : que o precede (com parênteses ou espaços, por exemplo), pois ${nome:-string} significa algo completamente diferente em bash (usa o valor de $nome, ou string se $nome for vazio).)

Se um índice negativo indicar uma posição antes do começo da string, o resultado é a string vazia. (Por quê? Por quê?)

${#nome} devolve o comprimento de $nome. Há outras modificações; para mais informações, procure por Parameter Expansion na manpage do bash.

Separando em pedacinhos

Uma operação comum sobre strings é separá-la em diversos componentes segundo algum caractere separador. Essa operação existe em diversas linguagens sob o nome de split. Por exemplo, em JavaScript:

js> x = "foo bar baz"
"foo bar baz"
js> partes = x.split(" ")
["foo", "bar", "baz"]
js> partes[1]
"bar"

(By the way: já experimentou dar Ctrl-Shift-K no Firefox?)

Bash é uma linguagem muito hábil em separar strings. Por sinal, bash é hábil demais em separar strings: qualquer parâmetro não envolto em aspas está sujeito ao infame word splitting: o valor resultante da expansão do parâmetro é separado em múltiplas "palavras", e cada palavra é tratada como um argumento separado. Por exemplo:

$ file="nome com espaços"
$ ls "$file"
ls: cannot access nome com espaços: No such file or directory
$ ls $file
ls: cannot access nome: No such file or directory
ls: cannot access com: No such file or directory
ls: cannot access espaços: No such file or directory

Isso significa que você deve colocar aspas maniacamente em torno de qualquer string com variáveis que você não quer que sejam splitadas. Isso também significa que você pode se aproveitar dessa feature para obter os pedacinhos individuais de uma string. Uma maneira de fazer isso é criando um array com o resultado da expansão:

partes=($file)
echo "${partes[0]}"     # nome
echo "${partes[1]}"     # com
echo "${partes[2]}"     # espaços
echo "${#partes[@]}"    # 3 (comprimento do array)
Outra maneira é substituir os argumentos posicionais do shell pelo resultado da expansão:
set -- $file
echo "$1"               # nome
echo "$2"               # com
echo "$3"               # espaços
echo "$#"               # 3
O que define uma "palavra" são os separadores contidos na variável IFS (internal field separator): essa variável contém todos os caracteres que são considerados separadores. Por padrão, os separadores são espaços, tabs e newlines. Você pode setar essa variável para quaisquer outros caracteres, e remover a variável (com unset IFS) para restaurar os separadores originais. Por exemplo, se quiséssemos escrever um script para procurar no arquivo /etc/passwd (cujos campos são separados por :) o nome do shell de um determinado usuário, poderíamos fazer assim:
#!/bin/bash
user="$1"

IFS=":"
while read line; do
    campos=($line)
    if [ "${campos[0]}" = "$user" ]; then
        echo "${campos[6]}"
    fi
done </etc/passwd

Update: Há uma solução melhor.

Matching

Às vezes queremos saber se uma string casa com um determinado padrão. A maneira mais comum de se fazer isso é através do comando case:

case "$file" in
    *.txt)
        echo "É um arquivo de texto."
        cat "$file"
        ;;
    *.gif|*.jpg|*.png)
        echo "É uma figurinha."
        xloadimage "$file"
        ;;
    *)
        echo "Que que é isso, medeus?"
        ;;
esac

Cada cláusula do case começa com uma pattern, seguida de ). Os tipo de pattern aceitos são os mesmos usados para expansão de nomes de arquivos e para remoções de prefixo/sufixo e outras substituições de string. Cada cláusula é terminada com ;;. (O ;; na última cláusula é opcional.) Múltiplas patterns, separadas por |, podem ser especificadas.

As patterns do shell são um tanto quanto limitadas, o que exige uma certa dose de treta na hora de usá-las. Por exemplo, imagine que queiramos fazer um script que exige um número como argumento, e queremos que o script teste se o argumento foi passado corretamente. Não temos como especificar uma pattern que diga que apenas números são aceitos. A solução é especificar que se o argumento contiver algum caractere que não seja um número, o script deve emitir um erro:

case "$1" in
    *[^0-9]*)
        echo "Erro: argumento deve ser um número."
        exit 1
        ;;
    *)
        echo "Eis um número: $1."
        ;;
esac

Parece bom? Quase: se uma string vazia for passada, nenhum dos caracteres que a compõem casa com [^0-9], e portanto a primeira pattern não casa, e portanto caímos na segunda cláusula do case, embora o argumento passado não seja um número. Para resolver esse problema, temos que incluir a string vazia na primeira cláusula:

case "$1" in
    *[^0-9]*|)
        echo "Erro: argumento deve ser um número."
        exit 1
        ;;
    *)
        echo "Eis um número: $1"
        ;;
esac

Comandos externos

Até agora utilizamos apenas recursos internos do shell. Mas temos também à disposição a vasta gama de comandos de manipulação de texto do Unix, tais como cut, sed e tr. Se quisermos alimentar um desses comandos com o conteúdo de uma variável, podemos usar o comando echo em uma pipeline, e capturar o resultado com a sintaxe $(comando):

dmy="27/05/1990"
ymd="$(echo "$dmy" | sed -re 's,(..)/(..)/(....),\3-\2-\1,')"
echo "$ymd"   # 1990-05-27

A partir do bash 3, é possível alimentar a entrada padrão de um comando com uma string através da sintaxe comando <<<string:

ymd="$(sed -re 's,(..)/(..)/(....),\3-\2-\1,' <<<"$dmy")"

Outras features

Este post não cobre todas as features de manipulação de strings no bash. (Em particular, faltou cobrir o comando [[ expressão ]] e suas habilidades com expressões regulares, as quais eu ainda não experimentei direito.) Para mais informações, dê uma olhada na manpage do bash. Sinta-se à vontade para acrescentar outras mandingas ou fazer perguntas nos comentários.

7 comentários

The googling people

2012-11-08 21:58 -0200. Tags: about, comp, bash, random

Pois ao invés de fazer alguma coisa útil com a vida, resolvi fazer uma análise dos termos de busca usados pelo povo que cai neste blog via Google. Primeiro alguns números:

Muitas das buscas são em forma de perguntas. Seguem algumas respostas e observações:

c como guardar o descritor da funçao open numa estrutura
Da mesma maneira como se guarda o descritor fora da estrutura: em um int.
Para que serve o quote (‘) quando colocado em frente a algo como um símbolo em Lisp?
Dada a inicial maiúscula e o fato de que o "quote" não é um quote de verdade ('), assumo que isso seja pergunta de exercício. O fato de ainda se usar Lisp em algum curso me surpreende. De qualquer forma, o quote serve para indicar que a próxima expressão não deve ser avaliada, e sim tomada como um dado literal (x se refere à variável x, 'x é um símbolo; (+ 2 3) é 5, '(+ 2 3) é uma lista de três elementos).
QUAL PROCESSADOR AQUECE MENOS
O desligado.
Usando fork() e exec():Faça um programa usando as chamadas de sistema fork() e exec() que produza as seguintes seqüências de ações:a.O processo pai deve criar um único processo filho. O filho será substituído pelo processo more ou cat.b.O parâmetro passado para o programa more ou cat deverá ser o nome do arquivo fonte do exercício. Por exemplo: "executa.c"
Whoa, uma abordagem totalmente nova para resolver exercícios. Vou lembrar disso para momentos apropriados.
alguma comando do prompt que aumente o tamanho dos caracteres
No xterm, Shift-<+ do keypad numérico>. No gnome-terminal, acho que só nas Preferências.

Surpreendentemente, existe uma seqüência ESC para trocar a fonte do xterm, desde que o recurso XTerm*allowFontOps: true esteja setado:

printf '\e]50;?\a'                # Descobre a fonte atual
printf '\e]50;nome-da-fonte\a'    # Muda a fonte atual
printf '\e]50;#N\a'               # Seleciona a fonte N da lista de fontes padrão do xterm
printf '\e]50;#+N\a'              # Escolhe a próxima N-ésima fonte
printf '\e]50;#-N\a'              # Escolhe a N-ésima fonte anterior

Só de ter descoberto isso já valeu ter escrito este post.

alterando strings bash
Digno de post futuro.
como acalmar o processador
Cante uma música para ele.
konsole mudou sozinho para "u@h:w$"
Chute: o shell padrão mudou do bash para o sh.
linguagem de programação mais ineficiente
Máquina de Turing.
listar inodes com 2 hard links
find . -links 2
nao  estou me fazendo de vitima
Será?
now write about yourself
Say 'please'.
o  que vai morar  de   pois    do    fim   do    mundo
Não sei se entendi, mas essas pausas dramáticas me deixaram até com medo.
o que acontece se eu criar um programa que chame o método fork() várias vezes seguidas, após cada fork, imprima o pid do processo e de seu pai.
Não tem resposta! [atira o leite de unicórnio de duas cabeças longe]
o que significa os 7 simbolos na palavra coexist
Huh...
oq é oq é muito dificil de descobrir
Eu sei, mas não conto pra ninguém.
os maias usavam o calendario para que?
Para datar eventos históricos e para trollar europeus.
passar conteudo variavel para outra prompt
Essa é interessante. Um jeito é usar o comando declare -p var, que imprime um comando que, quando executado, recria a variável var com o mesmo conteúdo. Podemos tentar automatizar a tarefa criando algumas funções e adicionando-as ao ~/.bashrc. O problema é que o comando declare cria variáveis locais, e conseqüentemente elas deixam de existir depois do término da função. É possível simplesmente remover os "declare" iniciais da saída do declare -p, mas com isso declarações de array não funcionam. Talvez o negócio seja implementar na mão mesmo:
VARDIR="$HOME/.vars"
[[ -d $VARDIR ]] || mkdir "$VARDIR"

save() {
    local __var __i __indexes __ref;
    for __var; do
        eval "__indexes=(\${!$__var[*]})"
        {
            echo "unset $__var"
            for __i in "${__indexes[@]}"; do
                __ref="$__var[$__i]"
                printf "%s[%d]=%q\n" "$__var" "$__i" "${!__ref}"
            done
        } >"$HOME/.vars/$__var"
    done
}

load() {
    local __var;
    for __var; do
        . "$VARDIR/$__var"
    done
}

The horror, the horror.

qual a diferença entre prompt string 1 e prompt string 2
PS1 é o prompt comum. PS2 é o prompt usado quando você digita um comando incompleto e o bash espera pela continuação; normalmente, o PS2 é > .
qual o melhor 3g em viamao?
Para o preço pelo qual é oferecido, o Oi pré-pago até que não é ruim (R$ 9,80 por mês). O da Tim é inutilizável. Vivo e Claro não tenho dados.
qual é o padrao de numero de recarga tim
Coméqueé?
quando se aluga taxa de iptu paga mensalmente
Não. É possível parcelar, da mesma maneira como o IPTU de casa própria, mas o valor do IPTU não tem ligação com o valor do aluguel (pelo menos no meu caso não teve).
quem paga a taxa de incendio no mesmo quintal
Taxa de incêndio é por imóvel, e é cobrada como uma percentagem sobre o valor do aluguel. Não faz diferença (que me conste) se há mais imóveis no mesmo terreno.
quer morar onde?
Reykjavík, mas só por um tempo.
unix saber quando hostname foi alterado
Meu deus, o que é que o(a) senhor(a) está fazendo?
vitor de araujo inf ufrgs etapa
Quem diabos procurou isto?

Não achei nenhuma pérola do nível de "blog de gente se explicando", todavia...

6 comentários

RU for you

2012-11-08 08:42 -0200. Tags: comp, prog, bash

Coisas que acontecem em noites insones:

[Screenshot de script que exibe o cardápio do RU]

O código está tenebroso, e provavelmente explode dependendo de variações de humor do HTML da página do cardápio do RU, mas é o que temos no momento. Divirta-se.

1 comentário

Log de papers

2012-10-21 05:36 -0200. Tags: comp, prog, bash

Não estou muito inspirado para escrever em língua de pessoas hoje. O que eu tenho para dizer é que acabei de escrever em língua de máquinas um pequeno script que supre (parcialmente) uma necessidade que eu tenho há um bom tempo: um log de papers. (Vejam vocês, minha falta de vida social é de tal magnitude que eu passo uma parte razoável do meu tempo livre lendo papers.)

A idéia é simples: ao invés de mandar o Firefox abrir arquivos PDF e PS com o Evince (ou qualquer que seja o visualizador de sua preferência), você o instrui a abri-lo com esse script (lembre-se de dar permissão de execução ao arquivo (chmod +x /blabla/evince-log.sh)). O script então extrai o título e o abstract do texto do PDF/PS, rouba a URL de onde o arquivo foi baixado de dentro do downloads.sqlite do Firefox, e grava essas informações em um arquivo ~/evince-log.txt. Não é lá grande coisa, mas pelo menos é mais provável que daqui a um ano eu consiga encontrar os papers que eu li hoje quando eu precisar deles.

O script requer (no Debian) os pacotes sqlite3 (para ler o log de downloads do Firefox), poppler-tools (que contém o pdftotext) e ghostscript (que contém o ps2txt). Se você executar o script sem parâmetros, ele lhe dirá se algum desses programas estiver faltando.

Disclaimer: O "algoritmo" para extrair o título e o abstract são uma baita de uma faconice; o resultado é vagamente correto, mas suficiente para os meus propósitos por ora. Considere o "abstract" como uma amostra do início do paper para facilitar a identificação, e nada mais. Sugestões de melhorias são aceitas.

Feliz fuso novo para todos.

Comentários

The art of the prompt string

2012-05-06 03:49 -0300. Tags: comp, unix, bash

Ofereço este post com conteúdo útil como sacrifício aos leitores, de modo a ter crédito para escrever posts reclamando da vida mais tarde.

O prompt padrão do bash na maior parte das distribuições de GNU/Linux costuma ser algo do tipo username@hostname diretório$ . Esse prompt pode ser customizado alterando-se o valor das variáveis PS1 ("prompt string 1") e companhia:

vitor@eukleides ~$ PS1='oi? '
oi? pwd
/home/vitor
oi? 

Você pode experimentar prompts diferentes alterando essa variável até achar um que seja do seu agrado, e então alterar um dos arquivos de configuração do bash (e.g., ~/.bashrc) para setar a variável para o valor desejado na inicialização do shell.

Backslash sequences

Como você pode imaginar, o conteúdo de PS1 não precisa ser texto estático. O bash substitui certas seqüências de \ seguido de um caractere (e.g., \u) na definição do prompt por valores específicos (e.g., o nome do usuário). O prompt padrão apresentado acima pode ser obtido pela string \u@\h \W\$ :

Uma lista completa das seqüências expandidas pelo bash no prompt pode ser obtida procurando por PROMPTING no manual (man bash). Outras seqüências úteis incluem \w (caminho completo do diretório atual) e \! (número do comando atual no histórico; útil para quem gosta de usar os comandos de recuperação do histórico, e.g., !42 para reexecutar o comando de número 42).

Escape sequences

Uma das maravilhas do terminal (que talvez seja digna de um post a respeito no futuro) são as escape sequences: seqüências de caracteres especiais que podem produzir toda sorte de firula, desde cores e outros efeitos de texto até alterações no título da janela de terminal. Por exemplo, a seqüência ESC [ 3 número m, onde ESC é o caractere ASCII número 27, seleciona a cor de texto com o número especificado (que varia de 0 a 7); a seqüência ESC [ 0 m retorna as cores e outros atributos para seus valores padrão. O caractere ESC pode ser representado no prompt pela seqüência \e. Assim:

PS1='\e[34m\u@\h \W\$ \e[0m'

lhe dará um prompt idêntico ao padrão, mas em azul. Existe uma manpage (man console_codes) com uma lista de seqüências ESC suportadas por diversos terminais.

Só tem um problema: o bash usa o tamanho da string de prompt expandida para calcular o tamanho que o prompt ocupa na tela; ele precisa dessa informação para determinar onde ocorre a quebra de linha, caso o comando digitado possua mais de uma linha, e para saber onde posicionar o cursor caso você tecle Home, entre outras coisas. O problema é que os caracteres de uma seqüência ESC não ocupam espaço na tela, mas aumentam o tamanho da prompt string. Para o bash poder calcular corretamente o tamanho do prompt, é necessário demarcar os trechos de seqüências ESC no prompt entre \[ e \]:

PS1='\[\e[34m\]u@\h \W\$ \[\e[0m\]'

Provavelmente a coisa mais útil que se costuma fazer com seqüências ESC no prompt é setar o título da janela de terminal para conter o diretório atual:

PS1='\[\e]0;\w\a\]\u@\h \W\$ '

ESC ] 0 ; título BEL é a seqüência que muda o título da janela do xterm e outros terminais gráficos. (BEL, representado no prompt por \a, é o caractere ASCII número 7; em situações normais, ele é o caractere que faz o terminal emitir um beep.)

Se você nem sempre acessa o terminal via interface gráfica, pode ser uma boa idéia testar se o terminal atual é gráfico antes de adicionar seqüências ESC no prompt:

case "$TERM" in
    xterm*|gnome*|konsole*) PS1='\[\e]0;\w\a\]\u@\h \W\$ ' ;;
    *) PS1='\u@\h \W\$ ' ;;
esac

Ou, alternativamente, para evitar duplicação:

# Primeiro adiciona a seqüência ESC.
case "$TERM" in
    xterm*|gnome*|konsole*) PS1='\[\e]0;\w\a\]' ;;
    *) PS1='' ;;
esac

# Depois, o resto do prompt (igual em ambos os casos).
PS1+='\u@\h \W\$ '

Variáveis

Além de backslash sequences, o bash substitui variáveis presentes na prompt string. Por exemplo, PS1='$USER@$HOSTNAME $PWD\$ ' tem mais ou menos o mesmo efeito que PS1='\u@\h \w\$ '. Ao se usar variáveis no prompt, é importante usar aspas simples em volta do valor que está sendo atribuído a PS1:

O bash também expande comandos (usando a sintaxe $(comando) ou `comando`) presentes na string de prompt. Novamente, é importante cuidar para usar aspas simples caso se queira que o comando execute toda vez que o prompt é impresso, e não apenas uma vez durante a definição do prompt. De modo geral, executar um comando (criar um processo) cada vez que o prompt é impresso não me parece uma boa idéia; normalmente, executar o comando uma vez só resolve a maior parte dos problemas e é menos custoso computacionalmente. (Se bem que hoje em dia criar um processo provavelmente não vai fazer uma diferença palpável na execução do shell ou do sistema; eu recomendaria não fazer isso com o prompt do root, entretanto. A gente nunca sabe quando vai ter que matar uma fork bomb ou um processo mal-comportado no sistema.)

Uma utilidade de usar variáveis e/ou comandos é incluir nome da tty atual no prompt. Isso é particularmente útil quando se está trabalhando no modo texto, pois pelo nome da tty sabe-se o número do console virtual (e lembrando o número pode-se rapidamente voltar para o console em questão teclando Alt-Fn).

tty="$(tty)"                # Recupera o nome completo da tty (e.g., /dev/tty1)
if [[ $tty == */pts/* ]]; then
    ttybase="pts${tty##*/}" # Transforma /dev/pts/0 em pts0
else
    ttybase="${tty##*/}"    # Transforma /dev/tty1 em tty1
fi

PS1='\u@$ttybase \W\$ '     # Usa o nome da tty ao invés do hostname.

Ganhamos o nome da tty, mas perdemos o hostname. Uma possibilidade é testar se a conexão atual é via ssh, e usar o hostname nesse caso:

tty="$(tty)"
if [[ $SSH_CONNECTION ]]; then
    ttybase="$HOSTNAME"
elif [[ $tty == */pts/* ]]; then
    ttybase="pts${tty##*/}"
else
    ttybase="${tty##*/}"
fi

PS1='\u@$ttybase \W\$ '

Outra informação útil de se incluir no prompt (uma que eu já não consigo viver sem, a ponto de alterar o prompt de outras máquinas quando vou usá-las por mais do que alguns minutos) é o exit status do último comando. O exit status é um valor ente 0 e 255 que cada processo retorna ao terminar. (É por isso que a main() retorna int, e não void, em C.) Por convenção, um programa retorna 0 se foi bem-sucedido, e um valor diferente de zero caso tenha ocorrido um erro (valores diferentes podem ser usados para erros diferentes). O exit status do último comando vive na pseudo-variável $?:

PS1='\u@$ttybase \W($?)\$ '

A presença do exit status no prompt é útil por mil razões. Primeiro, se você costuma escrever shell scripts, freqüentemente deseja saber que valor um comando retorna em certa situação. Além disso, o exit status presente no prompt dá um feedback imediato quanto ao sucesso ou falha do último comando, sem ter que ler toda a saída do comando. Por exemplo, ao copiar um diretório com muitos arquivos com cp -v, pode ser que uma mensagem de erro passe desapercebida no meio da saída normal do cp; com o exit status no prompt, o erro é imediatamente visível. Com o tempo, verificar o exit status é um ato inconsciente; para mim, hoje em dia, ele é um feedback tão importante quanto a saída do comando. Finalmente, existem alguns programas cretinos que não imprimem mensagens de erro, apenas retornam 1 para indicar que falharam. (Lamentavelmente, com o exit status no prompt, você possivelmente se sentirá inclinado a escrever programas que fazem o mesmo. Eu não tenho o direito de atirar pedras.)

The prompt command

A última maravilha promptística que tenho a apresentar é a variável PROMPT_COMMAND: se essa variável for setada, seu valor é interpretado como um comando a ser executado antes de imprimir o prompt. Você pode usá-la para chamar alguma função que seta uma variável usada na string do prompt.

Eis uma possibilidade: nos doces tempos do bash 1.14, o comportamento do \W era diferente: não havia abreviação do diretório home para ~; além disso, se o diretório estivesse diretamente abaixo do raiz (e.g., /mnt), o bash imprimia a / no início do nome, e não apenas mnt. O comportamento mudou no bash 2, e me foi a mi mui mal. Felizmente, é possível corrigir esse problema usando o PROMPT_COMMAND e uma variável no prompt:

reduced_pwd() {
    case "$PWD" in
        /*/*) REDPWD="${PWD##*/}" ;;
        *) REDPWD="$PWD" ;;
    esac
}

PROMPT_COMMAND="reduced_pwd"
PS1='\u@$ttybase $REDPWD($?)\$ '

Pode-se adaptar a reduced_pwd, com uma pequena dose de falcatrua, para mostrar os últimos dois itens do diretório atual, ao invés do caminho completo ou do último item:

reduced_pwd() {
    local prefix
    case "$PWD" in
        /*/*/*)
            # Se PWD = /mnt/foo/bar/baz/quux:
            prefix="${PWD%/*/*}"        # prefix = /mnt/foo/bar
            REDPWD="${PWD#"$prefix"/}"  # REDPWD = baz/quux
            ;;
        *)
            REDPWD="$PWD"
            ;;
    esac
}

Pode-se obter a temperatura da CPU, ou a carga da bateria da máquina, a partir dos arquivos em /sys. Pode-se baixar periodicamente informação climática de algum site e adicionar a temperatura ao prompt, mudando a cor dependendo de se o tempo está ensolarado ou nublado ou chuvoso. As possibilidades são infinitas.

Comentários

Main menu

Posts recentes

Comentários recentes

Tags

comp (114) prog (51) life (44) unix (32) random (27) lang (27) about (24) mind (22) mundane (21) pldesign (20) in-english (19) lisp (17) web (17) ramble (15) img (13) rant (12) privacy (10) scheme (8) freedom (8) lash (7) music (7) esperanto (7) bash (7) academia (7) home (6) mestrado (6) shell (6) conlang (5) copyright (5) misc (5) worldly (4) book (4) php (4) latex (4) editor (4) politics (4) etymology (3) wrong (3) android (3) film (3) tour-de-scheme (3) kbd (3) c (3) security (3) emacs (3) network (3) poem (2) cook (2) physics (2) comic (2) llvm (2) treta (2) lows (2) audio (1) wm (1) philosophy (1) kindle (1) pointless (1) perl (1)

Elsewhere

Quod vide


Copyright © 2010-2018 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.