Skip to content
Merged
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
38 changes: 38 additions & 0 deletions src/uu/tr/locales/en-US.ftl
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
tr-about = Translate or delete characters
tr-usage = tr [OPTION]... SET1 [SET2]
tr-after-help = Translate, squeeze, and/or delete characters from standard input, writing to standard output.

# Help messages
tr-help-complement = use the complement of SET1
tr-help-delete = delete characters in SET1, do not translate
tr-help-squeeze = replace each sequence of a repeated character that is listed in the last specified SET, with a single occurrence of that character
tr-help-truncate-set1 = first truncate SET1 to length of SET2

# Error messages
tr-error-missing-operand = missing operand
tr-error-missing-operand-translating = missing operand after { $set }
Two strings must be given when translating.
tr-error-missing-operand-deleting-squeezing = missing operand after { $set }
Two strings must be given when deleting and squeezing.
tr-error-extra-operand-deleting-without-squeezing = extra operand { $operand }
Only one string may be given when deleting without squeezing repeats.
tr-error-extra-operand-simple = extra operand { $operand }
tr-error-read-directory = read error: Is a directory
tr-error-write-error = write error

# Warning messages
tr-warning-unescaped-backslash = warning: an unescaped backslash at end of string is not portable
tr-warning-ambiguous-octal-escape = the ambiguous octal escape \{ $origin_octal } is being interpreted as the 2-byte sequence \0{ $actual_octal_tail }, { $outstand_char }

# Sequence parsing error messages
tr-error-missing-char-class-name = missing character class name '[::]'
tr-error-missing-equivalence-class-char = missing equivalence class character '[==]'
tr-error-multiple-char-repeat-in-set2 = only one [c*] repeat construct may appear in string2
tr-error-char-repeat-in-set1 = the [c*] repeat construct may not appear in string1
tr-error-invalid-repeat-count = invalid repeat count { $count } in [c*n] construct
tr-error-empty-set2-when-not-truncating = when not truncating set1, string2 must be non-empty
tr-error-class-except-lower-upper-in-set2 = when translating, the only character classes that may appear in set2 are 'upper' and 'lower'
tr-error-class-in-set2-not-matched = when translating, every 'upper'/'lower' in set2 must be matched by a 'upper'/'lower' in the same position in set1
tr-error-set1-longer-set2-ends-in-class = when translating with string1 longer than string2,
the latter string must not end with a character class
tr-error-complement-more-than-one-unique = when translating with complemented character classes,
string2 must map all characters in the domain to one
tr-error-backwards-range = range-endpoints of '{ $start }-{ $end }' are in reverse collating sequence order
tr-error-multiple-char-in-equivalence = { $chars }: equivalence class operand must be a single character
42 changes: 42 additions & 0 deletions src/uu/tr/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
tr-about = Traduire ou supprimer des caractères
tr-usage = tr [OPTION]... ENSEMBLE1 [ENSEMBLE2]
tr-after-help = Traduire, compresser et/ou supprimer des caractères de l'entrée standard, en écrivant vers la sortie standard.

# Messages d'aide
tr-help-complement = utiliser le complément d'ENSEMBLE1
tr-help-delete = supprimer les caractères dans ENSEMBLE1, ne pas traduire
tr-help-squeeze = remplacer chaque séquence d'un caractère répété qui est listé dans le dernier ENSEMBLE spécifié, avec une seule occurrence de ce caractère
tr-help-truncate-set1 = d'abord tronquer ENSEMBLE1 à la longueur d'ENSEMBLE2

# Messages d'erreur
tr-error-missing-operand = opérande manquant
tr-error-missing-operand-translating = opérande manquant après { $set }
Deux chaînes doivent être données lors de la traduction.
tr-error-missing-operand-deleting-squeezing = opérande manquant après { $set }
Deux chaînes doivent être données lors de la suppression et compression.
tr-error-extra-operand-deleting-without-squeezing = opérande supplémentaire { $operand }
Une seule chaîne peut être donnée lors de la suppression sans compression des répétitions.
tr-error-extra-operand-simple = opérande supplémentaire { $operand }
tr-error-read-directory = erreur de lecture : Est un répertoire
tr-error-write-error = erreur d'écriture

