Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-line script without shebang? #2192

Closed
drmacdon opened this issue Jun 25, 2024 · 11 comments
Closed

Multi-line script without shebang? #2192

drmacdon opened this issue Jun 25, 2024 · 11 comments

Comments

@drmacdon
Copy link

I currently use the shell initialization

set shell := ['nu', '-c']

Which is able to resolve my nu shell location in my PATH fine.

If I try the same thing in a recipe with a shebang it unable to locate.

#!nu -c 

I know I could place the absolute path or the use /usr/bin/env to locate, but a large reason for using nu was to keep my scripts as platform neutral as possible. If I place /usr/bin/env that makes Windows more difficult.

I like the fact that set shell is able to locate nu via the PATH which works great for single line recipes. Currently, I'm either having to escape my line endings with \ to simulate a single line command, or shell out to another nu script which breaks recipes up to avoid having to backslash.

I'd like some way to run a multi-line recipe without the shebang and use the configured shell.

Thanks!

@laniakea64
Copy link
Contributor

Try removing -c from the shebang. -c instructs the shell that the first positional argument is not a file, but a string of commands to run. But shebang recipes are run as a temporary file, so shebangs with -c won't work correctly.

@drmacdon
Copy link
Author

I'm not sure which -c you wanted me to remove, on the Justfile or the recipe but I can't get either scenario to help me.

Using the script:

set shell := ['nu', '-c']

_default:
   @{{just_executable()}} -f {{justfile()}} --list --unsorted

fail:
   #!nu
   let var = "World"
   print $"Hello ($var)"

@work:
   let var = "World"; \
   print $"Hello ($var)"

I get:

# just fail
error: Recipe `fail` with shebang `#!nu` execution error: No such file or directory (os error 2)
# just work
Hello World

If I switch the Justfile shell line to remove the -c:

set shell := ['nu']

I get:

# just fail
error: Recipe `fail` with shebang `#!nu` execution error: No such file or directory (os error 2)
#  just work
Error: nu::shell::file_not_found

  × File not found
   ╭─[source:1:1]
 1 │ nu
   · ▲
   · ╰── Could not access file 'let var = "World"; print $"Hello ($var)"': No such file or directory (os error 2)
   ╰────

@casey
Copy link
Owner

casey commented Jun 25, 2024

Currently, I don't think there's a good workaround for this. Shebang paths must be absolute, so there's no way to get them working on Windows and Unix without hackery.

I think the best solution would be implementing #1479, this would allow you to write:

[script("nu", "-c")]
foo:
   let var = "World"
   print $"Hello ($var)"

The foo recipe would be executed as a shebang recipe, and it would use whatever you provided in the [script(…)] annotation to run the script, which would work work on both Windows and Unix.

I'm a bit unsure about the name of the annotation. In current parlance, recipes are called either "shebang" recipes or "linewise" recipes, but [shebang(…)] is not a good name for the annotation, since it wouldn't actually be executed with a shebang. So maybe, along with the [script(…)] annotation, we would actually change references to "shenbang recipes" in the docs and code to call them "script recipes".

@casey casey closed this as completed Jun 25, 2024
@drmacdon
Copy link
Author

Would it be easier to go the other way and support a flag set linewise = off to opt out of line mode for the file?

@casey
Copy link
Owner

casey commented Jun 25, 2024

I'm not sure that would make a ton of sense, since if linewise is off, but you don't specify one otherwise, what is the interpreter?

@drmacdon
Copy link
Author

The default interpreter set shell := ['nu', '-c'] is actually able to find the nu from my PATH. The only reason I'm trying to do shebang is to opt out of linemode.

@casey
Copy link
Owner

casey commented Jun 25, 2024

The shell interpreter is used in more places than just linewise scripts. For example, it's used in backticks, which aren't written to disk, but instead passed as an argument. So the shell interpreter has to take the command as an argument, whereas the shebang interpreter (or a hypothetical [script(…)] attribute interpreter) has to take a path to the script, which has been written to disk.

Se the set shell interpreter can't be used for this purpose.

@drmacdon
Copy link
Author

I assumed when I specified the shell interpreter today that every recipe line invocation got it's own copy of a configured shell instance. So thought adding something like set linewise = off could just apply to the usage of the shell interpreter on the recipe toggling it from constructing one per line to reusing it.

@casey
Copy link
Owner

casey commented Jun 26, 2024

Ah, gotcha. It generally isn't possible to reuse an interpreter after the process returns. And just concatenating all the lines and running them may be possible, but might run into some argument length limit.

@drmacdon
Copy link
Author

I had to spend some time reading the code to fully understand what was happening.

The set shell := ['nu', '-c'] works great for run_linewise because it is forked as a process and thus able to be discovered via PATH. The -c invokes the single text line one at a time which works great for the run_linewise.

The run_shebang is writing a temporary preprocessed file, making it executable and invoking it analogous to running it in a standard shell with shebang semantics in which the absolute path to the shebang executable is a requirement. (So it will never work to specify a shebang inside the recipe and try to remain platform neutral.)

Really what I'm after is the running essentially the same as run_shebang but having the make_shebang_command invoke the shell in my case nu and pass the file as the first argument followed by the script arguments.

Which is why you earlier indicated that set shell was used in multiple spots - because the -c option provided on that was a requirement in the run_linewise context. And also explains why set linewise = off wouldn't work (you wouldn't know the command to invoke without the -c options).

So maybe a set recipe_shell := ['nu'] which if provided is essentially the same code as run_shebang but launching the specified process with the script as the first argument? But even that either needs someone to fake out the script with #! or a flag like set linewise=off to always force the run_shebang.

@drmacdon
Copy link
Author

Something like this might work.

   let recipe_shell = Some(["nu"]);  // Assume some shells may need args to run script?

   let script = path;
   let (path, args) = if let Some(shell_command) = recipe_shell {

      let script = script.to_str().unwrap_or_default();
      let mut args = shell_command.to_vec();
      args.push(script);

      let shell = args.remove(0);

      (Path::new(shell).to_path_buf(), Some(args))
    } else {
      (script, None)     // No recipe_shell provided default to running script path
    };

    // create command to run script
    let mut command =
      Platform::make_shebang_command(&path, self.working_directory(context.search), shebang)
        .map_err(|output_error| Error::Cygpath {
          recipe: self.name(),
          output_error,
        })?;

    if let Some(args) = args {
      if self.takes_positional_arguments(context.settings) {
        command.args(args);
        command.args(positional);
      } else {
        // Had no positional arguments, but still need to pass the args
        command.args(args);
      }
    } else {
      if self.takes_positional_arguments(context.settings) {
        command.args(positional);
      }
    }

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

No branches or pull requests

3 participants