just icon indicating copy to clipboard operation
just copied to clipboard

Allow interpolation inside of backticks and strings

Open casey opened this issue 9 years ago • 36 comments

To avoid making this a backwards incompatible change, only strings and backticks preceeded by an f will allow interpolation:

bar := "world"
foo := f"Hello, {{bar}}!"
baz :=  f`echo "Hello, {{bar}}"`

Both foo and baz evaluate to Hello, world!.

To keep this simple, we should only support single identifiers in interpolations, i.e., no arbitrary expressions like foo + bar.

casey avatar Oct 31 '16 06:10 casey

This does introduce the odd scenario where you can have interpolation inside a string inside an interpolation inside a string, etc, etc, etc.

casey avatar Nov 13 '16 09:11 casey

Closing. If anyone wants this, comment and I'll reopen it!

casey avatar Jan 09 '17 03:01 casey

Maybe thinking that we should do this. Reopening.

casey avatar Jan 23 '17 00:01 casey

If this is supported, I'm inclined to do it with python's f-string syntax: f'some string with{{interpolation}}'

casey avatar Apr 17 '19 05:04 casey

Interesting how nobody has taken this on yet! I am quite interested in having this and have some basic experience working w/ lexers/parsers for other things, so maybe I'll try to take a look soon-ish and see if I can help make this happen!

kwshi avatar May 05 '21 17:05 kwshi

I started working on this, in-progress branch here.

I marked this as "good first issue", but I think that's misleading, because it's super hard!

I've gotten lexing and parsing mostly working, but I haven't done evaluation, i.e. actually evaluating the fragments, concatenating them, and, in the case of format backticks, evaluating the result.

If your interested, you could pick up the branch as a starting point. I'm not sure when I'm going to work on it again.

casey avatar May 06 '21 03:05 casey

Is there any news on the subject?

edit: without this I'm forced to go back to make 😅

pyrsmk avatar Jan 22 '22 05:01 pyrsmk

Is there any news on the subject?

This is a big feature, so time to implementation is unknown.

casey avatar Jan 22 '22 16:01 casey

Could this crate be any helpful ?

MiniJinja -- Powerful but minimal dependency template engine for Rust

Announcement on r/rust (2021-09-24).

MiniJinja is a powerful but minimal dependency template engine for Rust which is based on the syntax and behavior of the Jinja2 template engine for Python.

It's implemented on top of serde and only has it as a single dependency. It supports a range of features from Jinja2 including inheritance, filters and more. The goal is that it should be possible to use some templates in Rust programs without the fear of pulling in complex dependencies for a small problem. Additionally it tries not to re-invent something but stay in line with prior art to leverage an already existing ecosystem of editor integrations.

Cheers

ngirard avatar Feb 03 '22 12:02 ngirard

Could this crate be any helpful ?

MiniJinja -- Powerful but minimal dependency template engine for Rust

The template language isn't so much the hard part, it's the rest of the integration, so a crate wouldn't be a huge help.

casey avatar Feb 03 '22 16:02 casey

For those who might be interested, I developed a simple task system based on the Ruby language.

I needed something effective, pretty simple to use and quick to develop for a project. It already proved itself to be quite powerful : https://github.com/pyrsmk/run

pyrsmk avatar Feb 04 '22 10:02 pyrsmk

Is a format expression an easier compromise? I can live without interpolation if there's a string.format(str, args*) method available.

foo := "github.com"
api_uri := "api/v2"
foo_url := string_format("https://%s/%s", foo, api_uri)

kallangerard avatar Apr 13 '23 11:04 kallangerard

i'm hoping for this addition.

regmicmahesh avatar May 04 '23 10:05 regmicmahesh

I'm working around this using replace:

value := env_var('VALUE')
result := replace('here is _, _ a pair of values', '_', value)

It's acceptable if you're only using one value multiple times like I am; it would get gnarly with multiple values and replacements.

crabmusket avatar May 14 '23 11:05 crabmusket

I'm also interested in this kind of functionality (edit: I'm interested in substitution in backticks in particular), but actually I don't care that this is provided using backticks: for my current use case, a function could be fine as well. Would something like an eval(string) function be simpler to implement? In that way, we could compose strings and use variables inside expressions:

world := "World!"

greet arg=world:
    echo "Hello {{ eval("echo " + arg) }}"

I took a quick look at the code and if the implementation is performed trivially, one could implement a new eval function that basically replicates what is being done in run_backtick, although it seems that there is no access to Settings from the FunctionContext (and I didn't investigate if that's something that belongs in there).

Edit 2: I would also consider functions to substitute in strings. I think it's a simpler approach that would provide the feature in backward-compatible way and does not rely on the parser.

akiross avatar Sep 13 '23 10:09 akiross

It's very much not clear in the documentation that you can't do this, currently. Perhaps a note should be added to the section on back-ticks.

blueforesticarus avatar Feb 25 '24 13:02 blueforesticarus

