Tại sao printf hẹp thu nhỏ um umut?


54

Nếu tôi thực thi đoạn script đơn giản sau:

#!/bin/bash
printf "%-20s %s\n" "Früchte und Gemüse"   "foo"
printf "%-20s %s\n" "Milchprodukte"        "bar"
printf "%-20s %s\n" "12345678901234567890" "baz"

Nó in:

Früchte und Gemüse foo
Milchprodukte        bar
12345678901234567890 baz

có nghĩa là, văn bản có âm sắc (chẳng hạn như ü) bị "thu nhỏ" bởi một ký tự cho mỗi âm sắc.

Chắc chắn, tôi có một số cài đặt sai ở đâu đó, nhưng tôi không thể tìm ra cái nào có thể.

Điều này xảy ra nếu mã hóa của tệp là UTF-8.

Nếu tôi thay đổi mã hóa thành Latin-1, căn chỉnh là chính xác, nhưng các ô được hiển thị sai:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

14
Bạn mong đợi printf nhận thức được UTF-8 và các bảng mã đa bào khác?
frostschutz

16
Có vẻ như nó đang đếm byte chứ không phải ký tự; xem echo Früchte und Gemüse | wc -c -mcho sự khác biệt
Stephen Kitt

7
@frostschutz Zsh printflà.
Stephen Kitt

10
Có, tôi thực sự mong đợi printf nhận thức được (ít nhất) UTF-8.
René Nyffalanger

12
À, không phải vậy. May mắn lớn. ;-)
frostschutz

Câu trả lời:


87

POSIX đòi hỏi printf 's %-20sđể đếm những 20 về byte không nhân vật mặc dù tạo ra rất ít cảm giác như printflà in văn bản , định dạng (xem thảo luận tại Tập đoàn Austin (POSIX) và bashmailing list).

Việc xây printfdựng bashvà hầu hết các vỏ POSIX khác tôn vinh điều đó.

zshbỏ qua yêu cầu ngớ ngẩn đó (ngay cả trong shthi đua) để printfhoạt động như bạn mong đợi ở đó. Tương tự đối với phần printfdựng sẵn của fish(không phải là vỏ giống POSIX).

ütự (U + 00FC), khi được mã hóa bằng UTF-8 được tạo thành từ hai byte (0xc3 và 0xbc), giải thích sự khác biệt.

$ printf %s 'Früchte und Gemüse' | wc -mcL
    18      20      18

Chuỗi đó được tạo thành từ 18 ký tự, rộng 18 cột ( -Lwcphần mở rộng GNU để báo cáo độ rộng hiển thị của dòng rộng nhất trong đầu vào) nhưng được mã hóa trên 20 byte.

Trong zshhoặc fish, văn bản sẽ được căn chỉnh chính xác.

Giờ đây, cũng có những ký tự có độ rộng 0 (như kết hợp các ký tự như U + 0308, diaresis kết hợp) hoặc có độ rộng gấp đôi như trong nhiều tập lệnh Asiatic (không đề cập đến các ký tự điều khiển như Tab) và thậm chí zshsẽ không căn chỉnh những cái đó đúng

Ví dụ, trong zsh:

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
 ü|
  ᄀ|

Trong bash:

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
 ü|
ü|
ᄀ|

ksh93có một %Lsđặc điểm kỹ thuật định dạng để đếm chiều rộng theo chiều rộng màn hình .

$ printf '%3Ls|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
  ü|
 ᄀ|

Điều đó vẫn không hoạt động nếu văn bản chứa các ký tự điều khiển như TAB (làm thế nào nó printfcó thể phải biết các điểm dừng cách xa nhau trong thiết bị đầu ra và vị trí bắt đầu in ở vị trí nào). Nó hoạt động một cách tình cờ với các ký tự backspace (như trong roffđầu ra trong đó X(in đậm X) được viết là X\bX) mặc dù ksh93coi tất cả các ký tự điều khiển là có chiều rộng -1.

Như các tùy chọn khác, bạn có thể thử:

printf '%s\t|\n' u ü $'u\u308' $'\u1100' | expand -t3

Điều đó hoạt động với một số expandtriển khai (mặc dù không phải GNU).

Trên các hệ thống GNU, bạn có thể sử dụng GNU awkprintfsố đếm bằng ký tự (không phải byte, không phải chiều rộng hiển thị, do đó vẫn không ổn đối với các ký tự 0 chiều rộng hoặc 2 chiều rộng, nhưng OK cho mẫu của bạn):

