O histórico de comportamento de preenchimento de comandos Unix V5 tr de set2 é diferente do que consideramos hoje o comportamento “clássico” do System V (1983-1988)?

5

O comando tr tem quase 40 anos. Aparentemente apareceu no Unix pela primeira vez em 1973 com o Unix V4 . A fonte para isso não está disponível. Aqui está provavelmente a segunda implementação Unix disponível mais antiga do comando de Unix V5 cerca de 6 meses depois, em junho de 1974:

int dflag 0;
int sflag 0;
int cflag 0;
int save 0;
char code[256];
char squeez[256];
char vect[256];
struct string { int last, max, rep; char *p; } string1, string2;
int inbuf[259];

main(argc,argv)
char **argv;
{
    int i, j;
    int c, d;
    char *compl;
    extern fout;

    string1.last = string2.last = 0;
    string1.max = string2.max = 0;
    string1.rep = string2.rep = 0;
    string1.p = string2.p = "";

    if(--argc>0) {
        argv++;
        if(*argv[0]=='-'&&argv[0][4]!=0) {
            while(*++argv[0])
                switch(*argv[0]) {
                case 'c':
                    cflag++;
                    continue;
                case 'd':
                    dflag++;
                    continue;
                case 's':
                    sflag++;
                    continue;
                }
            argc--;
            argv++;
        }
    }
    if(argc>0) string1.p = argv[0];
    if(argc>1) string2.p = argv[1];
    for(i=0; i<256; i++)
        code[i] = vect[i] = 0;
    if(cflag) {
        while(c = next(&string1))
            vect[c&0377] = 1;
        j = 0;
        for(i=1; i<256; i++)
            if(vect[i]==0) vect[j++] = i;
        vect[j] = 0;
        compl = vect;
    }
    for(i=0; i<256; i++)
        squeez[i] = 0;
    for(;;){
        if(cflag) c = *compl++;
        else c = next(&string1);
        if(c==0) break;
        d = next(&string2);
        if(d==0) d = c;
        code[c&0377] = d;
        squeez[d&0377] = 1;
    }
    while(d = next(&string2))
        squeez[d&0377] = 1;
    squeez[0] = 1;
    for(i=0;i<256;i++) {
        if(code[i]==0) code[i] = i;
        else if(dflag) code[i] = 0;
    }

    inbuf[0] = 0;
    fout = dup(1);
    close(1);
    while((c=getc(inbuf)) >=0 ) {
        if(c == 0) continue;
        if(c = code[c&0377]&0377)
            if(!sflag || c!=save || !squeez[c&0377])
                putchar(save = c);
    }
    flush();
}

next(s)
struct string *s;
{
    int a, b, c, n;
    int base;

    if(--s->rep > 0) return(s->last);
    if(s->last < s->max) return(++s->last);
    if(*s->p=='[') {
        nextc(s);
        s->last = a = nextc(s);
        s->max = 0;
        switch(nextc(s)) {
        case '-':
            b = nextc(s);
            if(b<a || *s->p++!=']')
                goto error;
            s->max = b;
            return(a);
        case '*':
            base = (*s->p=='0')?8:10;
            n = 0;
            while((c = *s->p)>='0' && c<'0'+base) {
                n = base*n + c - '0';
                s->p++;
            }
            if(*s->p++!=']') goto error;
            if(n==0) n = 1000;
            s->rep = n;
            return(a);
        default:
        error:
            write(1,"Bad string\n",11);
            exit();
        }
    }
    return(nextc(s));
}

nextc(s)
struct string *s;
{
    int c, i, n;

    c = *s->p++;
    if(c=='\') {
        i = n = 0;
        while(i<3 && (c = *s->p)>='0' && c<='7') {
            n = n*8 + c - '0';
            i++;
            s->p++;
        }
        if(i>0) c = n;
        else c = *s->p++;
    }
    if(c==0) *--s->p = 0;
    return(c&0377);
}

Com o tempo, isso evoluiu e, desde os primeiros dias, houve variações em como o comando lida com conjuntos de tamanhos diferentes, e aqui meu interesse é com o caso em que set2 é menor que set1.

O GNU Coreutils manual discute o situação:

When tr is performing translation, set1 and set2 typically have the same length. If set1 is shorter than set2, the extra characters at the end of set2 are ignored.

