From 8f9429dcab9e806a61a8541cd3fd7c1c19c17ae8 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Thu, 29 Jul 2021 12:03:07 -0400 Subject: [PATCH 1/6] add antiquotations to paths --- src/libexpr/eval.cc | 12 ++++-- src/libexpr/eval.hh | 3 +- src/libexpr/lexer.l | 88 +++++++++++++++++++++++++++++++++++++------- src/libexpr/parser.y | 25 +++++++++++-- 4 files changed, 106 insertions(+), 22 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index 419b377ba55..a8bfa7711a2 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1580,7 +1580,6 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) and none of the strings are allowed to have contexts. */ if (first) { firstType = vTmp.type(); - first = false; } if (firstType == nInt) { @@ -1601,7 +1600,12 @@ void ExprConcatStrings::eval(EvalState & state, Env & env, Value & v) } else throwEvalError(pos, "cannot add %1% to a float", showType(vTmp)); } else - s << state.coerceToString(pos, vTmp, context, false, firstType == nString); + /* skip canonization of first path, which would only be not + canonized in the first place if it's coming from a ./${foo} type + path */ + s << state.coerceToString(pos, vTmp, context, false, firstType == nString, !first); + + first = false; } if (firstType == nInt) @@ -1790,7 +1794,7 @@ std::optional EvalState::tryAttrsToString(const Pos & pos, Value & v, } string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context, - bool coerceMore, bool copyToStore) + bool coerceMore, bool copyToStore, bool canonizePath) { forceValue(v, pos); @@ -1802,7 +1806,7 @@ string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context, } if (v.type() == nPath) { - Path path(canonPath(v.path)); + Path path(canonizePath ? canonPath(v.path) : v.path); return copyToStore ? copyPathToStore(context, path) : path; } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index 47db8b2ba23..ad6fb7ff475 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -217,7 +217,8 @@ public: booleans and lists to a string. If `copyToStore' is set, referenced paths are copied to the Nix store as a side effect. */ string coerceToString(const Pos & pos, Value & v, PathSet & context, - bool coerceMore = false, bool copyToStore = true); + bool coerceMore = false, bool copyToStore = true, + bool canonizePath = true); string copyPathToStore(PathSet & context, const Path & path); diff --git a/src/libexpr/lexer.l b/src/libexpr/lexer.l index 27975dc9ecf..8ad6a195776 100644 --- a/src/libexpr/lexer.l +++ b/src/libexpr/lexer.l @@ -9,6 +9,9 @@ %s DEFAULT %x STRING %x IND_STRING +%x INPATH +%x INPATH_SLASH +%x PATH_START %{ @@ -97,9 +100,12 @@ ANY .|\n ID [a-zA-Z\_][a-zA-Z0-9\_\'\-]* INT [0-9]+ FLOAT (([1-9][0-9]*\.[0-9]*)|(0?\.[0-9]+))([Ee][+-]?[0-9]+)? -PATH [a-zA-Z0-9\.\_\-\+]*(\/[a-zA-Z0-9\.\_\-\+]+)+\/? -HPATH \~(\/[a-zA-Z0-9\.\_\-\+]+)+\/? -SPATH \<[a-zA-Z0-9\.\_\-\+]+(\/[a-zA-Z0-9\.\_\-\+]+)*\> +PATH_CHAR [a-zA-Z0-9\.\_\-\+] +PATH {PATH_CHAR}*(\/{PATH_CHAR}+)+\/? +PATH_SEG {PATH_CHAR}*\/ +HPATH \~(\/{PATH_CHAR}+)+\/? +HPATH_START \~\/ +SPATH \<{PATH_CHAR}+(\/{PATH_CHAR}+)*\> URI [a-zA-Z][a-zA-Z0-9\+\-\.]*\:[a-zA-Z0-9\%\/\?\:\@\&\=\+\$\,\-\_\.\!\~\*\']+ @@ -200,17 +206,73 @@ or { return OR_KW; } return IND_STR; } +{PATH_SEG}\$\{ | +{HPATH_START}\$\{ { + PUSH_STATE(PATH_START); + yyless(0); +} + +{PATH_SEG} { + POP_STATE(); + PUSH_STATE(INPATH_SLASH); + yylval->path = strdup(yytext); + return PATH; +} + +{HPATH_START} { + POP_STATE(); + PUSH_STATE(INPATH_SLASH); + yylval->path = strdup(yytext); + return HPATH; +} + +{PATH} { + if (yytext[yyleng-1] == '/') + PUSH_STATE(INPATH_SLASH); + else + PUSH_STATE(INPATH); + yylval->path = strdup(yytext); + return PATH; +} +{HPATH} { + if (yytext[yyleng-1] == '/') + PUSH_STATE(INPATH_SLASH); + else + PUSH_STATE(INPATH); + yylval->path = strdup(yytext); + return HPATH; +} + +\$\{ { + POP_STATE(); + PUSH_STATE(INPATH); + PUSH_STATE(DEFAULT); + return DOLLAR_CURLY; +} +{PATH}|{PATH_SEG}|{PATH_CHAR}+ { + POP_STATE(); + if (yytext[yyleng-1] == '/') + PUSH_STATE(INPATH_SLASH); + else + PUSH_STATE(INPATH); + yylval->e = new ExprString(data->symbols.create(string(yytext))); + return STR; +} +{ANY} | +<> { + /* if we encounter a non-path character we inform the parser that the path has + ended with a PATH_END token and re-parse this character in the default + context (it may be ')', ';', or something of that sort) */ + POP_STATE(); + yyless(0); + return PATH_END; +} + +{ANY} | +<> { + throw ParseError("path has a trailing slash"); +} -{PATH} { if (yytext[yyleng-1] == '/') - throw ParseError("path '%s' has a trailing slash", yytext); - yylval->path = strdup(yytext); - return PATH; - } -{HPATH} { if (yytext[yyleng-1] == '/') - throw ParseError("path '%s' has a trailing slash", yytext); - yylval->path = strdup(yytext); - return HPATH; - } {SPATH} { yylval->path = strdup(yytext); return SPATH; } {URI} { yylval->uri = strdup(yytext); return URI; } diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index f948dde4743..796e87cc153 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -290,13 +290,13 @@ void yyerror(YYLTYPE * loc, yyscan_t scanner, ParseData * data, const char * err %type formal %type attrs attrpath %type string_parts_interpolated ind_string_parts -%type string_parts string_attr +%type path_start string_parts string_attr %type attr %token ID ATTRPATH %token STR IND_STR %token INT %token FLOAT -%token PATH HPATH SPATH +%token PATH HPATH SPATH PATH_END %token URI %token IF THEN ELSE ASSERT WITH LET IN REC INHERIT EQ NEQ AND OR IMPL OR_KW %token DOLLAR_CURLY /* == ${ */ @@ -405,8 +405,11 @@ expr_simple | IND_STRING_OPEN ind_string_parts IND_STRING_CLOSE { $$ = stripIndentation(CUR_POS, data->symbols, *$2); } - | PATH { $$ = new ExprPath(absPath($1, data->basePath)); } - | HPATH { $$ = new ExprPath(getHome() + string{$1 + 1}); } + | path_start PATH_END { $$ = $1; } + | path_start string_parts_interpolated PATH_END { + $2->insert($2->begin(), $1); + $$ = new ExprConcatStrings(CUR_POS, false, $2); + } | SPATH { string path($1 + 1, strlen($1) - 2); $$ = new ExprApp(CUR_POS, @@ -452,6 +455,20 @@ string_parts_interpolated } ; +path_start + : PATH { + Path path(absPath($1, data->basePath)); + /* add back in the trailing '/' to the first segment */ + if ($1[strlen($1)-1] == '/') + path += "/"; + $$ = new ExprPath(path); + } + | HPATH { + Path path(getHome() + string($1 + 1)); + $$ = new ExprPath(path); + } + ; + ind_string_parts : ind_string_parts IND_STR { $$ = $1; $1->push_back($2); } | ind_string_parts DOLLAR_CURLY expr '}' { $$ = $1; $1->push_back($3); } From 624162c72973b2ff63ee4e840c4f20af1e490039 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Fri, 6 Aug 2021 07:06:52 -0400 Subject: [PATCH 2/6] add path antiqutations test --- tests/lang/eval-okay-path-antiquotation.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 tests/lang/eval-okay-path-antiquotation.nix diff --git a/tests/lang/eval-okay-path-antiquotation.nix b/tests/lang/eval-okay-path-antiquotation.nix new file mode 100644 index 00000000000..497d7c1c756 --- /dev/null +++ b/tests/lang/eval-okay-path-antiquotation.nix @@ -0,0 +1,12 @@ +let + foo = "foo"; +in +{ + simple = ./${foo}; + surrounded = ./a-${foo}-b; + absolute = /${foo}; + expr = ./${foo + "/bar"}; + home = ~/${foo}; + notfirst = ./bar/${foo}; + slashes = /${foo}/${"bar"}; +} From a6bfda7d956f88f5e38c0848b9dfd8ea0108f26b Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Fri, 6 Aug 2021 07:38:52 -0400 Subject: [PATCH 3/6] path antiquotations: rename confusing test --- ...{eval-fail-antiquoted-path.nix => eval-fail-nonexist-path.nix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/lang/{eval-fail-antiquoted-path.nix => eval-fail-nonexist-path.nix} (100%) diff --git a/tests/lang/eval-fail-antiquoted-path.nix b/tests/lang/eval-fail-nonexist-path.nix similarity index 100% rename from tests/lang/eval-fail-antiquoted-path.nix rename to tests/lang/eval-fail-nonexist-path.nix From 9da8f5e25d835e3bd1cc4280f2b69cfa4ab8d774 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Tue, 31 Aug 2021 08:02:04 -0400 Subject: [PATCH 4/6] path antiquotations: canonizePath -> canonicalizePath --- src/libexpr/eval.cc | 4 ++-- src/libexpr/eval.hh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc index a8bfa7711a2..ce3a626d349 100644 --- a/src/libexpr/eval.cc +++ b/src/libexpr/eval.cc @@ -1794,7 +1794,7 @@ std::optional EvalState::tryAttrsToString(const Pos & pos, Value & v, } string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context, - bool coerceMore, bool copyToStore, bool canonizePath) + bool coerceMore, bool copyToStore, bool canonicalizePath) { forceValue(v, pos); @@ -1806,7 +1806,7 @@ string EvalState::coerceToString(const Pos & pos, Value & v, PathSet & context, } if (v.type() == nPath) { - Path path(canonizePath ? canonPath(v.path) : v.path); + Path path(canonicalizePath ? canonPath(v.path) : v.path); return copyToStore ? copyPathToStore(context, path) : path; } diff --git a/src/libexpr/eval.hh b/src/libexpr/eval.hh index ad6fb7ff475..af000072afa 100644 --- a/src/libexpr/eval.hh +++ b/src/libexpr/eval.hh @@ -218,7 +218,7 @@ public: referenced paths are copied to the Nix store as a side effect. */ string coerceToString(const Pos & pos, Value & v, PathSet & context, bool coerceMore = false, bool copyToStore = true, - bool canonizePath = true); + bool canonicalizePath = true); string copyPathToStore(PathSet & context, const Path & path); From b2beb97f2ae5fbade5e5ac0763ffbefdc4057ee8 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Tue, 31 Aug 2021 08:17:17 -0400 Subject: [PATCH 5/6] add documentation for path antiquotations --- doc/manual/src/expressions/language-values.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/manual/src/expressions/language-values.md b/doc/manual/src/expressions/language-values.md index ce31029cc74..28fa23b58ea 100644 --- a/doc/manual/src/expressions/language-values.md +++ b/doc/manual/src/expressions/language-values.md @@ -139,6 +139,13 @@ Nix has the following basic data types: environment variable `NIX_PATH` will be searched for the given file or directory name. + Antiquotation is supported in any paths except those in angle brackets. + `./${foo}-${bar}.nix` is a more convenient way of writing + `./. + "/" + foo + "-" + bar + ".nix"` or `./. + "/${foo}-${bar}.nix"`. At + least one slash must appear *before* any antiquotations for this to be + recognized as a path. `a.${foo}/b.${bar}` is a syntactically valid division + operation. `./a.${foo}/b.${bar}` is a path. + - *Booleans* with values `true` and `false`. - The null value, denoted as `null`. From 1ffb9f197075975a00cd95cddcb39a29b15aed86 Mon Sep 17 00:00:00 2001 From: Taeer Bar-Yam Date: Tue, 31 Aug 2021 08:21:42 -0400 Subject: [PATCH 6/6] fix parse of `/${foo}`. was `// + foo` I don't think this changes the way any program would behave, but it's a cleaner internal representation. --- src/libexpr/parser.y | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libexpr/parser.y b/src/libexpr/parser.y index 796e87cc153..e3749783afe 100644 --- a/src/libexpr/parser.y +++ b/src/libexpr/parser.y @@ -459,7 +459,7 @@ path_start : PATH { Path path(absPath($1, data->basePath)); /* add back in the trailing '/' to the first segment */ - if ($1[strlen($1)-1] == '/') + if ($1[strlen($1)-1] == '/' && strlen($1) > 1) path += "/"; $$ = new ExprPath(path); }