diff --git a/doc/manual/rl-next/empty-attr-paths.md b/doc/manual/rl-next/empty-attr-paths.md new file mode 100644 index 000000000..726c61f41 --- /dev/null +++ b/doc/manual/rl-next/empty-attr-paths.md @@ -0,0 +1,10 @@ +--- +synopsis: "libexpr: allow empty attr-names in parseAttrPath if they are quoted" +cls: [5375] +category: "Miscellany" +credits: [ma27] +--- + +Empty strings are now allowed in attribute paths as consumed by e.g. `nix-build`. +I.e. `nix-build -A 'foo."".bar'` works now. +The quotes are necessary, i.e. `nix-build -A foo..bar` will throw an error. diff --git a/lix/libexpr/attr-path.cc b/lix/libexpr/attr-path.cc index 9051123d9..67aa78260 100644 --- a/lix/libexpr/attr-path.cc +++ b/lix/libexpr/attr-path.cc @@ -16,6 +16,22 @@ std::vector parseAttrPath(std::string_view const s) auto i = s.begin(); while (i != s.end()) { if (*i == '.') { + if (!haveData) { + if (res.empty()) { + throw ParseError( + "Leading dot in attribute selection path '%1%' is not allowed! If the attribute name " + "is an empty string, use '\"\".foo.bar'", + s + ); + } else { + throw ParseError( + "consecutive dots not allowed in selection path '%1%', use 'foo.\"\".bar' to denote " + "an " + "empty attribute name", + s + ); + } + } res.push_back(cur); haveData = false; cur.clear(); @@ -90,9 +106,6 @@ findAlongAttrPath(EvalState & state, const std::string & attrPath, Bindings & au according to what is specified in the attrPath. */ if (!attrIndex) { - if (attr.empty()) - throw Error("empty attribute name in selection path '%1%'", attrPath); - if (v.type() != nAttrs) { auto pathPart = std::vector(tokens.begin(), tokens.begin() + attrPathIdx); diff --git a/tests/functional2/eval/test_attr_paths.py b/tests/functional2/eval/test_attr_paths.py index c3197c7c9..5b5923fa7 100644 --- a/tests/functional2/eval/test_attr_paths.py +++ b/tests/functional2/eval/test_attr_paths.py @@ -23,12 +23,15 @@ ERR_CASES: list[ShouldError] = [ "1", """error: the expression selected by the selection path '1' should be a list but is a set: { }""", ), - ShouldError("{}", ".", """error: empty attribute name in selection path '.'"""), ShouldError( - '{ x."" = 2; }', 'x.""', """error: empty attribute name in selection path 'x.""'""" + "{}", + ".", + """error: Leading dot in attribute selection path '.' is not allowed! If the attribute name is an empty string, use '\"\".foo.bar'""", ), ShouldError( - '{ x."".y = 2; }', 'x."".y', """error: empty attribute name in selection path 'x."".y'""" + "{}", + "bla..blub", + """error: consecutive dots not allowed in selection path 'bla..blub', use 'foo."".bar' to denote an empty attribute name""", ), ShouldError( "[]", "1", """error: list index 1 in selection path '1' is out of range for list [ ]""" diff --git a/tests/unit/libexpr/attr-path.cc b/tests/unit/libexpr/attr-path.cc index 1c0d3f8fe..94487bd85 100644 --- a/tests/unit/libexpr/attr-path.cc +++ b/tests/unit/libexpr/attr-path.cc @@ -38,12 +38,56 @@ std::pair AttrPathEval::testFindAlongAttrPath(std::string expr, s // n.b. I do not know why we throw for empty attrs but they are apparently // disallowed. -TEST_F(AttrPathEval, emptyAttrsThrows) +TEST_F(AttrPathEval, emptyAttrsThrowsWithoutQuotes) { std::string expr = "{a.\"\".b = 2;}"; ASSERT_NO_THROW(testFindAlongAttrPath(expr, "a")); - ASSERT_THROW(testFindAlongAttrPath(expr, "a.\"\".b"), Error); - ASSERT_THROW(testFindAlongAttrPath(expr, "a.\"\""), Error); + ASSERT_NO_THROW(testFindAlongAttrPath(expr, "a.\"\".b")); + ASSERT_THROW(testFindAlongAttrPath(expr, "a..b"), Error); + ASSERT_NO_THROW(testFindAlongAttrPath(expr, "a.\"\"")); } +TEST(attr_path_eval, quotes) +{ + auto p1 = parseAttrPath("foo.\"foo bar\".baz"); + ASSERT_EQ(3, p1.size()); + ASSERT_EQ("foo", p1[0]); + ASSERT_EQ("foo bar", p1[1]); + ASSERT_EQ("baz", p1[2]); + + auto p2 = parseAttrPath("foo.\"foo bar\""); + ASSERT_EQ(2, p2.size()); + ASSERT_EQ("foo", p2[0]); + ASSERT_EQ("foo bar", p2[1]); + + auto p3 = parseAttrPath("\"foo bar\""); + ASSERT_EQ(1, p3.size()); + ASSERT_EQ("foo bar", p3[0]); +} + +TEST(attr_path_eval, quotes_empty) +{ + auto p1 = parseAttrPath("foo.\"\".bar"); + ASSERT_EQ(3, p1.size()); + ASSERT_EQ("foo", p1[0]); + ASSERT_EQ("", p1[1]); + ASSERT_EQ("bar", p1[2]); + + auto p2 = parseAttrPath("foo.\"\""); + ASSERT_EQ(2, p2.size()); + ASSERT_EQ("foo", p2[0]); + ASSERT_EQ("", p2[1]); + + auto p3 = parseAttrPath("\"\""); + ASSERT_EQ(1, p3.size()); + ASSERT_EQ("", p3[0]); +} + +TEST(attr_path_eval, quotes_syntax) +{ + ASSERT_THROW(parseAttrPath("foo.\"bar"), ParseError); + + // escaped quotes (\") are not supported + ASSERT_THROW(parseAttrPath("foo.\"bar\\\"\""), ParseError); +} }