mirror of
https://github.com/bennofs/nix-script
synced 2025-01-09 20:34:20 +01:00
158 lines
5.0 KiB
Haskell
Executable File
158 lines
5.0 KiB
Haskell
Executable File
-- | A shebang for running scripts inside nix-shell with defined dependencies
|
|
module Main where
|
|
|
|
import Control.Monad (when)
|
|
import Data.Maybe (fromMaybe)
|
|
import Data.Char (isSpace)
|
|
import Data.List (isPrefixOf, find, (\\))
|
|
import System.Environment (lookupEnv, getProgName, getArgs)
|
|
import System.Process (callProcess)
|
|
import System.Posix.Escape.Unicode (escapeMany)
|
|
|
|
-- | Enviroment variables
|
|
type Env = [String]
|
|
|
|
-- | Program arguments
|
|
type Args = [String]
|
|
|
|
-- | interpreter name and arguments
|
|
type Inter = (String, Args)
|
|
|
|
|
|
-- | Information about a language
|
|
data Language = Language
|
|
{ name :: String
|
|
-- ^ Name of the language
|
|
, depsTrans :: [String] -> [String]
|
|
-- ^ Transform language-specific dependencies to nix packages
|
|
, run :: FilePath -> Inter
|
|
-- ^ Command to run the given file as script
|
|
, repl :: FilePath -> Inter
|
|
-- ^ Command to load the given file in an interpreter
|
|
}
|
|
|
|
|
|
-- | Basic packages always present
|
|
basePackages :: [String]
|
|
basePackages = ["coreutils", "utillinux"]
|
|
|
|
-- | Preserved environment variables
|
|
baseEnv :: [String]
|
|
baseEnv = ["LOCALE_ARCHIVE", "SSL_CERT_FILE" ,"LANG", "TERMINFO", "TERM"]
|
|
|
|
|
|
-- | List of supported language definitions
|
|
languages :: [Language]
|
|
languages = [haskell, python 2, python 3, javascript, perl, shell]
|
|
where
|
|
haskell = Language "haskell" d r i where
|
|
d pkgs = pure ("haskellPackages.ghcWithPackages (hs: with hs; [" ++
|
|
unwords pkgs ++ "])")
|
|
r script = ("runghc" , [script])
|
|
i script = ("ghci" , [script])
|
|
|
|
python v = Language ("python" ++ show v) d r i where
|
|
d pkgs = pure ("python" ++ (show v) ++
|
|
".withPackages (py: with py; [" ++
|
|
unwords pkgs ++ "])")
|
|
r script = ("python" ++ show v, [script])
|
|
i script = ("python" ++ show v, ["-i", script])
|
|
|
|
javascript = Language "javascript" d r i where
|
|
d pkgs = "node" : map ("nodePackages." ++) pkgs
|
|
r script = ("node" , [script])
|
|
i script = ("node" , [])
|
|
|
|
perl = Language "perl" d r i where
|
|
d pkgs = pure ("perl.withPackages (pl: with pl; [" ++
|
|
unwords pkgs ++ "])")
|
|
r script = ("perl" , [script])
|
|
i script = ("perl" , ["-d", script])
|
|
|
|
shell = Language "shell" d r i where
|
|
d = mappend ("bash" : basePackages)
|
|
r script = ("bash", [script])
|
|
i _ = ("bash", [])
|
|
|
|
|
|
-- | Create ad-hoc definitions for unknown languages
|
|
passthrough :: String -> Language
|
|
passthrough name = Language name d r i where
|
|
d = mappend basePackages
|
|
r script = (name, [script])
|
|
i _ = (name, [])
|
|
|
|
|
|
-- | Find the appropriate language definition
|
|
lookupLang :: String -> Language
|
|
lookupLang n =
|
|
fromMaybe (passthrough n) (find ((n ==) . name) languages)
|
|
|
|
|
|
-- | Extract environment declaration from the header
|
|
filterEnv :: [String] -> (Env, [String])
|
|
filterEnv header = (vars env, header \\ env)
|
|
where
|
|
vars = concatMap (drop 2 . words)
|
|
env = filter (isPrefixOf "env" . dropWhile isSpace) header
|
|
|
|
|
|
-- | Parse dependencies declaration line
|
|
parseHeader :: String -> [String]
|
|
parseHeader = uncurry trans . split . words
|
|
where
|
|
trans lang = depsTrans (lookupLang lang)
|
|
split (lang : "|" : deps) = (lang, deps)
|
|
split line = error ("Invalid dependency declaration: " ++ unwords line)
|
|
|
|
|
|
-- | Find command to run/load the script
|
|
makeInter :: String -> Bool -> String -> Inter
|
|
makeInter lang interactive =
|
|
(if interactive then repl else run) (lookupLang lang)
|
|
|
|
|
|
-- | Create command to add the shell environment
|
|
makeCmd :: Inter -> Args -> Env -> String
|
|
makeCmd (program, args) args' defs =
|
|
env defs ++ interpreter ++ escapeMany args'
|
|
where
|
|
interpreter = program ++ " " ++ unwords args ++ " "
|
|
env defs = "env " ++ unwords defs ++ " "
|
|
|
|
|
|
-- | Create environment variable to run the script with
|
|
makeEnv :: Env -> IO Env
|
|
makeEnv extra = mapM format (baseEnv ++ extra) where
|
|
format var = maybe "" (\x -> var ++ "=" ++ x) <$> lookupEnv var
|
|
|
|
|
|
-- | run a script or load it in an interactive interpreter
|
|
main :: IO ()
|
|
main = do
|
|
progName <- getProgName
|
|
progArgs <- getArgs
|
|
|
|
when (null progArgs) (fail $ "usage: " ++ progName ++ " <file>")
|
|
|
|
let shebang = takeWhile (isPrefixOf "#!") . lines
|
|
header = drop 1 . map (drop 2) . shebang
|
|
(file:args) = progArgs
|
|
|
|
script <- readFile file
|
|
case header script of
|
|
(('>':identifier) : lines) -> do
|
|
let (env, deps) = filterEnv lines
|
|
pkgs = concatMap parseHeader deps
|
|
language = dropWhile isSpace identifier
|
|
interactive = last progName == 'i'
|
|
shell = last progName == 's'
|
|
interpreter = makeInter language interactive file
|
|
|
|
cmd <- makeCmd interpreter args <$> makeEnv env
|
|
if shell
|
|
then callProcess "nix-shell" ("-p" : pkgs)
|
|
else callProcess "nix-shell" ("--pure" : "--run" : cmd : "-p" : pkgs)
|
|
|
|
_ -> fail "missing or invalid header"
|