This is the only thing stopping me being able to convert a whole Makefile over to Just :(

I have managed to build up the command-line using +, but I can't see how I can then execute that for use in a new variable.

For example:

BLAH := some_example # this is the canonical source that we don't want to repeat through the whole file
VERSION := $(shell cargo metadata --format-version 1 | jq -r '.packages[] | select(.name=="my-prefix-${BLAH}") | .version')

I can then convert it to:

export BLAH := "some_example" # this is the canonical source that we don't want to repeat
version_cmd := "cargo metadata --format-version 1 | jq -r '.packages[] | select(.name==\"my-prefix-" + BLAH + "\") | .version'"
export VERSION = `...` # what can I do here to execute version_cmd

NickLarsenNZ avatar Feb 28 '24 08:02 NickLarsenNZ

I'm in the exact same situation as @NickLarsenNZ. Currently on the verge of convincing my organization to migrate over to Justfiles from Makefiles, but this is the final missing piece. I am tracking this issue closely in the hope it is implemented some time soon, otherwise the opportunity to switch will pass and we will be stuck with Make for the foreseeable future, or worst yet a custom an in-house solution.

@casey Apologies to ping you directly, but if you could provide a steer on your appetite to implement this and maybe a ballpark ETA, I can make a more informed decision for my team.

edit: For what it's worth, my preference is Python's f"Hello, {{name}}!" syntax, as mentioned elsewhere. It's backwards-compatible and common enough at this point that it wouldn't be weird to borrow the idea.

gg718 avatar Mar 14 '24 16:03 gg718

What kinds of workarounds are people using for the lack of way to use variables in backticks?

Recently ran into a case where variable in backticks would be useful: writing a justfile to build a soft-fork project that's distributed as a set of source code patches. As there maybe multiple upstream source tarball versions available locally, selecting the right one requires a (user-overridable) command. The output of that command (the selected tarball) needs to be stored in a variable for use by recipes. It's also needed in another variable backtick: since the recipes also have to know where the tarball will extract to, a second command is used to get the name of the subdirectory in the tarball. That second command requires the output of the first to know which tarball to work with.

The best I could come up with is making an external Python script to find the correct tarball, with an environment variable override option, and calling it in both backticks. But this is not a great solution: the code is run twice for the same result, and using an environment variable for the override risks build environment pollution.

With backtick interpolation, this would be straightforward, only a few lines in the justfile (probably without any external script), and the override could be done with just --set.

laniakea64 avatar Mar 22 '24 17:03 laniakea64

One of the reasons that this is hard is because it will be possible to nest interpolations recursively:

qux := f"{{f"{{f"{{bar}}"}}"}}"

If this is what makes this hard, maybe nesting f-strings could be explicitly disallowed, at least initially? Most if not all uses for nesting could be achieved with intermediate variables. Adding nesting support later if needed wouldn't affect backwards compatibility.

laniakea64 avatar Mar 29 '24 15:03 laniakea64

An alternative take is to just make a breaking change allowing interpolation in backticks, and call it v2. Then give instructions to v1 users on how to deal with literals which now have different behavior.

NickLarsenNZ avatar Mar 29 '24 15:03 NickLarsenNZ

An alternative take is to just make a breaking change allowing interpolation in backticks, and call it v2. Then give instructions to v1 users on how to deal with literals which now have different behavior.

https://just.systems/man/en/chapter_9.html

laniakea64 avatar Mar 29 '24 16:03 laniakea64

One of the reasons that this is hard is because it will be possible to nest interpolations recursively:

qux := f"{{f"{{f"{{bar}}"}}"}}"

If this is what makes this hard, maybe nesting f-strings could be explicitly disallowed, at least initially? Most if not all uses for nesting could be achieved with intermediate variables. Adding nesting support later if needed wouldn't affect backwards compatibility.

Or, there can be a default (overridable) max level of recursion, above which will cause a runtime error.

gyreas avatar May 08 '24 09:05 gyreas

What does @casey think about a fmt() function that provides Python (or printf) style formatting for the meantime?

gyreas avatar May 08 '24 09:05 gyreas

I think the first easiest step is to parse f"blah {{foo}} blah", where the contents of an interpolation may only be a single identifier. This covers the most common case, but avoids the complexity of the fully recursive case.

casey avatar May 15 '24 02:05 casey

In that case: no escaping, right?

gyreas avatar May 16 '24 21:05 gyreas

@casey, The branch you linked above is inexistent. I'll like to take this on, but I need a place to start from, otherwise it might take a while.

gyreas avatar May 20 '24 08:05 gyreas

The old branch is probably pretty out of date, so I'm not sure it will be much help.

casey avatar May 20 '24 19:05 casey

Alright, anyhoo. I'll look into how #2055 works, and figure it out from there.

gyreas avatar May 21 '24 11:05 gyreas

Yeah. I'd love that addition too. Python strings f"{{}}" would have my preference.

gl-yziquel avatar May 28 '24 08:05 gl-yziquel