set2 的歷史 Unix V5 tr 命令填充行為與我們今天認為的“經典” System V(1983-1988)行為不同嗎?
該
tr
命令已有近 40 年的歷史。它似乎在 1973 年與Unix V4一起首次出現在 Unix 中。這方面的來源不可用。這可能是大約 6 個月後的 1974 年 6 月Unix V5中該命令的第二古老的 Unix 實現: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); }
隨著時間的發展,從早期開始,命令處理不同長度的集合的方式發生了變化,這裡我感興趣的是 set2 比 set1 短的情況。
GNU Coreutils 手冊討論了這種情況:
當 tr 執行翻譯時,set1 和 set2 通常具有相同的長度。如果 set1 比 set2 短,則忽略 set2 末尾的多餘字元。
另一方面,使 set1 比 set2 長是不可移植的;POSIX 說結果是未定義的。在這種情況下,BSD tr 將 set2 填充到 set1 的長度,方法是根據需要多次重複 set2 的最後一個字元。System V tr 將 set1 截斷為 set2 的長度。
預設情況下,GNU tr 像 BSD tr 一樣處理這種情況。當給出 –truncate-set1 (-t) 選項時,GNU tr 會像 System V tr > 那樣處理這種情況。對於翻譯以外的操作,此選項將被忽略。
在這種情況下,像 System V tr 那樣行事打破了相對常見的 BSD 習語:
tr -cs A-Za-z0-9 '\012'
因為它只將零字節(set1 的補碼中的第一個元素)而不是所有非字母數字轉換為換行符。
The Open Group Base Specifications Issue 7 IEEE Std 1003.1 , 2013 Edition中也有這樣的討論:
當 string2 比 string1 短時,歷史 System V 和 BSD 系統之間會產生差異。BSD 系統用 string2 中的最後一個字元填充 string2。因此,可以執行以下操作:
tr 0123456789 d
這會將所有數字轉換為字母“d”。由於該領域在本卷 POSIX.1-2008 中未明確指定,因此 BSD 和 System V 行為都是允許的,但符合標準的應用程序不能依賴 BSD 行為。它必須按以下方式對範例進行編碼:
tr 0123456789 '
$$ d* $$'
現在,如果您閱讀V4和V5中 tr 命令的手冊頁,您會在兩者中看到以下參考:
If string2 is short, it is padded with corresponding characters from string1.
但是在 V6手冊和後來的 Unix 早期版本中省略了該引用,但是該命令的 V6 實現是與 V5 相同的行對行?所以你在手冊上有區別,但在程式碼上沒有?此外,這種實現似乎與所謂的“經典 BSD 或 System V”行為不同,即填充從 set2 元素添加或截斷到 set1 的長度。
那麼 V4-V5 實現與 System V 里程碑 Unix 有什麼不同,這種不同實現的基本原理是什麼,最終為什麼它被丟棄了?我怎樣才能找到有關這種早期命令設計的更多資訊?
區別僅在於 V4-V5 手冊中填充行為的措辭 - 但行為始終相同。就目前而言,V5 實現的結果與 System V 的結果相同,後者本身與帶有選項的 GNU
tr
行為相同。--truncate-set1
此外,“將 set1 截斷到 set2 的長度”與“用 string1 中的相應字元填充 string2”給出相同的結果。這在實踐中意味著同樣的事情。讓我們證明這一點。首先,您不必是開發人員就可以嘗試編譯它。將原始碼與幾乎相同的PWB/Unix 版本進行比較。您將看到唯一的區別是基本上依賴於“現代”stdio.h 資產,因此我已經剝離了它對 、 和 的引用的來源,並將其替換
inbuf
為PWB /Unix 所做的 - 但這絕不應該改變算法保持不變的行為。我已經註釋了我對原始文件所做的微不足道的更改:fout``dup``flush
#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); }
所以
cc tr.c
編譯:tr.c: In function ‘next’: tr.c:118:4: warning: incompatible implicit declaration of built-in function ‘exit’ [enabled by default] exit(0); ^
但是 a.out 在那裡並且有效,所以現在讓我們比較一下我們擁有的兩個程序的填充行為:
GNU 支持
#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 + 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
因此,我們的 V5 版本在這方面的行為與 System V 版本完全相同。此外,用 set1 顯式填充 set2 會為所有實現產生相同的結果,因為它確保 set1 和 set2 具有相同數量的元素(當你沒有這個時,結果會在歷史上有所不同)。
最後,就結果而言,顯式填充或
tr
pad set2 with set1
原始 V4-V5 手冊中描述的具有相同的含義truncating set1 to the length of set2
- 它是用於填充的經典System V實現並產生相同的結果。儘管手冊頁有所不同,但V5tr
並不是一個不同的實現。