wcwidth(3) and wcswidth(3): the fifth ruler, with a Unicode version baked in¶
Level: reference · for anyone who has typed man 3 wcwidth, seen four possible return values, and wanted to know which one an emoji gets
One line: wcwidth answers 0, 1, 2 or -1 for one code point from a table that has an edition, so this Mac (Unicode 15.0) calls U+2FFC unprintable while glibc 2.39 (Unicode 15.1) gives it two columns and both call every Unicode 16 emoji -1; wcswidth of a string with a TAB in it is -1 rather than a sum; and neither the shell's printf '%-10s' nor Python's '{:<10}' pads by the number either function returns.
The pages: wcwidth(3) (macOS, dated August 17, 2004) · wcswidth(3) (macOS, August 20, 2002) · wcwidth(3) (Linux man-pages 6.7, 2023-10-31). Dumped 2026-09-13 by dump.sh and dump_linux.sh; the machines are in raw/PROVENANCE-macos.txt and raw/PROVENANCE-linux.txt.
What the pages are for¶
Of the five rulers A code point is not a character lays along a string (bytes, code units, code points, grapheme clusters, terminal columns) the fifth is the one no language's standard library returns, and wcwidth is the C library's attempt at it. It lives in section 3, takes one wchar_t, and returns how many cells of a fixed-width display that character occupies; wcswidth adds it up over a string. fold(1), column(1), ls in a narrow window, every curses program and every terminal multiplexer call it to decide where a line wraps and where the cursor is, so it is the function behind the Polish word that wrapped early and the CJK table whose borders do not line up.
The two macOS pages are BSD's, from 2002 and 2004, and say what the functions return. The Linux page is from 2023 and mostly says what you must #define to be allowed to call the function. None of the three says where the numbers come from, and that is why the pages are here: the table behind wcwidth is a copy of one edition of the Unicode Character Database, the copy differs between the two machines, and the experiment below finds each edition by asking about characters added in successive years. The table has a version is the general rule; this page is the rule inside libc.
The page, with notes¶
wcwidth(3) on macOS: four answers, three of them named¶
The wcwidth() function returns 0 if the wc argument is a null wide
character (L'\0'), -1 if wc is not printable; otherwise, it returns the
number of column positions the character occupies.
0 is named only for L'\0', but it is also the answer for every character that occupies no cell of its own: a combining mark such as U+0301, a zero-width joiner, a variation selector, a zero-width space. 1 is a narrow character: A, é, ż, €, and a regional-indicator letter on its own. 2 is a wide one: a CJK ideograph, a fullwidth Latin letter, an ideographic space, and every emoji with emoji presentation. -1, not printable, covers TAB, ESC and DEL on both machines, SOFT HYPHEN on this Mac only, and on both anything the table has never heard of, which is how the table's edition becomes visible from outside. Control characters is the page for the first group; the last group is the experiment below.
while ((ch = getwchar()) != WEOF) {
w = wcwidth(ch);
if (w > 0 && column + w >= 20) {
putwchar(L'\n');
column = 0;
}
putwchar(ch);
if (ch == L'\n')
column = 0;
else if (w > 0)
column += w;
}
The example is fold(1) in twelve lines, and the w > 0 test is what lets it survive the other two answers: zero-width and unprintable characters are copied through without moving the column. It is worth reading for what it assumes. It assumes the terminal agrees with wcwidth about every character, which the family emoji on the ruler page disproves, and it assumes a line may break between any two characters, which Where a line may break disproves: at column 20 it will split an e from the U+0301 that follows it.
wcswidth(3): a sum, unless¶
The wcswidth() function returns 0 if pwcs is an empty string (L""), -1 if
a non-printing wide character is encountered; otherwise, it returns the
number of column positions occupied.
One unprintable character poisons the whole sum. wcswidth(L"a\tb", 3) is -1, not 2, so a caller padding a column must have stripped every control character first or must test the sign, and much code does neither. What it adds up correctly is the point of the ruler page: café is 4 whether it is four code points or five with a combining acute, 中文 is 4 for two characters, and a ZWJ b is 2 for three. The number is neither a byte count nor a code point count, and the last experiment holds all three side by side.
wcwidth(3) on Linux: the feature macro, and the locale¶
SYNOPSIS
#define _XOPEN_SOURCE /* See feature_test_macros(7) */
#include <wchar.h>
int wcwidth(wchar_t c);
...
NOTES
The behavior of wcwidth() depends on the LC_CTYPE category of the
current locale.
wcwidth is in POSIX's XSI option, not in ISO C, so glibc's <wchar.h> hides the declaration unless the program asks for it with _XOPEN_SOURCE; without it gcc 13 warns implicit declaration of function and links anyway, as the experiment shows, while the macOS header declares it unconditionally. The NOTES line is the one that matters and it is on the Linux page only: in the C locale wcwidth(L'é') is -1 on both machines, because in the C locale é is not a character, so a program that never called setlocale gets -1 for everything above 0x7F and wraps nothing correctly. That is Locale and LC_CTYPE as a return value.
Terms on the page¶
| On the page | What it means | Read more |
|---|---|---|
| column position | One cell of a fixed-width terminal; the fifth ruler, the one people mean when they ask how long a string is | A code point is not a character |
wide character, wchar_t |
One decoded character as one integer; on glibc always a code point, on this Mac only in a UTF-8 locale | multibyte(3) |
wint_t, WEOF |
wchar_t widened so that end-of-file has a value that is not a character; the wide int and EOF |
multibyte(3) |
null wide character L'\0' |
The wide NUL, width 0 by definition, and the only 0 the page names | The NUL byte |
| not printable, non-printing | iswprint is false: a control character, or a code point the table does not know |
Control characters |
| combining character | A mark drawn over the previous character and given width 0, so e + U+0301 is one column |
Normalization |
| East_Asian_Width | The Unicode property the table is built from: W wide and F fullwidth are 2, N narrow and Na are 1, A ambiguous is a decision the table makes for you |
uni -h, line by line |
| fullwidth | The double-cell copies of ASCII at U+FF01 onward, kept so CJK text lines up |
The CJK pages |
| emoji presentation, VS16 | Whether a character is drawn as a coloured picture two cells wide; U+FE0F asks for it and has width 0 itself |
A code point is not a character |
| ZERO WIDTH JOINER | U+200D, width 0, gluing emoji into one glyph the table cannot see |
A code point is not a character |
| regional indicator | U+1F1E6 to U+1F1FF, width 1 each; a pair is drawn as one flag two cells wide, so the sum is right by coincidence |
Logical and visual order |
_XOPEN_SOURCE, XSI |
The feature-test macro that unlocks the X/Open extensions in glibc's headers; wcwidth is one |
locale(1) and setlocale(3) |
LC_CTYPE |
The locale category the table belongs to; in the C locale nothing above 0x7F is printable |
Locale and LC_CTYPE |
getwchar, putwchar |
The wide getchar and putchar; the decode and encode happen inside them |
stdio(3) and the wide stream functions |
fold(1) |
The line-wrapping filter the example reimplements | Columns and characters |
| MT-Safe locale | The Linux page's thread-safety note: safe unless another thread changes the locale meanwhile |
Try it on your machine¶
Which edition each table is. Thirty-one code points through wcwidth in a UTF-8 locale, chosen so that the last few were added to Unicode in successive years. The C program sets en_US.UTF-8 on the Mac and C.UTF-8 on Ubuntu, where en_US is not installed.
macOS ubuntu:24.04
U+00000 NUL 0 0
U+00009 TAB -1 -1
U+0001B ESC -1 -1
U+0007F DEL -1 -1
U+00041 A 1 1
U+000A0 NO-BREAK SPACE 1 1
U+000AD SOFT HYPHEN -1 1
U+000E9 e-acute 1 1
U+0017C z-dot-above 1 1
U+020AC EURO SIGN 1 1
U+00301 COMBINING ACUTE ACCENT 0 0
U+0200B ZERO WIDTH SPACE 0 0
U+0200D ZERO WIDTH JOINER 0 0
U+0FE0F VARIATION SELECTOR-16 0 0
U+02764 HEAVY BLACK HEART (text default) 1 1
U+04E2D CJK ideograph zhong 2 2
U+0FF21 FULLWIDTH LATIN CAPITAL A 2 2
U+03000 IDEOGRAPHIC SPACE 2 2
U+0E000 private use 1 1
U+1F1F5 REGIONAL INDICATOR P (Unicode 6.0, 2010) 1 1
U+1F600 GRINNING FACE (Unicode 6.1, 2012) 2 2
U+1F3FB EMOJI MODIFIER TYPE-1-2 (Unicode 8.0, 2015) 2 2
U+1F97A FACE WITH PLEADING EYES (Unicode 11.0, 2018) 2 2
U+1F6D7 ELEVATOR (Unicode 13.0, 2020) 2 2
U+1FAE0 MELTING FACE (Unicode 14.0, 2021) 2 2
U+1FA77 PINK HEART (Unicode 15.0, 2022) 2 2
U+31350 CJK Extension H start (Unicode 15.0, 2022) 2 2
U+02FFC IDEOGRAPHIC DESCRIPTION SURROUND FROM RIGHT (Unicode 15.1, 2023) -1 2
U+1FAE9 FACE WITH BAGS UNDER EYES (Unicode 16.0, 2024) -1 -1
U+1FADC ROOT VEGETABLE (Unicode 16.0, 2024) -1 -1
U+1FBFA unassigned as of Unicode 16 -1 -1
Read the bottom five rows as a calendar. Everything through Unicode 15.0 is wide on both machines. U+2FFC, added in 15.1 (September 2023), is wide on glibc 2.39 and unprintable on this Mac: the Mac's table stops at 15.0, glibc's at 15.1. Both stop before 16.0 (September 2024), so every emoji from that release is not printable to wcwidth, and a program using the fold loop above will copy it through as if it took no room, on a machine whose terminal draws it two cells wide. One older row differs too: SOFT HYPHEN is -1 here and 1 on glibc, a disagreement about what printable means for a character that is invisible until a line breaks at it. This Mac's python3 (unicodedata.unidata_version is 16.0.0) knows the names of both 16.0 characters, and the uni tool here is on 17.0; the libc table is the oldest edition on the machine.
A sum, and its sign. The same program's wcswidth calls, byte-identical on both machines.
wcswidth("caf\u00e9" NFC) = 4 (4 wchar_t)
wcswidth("cafe\u0301" NFD) = 4 (5 wchar_t)
wcswidth("\u0141\u00f3d\u017a") = 4 (4 wchar_t)
wcswidth("\u4e2d\u6587") = 4 (2 wchar_t)
wcswidth("\U0001F600" ) = 2 (1 wchar_t)
wcswidth("a\tb" ) = -1 (3 wchar_t)
wcswidth("a\u200db" ) = 2 (3 wchar_t)
café composed and decomposed are the same width and different lengths, which is the reason Normalization exists; Łódź is four columns for seven bytes; two ideographs are four columns for two code points; and the TAB row is the sign test the page's RETURN VALUES warns about, one control character turning a width into an error.
The feature macro. Compiling the same source on the two machines.
$ gcc -std=c11 -Wall -Wextra -o m m.c (ubuntu, no _XOPEN_SOURCE)
m.c: In function ‘widths’:
m.c:42:65: warning: implicit declaration of function ‘wcswidth’ [-Wimplicit-function-declaration]
m.c: In function ‘main’:
m.c:51:69: warning: implicit declaration of function ‘wcwidth’ [-Wimplicit-function-declaration]
$ gcc -std=c11 -D_XOPEN_SOURCE=700 -Wall -Wextra -o m m.c (ubuntu: no diagnostics, output identical)
$ cc -std=c11 -Wall -Wextra -o m m.c (macOS: no diagnostics)
The Linux page's first SYNOPSIS line is not decoration. Without it the function is called through an implicit int wcwidth() declaration, which happens to work on x86-64 and is an error in C23.
What pads by what. printf '%-10s' in both shells, /usr/bin/printf on both machines, and Python's format, given a four-letter Polish word and a two-character Chinese one.
Łódź is 7 bytes, 4 characters
中文 is 6 bytes, 2 characters
builtin printf [%-10s] -> [Łódź ]
/usr/bin/printf [%-10s] -> [Łódź ]
builtin printf [%-10s] -> [中文 ]
/usr/bin/printf [%-10s] -> [中文 ]
python {:<10} -> [Łódź ]
python {:<10} -> [中文 ]
Count the spaces. printf gave Łódź three and 中文 four: it padded to ten bytes, on four implementations. Python gave them six and eight: it padded to ten code points. Neither padded to ten columns, which would be six spaces for both words, and the 中文 line shows why the difference is visible: two code points that are six bytes and four columns cannot be aligned by anything that does not call wcwidth. That is the third ruler in a shell that was written for the first, and it is why column -t and ls line up CJK text and printf never will.
Where the page is dated, and what it does not say¶
wcswidth(3) is dated August 20, 2002 and wcwidth(3) August 17, 2004, both citing POSIX.1-2001; the Linux page is dated 2023-10-31 and cites POSIX.1-2008. None of the three mentions Unicode, East_Asian_Width, or that there is a table with an edition. The experiment above dates the two tables to Unicode 15.0 and 15.1, on pages that do not know the word.
No page says what a sequence is worth. wcwidth is per code point and wcswidth is a sum, so a family emoji built from four people and three joiners is worth 8 to wcswidth and 2 to the terminal that draws it as one glyph. The ruler page lists three defensible answers for that string; libc gives one and does not say it is one of three.
No page says ambiguous. é and U+0301 are East_Asian_Width A, one cell in a Western terminal and two in a legacy East Asian one, and wcwidth returns 1 and 0 with no way to say it depends. The table decided for you, in favour of the Western answer.
The two libcs disagree about SOFT HYPHEN, -1 here and 1 on glibc, and neither page lists which characters count as printable.
The shell is not on either page. printf(1)'s %-10s pads by bytes on both machines, and printf(3)'s %-10s does the same in C; the width these pages compute is available only to programs that ask for it. Columns and characters is the page for the tools that do.
See also¶
multibyte(3)— how a byte string becomes thewchar_tthese functions take, and why that is locale-dependentctype(3)andwctype(3)—iswprint, the function that decides -1utf8(5)andutf-8(7)— the Linux page that nameswcswidthas the column counter andmbsrtowcsas the character counter- Columns and characters —
fold,expand,cut -candwc -m, the tools that need this function tty(4)andstty(1)— the terminal that has its own opinion about width- A code point is not a character — the five rulers, and the family emoji with three column counts
- Control characters — the -1 group
uni -h, line by line — its width column, which prints the East_Asian_Width property the table is built from- Logical and visual order — the other thing a column count cannot tell you
- The table has a version — why the answer for
U+1FAE9will change on the next OS update - Normalization —
caféat two lengths and one width - A page has a date — a 2004 page on a 2026 table