On the other hand, making set1 longer than set2 is not portable; POSIX says that the result is undefined. In this situation, BSD tr pads set2 to the length of set1 by repeating the last character of set2 as many times as necessary. System V tr truncates set1 to the length of set2.

By default, GNU tr handles this case like BSD tr. When the --truncate-set1 (-t) option is given, GNU tr handles this case like the System V tr >instead. This option is ignored for operations other than translation.

Acting like System V tr in this case breaks the relatively common BSD idiom:

 tr -cs A-Za-z0-9 '2'

because it converts only zero bytes (the first element in the complement of set1), rather than all non-alphanumerics, to newlines.

Existe também tal discussão em The Open Group Especificações Básicas Edição 7 IEEE Std 1003.1 , edição de 2013:

When string2 is shorter than string1, a difference results between historical System V and BSD systems. A BSD system pads string2 with the last character found in string2. Thus, it is possible to do the following:

tr 0123456789 d

which would translate all digits to the letter 'd'. Since this area is specifically unspecified in this volume of POSIX.1-2008, both the BSD and System V behaviors are allowed, but a conforming application cannot rely on the BSD behavior. It would have to code the example in the following way:

tr 0123456789 '[d*]'

Agora, se você ler as man pages do comando tr em V4 e V5 , você vê a seguinte referência em ambos:

If string2 is short, it is padded with corresponding characters from string1.

Mas essa referência é omitida no manual V6 e versões posteriores do Unix, mas a implementação V6 do comando é linha para linha idêntica à V5? Então você tem uma diferença nos manuais mas não no código? Além disso, essa implementação parece diferente do que é considerado "comportamento clássico do BSD ou do System V", ou seja, pad de adicionar elementos set2 ou truncar ao comprimento de set1.

Assim, a implementação da V4-V5 é diferente do marco Unix do System V e qual é a justificativa para essa implementação diferente e, por fim, por que foi descartada no futuro? Como posso descobrir mais informações sobre um projeto inicial do comando?

    
por jus cogens prime 04.01.2014 / 04:37

1 resposta

4

A diferença está apenas no texto do comportamento de preenchimento no manual da V4-V5 - mas o comportamento é o mesmo. Como está, os resultados da implementação da V5 são idêntico ao do System V, o qual é ele mesmo idêntico ao comportamento do tr do GNU com a opção --truncate-set1 . Além disso, "truncar set1 para o comprimento de set2" fornece o mesmo resultado que "padding string2 com caracteres correspondentes de string1". Isso significa a mesma coisa na prática. Vamos demonstrar isso.

Primeiro, você não precisa ser um desenvolvedor para tentar compilar isso. Compare o código-fonte com a versão PWB / Unix quase idêntica . Você verá que a única diferença é confiar nos ativos "stdio.h" modernos basicamente, então eu tirei a fonte de suas referências para inbuf , fout , dup e flush e substituí-lo com o que O PWB / Unix faz - mas isso não deve alterar o comportamento, pois os algoritmos permanecem intocados. Anotei as alterações triviais que fiz do original:

#include <stdio.h>    <------ added
int dflag = 0;        <------ added "=" sign to those
int sflag = 0;
int cflag = 0;
int save = 0;
char code[256];
char squeez[256];
char vect[256];
struct string { int last, max, rep; char *p; } string1, string2;
FILE *input;          <------ part of the stdio framework I guess;

