Truques de shellscript do Rich |
Esse artigo é uma tradução livre do Rich’s sh (POSIX shell) tricks de Rich Felker (autor da biblioteca musl).
Essa página tem como objetivo ser um repositório de truques uteis que o Rich encontrou (e alguns ele provavelmente inventou) para a escriptação a POSIX shell (com alguma intenção para a portabilidade para shells com não conformidade também, espalhados aqui e alí). Sou um forte crente que linguagens derivadas do Bourne são extremamente ruins, na mesma ordem de ruim como Perl, para programação, e considero programação sh para qualquer finalidade que não seja super-portátil, plataforma de menor denominador comum para scripts de construção ou de bootstrap e afins, como um esforço extremamente equivocado. Como tal, você não me verá gastando muitas palavras em extensões específicas para ksh, Bash ou qualquer outro shell que possa ser popular.
IMPRIMINDO O VALOR DE UMA VARIÁVEL
printf %s\\n "$var"
O “\\n” pode ser omitido se uma nova linha a seguir não for desejada. As aspas são essenciais. O seguinte NÃO é um substituto válido:
echo "$var"
NUNCA utilize echo assim. De acordo com a POSIX, echo possui comportamento não não especificado se algum destes argumentos contiver “\” ou se seu primeiro argumento for “-n”. Os padrões Unix™ preenchem nessa área específica por implementações XSI-conformant, ao especificar o comportamento indesejável desagradável que ninguém quer (“\” é interpretado como um estilo escape C-string-literal), e outras implementações populares tal como Bash que interpretam valores de argumento diferentes de “-n” como opções especiais mesmo quando em modo de “compatibilidade POSIX”, renderizando então não conformes. O jist disso é:
Saída Comando POSIX Unix Bash echo "-e" “-e” “-e” “”echo "\\n" não especificado “” “\n”echo -n hello não especificado “hello” “hello” echo -ne hello “-ne hello” “-ne hello”“hello”
Esses problemas significam que echo "$var" podem ferroá-lo sempre que você não tiver garantias estritas a respeito do conteúdo de var (por exemplo, que contém um inteiro não negativo). Mesmo se você for um idiota centrado em GNU/Linux* que pensa que o mundo inteiro é um Bash e você não se importa com a portabilidade**, você vai se deparar com problema um dia quando acontecer de ler um “-n” ou um “-e” ou “-neEenenEene” dentro de var e de repente seu script quebrar.
Se você está realmente apegado ao nome “echo” e quer utilizá-lo em seus scripts, tente esta função que 'repara' o echo para se comportar de uma maneira razoável (muito parecido com o comando Bash echo, mas com a estipulação adicional de que o último argumento nunca será interpretado como uma opção, assim echo "$var" fica seguro mesmo quando o conteúdo de var parecer uma opção):
echo () (fmt=%s end=\\n IFS=" "while [ $# -gt 1 ] ; docase "$1" in[!-]*|-*[!ne]*) break ;;*ne*|*en*) fmt=%b end= ;;*n*) end= ;;*e*) fmt=%b ;;esacshiftdoneprintf "$fmt$end" "$*")
Adicionar esse código no topo do seu script corrigirá todos os sutis bugs devido ao dano cerebral do comando padrão echo. Ou, se você acha que toda a ideia de echo ter opções é absurda, tente essa versão mais simples (o uso de “$*” ao invés de “$@” é bem intencional aqui):
echo () { printf %s\\n "$*" ; }
Você nunca imaginou que exibir o valor de uma variável poderia ser tão difícil. Agora você vê por que eu digo que linguagens derivativas de Bourne nunca deveriam ser utilizadas para programações sérias...
LENDO A ENTRADA PADRÃO LINHA POR LINHA
IFS= read -r var
Esse comando lê uma linha de entrada, terminada por uma nova linha ou final de arquivo ou condição de erro, a partir da entrada padrão (stdin) e armazena o resultado em var. O status de saída será 0 (zero = sucesso) se uma nova linha for alcançada, e diferente de zero (falha) se um erro de leitura ou final de arquivo terminar a linha. Scripts robustos podem desejar distinguir entre esses casos. De acordo com minha leitura da POSIX, os conteúdos de var devem ser preenchidos com os dados lidos se um erro ou final prematuro de arquivo encerre a leitura, mas não tenho certeza se todas as implementações se comportam como tal e se é estritamente necessária. Comentários de especialista são bem vindos.
Uma armadilha comum é tentar ler a saída canalizada de comandos, como:
foo | IFS= read var
POSIX permite qualquer ou todos os comandos em uma pipeline ser serem executados em subshells, e qual comando (se qualquer) executa no shell principal varia muito entre implementações — em particular Bash e ksh diferem aqui. O idioma padrão para superar esse problema é utilizar um here document***:
IFS= read var << EOF$(foo)EOFReading input byte-by-byteread dummy oct << EOF$(dd bs=1 count=1|od -b)EOF
Esse comando deixa o valor octal de um byte de entrada na variável oct. Note que dd é o único comando padrão que pode com segurança ler exatamente um byte de entrada com uma garantia que nenhum byte adicional será armazenado (em buffer) e perdido. Além de falhar de ser portável head -c 1 pode ser implementado utilizando funções C stdio com buffering.
Conversão para algum formato escape (octal nesse caso) é necessário pois o comando de leitura lida com arquivos de texto. Ele não pdoe cuidar de bytes arbitrário; em particular não há como armazenar um byte NUL em uma variável shell. Outros problemas com bytes que não sejam ASCII também podem existir dependendo da sua implementação e sua localidade. É possível modificar esse código para ler vários bytes de uma só vez mas tome cuidado para levar em conta todos os vários comportamentos ruins de programa od como considerar longas execuções de zeros.
Conversão de octal de volta para binário pode ser realizada através do próximo truque sh.
ESCREVENDO BYTES PARA A SAÍDA PADRÃO PELO VALOR NUMÉRICO
writebytes () { printf %b `printf \\\\%03o "$@"` ; }writebytes 65 66 67 10
Essa função permite especificação de valores de byte na base 8, 10 ou 16. Valores Octal e hexadecimal devem ser prefixada com 0 ou 0x, respectivamente. Se você quiser que os argumentos sempre seja tratados como octal, por exemplo quando processar valores pelo truque anterior por ler dado binário, tente essa versão:
writeoct () { printf %b `printf \\\\%s "$@"` ; }
Fique ciente de que ele quebrará se seus valores octal foram maiores do que digitos 3, então não coloque um 0 à esquerda. A versão a seguir é muito mais lenta, mas evita esse problema:
writeoct2 () { printf %b $(printf \\%03o $(printf 0%s\ "$@")) ; }
Utilizando find com xags os fans de GNU estão acostumados a utilizar as opções -print0 e -0 para o find e xargs, respectivamente, for aplicações robustas e eficientes de um comando para todos os resultados do comando find. Sem estenções do GNU, a saída do comando find é delimitado por nova linha, significa que não há como recuperar os nomes de caminho reais encontrados se alguns dos nomes de caminho contiverem novas linhas incorporadas.
Se você não se incomoda que seus scripts quebrem quando os nomes de caminhos conterem novas linhas, ao menos certifique-se de que o processamento incorreto que eles resultarão possam não levá-lo a um comprometimento de privilégio, e então tente o seguinte:
find ... | sed 's/./\\&/g' | xargs command
O comando sed aqui é obrigatório. Ao contrário da crença popular (bom, bem, era popular o suficiente para que eu acreditasse erroneamente por um longo tempo), xargs NÃO aceita listas delimitadas por nova linha. Em vez disso, aceita listas entre aspas do shell como por exemplo alista de entrada é separada por espaço em branco e todo o espaço em branco interno deve estar em aspas. O coando acima simplesmente coloca dentro de aspas todos os caracteres com barras invertidas para satisfazer esse requerimento. Protegendo espaço em branco embarcado em nomes de arquivos.
UTILIZANDO find COM +
Claro que o jeito muito mais esperto de se utilizar o find para eficientemente aplicar comando aos arquivos é com o -exec e um “+” substituindo o “;”:
find path -exec command '{}' +
Isso faz com que find coloque quantos nomes de arquivo couberem na linha de comando no lugar do “{}”, cada um como seu próprio argumento. Não há problema com novas linhas incorporadas sendo mal interpretadas. Infelizmente, apesar de sua presença na POSIX há muito tempo, a implementação popular do GNU do comando find não suportava "+" por muito tempo, e assim seu uso não é portátil na prática. Uma solução razoável seria escrever um test para o suporte de "+" e utilizar ";" no lugar de "+" (com a perda de eficiência naturalmente severa) em sistemas quebrados onde find não está em conformidade.
Aqui está um comando que deve ter sucesso em qualquer sistema com conformidade POSIX mas que falhará se o comando find não possuir suporte ao “+” devido à falta de um argumento “;”:
find /dev/null -exec true '{}' +
Isso tira vantagem do fato de que “/dev/null” é um dos três únicos nomes de não-diretórios de caminho absolutos garantidos pelo POSIX a existir.
Versão portável do find -print0
find path -exec printf %s\\0 '{}' +
Portabilidade está sujeita a notas acima na falta de suporte do GNU find para o “+” até recentes versões, então provavelmente é uma boa ideia voltar atrás e utilizar “;” ao invés de “+” se necessário.
Note que esse truque é provavelmente inutil, desde que a saída não é um arquivo de texto. Nada mais a não ser o o GNU xargs deve ser aceieto para analisá-lo.
Utilizando a saída do find -print robustamente. Apesar do problema de emulação de separador de campo de nova linha incorporada do find, é possível analisar a saída de forma robusta. Apenas se lembre, “barra não salva o dia.” Para cada caminho absoluto sendo buscado, prefixe a inicial “/” com “/.”, e da mesma forma prefixe cada caminho relativo a ser pesquisado com “././” — a string “/./” então se torna um marcador mágico de sincronização para determinar se uma nova linha foi produzida como um separador de campo ou devido a novas linhas incorporadas em um nome de caminho.
Processamento de saída é deixado como um exercício para o leitor. Obter a saída não sobrecarregada da substituição de comando, o seguinte não é seguro:
var=$(dirname "$f")
Devido a muitos comandos escrevendo uma nova linha ao final de sua saída, substituição de comando estilo Bourne foi projetado para remover novas linhas da saída. Mas ela não remove somente uma nova linha; ele remove todas. No comando acima, se f contem quaisquer novas linhas no ultimo componente de diretório, eles serão removidos, gerando um nome de diretório diferente. Embora ninguém sensato colocaria novas linhas em nomes de diretórios, tal corrupção dos resultados poderia levar a vulnerabilidades exploráveis em scripts.
A solução para esse problema é muito simples: Adicionar um caractere seguro depois da ultima linha, daí utilize a substituição de parâmetro do shell para remover o caractere de segurança:
var=$(command ; echo x) ; var=${var%?}
No caso do comando dirname, alguém também deseja remover a nova linha final única adicionada por dirname, exemplo:
var=$(dirname "$f" ; echo x) ; var=${var%??}
Claro que há um jeito mais fácil de obter a parte do diretório de um pathname desde que você não se importa com algumas das estranhas semânticas de caixa de canto do comando dirname:
var=${f%/*}
Isso vai falhar para arquivos no diretório root, entre outros casos, então uma boa abordagem seria escrever uma função shell para considerar tais casos especiais. Note, no entanto, que tal função dever de alguma forma armazenar seus resultados em uma variável. Se ela exibi-los na stdout, como é prática comum quando escrever funções shell para processar strings, nos depararíamos com o problema de “$(...)” removendo novas linhas à direita mais uma vez e retornando para onde começamos... Retornando strings a partir de uma função shell. Assim como pode ser visto a partir da armadilha acima de substituição de comando, stdout não é um bom caminho para funções shell retornarem strings a seus invocador, ao menos que a saída seja em um formato onde as novas linhas à direita são insignificantes. Certamente tal prática não é aceitável para funções lidarem com strings arbitrárias. Então, o que pode ser feito?
Tente isso:
func () {body hereeval "$1=\${foo}"}
Claro que ${foo} poderia ser substituído por qualquer tipo de substituição. O truque aqui é a linha de aval e o uso de fuga. A “$1” é expandida quando o argumento para eval é contruída pelo comando principal analisador de comando. Mas a “${foo}” não é expandida nesse estágio, porque a “$” foi cotada. Ao invés disso, ela é expandida quando eval evoluiu seu argumento. Se não estiver limpo porque é importa, considere como o seguinte poderia ser ruim:
foo='hello ; rm -rf /'dest=bareval "$dest=$foo"
Mas claro que a seguinte versão é perfeitamente segura:
foo='hello ; rm -rf /'dest=bareval "$dest=\$foo"
Note que no exemplo original, “$1” foi utilizada para permitir o invocador passar o nome da variável de destino como um argumento na função. Se sua função precisar utilizar o comando shift (man 1 shift), por exemplo, para lidar com os argumentos restantes como “$@”, então ele pode ser útil para salvar o valor de “$1” em uma variável temporária no inicio da função.
Strings arbitrárias de shell-quoting. As vezes é necessário colocar um string em uma forma de shell-quote, por exemplo se o string precisar ser expandida em um comando que será evoluído com eval, escrito em um script gerado, ou similar. Há vários metodos, mas muitos deles falham se o string contiver novas linhas. Aqui está uma versão que funciona:
quote () { printf %s\\n "$1" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/" ; }
Essa funçãço simplesmente substituiu cada instancia de «'» (apostrofo) dentro do string com «'\''» (apostrofo, barra invertida, apostrofo, apostrofo), depois insere-se apostrofos no inicio e no fial do string. Desde que o único caracter cujo significado é especial dentro de apostrofos é o próprio caractere apostrofo, isso é totalmente seguro. As novas linhas são tratadas corretamente, e o apostrofo no final dobra como um caractere de segurança para evitar que a substituição de comando atrapalhe as novas linhas, caso alguém queira fazer algo como:
quoted=$(quote "$var")
Trabalhando com arrays. Diferente de “enhanced” Bourne shells tal como Bash, o POSIX shell não possui tipos de arrays. No entanto, com um bit de eficiência, você pode obter semânticas de tipo de array em um piscar de olhos utilizando POSIX sh puro. O truque é que você tem que você possui uma array (e somente uma) — os parametros posicionais “$1”, “$2”, etc. — e você pode trocar coisas dentro e fora dessa array.
Substituindo o conteúdo da array “$@” é facil:
set -- foo bar baz boo
Ou, talvez mais util:
set -- *
O que não está claro é como salvar os conteúdos atuais de “$@” para que você possa recuperá-lo após substituí-lo, e como programaticamente gerar essas ‘arrays’. Tente essa função baseada no truque anterior com quoting:
save () {for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; doneecho " "}
O uso é algo como:
myarray=$(save "$@")set -- foo bar baz booeval "set -- $myarray"
Aqui, a quoting possui “$array” preparada para uso com o comando eval, para armazenar os parametros posicionais. Outras possibilidades tal como myarray=$(save *) também são possíveis, assim como geração programática de valores para a variável ‘array’.
Pode-se também gerar uma variável ‘array’ a partir da saída do comando find, utilizando comando habilmente construído com a opçao -exec ou ignorando a possibilidade de novas linhas nos pathnames e utilizando o comando sed para preparar os resultaddos de find para o comandos xargs.
findarray () {find "$@" -exec sh -c "for i do printf %s\\\\n \"\$i\" \\| sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\\$s/\\\$/' \\\\\\\\/\"done" dummy '{}' +}
Tal script permite coisas como:
old=$(save "$@")eval "set -- $(findarray path)"for i do command "$i" ; doneeval "set -- $old"
Note que isso duplica a funcionalidade pretendida do horrivelmente incorreto mas frequentemente citada construção “for i in `find ...` ; do ...”.
Um determinado string corresponde com um daterminado padrão de nome de arquivo?
fnmatch () { case "$2" in $1) return 0 ;; *) return 1 ;; esac ; }
Agora você pode fazer coisas como:
if fnmatch 'a??*' "$var" ; then ... ; fi
Tanto por depender do comando “[[” do Bash... Contando ocorrências de um caractere.
tr -dc 'a' | wc -c
Isso irá contar o número de ocorrencias do caractere “a”, ao excluir todos os outros caracteres. No entanto, não é claro o que tr -dc faz qunado encontrar bytes noncharacter na entrada; POSIX é não é clara nessa questão e implementações provavelmente diferem. Lógicas fundamentais apreciarão isso como uma dificuldade pratica do mundo real em trabalhar com complementes do set e com o set universal. Ao inves disso, tente o seguinte:
tr a\\n \\na | wc -l
O comando wc -l conta o número de nova linha de caractres visto, então utilizar o tr para trocar ocorrencias de “a” com novas linhas permite que o tr contar “a”s. Substituindo categorias de locale. O seguinte não necessáriamente funcionar como pretendido:
LC_COLLATE=C ls
Isso ocorre porque LC_ALL pode estar presente no ambiente, substituindo qualquer uma das variáveis específicas da categoria. Desconfigurar LC_ALL também porpornciona um comportamento incorreto, pois possivelmente altera todas as categorias. Em vez disso, tente:
eval export `locale` ; unset LC_ALL
Esse comando explicitamente define todas as variáveis de localidade específicas de categoria de acordo com os valores inplicitos que eles recebem, seja a partir da variável LANG, a variável de categoria em si, ou LC_ALL. Seu script pode subsequentemente substituir categrias individuais utilizando comandos como esse no topo dessa seção.
Tenha em mente que os únicos valores que um script portável pode definir as variáveis locais é “C” (ou seus alias “POSIX”), e que essa localidade não necessariamente possui todas as propriedades que a implementação GNU instila. Coisas que você pode assumir na localidade “C” (com a categoria relevante em parenteses):
Ranges como [a-z] funcionam em padrões glob e em expressões regulares, e são baseados na ordenação de ponto de código ASCII, não agrupamento de linguagem natural nem nem conjuntos de ordenadoção de caracteres falsos incompatíveis com ASCII (ex.: EBCDIC). Isso também se aplica a ranges de caracteres para o comando tr. (LC_COLLATE) O comando sort ordena baseado em ordenador de ponto de código ASCII (LC_COLLATE). O mapeamento de caso para “I”/“i” é sensato, sem bagunça turca (LC_CTYPE). Datas são exibidas nos padrões dos jeitos Unix tradicionais. (LC_TIME) e coisas que você não assumir ou que podem ser “mais quebradas” no locale “C” do que seja o que for a saída local fosse:
Bytes fora do do conjunto de caracter portável ASCII não são necessariamente caracteres. Dependendo da implementação que ele podem ser bytes não caractere, tratados como caracteres ISO Latin-1. tratados como algum tipo de caracteres abstratos sem propriedades, ou mesmo tratados como bytes constituintes de caracteres UTF-8. Isso afeta se (e se sim, como) eles podem ser combinados em globs e expressões regulares; (LC_CTYPE) SE LC_TYPE for alterado, outras categorias de localidade que o dado depende do codificação de caracteres (por exemplo, nomes de meses LC_CTIME, strings LC_MESSAGE, elementos de colocação LC_COLLATE, etc...) contem comportamento indefinido. (LC_CTYPE) não é claro se a POSIX especifica isso ou não, mas o motor de expressão regular da biblioteca C do GNU historicamente quebra se LC_COLLATE for definido para caracteres “C” e non-ASCII para aparecer em uma expressão de intervalo.
Como tal, as vezes é seguro substituir categorias individuais como LC_COLLATE ou LC_TIME por “C” para obter saída previsível, mas substituir LC_TYPE não é seguro a não ser que você substitua LC_ALL. Substituir LC_TYPE pode ser em raras ocasiões desejada para inibir mapeamentos estranhos e inseguros. Mas em um pior cenário ele poderia interiamente evitar acesso a todos os arquivos que contenham nomes com caracteres non-ASCII. Essa é uma área que não possui fácil solução.
Removendo todos os exports
unexport_all () {eval set -- `export -p`for i do case "$i" in*=*) unset ${i%%=*} ; eval "${i%%=*}=\${i#*=}" ;;esac ; done}
Utilizando globs para corresponder com dotfiles
.[!.]* ..?*
Os primeiros destes dois globs combinam com todos os nomes de arquivos que começam com um ponto. O segundo combina todos os filenames iniciando com dois pontos e ao menos um outro caracter. entre os dois deles, eles combinam todos os filenames iniciando com ponto exceto para “.” e “..” que possuem seus sentidos especiais óbivos.
Tenha em mente que se um glob não corresponder com nenhum dos filenames, ela permanecerá como uma única palavra não expandida em vez de desaparecer completamente do comando. Você pode precisar considerar isso testando a existência de correspondências ou ignorando/ocultando erros.
Determinando se um diretório está vazio
is_empty () (cd "$1"set -- .[!.]* ; test -f "$1" && return 1set -- ..?* ; test -f "$1" && return 1set -- * ; test -f "$1" && return 1return 0 )
Esse código utiliza os 3 globs mágicos que são necessários para combinar todos nomes possíveis exceto “.” e “..”, e também cuida de casos onde o glob combina um nome literal idêntico ao string glob.
Se você não se importa em preservar permissões, uma implementação mais simples seria:
is_empty () { rmdir "$1" && mkdir "$1" ; }
Naturalmente ambos as abordagens possuem race conditions se o diretório for gravável por outros usuários ou se outros processos puderem modificá-lo. Assim, uma abordagem como a ultima mas com um umask corretamente restritivo em efeito pode na verdade ser preferível, já que seu resultado possuem propriedades de atomicidade corretas:
is_empty_2 () ( umask 077 ; rmdir "$1" && mkdir "$1" )
Consultando o diretório inicial de um determinado usuário Isso não funciona:
foo=~$user
Ao invés disso, tente:
eval "foo=~$user"
Certifique-se de que os conteúdos d variável user estejam seguros, caso contrário coisas muito ruins poderiam acontecer. É possível fazer isso em uma função:
her_homedir () { eval "$1=~$2" ; }her_homedir foo alice
A variável foo conterá os resultados da expansão ~alice.
Processamento recursivo de diretório sem find, Desde que o find é difícil ou impossível de utilizar com robustez, ao invés disso, por que não escrever a recursão em shell script? Infelizmente eu não tenho trabalhado em um jeito de fazer isso que não exija um nível de subshell nesting por nível de arvore de diretório, mas aqui está uma tentativa com subshells:
myfind () (cd -P -- "$1"[ $# -lt 3 ] || [ "$PWD" = "$3" ] || exit 1for i in ..?* .[!.]* * ; do[ -e "$i" ] && eval "$2 \"\$i\""[ -d "$i" ] && myfind "$i" "$2" "${PWD%/}/$i"done)
O uso é então algo como:
handler () { case "$1" in *~) [ -f "$1" ] && rm -f "$1" ;; esac ; }myfind /tmp handler # Remove all backup files found in /tmp
Para cada arquivo na travessia recursiva de “$1”, uma função ou um comando “$2” será avaliado com o diretório contendo o arquivos como diretório de trabalho presente e com o nome de arquivo anexado ao final da linha de comando. O terceiro paramento posicional “$3” é utilizado internamente na recursão para proteger de symlink traversal; ela contem o pathname físico esperado que PWD deva conter depois que o comando cd -P "$1" complete desde que “$1” não for um link simbolico.
Segundo desde que época tristemente, o formato GNU %s para data não é portável. Então ao invés de:
secs=`date +%s`
Tente o seguinte:
secs=$((`TZ=GMT0 date \+"((%Y-1600)*365+(%Y-1600)/4-(%Y-1600)/100+(%Y-1600)/400+1%j-1000-135140)\*86400+(1%H-100)*3600+(1%M-100)*60+(1%S-100)"`))
O único número mágico aqui é 135140, o número de dias entre 1600-01-01 e 1970-01-01 tratando ambos como datas gregorianas. 1600 é utilizado como o múltiplo de 400 epoch aqui e ao invés de 2000 desde que a divisão C-style se comporta mal com dividendos negativos.
Considerações finais
Espero que essa página de truques cresça ao longo do tempo assim que eu encontrar mais coisas para adicionar. É a minha esperança que esses truques sirva para mostrar que É possível escrever programas corretos e robustos utilizando o shell POSIX simples, apesar das armadilhas comuns, mas também que os comprimentos necessários para fazer tal sejam com frequência extremamente perversos e ineficientes. Se vendo as hacks acima tem inspirado alguém a escrever programas em uma linguagem real ao invés de sh/Bash/sejaLáOqueFor, ouo concertar os bugs de casos específicos erguendo-se a partir da maldade da linguagem shell, ficarei feliz. Por favor, envie comentários, flames, sugestões de mais truques para incluir, e assim por diante, para “dalias” no Freenode IRC.
*Antes que comecem os ataques histéricos por GNU, essa foi uma tradução do texto de Rich Felker
**Estranho ler isso sabendo que a galera culpa o systemd por não ser portável...
Nenhum comentário:
Postar um comentário
Viu algum erro e quer compartilhar seu conhecimento? então comente aí.
Observação: somente um membro deste blog pode postar um comentário.