From 0b202d55f0ae1ce53d559be77dc5bc559fdacc5d Mon Sep 17 00:00:00 2001 From: 132ikl <132@ikl.sh> Date: Mon, 23 Jun 2025 17:29:58 -0400 Subject: [PATCH] Add `only` command to `std-rfc/iter` (#16015) # Description This PR adds the `only` command to `std-rfc/iter`, which is a command I wrote a while ago that I've found so useful that I think it could have a place in the standard library. It acts similarly to `get 0`, but ensures that the value actually exists, and there aren't additional values. I find this most useful when chained with `where`, when you want to be certain that no additional elements are accidentally selected when you only mean to get a single element. I'll copy the help page here for additional explanation: > Get the only element of a list or table, ensuring it exists and there are no extra elements. > > Similar to `first` with no arguments, but errors if there are no additional > items when there should only be one item. This can help avoid issues when more > than one row than expected matches some criteria. > > This command is useful when chained with `where` to ensure that only one row > meets the given condition. > > If a cell path is provided as an argument, it will be accessed after the first > element. For example, `only foo` is roughly equivalent to `get 0.foo`, with > the guarantee that there are no additional elements. > > Note that this command currently collects streams. > Examples: > > Get the only item in a list, ensuring it exists and there's no additional items > ```nushell > [5] | only > # => 5 > ``` > > Get the `name` column of the only row in a table > ```nushell > [{name: foo, id: 5}] | only name > # => foo > ``` > > Get the modification time of the file named foo.txt > ```nushell > ls | where name == "foo.txt" | only modified > ``` Here's some additional examples showing the errors: ![image](https://github.com/user-attachments/assets/d5e6f202-db52-42e4-a2ba-fb7c4f1d530a) ![image](https://github.com/user-attachments/assets/b080da2a-7aff-48a9-a523-55c638fdcce3) Most of the time I chain this with a simple `where`, but here's a couple other real world examples of how I've used this: [With `parse`, which outputs a table](https://git.ikl.sh/132ikl/dotfiles/src/branch/main/.scripts/manage-nu#L53): ```nushell let commit = $selection | parse "{start}.g{commit}-{end}" | only commit ``` [Ensuring that only one row in a table has a name that ends with a certain suffix](https://git.ikl.sh/132ikl/dotfiles/src/branch/main/.scripts/btconnect): ```nushell $devices | where ($chosen_name ends-with $it.name) | only ``` Unfortunately to get these nice errors I had to collect the stream (and I think the errors are more useful for this). This should be to be mitigated with (something like) #16014. Putting this in `std/iter` might be pushing it, but it seems *just* close enough that I can't really justify putting it in a different/new module. # User-Facing Changes * Adds the `only` command to `std-rfc/iter`, which can be used to ensure that a table or list only has a single element. # Tests + Formatting Added a few tests for `only` including error cases # After Submitting N/A --------- Co-authored-by: Bahex --- crates/nu-std/std-rfc/iter/mod.nu | 48 ++++++++++++++++++++++++ crates/nu-std/tests/test_std-rfc_iter.nu | 42 +++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/crates/nu-std/std-rfc/iter/mod.nu b/crates/nu-std/std-rfc/iter/mod.nu index 5625af80a6..944fb990ed 100644 --- a/crates/nu-std/std-rfc/iter/mod.nu +++ b/crates/nu-std/std-rfc/iter/mod.nu @@ -172,3 +172,51 @@ export def recurse [ generate $fn [{path: ($.), item: ($in) }] | if not $depth_first { flatten } else { } } + +# Helper for `only` errors +def only-error [msg: string, meta: record, label: string]: nothing -> error { + error make { + msg: $msg, + label: { + text: $label, + span: $meta.span, + } + } +} + +# Get the only element of a list or table, ensuring it exists and there are no extra elements. +# +# Similar to `first` with no arguments, but errors if there are additional +# items when there should only be one item. This can help avoid issues when more +# than one row than expected matches some criteria. +# +# This command is useful when chained with `where` to ensure that only one row +# meets the given condition. +# +# If a cell path is provided as an argument, it will be accessed after the first +# element. For example, `only foo` is roughly equivalent to `get 0.foo`, with +# the guarantee that there are no additional elements. +# +# Note that this command currently collects streams. +@search-terms first single +@category filters +@example "Get the only item in a list, ensuring it exists and there's no additional items" --result 5 { + [5] | only +} +@example "Get the `name` column of the only row in a table" --result "foo" { + [{name: foo, id: 5}] | only name +} +@example "Get the modification time of the file named foo.txt" { + ls | where name == "foo.txt" | only modified +} +export def only [ + cell_path?: cell-path # The cell path to access within the only element. +]: [table -> any, list -> any] { + let pipe = {in: $in, meta: (metadata $in)} + let path = [0 $cell_path] | cell-path-join + match ($pipe.in | length) { + 0 => (only-error "expected non-empty table/list" $pipe.meta "empty") + 1 => ($pipe.in | get $path) + _ => (only-error "expected only one element in table/list" $pipe.meta "has more than one element") + } +} diff --git a/crates/nu-std/tests/test_std-rfc_iter.nu b/crates/nu-std/tests/test_std-rfc_iter.nu index b215cc1ab6..46b5b9f964 100644 --- a/crates/nu-std/tests/test_std-rfc_iter.nu +++ b/crates/nu-std/tests/test_std-rfc_iter.nu @@ -79,3 +79,45 @@ def recurse-example-closure [] { assert equal $out $expected } + +@test +def only-example-list [] { + let out = [5] | only + assert equal $out 5 +} + +@test +def only-example-table [] { + let out = [{name: foo, id: 5}] | only name + assert equal $out foo +} + +@test +def only-more-than-one-list [] { + try { + [1 2 3] | only + assert false + } catch {|err| + assert ($err.msg has "expected only one") + } +} + +@test +def only-more-than-one-table [] { + try { + [[name, id]; [foo bar] [5 6]] | only foo + assert false + } catch {|err| + assert ($err.msg has "expected only one") + } +} + +@test +def only-none [] { + try { + [] | only + assert false + } catch {|err| + (assert ($err.msg has "non-empty")) + } +}