Skip to content

join: support -o field lists supplied as separate arguments - #15245

Open
ozenyunus35 wants to merge 3 commits into
uutils:mainfrom
ozenyunus35:fix/join-o-separate-operands
Open

ozenyunus35 wants to merge 3 commits into
uutils:mainfrom
ozenyunus35:fix/join-o-separate-operands

Conversation

@ozenyunus35

@ozenyunus35 ozenyunus35 commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #15279

Summary

Partially addresses #14705.

GNU join supports specifying the -o output field list as separate command-line arguments:

join -o 1.1 1.2 2.2 file1 file2

Previously, uutils join interpreted the additional field specifications as file operands and failed with an extra operand error.

This PR updates operand parsing to support the GNU-compatible behavior.

Changes

  • Parse command-line operands in order to identify field specifications belonging to -o.
  • Combine separately supplied field specifications into the output format.
  • Preserve the expected handling of missing files and extra operands.
  • Maintain option-validation precedence over operand errors.
  • Add regression coverage for valid field lists and invalid operand combinations.

Regression Tests

Added tests covering:

  1. Multiple -o field specifications supplied as separate arguments.
  2. Missing-file handling when a field specification must be interpreted as a file operand.
  3. Rejection of extra arguments that are not valid field specifications.

The new regression test fails on the original implementation and passes with this change.

Validation

  • test_join: 44/44 passed
  • cargo fmt --check: passed
  • Clippy with warnings denied, including pedantic checks: passed
  • CSpell: passed

GNU's tests/join/join.pl was also used as a black-box compatibility test.

Before this fix, three o-sep-* cases failed:

  • o-sep-unquoted-spaces
  • o-sep-unquoted-tabs
  • o-sep-space-enoent

All three pass with this change.

The two remaining failures, ee1 and ee2, are addressed separately in #15244. Applying both PRs together makes the complete GNU join.pl test pass.

The changes were also tested together in a temporary worktree and applied without conflicts.

Scope and Compatibility

This PR focuses on -o argument parsing. Ordinary two-file invocations remain unchanged.

GNU's behavior and test expectations were used as compatibility references. No GNU implementation code was copied.

AI Assistance

Claude Code assisted with implementation and testing. The changes were reviewed before submission.

GNU join accepts the fields of -o as separate operands, as in
`join -o 1.1 1.2 2.2 file1 file2`: when an operand comes on top of two
pending ones right after -o, the oldest pending one is taken as more of
the field list if it is one. uutils reported an extra operand instead.

Do the same, taking only as many operands as needed to leave two files,
so `join -o 1.1 1.2 2.2 f1` still tries to open 2.2. This fixes the
o-sep-unquoted-spaces, o-sep-unquoted-tabs and o-sep-space-enoent cases
of the GNU test tests/join/join.pl.
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

GNU testsuite comparison:

Congrats! The gnu test tests/join/join is no longer failing!

On Windows the name in the error is quoted ('2.2'), so check the parts
of the message instead of the exact line.
@xtqqczze

Copy link
Copy Markdown
Collaborator

@ozenyunus35 Please avoid using "Partially addresses" in the PR description when referencing a tracker issue. Instead, please open a new issue to track the work if one doesn't already exist.

Comment thread src/uu/join/src/join.rs Outdated
let mut specs = vec![];
for format in formats {
let mut operand_formats = operand_formats.into_iter().peekable();
let indices = matches.indices_of("o").into_iter().flatten();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need to interleave by clap index? quite a lot of logic for a rare case, please add a test showing the order matters if we keep it

Comment thread src/uu/join/src/join.rs Outdated
indices.map(move |index| (index, id))
})
.collect();
let follows_o = |index: usize| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this checks the option before the third operand, not the one before the field list itself.
so join -o 1.1 1.2 -t , a b still says extra operand, and join -t , 1.2 -o 1.1 a b is accepted. could you please check what GNU does and add tests for both?

Comment thread src/uu/join/src/join.rs Outdated
let mut formats = vec![];
for (index, name) in operands {
if names.len() == 2 {
match names[0].to_str().and_then(parse_field_list) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only names[0] is tried here, so join a -o 1.1 1.2 b fails with extra operand. is that expected? please add a test either way

Comment thread tests/by-util/test_join.rs Outdated
}

#[test]
fn format_list_in_separate_operands() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please prefix it with test_

Comment thread src/uu/join/src/join.rs Outdated
translate!("join-error-extra-operand", "operand" => extra.quote()),
));
}
// Operand errors are reported after option errors, as before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the as_mut/mem::take dance is hard to follow, could parse_settings just take &[...] and we ? the operands afterwards?
and "as before" won't mean anything once this is merged

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"as before" won't mean anything once this is merged

@sylvestre I’ve noticed quite a few similar "as before" comments in code. I wonder if Claude Code tends to treat comments as a way to narrate the diff rather than explain the code itself. Personally, I’d rather AI didn’t write comments at all.

Comment thread tests/by-util/test_join.rs Outdated
.args(&["-o", "1.1", "1.2", "2.2", "a"])
.fails_with_code(1)
.no_stdout()
.stderr_contains("2.2")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit weak, could you please check the full line, e.g. join: 2.2: No such file or directory?

Follow GNU's add_file_name: an operand that follows a -o field list (or
another such operand) might be more of it, and the older pending one is
taken when a third operand comes. This also accepts other options in
between and keeps the fields in the order GNU adds them.
@ozenyunus35

Copy link
Copy Markdown
Contributor Author

@sylvestre reworked in 645b35e: operands are now taken as field lists the same way GNU's add_file_name does it, so the clap index interleaving and the mem::take are gone and join a -o 1.1 1.2 b works. Also added the test_ prefix, checked the full error line, and opened #15279 for tracking.

@codspeed

codspeed Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Merging this PR will regress 1 benchmark

⚠️ 116 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

ℹ️ 1 benchmark has no flame graph

The flamegraph could not be generated because too much time was spent in runtime internals, making the visualization not useful for performance analysis.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
❌ 1 regressed benchmark
✅ 399 untouched benchmarks
⏩ 54 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Simulation five_38_bit_primes 1.7 s 1.8 s -6.42%
⚡ Simulation three_39_bit_primes 490.4 ms 413.9 ms +18.48%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ozenyunus35:fix/join-o-separate-operands (645b35e) with main (efca981)2

Open in CodSpeed

Footnotes

  1. 54 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (d43f2b8) during the generation of this report, so efca981 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

join: -o field list given as separate operands is rejected as extra operands

3 participants