# Messages d'avertissement
tr-warning-unescaped-backslash = avertissement : une barre oblique inverse non échappée à la fin de la chaîne n'est pas portable
tr-warning-ambiguous-octal-escape = l'échappement octal ambigu \{ $origin_octal } est en cours
d'interprétation comme la séquence de 2 octets \0{ $actual_octal_tail }, { $outstand_char }

# Messages d'erreur d'analyse de séquence
tr-error-missing-char-class-name = nom de classe de caractères manquant '[::]'
tr-error-missing-equivalence-class-char = caractère de classe d'équivalence manquant '[==]'
tr-error-multiple-char-repeat-in-set2 = seule une construction de répétition [c*] peut apparaître dans string2
tr-error-char-repeat-in-set1 = la construction de répétition [c*] ne peut pas apparaître dans string1
tr-error-invalid-repeat-count = nombre de répétitions invalide { $count } dans la construction [c*n]
tr-error-empty-set2-when-not-truncating = quand on ne tronque pas set1, string2 doit être non-vide
tr-error-class-except-lower-upper-in-set2 = lors de la traduction, les seules classes de caractères qui peuvent apparaître dans set2 sont 'upper' et 'lower'
tr-error-class-in-set2-not-matched = lors de la traduction, chaque 'upper'/'lower' dans set2 doit être associé à un 'upper'/'lower' à la même position dans set1
tr-error-set1-longer-set2-ends-in-class = lors de la traduction avec string1 plus long que string2,
cette dernière chaîne ne doit pas se terminer par une classe de caractères
tr-error-complement-more-than-one-unique = lors de la traduction avec des classes de caractères complémentées,
string2 doit mapper tous les caractères du domaine vers un seul
tr-error-backwards-range = les points de fin de plage de '{ $start }-{ $end }' sont dans l'ordre inverse de la séquence de collation
tr-error-multiple-char-in-equivalence = { $chars } : l'opérande de classe d'équivalence doit être un seul caractère
86 changes: 65 additions & 21 deletions src/uu/tr/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use std::{
ops::Not,
};
use uucore::error::{FromIo, UError, UResult};
use uucore::locale::{get_message, get_message_with_args};
use uucore::show_warning;

