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
1 change: 1 addition & 0 deletions docs/src/extensions-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ the difference:
| `stat` | the failing directive of a `-c`/`--format` or `--printf` format | [`stat -c %d%.3 fruits.txt`](https://uutils.org/playground/?cmd=stat+-c+%25d%25.3+fruits.txt) |
| `env` | the failing part of a `-S`/`--split-string` string | [`env -S 'echo ${1FOO}'`](https://uutils.org/playground/?cmd=env+-S+%27echo+%24%7B1FOO%7D%27) |
| `dd` | the failing key, value or flag of a `KEY=VALUE` operand | [`dd conv=ucase,zap`](https://uutils.org/playground/?cmd=dd+conv%3Ducase%2Czap) |
| `join` | the failing field of the output format given to `-o` | [`join -o 1.2,2.x fruits.txt fruits.txt`](https://uutils.org/playground/?cmd=join+-o+1.2%2C2.x+fruits.txt+fruits.txt) |
| `cut` | the failing range in the list given to `-b`, `-c`, `-f` or `-F` | [`cut -f 1,4-2 fruits.txt`](https://uutils.org/playground/?cmd=cut+-f+1%2C4-2+fruits.txt) |
| `split` | the failing part of the SIZE given to `-b`, `-C` or `-l` | [`split -b 7zq fruits.txt`](https://uutils.org/playground/?cmd=split+-b+7zq+fruits.txt) |
| `shred` | the failing part of the SIZE given to `-s`/`--size` | [`shred -s 4vv fruits.txt`](https://uutils.org/playground/?cmd=shred+-s+4vv+fruits.txt) |
Expand Down
3 changes: 3 additions & 0 deletions src/uu/join/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ join-error-invalid-field-number = invalid field number: { $value }
join-error-incompatible-fields = incompatible join fields { $field1 }, { $field2 }
join-error-not-sorted = { $file }:{ $line_num }: is not sorted: { $content }
join-error-input-not-sorted = input is not in sorted order

# Diagnostics
join-diag-help-format = an output field is FILENUM.FIELD, as in -o 1.2,2.1; 0 stands for the join field
3 changes: 3 additions & 0 deletions src/uu/join/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ join-error-invalid-field-number = numΓ©ro de champ invalide : { $value }
join-error-incompatible-fields = champs de jointure incompatibles { $field1 }, { $field2 }
join-error-not-sorted = { $file }:{ $line_num } : n'est pas triΓ© : { $content }
join-error-input-not-sorted = l'entrΓ©e n'est pas dans l'ordre triΓ©

# Diagnostics
join-diag-help-format = un champ de sortie s'Γ©crit NUMFICHIER.CHAMP, comme dans -o 1.2,2.1 ; 0 dΓ©signe le champ de jointure
32 changes: 27 additions & 5 deletions src/uu/join/src/join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::num::IntErrorKind;
use std::os::unix::ffi::OsStrExt;
use thiserror::Error;
use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError, set_exit_code};
use uucore::error::{FromIo, UError, UResult, USimpleError, quiet_if_reported, set_exit_code};
use uucore::i18n::collator::{
AlternateHandling, CollatorOptions, locale_cmp, should_use_locale_collation, try_init_collator,
};
Expand Down Expand Up @@ -764,7 +764,7 @@ fn get_and_parse_field_number(matches: &clap::ArgMatches, key: &str) -> UResult<
/// This function takes the matches from the command-line arguments, processes them,
/// and returns a `Settings` struct that encapsulates the configuration for the program.
#[allow(clippy::field_reassign_with_default)]
fn parse_settings(matches: &clap::ArgMatches) -> UResult<Settings> {
fn parse_settings(matches: &clap::ArgMatches, diag_args: Option<&[OsString]>) -> UResult<Settings> {
let keys = get_and_parse_field_number(matches, "j")?;
let key1 = get_and_parse_field_number(matches, "1")?;
let key2 = get_and_parse_field_number(matches, "2")?;
Expand All @@ -788,8 +788,27 @@ fn parse_settings(matches: &clap::ArgMatches) -> UResult<Settings> {
settings.autoformat = true;
} else {
let mut specs = vec![];
// Where the current field sits in the value, so that the caret can
// take the one field that is at fault out of a long list.
let mut at = 0;
for part in format.split([' ', ',', '\t']) {
specs.push(Spec::parse(part)?);
specs.push(Spec::parse(part).map_err(|error| {
let message = error.to_string();
let reported = diag_args.is_some_and(|args| {
uucore::diagnostics::Snapshot::with_program(args).render_option_value(
format,
Some('o'),
None,
at..at + part.len(),
&message,
None,
Some(&translate!("join-diag-help-format")),
)
});
quiet_if_reported(reported, error)
})?);
// Every separator is one byte wide.
at += part.len() + 1;
}
settings.format = specs;
}
Expand Down Expand Up @@ -818,13 +837,16 @@ fn parse_settings(matches: &clap::ArgMatches) -> UResult<Settings> {

#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
let raw_args: Vec<OsString> = args.collect();
// Kept for the caret in `-o` diagnostics, which needs the list as typed.
let diag_args = uucore::diagnostics::capture(&raw_args);
let matches = uucore::clap_localization::handle_clap_result(uu_app(), raw_args)?;

let mut opts = CollatorOptions::default();
opts.alternate_handling = Some(AlternateHandling::Shifted);
let _ = try_init_collator(opts);

let settings = parse_settings(&matches)?;
let settings = parse_settings(&matches, diag_args.as_deref())?;

let file1 = matches.get_one::<OsString>("file1").unwrap();
let file2 = matches.get_one::<OsString>("file2").unwrap();
Expand Down
51 changes: 50 additions & 1 deletion tests/by-util/test_join.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (words) autoformat nocheck
// spell-checker:ignore (words) autoformat nocheck FILENUM

#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "netbsd"))]
use std::fs::OpenOptions;
Expand Down Expand Up @@ -662,3 +662,52 @@ fn test_locale_collation() {
.stdout_contains("abc:d 2 y")
.stdout_contains("ab:d 1 x");
}

#[cfg(all(feature = "feat_diagnostics", not(wasi_runner)))]
mod diagnostics {
use super::*;

#[cfg(unix)]
#[test]
fn test_snippet_points_at_the_failing_field_of_a_list() {
let result = new_ucmd!()
.terminal_sim_stderr()
.args(&["-o", "1.2,2.x", "/dev/null", "/dev/null"])
.fails_with_code(1);

// The first field is fine; only the second one is at fault.
assert_eq!(
result.stderr_as_displayed(),
"\
join: invalid field number: 'x'
╭─[ join:1:13 ]
β”‚
1 β”‚ join -o 1.2,2.x /dev/null /dev/null
β”‚ ───
β”‚
β”‚ Help: an output field is FILENUM.FIELD, as in -o 1.2,2.1; 0 stands for the join field
───╯"
);
}

#[cfg(unix)]
#[test]
fn test_snippet_points_inside_a_glued_short_option() {
let result = new_ucmd!()
.terminal_sim_stderr()
.args(&["-o1.2,0.4", "/dev/null", "/dev/null"])
.fails_with_code(1);
let stderr = result.stderr_as_displayed();

assert!(stderr.contains("join:1:12"), "{stderr}");
assert!(stderr.contains("invalid field specifier"), "{stderr}");
}

#[test]
fn test_plain_message_when_stderr_is_a_pipe() {
new_ucmd!()
.args(&["-o", "1.2,2.x", "/dev/null", "/dev/null"])
.fails_with_code(1)
.stderr_is("join: invalid field number: 'x'\n");
}
}
Loading