เหตุใด printf“ umlaut” umlaut?


54

ถ้าฉันรันสคริปต์ง่าย ๆ ต่อไปนี้:

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

มันพิมพ์:

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

นั่นคือข้อความที่มีเครื่องหมาย umlauts (เช่นü) คือ "หด" โดยอักขระหนึ่งตัวต่อ umlaut

แน่นอนฉันมีการตั้งค่าผิดบางแห่ง แต่ฉันไม่สามารถคิดได้ว่าจะเป็นแบบไหน

สิ่งนี้จะเกิดขึ้นหากการเข้ารหัสไฟล์เป็น UTF-8

หากฉันเปลี่ยนการเข้ารหัสเป็น latin-1 การจัดตำแหน่งนั้นถูกต้อง แต่ umlauts แสดงผลไม่ถูกต้อง:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

14
คุณคาดหวังให้ printf ระวัง UTF-8 และชุดอักขระหลายไบต์หรือไม่
frostschutz

16
ดูเหมือนว่าจะนับไบต์มากกว่าตัวอักษร ดูecho Früchte und Gemüse | wc -c -mความแตกต่าง
สตีเฟ่น Kitt

7
@frostschutz Zsh's printfคือ
Stephen Kitt

10
ใช่ฉันคาดหวังว่า printf ต้องระวัง (อย่างน้อย) UTF-8
René Nyffenegger

12
ก็ไม่เป็นไร โชคดี ;-)
frostschutz

คำตอบ:


87

POSIX ต้อง printfของ%-20sการนับผู้ที่ 20 ในแง่ของไบต์ไม่ได้ตัวละครแม้ว่าที่ทำให้ความรู้สึกเล็ก ๆ น้อย ๆ เป็นprintfคือการพิมพ์ข้อความในรูปแบบ (ดูการอภิปรายที่ออสตินกลุ่ม (POSIX) และbashรายการทางไปรษณีย์)

printfbuiltin ของbashและส่วนใหญ่เปลือกหอย POSIX อื่น ๆ ที่ให้เกียรติ

zshไม่สนใจข้อกำหนดโง่ ๆ (แม้จะเป็นshอีมูเลชัน) ดังนั้นprintfทำงานได้ตามที่คุณคาดหวัง เหมือนกันสำหรับบิวด์อินprintfของfish(ไม่ใช่เชลล์เหมือน POSIX)

üตัวอักษร (U + 00FC) เมื่อการเข้ารหัสใน UTF-8 ทำสองไบต์ (0xc3 และ 0xbc) ซึ่งอธิบายถึงความแตกต่าง

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

สตริงนั้นทำจาก 18 อักขระกว้าง 18 คอลัมน์ ( -Lเป็นwcส่วนขยายของGNU เพื่อรายงานความกว้างในการแสดงผลของบรรทัดที่กว้างที่สุดในอินพุต) แต่ถูกเข้ารหัสบน 20 ไบต์

ในzshหรือfishข้อความจะถูกจัดตำแหน่งอย่างถูกต้อง

ตอนนี้ยังมีอักขระที่มีความกว้าง 0 (เช่นการรวมอักขระเช่น U + 0308, diaresis รวม) หรือมีความกว้างสองเท่าเช่นในสคริปต์ Asiatic จำนวนมาก (ไม่พูดถึงอักขระควบคุมเช่น Tab) และแม้zshจะไม่จัดตำแหน่ง เหล่านั้นอย่างถูกต้อง

ตัวอย่างในzsh:

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

ในbash:

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

ksh93มี%Lsข้อกำหนดรูปแบบเพื่อนับความกว้างในแง่ของความกว้างของจอแสดงผล

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

ยังไม่สามารถใช้งานได้หากข้อความมีอักขระควบคุมเช่น TAB (จะเป็นไปได้อย่างไรprintfจะต้องทราบว่าแท็บหยุดห่างกันมากแค่ไหนในอุปกรณ์แสดงผลและตำแหน่งที่เริ่มพิมพ์) มันไม่ทำงานโดยอุบัติเหตุกับตัวละคร Backspace (เช่นเดียวกับในroffการส่งออกที่X(ตัวหนาX) เขียนเป็นX\bX) แต่เป็นพิจารณาตัวควบคุมทั้งหมดที่มีความกว้างของksh93-1

เช่นเดียวกับตัวเลือกอื่น ๆ คุณสามารถลอง:

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

ที่ทำงานกับexpandการใช้งานบางอย่าง(ไม่ใช่ของ GNU)

บนระบบ GNU คุณสามารถใช้ GNU awkที่มีการprintfนับเป็นตัวอักษร (ไม่ใช่ไบต์ไม่ใช่ความกว้างของหน้าจอดังนั้นจึงยังไม่ตกลงสำหรับอักขระความกว้าง 0 หรือ 2 ความกว้าง แต่ตกลงสำหรับตัวอย่างของคุณ):

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

หากเอาต์พุตไปที่เทอร์มินัลคุณสามารถใช้ลำดับการหลีกเลี่ยงตำแหน่งเคอร์เซอร์ได้ ชอบ:

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