#[derive(Debug, Clone)]
Expand All @@ -45,44 +46,65 @@ pub enum BadSequence {
impl Display for BadSequence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingCharClassName => write!(f, "missing character class name '[::]'"),
Self::MissingCharClassName => {
write!(f, "{}", get_message("tr-error-missing-char-class-name"))
}
Self::MissingEquivalentClassChar => {
write!(f, "missing equivalence class character '[==]'")
write!(
f,
"{}",
get_message("tr-error-missing-equivalence-class-char")
)
}
Self::MultipleCharRepeatInSet2 => {
write!(f, "only one [c*] repeat construct may appear in string2")
write!(
f,
"{}",
get_message("tr-error-multiple-char-repeat-in-set2")
)
}
Self::CharRepeatInSet1 => {
write!(f, "the [c*] repeat construct may not appear in string1")
write!(f, "{}", get_message("tr-error-char-repeat-in-set1"))
}
Self::InvalidRepeatCount(count) => {
write!(f, "invalid repeat count '{count}' in [c*n] construct")
write!(
f,
"{}",
get_message_with_args(
"tr-error-invalid-repeat-count",
HashMap::from([("count".to_string(), format!("'{}'", count))])
)
)
}
Self::EmptySet2WhenNotTruncatingSet1 => {
write!(f, "when not truncating set1, string2 must be non-empty")
}
Self::ClassExceptLowerUpperInSet2 => {
write!(
f,
"when translating, the only character classes that may appear in set2 are 'upper' and 'lower'"
"{}",
get_message("tr-error-empty-set2-when-not-truncating")
)
}
Self::ClassInSet2NotMatchedBySet1 => {
Self::ClassExceptLowerUpperInSet2 => {
write!(
f,
"when translating, every 'upper'/'lower' in set2 must be matched by a 'upper'/'lower' in the same position in set1"
"{}",
get_message("tr-error-class-except-lower-upper-in-set2")
)
}
Self::ClassInSet2NotMatchedBySet1 => {
write!(f, "{}", get_message("tr-error-class-in-set2-not-matched"))
}
Self::Set1LongerSet2EndsInClass => {
write!(
f,
"when translating with string1 longer than string2,\nthe latter string must not end with a character class"
"{}",
get_message("tr-error-set1-longer-set2-ends-in-class")
)
}
Self::ComplementMoreThanOneUniqueInSet2 => {
write!(
f,
"when translating with complemented character classes,\nstring2 must map all characters in the domain to one"
"{}",
get_message("tr-error-complement-more-than-one-unique")
)
}
Self::BackwardsRange { end, start } => {
Expand All @@ -94,17 +116,25 @@ impl Display for BadSequence {
}
}
}

write!(
f,
"range-endpoints of '{}-{}' are in reverse collating sequence order",
end_or_start_to_string(start),
end_or_start_to_string(end)
"{}",
get_message_with_args(
"tr-error-backwards-range",
HashMap::from([
("start".to_string(), end_or_start_to_string(start)),
("end".to_string(), end_or_start_to_string(end))
])
)
)
}
Self::MultipleCharInEquivalence(s) => write!(
f,
"{s}: equivalence class operand must be a single character"
"{}",
get_message_with_args(
"tr-error-multiple-char-in-equivalence",
HashMap::from([("chars".to_string(), s.clone())])
)
),
}
}
Expand Down Expand Up @@ -364,11 +394,25 @@ impl Sequence {
let origin_octal: &str = std::str::from_utf8(input).unwrap();
let actual_octal_tail: &str = std::str::from_utf8(&input[0..2]).unwrap();
let outstand_char: char = char::from_u32(input[2] as u32).unwrap();
show_warning!("the ambiguous octal escape \\{origin_octal} is being\n interpreted as the 2-byte sequence \\0{actual_octal_tail}, {outstand_char}");
show_warning!(
"{}",
get_message_with_args(
"tr-warning-ambiguous-octal-escape",
HashMap::from([
("origin_octal".to_string(), origin_octal.to_string()),
(
"actual_octal_tail".to_string(),
actual_octal_tail.to_string()
),
("outstand_char".to_string(), outstand_char.to_string())
])
)
);
}
result
},
).parse(input)
)
.parse(input)
}

fn parse_octal_two_digits(input: &[u8]) -> IResult<&[u8], u8> {
Expand Down Expand Up @@ -666,7 +710,7 @@ where

output
.write_all(&output_buf)
.map_err_context(|| "write error".into())?;
.map_err_context(|| get_message("tr-error-write-error"))?;

buf.clear();
output_buf.clear();
Expand Down
51 changes: 27 additions & 24 deletions src/uu/tr/src/tr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ use clap::{Arg, ArgAction, Command, value_parser};
use operation::{
Sequence, SqueezeOperation, SymbolTranslator, TranslateOperation, translate_input,
};
use std::collections::HashMap;
use std::ffi::OsString;
use std::io::{BufWriter, Write, stdin, stdout};
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult, USimpleError, UUsageError};
use uucore::fs::is_stdin_directory;
use uucore::{format_usage, os_str_as_bytes, show};

use uucore::locale::get_message;
use uucore::locale::{get_message, get_message_with_args};