gawk 'BEGIN {for (i = 1; i < ARGC; i++) printf "%-3s|\n", ARGV[i]}
     ' u ü $'u\u308' $'\u1100'

Nếu đầu ra đi đến một thiết bị đầu cuối, bạn cũng có thể sử dụng các chuỗi thoát định vị con trỏ. Như:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"

2
Điều đó là không chính xác. Các ücaracter có thể được cấu thành là u+ ¨, là 3 byte. Trong trường hợp câu hỏi, nó được mã hóa thành 2 ký tự, nhưng không phải tất cả üđều được tạo như nhau.
Ismael Miguel

6
@IsmaelMiguel, u\u308là hai ký tự ( wc -mít nhất là trong Unix / nghĩa) cho một cụm glyph / graphem / graphem-cluster và đã được đề cập và đưa vào câu trả lời này.
Stéphane Chazelas

"Điều đó có ý nghĩa rất nhỏ khi printf là in văn bản" Chà, người ta có thể lập luận rằng printf xử lý các ký tự C (byte); nó không nên xử lý các ngôn ngữ văn bản và nó không có gánh nặng để hiểu mã hóa bộ ký tự (có thể là đa khối). Nhưng dòng bảo vệ này mâu thuẫn với các yêu cầu (ISO C99) rằng việc cắt ngắn byte "% s" không được dẫn đến các văn bản "không hợp lệ" (ký tự cắt ngắn). Glibc thậm chí thất bại trong trường hợp đó (nó không in gì cả). Một mớ hỗn độn thực sự. postgresql.org/message-id/ từ
leonbloy

@leonbloy, mà có thể làm cho tinh thần của C printf(3)(chút cảm giác sau đó yêu cầu C99 bạn đang nhắc đến, nhờ đó), nhưng không phải là printf(1)tiện ích như mỗi nhà khai thác vỏ hoặc tiện ích thỏa thuận văn bản khác với các nhân vật (hoặc đã được sửa đổi để còn đối phó với các nhân vật giống như wccó một -m(trong khi -cở lại byte ) hoặc cut-bsau đó -ccó thể có nghĩa gì đó khác với byte).
Stéphane Chazelas

Ngay cả khi nó sử dụng các ký tự thay vì byte, nó vẫn không phù hợp để căn chỉnh các cột. Bạn cần biết mỗi nhân vật chiếm bao nhiêu ô, thay đổi theo ký tự (0-2).
R ..

10

Nếu tôi thay đổi mã hóa thành Latin-1, căn chỉnh là chính xác, nhưng các ô được hiển thị sai:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

Trên thực tế, không, nhưng thiết bị đầu cuối của bạn không nói tiếng Latin-1, và do đó bạn nhận được rác thay vì ô dù.

Bạn có thể khắc phục điều này bằng cách sử dụng iconv:

printf foo bar | iconv -f ISO8859-1 -t UTF-8

(hoặc chỉ chạy toàn bộ tập lệnh shell được đưa vào iconv)


3
Đây là một nhận xét hữu ích nhưng không trả lời câu hỏi cốt lõi.
gerrit

1
@gerrit thế nào vậy? Nếu printf thực hiện đúng khi in bằng latin1, sau đó in bằng Latin1 và chuyển đổi thành UTF-8 sau? Có vẻ như một sửa chữa thích hợp cho câu hỏi cốt lõi với tôi.
Wouter Verhelst

1
Câu hỏi cốt lõi là "Tại sao nó thu nhỏ âm sắc", câu trả lời (như trong các câu trả lời khác) là "bởi vì nó không hỗ trợ utf-8". Nó không hỏi tại sao các ô được hiển thị sai hoặc làm cách nào tôi có thể sửa kết xuất âm thanh . Dù bằng cách nào, đề xuất của bạn hữu ích cho tập hợp con của utf-8 có thể được biểu diễn dưới dạng iso8859-1 (chỉ).
gerrit

4
@WouterVerhelst, có, mặc dù điều đó chỉ có thể áp dụng cho văn bản có thể được mã hóa trong bộ ký tự một byte.
Stéphane Chazelas

3
Tôi cũng đọc câu hỏi là "làm thế nào tôi có thể có đầu ra đúng" thay vì "Tôi không bận tâm đến đầu ra bị lỗi, miễn là tôi biết tại sao".
Ông Lister
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.