Allow nix-shell to be used as a #! interpreter

This allows scripts to fetch their own dependencies via nix-shell. For
instance, here is a Haskell script that, when executed, pulls in GHC
and the HTTP package:

  #! /usr/bin/env nix-shell
  #! nix-shell -i runghc -p haskellPackages.ghc haskellPackages.HTTP

  import Network.HTTP

  main = do
    resp <- Network.HTTP.simpleHTTP (getRequest "http://nixos.org/")
    body <- getResponseBody resp
    print (take 100 body)

Or a Perl script that pulls in Perl and some CPAN packages:

  #! /usr/bin/env nix-shell
  #! nix-shell -i perl -p perl perlPackages.HTMLTokeParserSimple perlPackages.LWP

  use HTML::TokeParser::Simple;

  my $p = HTML::TokeParser::Simple->new(url => 'http://nixos.org/');

  while (my $token = $p->get_tag("a")) {
      my $href = $token->get_attr("href");
      print "$href\n" if $href;
  }

Note that the options to nix-shell must be given on a separate line
that starts with the magic string ‘#! nix-shell’. This is because
‘env’ does not allow passing arguments to an interpreter directly.
This commit is contained in:
Eelco Dolstra 2015-01-08 14:32:45 +01:00
parent 7ba0e9cb48
commit a957893b26

View file

@ -25,6 +25,8 @@ my @envExclude = ();
my $myName = $runEnv ? "nix-shell" : "nix-build"; my $myName = $runEnv ? "nix-shell" : "nix-build";
my $inShebang = 0;
my $script;
my $tmpDir = mkTempDir($myName); my $tmpDir = mkTempDir($myName);
@ -35,6 +37,29 @@ my $drvLink = "$tmpDir/derivation";
$SIG{'INT'} = sub { exit 1 }; $SIG{'INT'} = sub { exit 1 };
# Heuristic to see if we're invoked as a shebang script, namely, if we
# have a single argument, it's the name of an executable file, and it
# starts with "#!".
if ($runEnv && scalar @ARGV == 1) {
$script = $ARGV[0];
if (-f $script && -x $script) {
open SCRIPT, "<$script" or die "$0: cannot open $script: $!\n";
my $first = <SCRIPT>;
if ($first =~ /^\#\!/) {
$inShebang = 1;
@ARGV = ();
while (<SCRIPT>) {
chomp;
if (/^\#\!\s*nix-shell (.*)$/) {
@ARGV = split / /, $1;
}
}
}
close SCRIPT;
}
}
for (my $n = 0; $n < scalar @ARGV; $n++) { for (my $n = 0; $n < scalar @ARGV; $n++) {
my $arg = $ARGV[$n]; my $arg = $ARGV[$n];
@ -155,6 +180,18 @@ for (my $n = 0; $n < scalar @ARGV; $n++) {
$packages = 1; $packages = 1;
} }
elsif ($inShebang && $arg eq "-i") {
$n++;
die "$0: $arg requires an argument\n" unless $n < scalar @ARGV;
my $interpreter = $ARGV[$n];
# Überhack to support Perl. Perl examines the shebang and
# executes it unless it contains the string "perl" or "indir",
# or (undocumented) argv[0] does not contain "perl". Exploit
# the latter by doing "exec -a".
my $execArgs = $interpreter =~ /perl/ ? "-a PERL" : "";
$envCommand = "exec $execArgs $interpreter $script";
}
elsif (substr($arg, 0, 1) eq "-") { elsif (substr($arg, 0, 1) eq "-") {
push @buildArgs, $arg; push @buildArgs, $arg;
} }