@@ -13,6 +13,7 @@ use std::io::{BufRead, BufReader, BufWriter, IsTerminal, Read, Write, stdin, std
1313use std:: path:: Path ;
1414use uucore:: display:: Quotable ;
1515use uucore:: error:: { FromIo , UResult , USimpleError , set_exit_code} ;
16+ use uucore:: i18n:: charmap:: mb_char_len;
1617use uucore:: line_ending:: LineEnding ;
1718use uucore:: os_str_as_bytes;
1819
@@ -29,6 +30,8 @@ struct Options<'a> {
2930 out_delimiter : Option < & ' a [ u8 ] > ,
3031 line_ending : LineEnding ,
3132 field_opts : Option < FieldOptions < ' a > > ,
33+ /// `-n`: in byte mode, do not split multi-byte characters.
34+ byte_no_split : bool ,
3235}
3336
3437enum Delimiter < ' a > {
@@ -104,6 +107,107 @@ fn cut_bytes<R: Read, W: Write>(
104107 Ok ( ( ) )
105108}
106109
110+ /// Fill `spans` with the byte spans `[start, end)` of `line`'s characters, using
111+ /// the current locale's encoding. Invalid/incomplete sequences count as one
112+ /// byte. `spans` is cleared first; its capacity is reused across calls.
113+ fn char_spans_into ( line : & [ u8 ] , spans : & mut Vec < ( usize , usize ) > ) {
114+ spans. clear ( ) ;
115+ let mut i = 0 ;
116+ while i < line. len ( ) {
117+ let len = mb_char_len ( & line[ i..] ) . clamp ( 1 , line. len ( ) - i) ;
118+ spans. push ( ( i, i + len) ) ;
119+ i += len;
120+ }
121+ }
122+
123+ /// Character mode (`-c`): ranges index whole (possibly multi-byte) characters.
124+ fn cut_characters < R : Read , W : Write > (
125+ reader : R ,
126+ out : & mut W ,
127+ ranges : & [ Range ] ,
128+ opts : & Options ,
129+ ) -> UResult < ( ) > {
130+ let newline_char = opts. line_ending . into ( ) ;
131+ let mut buf_in = BufReader :: new ( reader) ;
132+ let out_delim = opts. out_delimiter . unwrap_or ( b"\t " ) ;
133+ let mut spans: Vec < ( usize , usize ) > = Vec :: new ( ) ;
134+
135+ let result = buf_in. for_byte_record ( newline_char, |line| {
136+ char_spans_into ( line, & mut spans) ;
137+ let mut print_delim = false ;
138+ for & Range { low, high } in ranges {
139+ if low > spans. len ( ) {
140+ break ;
141+ }
142+ if print_delim {
143+ out. write_all ( out_delim) ?;
144+ } else if opts. out_delimiter . is_some ( ) {
145+ print_delim = true ;
146+ }
147+ let high = high. min ( spans. len ( ) ) ;
148+ out. write_all ( & line[ spans[ low - 1 ] . 0 ..spans[ high - 1 ] . 1 ] ) ?;
149+ }
150+ out. write_all ( & [ newline_char] ) ?;
151+ Ok ( true )
152+ } ) ;
153+
154+ if let Err ( e) = result {
155+ return Err ( USimpleError :: new ( 1 , e. to_string ( ) ) ) ;
156+ }
157+
158+ Ok ( ( ) )
159+ }
160+
161+ /// Byte mode with `-n`: ranges index bytes, but a multi-byte character is
162+ /// emitted in full when (and only when) the range includes its last byte.
163+ fn cut_bytes_no_split < R : Read , W : Write > (
164+ reader : R ,
165+ out : & mut W ,
166+ ranges : & [ Range ] ,
167+ opts : & Options ,
168+ ) -> UResult < ( ) > {
169+ let newline_char = opts. line_ending . into ( ) ;
170+ let mut buf_in = BufReader :: new ( reader) ;
171+ let out_delim = opts. out_delimiter . unwrap_or ( b"\t " ) ;
172+ let mut spans: Vec < ( usize , usize ) > = Vec :: new ( ) ;
173+
174+ let result = buf_in. for_byte_record ( newline_char, |line| {
175+ char_spans_into ( line, & mut spans) ;
176+ let mut print_delim = false ;
177+ for & Range { low, high } in ranges {
178+ if low > line. len ( ) {
179+ break ;
180+ }
181+ let high = high. min ( line. len ( ) ) ;
182+ // A character's last byte is at 1-based position `end` (exclusive 0-based end).
183+ // Emit the output delimiter lazily, only once this range has actually
184+ // selected a character, so a range that matches nothing adds no delimiter.
185+ let mut range_emitted = false ;
186+ for & ( start, end) in & spans {
187+ if end >= low && end <= high {
188+ if !range_emitted {
189+ if print_delim {
190+ out. write_all ( out_delim) ?;
191+ } else if opts. out_delimiter . is_some ( ) {
192+ print_delim = true ;
193+ }
194+ range_emitted = true ;
195+ }
196+ out. write_all ( & line[ start..end] ) ?;
197+ }
198+ }
199+ }
200+ out. write_all ( & [ newline_char] ) ?;
201+ Ok ( true )
202+ } ) ;
203+
204+ if let Err ( e) = result {
205+ return Err ( USimpleError :: new ( 1 , e. to_string ( ) ) ) ;
206+ }
207+
208+ Ok ( ( ) )
209+ }
210+
107211/// Output delimiter is explicitly specified
108212fn cut_fields_explicit_out_delim < R : Read , W : Write , M : Matcher > (
109213 reader : R ,
@@ -458,8 +562,10 @@ where
458562 }
459563
460564 show_if_err ! ( match mode {
565+ Mode :: Bytes ( ranges, opts) if opts. byte_no_split =>
566+ cut_bytes_no_split( stdin( ) , & mut out, ranges, opts) ,
461567 Mode :: Bytes ( ranges, opts) => cut_bytes( stdin( ) , & mut out, ranges, opts) ,
462- Mode :: Characters ( ranges, opts) => cut_bytes ( stdin( ) , & mut out, ranges, opts) ,
568+ Mode :: Characters ( ranges, opts) => cut_characters ( stdin( ) , & mut out, ranges, opts) ,
463569 Mode :: Fields ( ranges, opts) => cut_fields( stdin( ) , & mut out, ranges, opts) ,
464570 } ) ;
465571
@@ -482,8 +588,12 @@ where
482588 . map_err_context( || filename. maybe_quote( ) . to_string( ) )
483589 . and_then( |file| {
484590 match & mode {
485- Mode :: Bytes ( ranges, opts) | Mode :: Characters ( ranges, opts) => {
486- cut_bytes( file, & mut out, ranges, opts)
591+ Mode :: Bytes ( ranges, opts) if opts. byte_no_split => {
592+ cut_bytes_no_split( file, & mut out, ranges, opts)
593+ }
594+ Mode :: Bytes ( ranges, opts) => cut_bytes( file, & mut out, ranges, opts) ,
595+ Mode :: Characters ( ranges, opts) => {
596+ cut_characters( file, & mut out, ranges, opts)
487597 }
488598 Mode :: Fields ( ranges, opts) => cut_fields( file, & mut out, ranges, opts) ,
489599 }
@@ -514,12 +624,16 @@ fn get_delimiters(matches: &ArgMatches) -> UResult<(Delimiter<'_>, Option<&[u8]>
514624 if os_string. is_empty ( ) {
515625 Delimiter :: Slice ( b"\0 " )
516626 } else {
517- // For delimiter `-d` option value - allow both UTF-8 (possibly multi-byte) characters
518- // and Non UTF-8 (and not ASCII) single byte "characters", like `b"\xAD"` to align with GNU behavior
627+ // For delimiter `-d` option value - allow a single character: a UTF-8
628+ // character in a UTF-8 locale, or a single (possibly multi-byte)
629+ // character of the current locale's encoding, e.g. a 2-byte GB18030
630+ // character or any single byte like `b"\xAD"`, to align with GNU.
519631 let bytes = os_str_as_bytes ( os_string) ?;
520- if os_string. to_str ( ) . is_some_and ( |s| s. chars ( ) . count ( ) > 1 )
521- || os_string. to_str ( ) . is_none ( ) && bytes. len ( ) > 1
522- {
632+ let is_single_char = match os_string. to_str ( ) {
633+ Some ( s) => s. chars ( ) . count ( ) == 1 ,
634+ None => mb_char_len ( bytes) == bytes. len ( ) ,
635+ } ;
636+ if !is_single_char {
523637 return Err ( USimpleError :: new (
524638 1 ,
525639 translate ! ( "cut-error-delimiter-must-be-single-character" ) ,
@@ -583,6 +697,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
583697
584698 let ( delimiter, out_delimiter) = get_delimiters ( & matches) ?;
585699 let line_ending = LineEnding :: from_zero_flag ( matches. get_flag ( options:: ZERO_TERMINATED ) ) ;
700+ // `-n`: only meaningful with `-b`; keeps multi-byte characters intact.
701+ let byte_no_split = matches. get_flag ( options:: NOTHING ) ;
586702
587703 // Only one, and only one of cutting mode arguments, i.e. `-b`, `-c`, `-f`,
588704 // is expected. The number of those arguments is used for parsing a cutting
@@ -610,6 +726,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
610726 out_delimiter,
611727 line_ending,
612728 field_opts : None ,
729+ byte_no_split,
613730 } ,
614731 )
615732 } )
@@ -623,6 +740,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
623740 out_delimiter,
624741 line_ending,
625742 field_opts : None ,
743+ byte_no_split,
626744 } ,
627745 )
628746 } )
@@ -639,6 +757,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
639757 delimiter,
640758 only_delimited,
641759 } ) ,
760+ byte_no_split,
642761 } ,
643762 )
644763 } )
@@ -776,7 +895,7 @@ pub fn uu_app() -> Command {
776895 . arg (
777896 Arg :: new ( options:: NOTHING )
778897 . short ( 'n' )
779- . help ( "(ignored)" )
898+ . help ( translate ! ( "cut-help-no-split-multibyte" ) )
780899 . action ( ArgAction :: SetTrue ) ,
781900 )
782901}
0 commit comments