from Hacker News

Scriptisto: "Shebang interpreter" that enables writing scripts in compiled langs

by ronjouch on 2/6/24, 10:41 AM with 35 comments

  • by hawski on 2/7/24, 7:16 PM

    I prefer to take advantage of the fact that without a proper shebang the file will be first executed by the shell. Then it is easy to do almost whatever you want.

    For example I put this in front of my test C files:

      #if 0
      set -e; [ "$0" -nt "$0.bin" ] &&
      gcc -Wall -Wextra -pedantic -std=c99 "$0" -o "$0.bin"
      exec "$0.bin" "$@"
      #endif
    
    I call those: polyglot shell scripts. All my examples are valid files for the language while working as shell scripts at the same time.

    In the past when Python 3 was still not obviously there and under different names it was once useful for me to have this on top of a python script:

      "set" "-e"
      "exec" "$(for p in python3.7 python3 python; do which p && break; done)" "$0" 
      "$@"
    
    It works, because Python doesn't care about stray strings.

    You can also use the trick with Makefiles, although I never needed to use it:

      #\
      set -e
      #\
      exec make -f "$0" "$@"
    
    This is not a useful example, but it works. This works, because make will treat slash followed by a new-line as a comment continuation and bash will ignore it.
  • by mirekrusin on 2/7/24, 10:52 PM

    Executable, single files with dependency specification should have first class support in all languages.

    But that's not going to happen.

    So next best option is probably weirdly a markdown file executor which runs triple quoted blocks.

    The upside is that you could mix different languages, which is interesting.

  • by IshKebab on 2/7/24, 7:24 PM

    The problem with these systems is third party dependencies and IDE support. You really want to be able to use third party dependencies, and have IDEs know about them.

    The only systems I know of that do that properly are Deno and I believe F#, though I haven't tried the latter.

    Unfortunately Deno has a stupid issue where `deno run` will check for updates and print a "new version of Deno is available" message which rather sours the otherwise great experience of using it for shell scripting.

  • by ComputerGuru on 2/7/24, 6:42 PM

    For the rustaceans in the crowd:

    It's not a universal "for all compiled langs," but I published a two-liner [0] with zero (non-standard) dependencies or pre-requisites that can be copy-and-pasted to the top of any rust code to turn it into an executable script (cached! also correctly cache-invalidating!) back in 2020.

    It even lets you optionally omit the `fn main() { ... }` surrounding your code as well, if you really want to feel like you're just shell scripting <insert appropriate emoji here>.

    It's an example of "polyglot source code" that is valid under two languages at once (in this case, standard sh and rust), (ab)using the rust `#[allow(...)]` "macro" to do double-duty as a shell script comment opener.

    [0] https://neosmart.net/blog/self-compiling-rust-code/

  • by foobarqux on 2/7/24, 8:41 PM

    For python

        #!/usr/bin/env -S PIP_RUN_RETENTION_STRATEGY=persist pip-run
    
        # Requirements:
        # pendulum>=1.0.0
        # requests
    
        import requests
        print('hello')
  • by 12_throw_away on 2/7/24, 7:21 PM

    In my experience, writing small but critical shell-script-style mini-programs in rust can actually be a really good idea. The biggest drawback is, IMO, all the overhead of setting up an entire crate just to compile a tiny script, so I'm really happy to see projects like this (and other tools [1]) for this workflow.

    [1] https://rust-lang.github.io/rfcs/3424-cargo-script.html#prio...

  • by mise_en_place on 2/7/24, 11:18 PM

    It's interesting being able to execute the source file like this, by passing in build flags and compilation instructions. But in Emacs I can do this by simply writing my code in scratch and then evaluating each piece of it with ctrl-j. I will immediately get output. The LISP read-eval-print loop enables this type of power.
  • by elitepleb on 2/7/24, 6:58 PM

    not too dissimilar an idea from Tom Duff's com executable https://web.archive.org/web/20210204050841/http://www.iq0.co...
  • by UncleOxidant on 2/7/24, 6:40 PM

    It looks like it invokes the compiler every time the script is run? That's nice if you've changed the file, but takes extra time if you haven't. It seems neat at first, but I guess I don't see the advantage over just compiling to an executable and then running that.
  • by ronjouch on 2/6/24, 10:42 AM

  • by lambdaba on 2/7/24, 7:20 PM

    Somewhat related: nix-shell supports an additional shebang, allowing summoning of any combination of packages, to be made available inside a script. An example from the docs[1]:

      #!/usr/bin/env nix-shell
      #![allow()] /*
      #!nix-shell -i bash -p rustc
      rsfile="$(readlink -f $0)"
      binfile="/tmp/$(basename "$rsfile").bin"
      rustc "$rsfile" -o "$binfile" --edition=2021 && exec "$binfile" $@ || exit $?
      */
      fn main() {
          for argument in std::env::args().skip(1) {
              println!("{}", argument);
          };
          println!("{}", std::env::var("HOME").expect(""));
      }
    
    and another:

      #! /usr/bin/env nix-shell
      #! nix-shell -p "haskellPackages.ghcWithPackages (p: with p; [turtle])" -i runghc
    
      {-# LANGUAGE OverloadedStrings #-}
    
      import Turtle
    
      main = echo "Hello world!"
    
    
    1. https://nixos.wiki/wiki/Nix-shell_shebang
  • by zx8080 on 2/7/24, 11:10 PM

    In both Linux and Macos source files are usually not executable. So what's the point of this? Applicable only in Windows with ex-FAT/FAT32 (probably, also not a thing in NTFS, but not sure whether needs explicitly forbidding 'execute' flag or how it's called there) which is able to run any file.