main(argc,argv)
char **argv;
{
    int i, j;
    int c, d;
    char *compl;

    string1.last = string2.last = 0;
    string1.max = string2.max = 0;
    string1.rep = string2.rep = 0;
    string1.p = string2.p = "";

    if(--argc>0) {
        argv++;
        if(*argv[0]=='-'&&argv[0][1]!=0) {
            while(*++argv[0])
                switch(*argv[0]) {
                case 'c':
                    cflag++;
                    continue;
                case 'd':
                    dflag++;
                    continue;
                case 's':
                    sflag++;
                    continue;
                }
            argc--;
            argv++;
        }
    }
    if(argc>0) string1.p = argv[0];
    if(argc>1) string2.p = argv[1];
    for(i=0; i<256; i++)
        code[i] = vect[i] = 0;
    if(cflag) {
        while(c = next(&string1))
            vect[c&0377] = 1;
        j = 0;
        for(i=1; i<256; i++)
            if(vect[i]==0) vect[j++] = i;
        vect[j] = 0;
        compl = vect;
    }
    for(i=0; i<256; i++)
        squeez[i] = 0;
    for(;;){
        if(cflag) c = *compl++;
        else c = next(&string1);
        if(c==0) break;
        d = next(&string2);
        if(d==0) d = c;
        code[c&0377] = d;
        squeez[d&0377] = 1;
    }
    while(d = next(&string2))
        squeez[d&0377] = 1;
    squeez[0] = 1;
    for(i=0;i<256;i++) {
        if(code[i]==0) code[i] = i;
        else if(dflag) code[i] = 0;
    }

    input = stdin;                     <------ again stdio
    while((c=getc(input)) != EOF ) {   <------
        if(c == 0) continue;
        if(c = code[c&0377]&0377)
            if(!sflag || c!=save || !squeez[c&0377])
                putchar(save = c);
    }

}

next(s)
struct string *s;
{
    int a, b, c, n;
    int base;

    if(--s->rep > 0) return(s->last);
    if(s->last < s->max) return(++s->last);
    if(*s->p=='[') {
        nextc(s);
        s->last = a = nextc(s);
        s->max = 0;
        switch(nextc(s)) {
        case '-':
            b = nextc(s);
            if(b<a || *s->p++!=']')
                goto error;
            s->max = b;
            return(a);
        case '*':
            base = (*s->p=='0')?8:10;
            n = 0;
            while((c = *s->p)>='0' && c<'0'+base) {
                n = base*n + c - '0';
                s->p++;
            }
            if(*s->p++!=']') goto error;
            if(n==0) n = 1000;
            s->rep = n;
            return(a);
        default:
        error:
            write(1,"Bad string\n",11);
            exit(0);     <------original was exit();
        }
    }
    return(nextc(s));
}

nextc(s)
struct string *s;
{
    int c, i, n;

    c = *s->p++;
    if(c=='\') {
        i = n = 0;
        while(i<3 && (c = *s->p)>='0' && c<='7') {
            n = n*8 + c - '0';
            i++;
            s->p++;
        }
        if(i>0) c = n;
        else c = *s->p++;
    }
    if(c==0) *--s->p = 0;
    return(c&0377);
}

Então, cc tr.c compila:

tr.c: In function ‘next’:
tr.c:118:4: warning: incompatible implicit declaration of built-in function ‘exit’ 
[enabled by default]
exit(0);
^

Mas a.out está lá e funciona, então vamos comparar o comportamento de preenchimento dos dois programas que temos:

GNU tr

#tr 0123456789 d     
0123456789 input
dddddddddd output             <----- BSD classic behavior

#tr 0123456789 d123456789     <----- padding set2 with set1 explicitly 
0123456789 i
d123456789 o
01234567890123456789 i
d123456789d123456789 o

#tr -t 0123456789 d           <----- --truncate-set1 i.e. System V behavior
0123456789 i
d123456789 o                  <----- concretely, this is what is meant by a result 
0012 i                               where set2 was padded with set1
dd12 o

#tr -t 0123456789 d123456789  <----- padding set2 with set1 explicitly
0123456789 i                  
d123456789 o                  <----- note this is identical to the last results

Unix V5 tr + mod de stdio

#./a.out 0123456789 d         <----- our compiled version with the classic example
0123456789 i
d123456789 o

./a.out 0123456789 d123456789 <----- padding set2 with set1 explicitly
0123456789 i
d123456789 o

Portanto, nossa versão V5 se comporta exatamente como a versão do System V a esse respeito. Além disso, o preenchimento explícito de set2 com set1 produz o mesmo resultado para todas as implementações, pois garante que set1 e set2 tenham o mesmo número de elementos (e é quando você não tem isso que os resultados variam historicamente).

Por fim, preencher explicitamente ou ter tr pad set2 with set1 como descrito nos manuais originais da V4-V5 significa a mesma coisa que truncating set1 to the length of set2 no que se refere aos resultados - é IS o clássico < em> System V implementação para preenchimento e produz os mesmos resultados. V5 tr não é uma implementação diferente, apesar da diferença nas páginas man.

    
por 04.01.2014 / 04:39

Tags