Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions src/uu/csplit/src/csplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,11 @@ where
if let Some((_, line)) = input_iter.next() {
// There is remaining input: create a final split and copy remainder
split_writer.new_writer()?;
split_writer.writeln(&line?)?;
let line = split_writer.or_finish_split(line)?;
split_writer.writeln(&line)?;
for (_, line) in input_iter {
split_writer.writeln(&line?)?;
let line = split_writer.or_finish_split(line)?;
split_writer.writeln(&line)?;
}
split_writer.finish_split()
} else if all_up_to_line && options.suppress_matched {
Expand Down Expand Up @@ -332,6 +334,27 @@ impl SplitWriter<'_> {
Ok(())
}

/// The line, or the read error once the split the line was being read for
/// has been closed. GNU says how much of a split it wrote before it says
/// why there is no more input to write:
///
/// ```text
/// $ csplit a_directory '/^a/'
/// csplit: read error: Is a directory
/// 0
/// ```
///
/// # Errors
///
/// The read error, or the error from closing the split if closing it is
/// what went wrong.
fn or_finish_split(&mut self, line: UResult<String>) -> Result<String, CsplitError> {
match line {
Ok(line) => Ok(line),
Err(err) => self.finish_split().and(Err(err.into())),
}
}

/// Removes all the split files that were created.
///
/// # Errors
Expand Down Expand Up @@ -372,7 +395,7 @@ impl SplitWriter<'_> {