2
มันไม่ถูกต้อง ücaracter สามารถประกอบเป็นu+ ¨ซึ่งเป็น 3 ไบต์ ในกรณีของคำถามนั้นจะถูกเข้ารหัสเป็น 2 ตัวอักษร แต่ไม่ใช่ทั้งหมดüจะถูกสร้างขึ้นอย่างเท่าเทียมกัน
Ismael Miguel

6
@IsmaelMiguel u\u308เป็นอักขระสองตัว (ใน Unix / wc -mอย่างน้อย) สำหรับหนึ่ง glyph / graphem / graphem-cluster และถูกกล่าวถึงแล้วและรวมอยู่ในคำตอบนี้
Stéphane Chazelas

"ที่ไม่เหมาะสมสำหรับ printf คือการพิมพ์ข้อความ" ดีเราอาจโต้เถียงว่า printf เกี่ยวข้องกับ C chars (ไบต์); ไม่ควรจัดการกับโลแคลข้อความและไม่ควรมีภาระในการทำความเข้าใจการเข้ารหัสชุดอักขระ (อาจมีหลายไบต์) แต่แนวป้องกันนี้ขัดแย้งกับข้อกำหนด (ISO C99) ที่การตัดไบต์แบบ "% s" ไม่ควรส่งผลให้ข้อความ "ไม่ถูกต้อง" (ตัวอักษรที่ถูกตัดทอน) Glibc ถึงกับล้มเหลวในกรณีนั้น ระเบียบจริง postgresql.org/message-id/…
leonbloy

@leonbloy ที่อาจทำให้ความรู้สึกของ C printf(3)(ความรู้สึกเล็กน้อยหลังจากที่ความต้องการ C99 ว่าคุณกำลังกล่าวขวัญขอบคุณสำหรับที่) แต่ไม่ได้เป็นprintf(1)ยูทิลิตี้เป็นผู้ประกอบการหรือเปลือกทุกจัดการยูทิลิตี้อื่น ๆ ที่มีข้อความตัวอักษร (หรือมีการแก้ไขไปยังจัดการกับตัวอักษร เช่นwcที่ได้รับ-m(ในขณะที่ยัง-cคงอยู่ไบต์ ) หรือcutที่ได้รับ-bหลังจากนั้น-cอาจหมายถึงสิ่งอื่นที่ไม่ใช่ไบต์
Stéphane Chazelas

แม้ว่าจะใช้อักขระมากกว่าไบต์ แต่ก็ยังไม่เหมาะสำหรับการจัดแนวคอลัมน์ คุณจำเป็นต้องรู้จำนวนเทอร์มินัลเซลล์แต่ละอักขระที่ใช้ซึ่งแตกต่างกันไปตามอักขระ (0-2)
..

10

หากฉันเปลี่ยนการเข้ารหัสเป็น latin-1 การจัดตำแหน่งนั้นถูกต้อง แต่ umlauts แสดงผลไม่ถูกต้อง:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

ที่จริงแล้วไม่มี แต่เทอร์มินัลของคุณไม่พูดภาษาละติน -1 ดังนั้นคุณจึงได้รับขยะมากกว่า umlauts

คุณสามารถแก้ไขได้โดยใช้ iconv:

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

(หรือเพียงแค่เรียกใช้สคริปต์เปลือกทั้งหมดลงใน iconv)


3
นี่เป็นความคิดเห็นที่มีประโยชน์ แต่ไม่ตอบคำถามหลัก
gerrit

1
@gerrit เหรอ? หาก printf ทำสิ่งที่ถูกต้องเมื่อพิมพ์ใน latin1 ให้พิมพ์เป็น latin1 แล้วแปลงเป็น UTF-8 ในภายหลังหรือไม่ ดูเหมือนว่าการแก้ไขที่เหมาะสมสำหรับคำถามหลักสำหรับฉัน
Wouter Verhelst

1
คำถามหลักคือ "ทำไมมันลดขนาด umlaut" คำตอบ (เช่นเดียวกับคำตอบอื่น ๆ ) คือ "เพราะมันไม่สนับสนุน utf-8" ไม่ได้ถามว่าเพราะเหตุใดการแสดงผล umlauts ผิดหรือฉันจะแก้ไขการแสดงผล umlautได้อย่างไร ไม่ว่าจะด้วยวิธีใดข้อเสนอแนะของคุณมีประโยชน์สำหรับชุดย่อยของ utf-8 ที่สามารถแสดงเป็น iso8859-1 (เท่านั้น)
gerrit

4
@WouterVerhelst ใช่ว่าสามารถใช้ได้กับข้อความที่สามารถเข้ารหัสในชุดอักขระไบต์เดียว
Stéphane Chazelas

3
ฉันก็อ่านคำถามด้วยว่า "ฉันจะเอาท์พุทที่ถูกต้องได้อย่างไร" แทนที่จะเป็น "ฉันไม่รังเกียจที่จะเอาท์พุทผิดพลาด
Mr Lister
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.