mod options {
pub const COMPLEMENT: &str = "complement";
Expand Down Expand Up @@ -53,45 +54,51 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let sets_len = sets.len();

if sets.is_empty() {
return Err(UUsageError::new(1, "missing operand"));
return Err(UUsageError::new(1, get_message("tr-error-missing-operand")));
}

if !(delete_flag || squeeze_flag) && sets_len < 2 {
return Err(UUsageError::new(
1,
format!(
"missing operand after {}\nTwo strings must be given when translating.",
sets[0].quote()
get_message_with_args(
"tr-error-missing-operand-translating",
HashMap::from([("set".to_string(), sets[0].quote().to_string())]),
),
));
}

if delete_flag & squeeze_flag && sets_len < 2 {
return Err(UUsageError::new(
1,
format!(
"missing operand after {}\nTwo strings must be given when deleting and squeezing.",
sets[0].quote()
get_message_with_args(
"tr-error-missing-operand-deleting-squeezing",
HashMap::from([("set".to_string(), sets[0].quote().to_string())]),
),
));
}

if sets_len > 1 {
let start = "extra operand";
if delete_flag && !squeeze_flag {
let op = sets[1].quote();
let msg = if sets_len == 2 {
format!(
"{start} {op}\nOnly one string may be given when deleting without squeezing repeats.",
get_message_with_args(
"tr-error-extra-operand-deleting-without-squeezing",
HashMap::from([("operand".to_string(), op.to_string())]),
)
} else {
format!("{start} {op}")
get_message_with_args(
"tr-error-extra-operand-simple",
HashMap::from([("operand".to_string(), op.to_string())]),
)
};
return Err(UUsageError::new(1, msg));
}
if sets_len > 2 {
let op = sets[2].quote();
let msg = format!("{start} {op}");
let msg = get_message_with_args(
"tr-error-extra-operand-simple",
HashMap::from([("operand".to_string(), op.to_string())]),
);
return Err(UUsageError::new(1, msg));
}
}
Expand All @@ -103,7 +110,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
// The trailing backslash has a non-backslash character before it.
show!(USimpleError::new(
0,
"warning: an unescaped backslash at end of string is not portable"
get_message("tr-warning-unescaped-backslash")
));
}
}
Expand All @@ -125,7 +132,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
)?;

if is_stdin_directory(&stdin) {
return Err(USimpleError::new(1, "read error: Is a directory"));
return Err(USimpleError::new(1, get_message("tr-error-read-directory")));
}

// '*_op' are the operations that need to be applied, in order.
Expand Down Expand Up @@ -156,7 +163,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {

buffered_stdout
.flush()
.map_err_context(|| "write error".into())?;
.map_err_context(|| get_message("tr-error-write-error"))?;

Ok(())
}
Expand All @@ -173,35 +180,31 @@ pub fn uu_app() -> Command {
.visible_short_alias('C')
.short('c')
.long(options::COMPLEMENT)
.help("use the complement of SET1")
.help(get_message("tr-help-complement"))
.action(ArgAction::SetTrue)
.overrides_with(options::COMPLEMENT),
)
.arg(
Arg::new(options::DELETE)
.short('d')
.long(options::DELETE)
.help("delete characters in SET1, do not translate")
.help(get_message("tr-help-delete"))
.action(ArgAction::SetTrue)
.overrides_with(options::DELETE),
)
.arg(
Arg::new(options::SQUEEZE)
.long(options::SQUEEZE)
.short('s')
.help(
"replace each sequence of a repeated character that is \
listed in the last specified SET, with a single occurrence \
of that character",
)
.help(get_message("tr-help-squeeze"))
.action(ArgAction::SetTrue)
.overrides_with(options::SQUEEZE),
)
.arg(
Arg::new(options::TRUNCATE_SET1)
.long(options::TRUNCATE_SET1)
.short('t')
.help("first truncate SET1 to length of SET2")
.help(get_message("tr-help-truncate-set1"))
.action(ArgAction::SetTrue)
.overrides_with(options::TRUNCATE_SET1),
)
Expand Down
2 changes: 1 addition & 1 deletion tests/by-util/test_tr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1521,7 +1521,7 @@ fn test_multibyte_octal_sequence() {
.args(&["-d", r"\501"])
.pipe_in("(1Ł)")
.succeeds()
.stderr_is("tr: warning: the ambiguous octal escape \\501 is being\n interpreted as the 2-byte sequence \\050, 1\n")
.stderr_is("tr: warning: the ambiguous octal escape \\501 is being interpreted as the 2-byte sequence \\050, 1\n")
.stdout_is("Ł)");
}

Expand Down
Loading