let mut ret = Err(CsplitError::LineOutOfRange(pattern_as_str.to_string()));
while let Some((ln, line)) = input_iter.next() {
let line = line?;
let line = self.or_finish_split(line)?;
match n.cmp(&(&ln + 1)) {
Ordering::Less => {
assert!(
Expand Down Expand Up @@ -430,7 +453,7 @@ impl SplitWriter<'_> {
input_iter.set_size_of_buffer(1);

while let Some((ln, line)) = input_iter.next() {
let line = line?;
let line = self.or_finish_split(line)?;
let l = line
.strip_suffix("\r\n")
.unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line));
Expand Down Expand Up @@ -458,7 +481,8 @@ impl SplitWriter<'_> {
// write the extra lines required by the offset
while offset > 0 {
if let Some((_, line)) = input_iter.next() {
self.writeln(&line?)?;
let line = self.or_finish_split(line)?;
self.writeln(&line)?;
} else {
self.finish_split()?;
return Err(CsplitError::LineOutOfRange(pattern_as_str.to_string()));
Expand All @@ -484,7 +508,7 @@ impl SplitWriter<'_> {
let offset_usize = offset.unsigned_abs() as usize;
input_iter.set_size_of_buffer(offset_usize);
while let Some((ln, line)) = input_iter.next() {
let line = line?;
let line = self.or_finish_split(line)?;
let l = line
.strip_suffix("\r\n")
.unwrap_or_else(|| line.strip_suffix('\n').unwrap_or(&line));
Expand Down
14 changes: 7 additions & 7 deletions src/uu/dd/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,21 @@ dd-error-not-directory = setting flags for '{ $file }': Not a directory
dd-error-failed-discard-cache = failed to discard cache for: { $file }

# Parse errors
dd-error-unrecognized-operand = unrecognized operand '{ $operand }'
dd-error-unrecognized-operand = unrecognized operand { $operand }
dd-error-multiple-format-table = cannot combine any two of {"{"}ascii,ebcdic,ibm{"}"}
dd-error-multiple-case = cannot combine lcase and ucase
dd-error-multiple-block = cannot combine block and unblock
dd-error-multiple-excl = cannot combine excl and nocreat
dd-error-invalid-flag = invalid input flag: '{ $flag }'
dd-error-invalid-output-flag = invalid output flag: '{ $flag }'
dd-error-conv-flag-no-match = invalid conversion: '{ $flag }'
dd-error-multiplier-parse-failure = invalid number: '{ $input }'
dd-error-invalid-flag = invalid input flag: { $flag }
dd-error-invalid-output-flag = invalid output flag: { $flag }
dd-error-conv-flag-no-match = invalid conversion: { $flag }
dd-error-multiplier-parse-failure = invalid number: { $input }
dd-error-multiplier-overflow = Multiplier string would overflow on current system -> { $input }
dd-error-block-without-cbs = conv=block or conv=unblock specified without cbs=N
dd-error-status-not-recognized = invalid status level: '{ $level }'
dd-error-status-not-recognized = invalid status level: { $level }
dd-error-unimplemented = feature not implemented on this system -> { $feature }
dd-error-bs-out-of-range = { $param }=N cannot fit into memory
dd-error-invalid-number = invalid number: '{ $input }'
dd-error-invalid-number = invalid number: { $input }

# Progress messages
dd-progress-records-in = { $complete }+{ $partial } records in
Expand Down
14 changes: 7 additions & 7 deletions src/uu/dd/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -91,21 +91,21 @@ dd-error-not-directory = définir les indicateurs pour '{ $file }' : N'est pas u
dd-error-failed-discard-cache = échec de la suppression du cache pour : { $file }

# Parse errors
dd-error-unrecognized-operand = opérande non reconnue '{ $operand }'
dd-error-unrecognized-operand = opérande non reconnue { $operand }
dd-error-multiple-format-table = impossible de combiner deux options parmi {"{"}ascii,ebcdic,ibm{"}"}
dd-error-multiple-case = impossible de combiner lcase et ucase
dd-error-multiple-block = impossible de combiner block et unblock
dd-error-multiple-excl = impossible de combiner excl et nocreat
dd-error-invalid-flag = indicateur d'entrée invalide : '{ $flag }'
dd-error-invalid-output-flag = indicateur de sortie invalide : '{ $flag }'
dd-error-conv-flag-no-match = conversion invalide : '{ $flag }'
dd-error-multiplier-parse-failure = nombre invalide : '{ $input }'
dd-error-invalid-flag = indicateur d'entrée invalide : { $flag }
dd-error-invalid-output-flag = indicateur de sortie invalide : { $flag }
dd-error-conv-flag-no-match = conversion invalide : { $flag }
dd-error-multiplier-parse-failure = nombre invalide : { $input }
dd-error-multiplier-overflow = La chaîne de multiplicateur déborderait sur le système actuel -> { $input }
dd-error-block-without-cbs = conv=block ou conv=unblock spécifié sans cbs=N
dd-error-status-not-recognized = niveau d'état invalide : '{ $level }'
dd-error-status-not-recognized = niveau d'état invalide : { $level }
dd-error-unimplemented = fonctionnalité non implémentée sur ce système -> { $feature }
dd-error-bs-out-of-range = { $param }=N ne peut pas tenir en mémoire
dd-error-invalid-number = nombre invalide : '{ $input }'
dd-error-invalid-number = nombre invalide : { $input }

# Progress messages
dd-progress-records-in = { $complete }+{ $partial } enregistrements en entrée
Expand Down
42 changes: 34 additions & 8 deletions src/uu/dd/src/parseargs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,39 @@ use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::UError;
use uucore::parser::parse_size::{ParseSizeError, Parser as SizeParser};
use uucore::quoting_style::{Quotes, QuotingStyle, locale_aware_escape_name};
use uucore::show_warning;
use uucore::translate;

/// A value in single quotes, with the characters a terminal would not show
/// spelled out, as GNU's `quote` does: `status=$'\1'` is reported as `'\001'`
/// rather than as the byte itself.
fn quote(value: &str) -> String {
escape(
value,
QuotingStyle::C {
quotes: Quotes::Single,
},
)
}

/// The same, in the shell syntax GNU quotes numbers and operands with, where
/// an unprintable byte ends the quoted run rather than being escaped inside
/// it: `''$'\001'`.
fn shell_quote(value: &str) -> String {
escape(value, QuotingStyle::SHELL_ESCAPE_QUOTE)
}

fn escape(value: &str, style: QuotingStyle) -> String {
locale_aware_escape_name(value.as_ref(), style)
.into_string()
.expect("escaping a str only ever adds ASCII")
}

/// Parser Errors describe errors with parser input
#[derive(Debug, PartialEq, Eq, Error)]
pub enum ParseError {
#[error("{}", translate!("dd-error-unrecognized-operand", "operand" => .0))]
#[error("{}", translate!("dd-error-unrecognized-operand", "operand" => shell_quote(.0)))]
UnrecognizedOperand(String),
#[error("{}", translate!("dd-error-multiple-format-table"))]
MultipleFmtTable,
Expand All @@ -30,27 +56,27 @@ pub enum ParseError {
MultipleBlockUnblock,
#[error("{}", translate!("dd-error-multiple-excl"))]
MultipleExclNoCreate,
#[error("{}", translate!("dd-error-invalid-flag", "flag" => .0))]
#[error("{}", translate!("dd-error-invalid-flag", "flag" => quote(.0)))]
FlagNoMatch(String),
#[error("{}", translate!("dd-error-invalid-output-flag", "flag" => .0))]
#[error("{}", translate!("dd-error-invalid-output-flag", "flag" => quote(.0)))]
OutputFlagNoMatch(String),
#[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => .0))]
#[error("{}", translate!("dd-error-conv-flag-no-match", "flag" => quote(.0)))]
ConvFlagNoMatch(String),
#[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => .0))]
#[error("{}", translate!("dd-error-multiplier-parse-failure", "input" => shell_quote(.0)))]
MultiplierStringParseFailure(String),
#[error("{}", translate!("dd-error-multiplier-overflow", "input" => .0))]
MultiplierStringOverflow(String),
#[error("{}", translate!("dd-error-block-without-cbs"))]
BlockUnblockWithoutCBS,
#[error("{}", translate!("dd-error-status-not-recognized", "level" => .0))]
#[error("{}", translate!("dd-error-status-not-recognized", "level" => quote(.0)))]
StatusLevelNotRecognized(String),
#[error("{}", translate!("dd-error-unimplemented", "feature" => .0))]
Unimplemented(String),
#[error("{}", translate!("dd-error-bs-out-of-range", "param" => .0))]
BsOutOfRange(String),
#[error("{}", translate!("dd-error-invalid-number", "input" => .0))]
#[error("{}", translate!("dd-error-invalid-number", "input" => shell_quote(.0)))]
InvalidNumber(String),
#[error("invalid number: '{0}': {1}")]
#[error("invalid number: {}: {}", shell_quote(.0), .1)]
InvalidNumberWithErrMsg(String, String),
}

Expand Down
31 changes: 30 additions & 1 deletion tests/by-util/test_csplit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1549,16 +1549,45 @@ fn test_directory_input_file() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("test_directory");

// The split that was open when the read failed is still accounted for:
// GNU prints its size before it gives up on the input.
#[cfg(unix)]
ucmd.args(&["test_directory", "1"])
.fails_with_code(1)
.stderr_only("csplit: read error: Is a directory\n");
.stdout_is("0\n")
.stderr_is("csplit: read error: Is a directory\n");
#[cfg(windows)]
ucmd.args(&["test_directory", "1"])
.fails_with_code(1)
.stderr_only("csplit: cannot open 'test_directory' for reading: Permission denied\n");
}

/// The count of the split that was open when the input went wrong is printed
/// the same way a completed one is: `--quiet` still silences it, `-z` still
/// drops the empty split, and `-k` keeps the file it counted.
#[cfg(unix)]
#[test]
fn test_read_error_reports_the_size_of_the_open_split() {
for (options, stdout) in [
(&[][..], "0\n"),
(&["-k"][..], "0\n"),
(&["-s"][..], ""),
(&["-z"][..], ""),
] {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("dir");

let mut args = vec!["dir", "/^a/"];
args.extend_from_slice(options);
ucmd.args(&args)
.fails_with_code(1)
.stdout_is(stdout)
.stderr_is("csplit: read error: Is a directory\n");

assert_eq!(at.file_exists("xx00"), options == ["-k"], "{options:?}");
}
}

#[test]
fn test_stdin_no_trailing_newline() {
new_ucmd!()
Expand Down
43 changes: 43 additions & 0 deletions tests/by-util/test_dd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,49 @@ fn test_invalid_flag_arg_gnu_compatibility() {
}
}

/// A value that would not survive being printed is spelled out instead, as
/// GNU does, rather than sending the raw byte to the terminal.
#[test]
fn test_operand_value_is_quoted_like_gnu() {
// The choices GNU quotes plainly: single quotes with C escapes inside.
for (operand, message) in [
("status=\u{1}", r"invalid status level: '\001'"),
("status=a\tb", r"invalid status level: 'a\tb'"),
("status=a'b", r"invalid status level: 'a\'b'"),
("status=a\\b", r"invalid status level: 'a\\b'"),
("status=a b", "invalid status level: 'a b'"),
("iflag=\u{1}", r"invalid input flag: '\001'"),
("oflag=\u{1}", r"invalid output flag: '\001'"),
("conv=\u{1}", r"invalid conversion: '\001'"),
] {
new_ucmd!().args(&[operand]).fails().usage_error(message);
}

// Numbers and operands take the shell syntax GNU uses for them, where an
// unprintable byte ends the quoted run rather than being escaped inside
// it, and a quote switches which quotes are used.
new_ucmd!()
.args(&["\u{1}=1"])
.fails()
.usage_error(r"unrecognized operand ''$'\001''=1'");

new_ucmd!()
.args(&["bs=\u{1}"])
.fails()
.stderr_is("dd: invalid number: ''$'\\001'\n");

new_ucmd!()
.args(&["bs=a'b"])
.fails()
.stderr_is("dd: invalid number: \"a'b\"\n");

// An ordinary value is quoted too, which is what it already was.
new_ucmd!()
.args(&["bs=29d"])
.fails()
.stderr_is("dd: invalid number: '29d'\n");
}

#[test]
fn test_invalid_file_arg_gnu_compatibility() {
new_ucmd!()
Expand Down
Loading