packages feed

shebanger (empty) → 1.0.0.0

raw patch · 15 files changed

+804/−0 lines, 15 filesdep +QuickCheckdep +basedep +base64-bytestringbuild-type:Customsetup-changed

Dependencies added: QuickCheck, base, base64-bytestring, bytestring, containers, directory, doctest, filepath, from-sum, optparse-applicative, pretty-simple, process, shebanger, tasty, tasty-hunit, template-haskell, text, time, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,4 @@++## 1.0.0.0++*   Initial release.
+ LICENSE view
@@ -0,0 +1,28 @@+BSD 3-Clause License++Copyright (c) 2024, Dennis Gosnell++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice, this+   list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from+   this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,134 @@+# shebanger++`shebanger` is a small CLI tool to transform a _boring_ shell script into an+**exciting** series of shebang lines!++`shebanger` takes a shell script as input, and outputs a series of shell+scripts. Each of the output shell scripts only consists of a shebang line, they+don't have any code in their body.  Executing the first output shell script will+run your original input shell program.++## Installation++This section lists installation instructions for various Linux distros.++### Generic Linux++There is a statically-linked x86\_64 Linux ELF binary for `shebanger` available on each of+the [GitHub Releases](https://github.com/cdepillabout/shebanger/releases).++### Nix / NixOS++Cloudy can be built with Nix by using the `.nix` files in this repo:++```console+$ nix-build+```++You can find the `shebanger` binary in `./result/bin/shebanger`.++You can also install `shebanger` from Nixpkgs by using the `haskellPackages.shebanger`+derivation:++```console+$ nix-build '<nixpkgs>' -A haskellPackages.shebanger+```++### Other Distros++PRs are welcome adding instructions for installing Cloudy on other Linux+distributions!++## Usage++`shebanger` assumes it will be available on your `PATH`.  You might see errors+with the following commands if `shebanger` can't be found on your `PATH`.++There is an example shell script to use with `shebanger` in this repo called+[`test.sh`](./test.sh):++```console+$ cat test.sh+#!/usr/bin/env bash++echo "arguments to this script: $@"+...+```++First, run `shebanger` on this script:++```console+$ shebanger ./test.sh+```++This outputs a bunch of scripts named like the following:++```console+$ ls test.sh.shebanged*+test.sh.shebanged+test.sh.shebanged.1+test.sh.shebanged.2+test.sh.shebanged.3+test.sh.shebanged.4+test.sh.shebanged.5+...+```++Each of these scripts is just a single line, and they look like the following:++```console+$ cat ./test.sh.shebanged+#!/usr/bin/env -S shebanger exec IyEvdXNyL2Jpbi9lbnYgYmFzaAoKZWNobyAiYXJndW1lbnRzIHRvIHRoaXMgc2NyaXA=+```++You can see that this is a script that only consists of a shebang line.  The+contents of the original script are split into 50-byte chunks, base-64 encoded,+passed within the shebang lines of the output scripts.++If you run `./test.sh.shebanged`, the original `test.sh` will be reconstructed+and executed:++```console+$ ./test.sh.shebanged hello this is an argument+arguments to this script: hello this is an argument+...+```++## How Does `shebanger` Work?++`shebanger` works by base-64-encoding the original input script, and splitting+it up into the shebang lines of a series of output scripts.  Each of these+output scripts consist of only a shebang line.++When running the output script, by executing each output script one-by-one in+turn, `shebanger` collects the contents of the original script, reconstructs+it, and executes it.++`shebanger` actually doesn't require the input to be a script, it works on any+executable.  However, see the following sections on limitations of this.++## Limitations++Currently `shebanger` collects the contents of the original scripts into an+environment variable, and passes that along when `exec`'ing the next script.++This means that `shebanger` is not able to execute scripts longer than a few+hundred kilobytes, since they tend to overflow the max size of all environment+variables.++## Inspiration++`shebanger` is inspired by [bangscript](https://github.com/viperML/bangscript)+by [@viperML](https://github.com/viperML).  The `About` explanation of `bangscript` is:++> Embed scripts in a shebang++When I read this, I immediately thought it would be what `shebanger` currently+is.  Since `bangscript` is instead something actually useful, I knew I needed to write+`shebanger`.++## FAQs++1.  **Q: Why would someone ever actually use `shebanger`?**++    A: ...
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main where++import Distribution.Extra.Doctest (defaultMainWithDoctests)++main :: IO ()+main = defaultMainWithDoctests "doctests"
+ app/Main.hs view
@@ -0,0 +1,7 @@++module Main where++import Shebanger (defaultMain)++main :: IO ()+main = defaultMain
+ default.nix view
@@ -0,0 +1,3 @@+{...}:++(import ./nix {}).shebanger-just-exe
+ nix/default.nix view
@@ -0,0 +1,15 @@++{...}:++let+  nixpkgs-src = builtins.fetchTarball {+    # haskell-updates as of 2024-10-05+    url = "https://github.com/NixOS/nixpkgs/archive/edd71fd02c1f9a5dee102e2df2281327244c0119.tar.gz";+    sha256 = "03zs0ghm8kadgdpv9fgrppldnhxwsna3rcxy5wimjgs5y32jif6q";+  };++  my-overlay = import ./overlay.nix;++in++import nixpkgs-src { overlays = [ my-overlay ]; }
+ nix/overlay.nix view
@@ -0,0 +1,121 @@+final: prev: {++  ###############################+  ## Haskell package overrides ##+  ###############################++  myhaskell =+    let+      myHaskellOverride = oldAttrs: {+        overrides =+          final.lib.composeExtensions+            (oldAttrs.overrides or (_: _: {}))+            (hfinal: hprev: {+              shebanger =+                let+                  filesToIgnore = [+                    ".git"+                    ".github"+                    ".stack-work"+                    ".travis.yml"+                    "cabal.project"+                    "default.nix"+                    "flake.lock"+                    "flake.nix"+                    "nix"+                    "result"+                    "shell.nix"+                    "stack-nightly.yaml"+                    "stack.yaml"+                  ];++                  src =+                    builtins.path {+                      name = "shebanger-src";+                      path = ./..;+                      filter = path: type:+                        with final.lib;+                        ! elem (baseNameOf path) filesToIgnore &&+                        ! any (flip hasPrefix (baseNameOf path)) [ "dist" ".ghc" ];+                    };++                  extraCabal2nixOptions = "";+                in+                hfinal.callCabal2nixWithOptions "shebanger" src extraCabal2nixOptions {};+            });+      };+    in+    prev.haskell // {+      packages = prev.haskell.packages // {+        ${final.shebanger-ghc-version} =+          prev.haskell.packages.${final.shebanger-ghc-version}.override+            myHaskellOverride;++        native-bignum = prev.haskell.packages.native-bignum // {+          ${final.shebanger-ghc-version} =+            prev.haskell.packages.native-bignum.${final.shebanger-ghc-version}.override+              myHaskellOverride;+        };+      };+    };++  shebanger-ghc-version-short = "948";++  shebanger-ghc-version = "ghc" + final.shebanger-ghc-version-short;++  shebanger-haskell-pkg-set = final.myhaskell.packages.${final.shebanger-ghc-version};++  shebanger = final.shebanger-haskell-pkg-set.shebanger;++  shebanger-just-exe =+    final.lib.pipe+      final.shebanger+      [+        # TODO: No optparse-applicative yet+        # (final.shebanger-haskell-pkg-set.generateOptparseApplicativeCompletions ["shebanger"])+        final.haskell.lib.justStaticExecutables+      ];++  shebanger-shell = final.shebanger-haskell-pkg-set.shellFor {+    packages = hpkgs: [ hpkgs.shebanger ];+  };++  my-haskell-language-server =+    final.haskell-language-server.override {+      supportedGhcVersions = [ final.shebanger-ghc-version-short ];+    };++  dev-shell = final.mkShell {+    nativeBuildInputs = [+      final.cabal-install+      final.my-haskell-language-server+    ];+    inputsFrom = [+      final.shebanger-shell+    ];+    # These environment variables are important. Without these,+    # doctest doesn't pick up nix's version of ghc, and will fail+    # claiming it can't find your dependencies+    shellHook = ''+      export NIX_GHC="${final.shebanger-shell.NIX_GHC}"+      export NIX_GHC_LIBDIR="${final.shebanger-shell.NIX_GHC_LIBDIR}"+    '';+  };++  ##############################+  ## Statically-linked builds ##+  ##############################++  shebanger-static-haskell-pkg-set = final.pkgsStatic.myhaskell.packages.native-bignum.${final.shebanger-ghc-version};++  shebanger-static = final.shebanger-static-haskell-pkg-set.shebanger;++  shebanger-static-just-exe =+    final.lib.pipe+      final.shebanger-static+      [+        (final.shebanger-static-haskell-pkg-set.generateOptparseApplicativeCompletions ["shebanger"])+        final.haskell.lib.justStaticExecutables+      ];+}+
+ shebanger.cabal view
@@ -0,0 +1,117 @@+cabal-version:       3.4++name:                shebanger+version:             1.0.0.0+synopsis:            Transform a shell script into a series of scripts with only shebang lines+-- description:         CLI tool+homepage:            https://github.com/cdepillabout/shebanger+license:             BSD-3-Clause+license-file:        LICENSE+author:              Dennis Gosnell+maintainer:          cdep.illabout@gmail.com+copyright:           2024-2024 Dennis Gosnell+category:            Productivity+build-type:          Custom+extra-source-files:  README.md+                   , CHANGELOG.md+                   , default.nix+                   , nix/default.nix+                   , nix/overlay.nix+                   , shell.nix++custom-setup+                   -- Hackage apparently gives an error if you don't specify an+                   -- upper bound on your setup-depends. But I don't want to+                   -- have to remember to bump the upper bound of each of these+                   -- setup depends every time I want to make a release of shebanger,+                   -- especially sense I already have upper bounds on important things+                   -- like base below.+  setup-depends:     base <999+                   , cabal-doctest >=1.0.2 && <1.1++library+  default-language:    Haskell2010+  hs-source-dirs:      src+  exposed-modules:     Shebanger+                     , Shebanger.Cli+  other-modules:       Paths_shebanger+  autogen-modules:     Paths_shebanger+  build-depends:       base < 5+                     , base64-bytestring+                     , bytestring+                     , containers+                     , directory+                     , filepath+                     , from-sum+                     , optparse-applicative+                     , pretty-simple+                     , process+                     , text+                     , time+                     , unix+  ghc-options:         -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates+  default-extensions:  DataKinds+                       DeriveDataTypeable+                       DeriveFunctor+                       DeriveGeneric+                       DerivingStrategies+                       DuplicateRecordFields+                       GeneralizedNewtypeDeriving+                       InstanceSigs+                       LambdaCase+                       MultiParamTypeClasses+                       NamedFieldPuns+                       NumericUnderscores+                       OverloadedLabels+                       OverloadedRecordDot+                       OverloadedStrings+                       PolyKinds+                       RankNTypes+                       RecordWildCards+                       ScopedTypeVariables+                       StandaloneKindSignatures+                       TupleSections+                       TypeApplications+                       TypeFamilies+                       TypeOperators+  other-extensions:    DeriveAnyClass+                       OverloadedRecordDot+                       QuasiQuotes+                       TemplateHaskell+                       UndecidableInstances++executable shebanger+  default-language:    Haskell2010+  main-is:             Main.hs+  hs-source-dirs:      app+  build-depends:       base < 5+                     , shebanger+  ghc-options:         -Wall -threaded -rtsopts -with-rtsopts=-N++test-suite doctests+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  main-is:             DocTest.hs+  hs-source-dirs:      test+  build-depends:       base < 5+                     , doctest+                     , QuickCheck+                     , template-haskell+                     , shebanger+  ghc-options:         -Wall++test-suite shebanger-tests+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  main-is:             Test.hs+  hs-source-dirs:      test+  other-modules:       Test.Shebanger+  build-depends:       base < 5+                     , shebanger+                     , tasty+                     , tasty-hunit+  ghc-options:         -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -threaded -rtsopts -with-rtsopts=-N++source-repository head+  type:     git+  location: git@github.com:cdepillabout/shebanger.git
+ shell.nix view
@@ -0,0 +1,3 @@+{...}:++(import ./nix {}).dev-shell
+ src/Shebanger.hs view
@@ -0,0 +1,208 @@+module Shebanger where++import Control.Exception (finally)+import Data.ByteString (ByteString)+import qualified Data.ByteString as ByteString+import Data.ByteString.Base64 (encode)+import Data.Foldable (for_)+import Data.List (isSuffixOf)+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NonEmpty+import Data.Maybe (fromMaybe)+import Shebanger.Cli (Command (..), ExecArgs (..), TranslateArgs (..), parseCliOpts)+import System.Directory (doesFileExist, removeFile)+import System.FilePath (takeFileName, (<.>), (-<.>))+import System.Posix (setFileMode, fileMode, getFileStatus, unionFileModes, ownerExecuteMode, groupExecuteMode, otherExecuteMode)+import System.Posix.Process (executeFile)+import System.Posix.ByteString (getEnv, setEnv, unsetEnv)+import System.Process (callProcess)+import Text.Read (readMaybe)++-- $setup+--+-- We need things from QuickCheck for some of the tests.+--+-- >>> import Test.QuickCheck++defaultMain :: IO ()+defaultMain = do+  cmd <- parseCliOpts+  runCmd cmd++runCmd :: Command -> IO ()+runCmd = \case+  Translate transArgs -> runCmdTranslate transArgs+  Exec execArgs -> runCmdExec execArgs++-- | The length of chunks of the input script.+--+-- The input script will be chunked into parts of this many bytes, then base-64+-- encoded (which increases the length by about 33%).  This base-64-encoded string+-- will then be put into the shebang line of all the shebanged scripts.+--+-- Note that Linux has a limit on how long a shebang line can be, so in practice this+-- has to be below 150 characters or so.+inputScriptChunkLength :: Int+inputScriptChunkLength = 50++runCmdTranslate :: TranslateArgs -> IO ()+runCmdTranslate transArgs = do+  let inputScriptFileName = takeFileName transArgs.scriptFilePath+  inputScriptContents <- ByteString.readFile transArgs.scriptFilePath+  let chunkedInputScript = chunkByteString inputScriptChunkLength inputScriptContents+      b64ChunkedInputScript = fmap encode chunkedInputScript+  writeShebanged inputScriptFileName b64ChunkedInputScript++writeShebanged :: String -> [ByteString] -> IO ()+writeShebanged scriptName b64Chunks =+  for_ (zip [0..] b64Chunks) $ \(i :: Int, chunk) -> do+    let fnameBase = scriptName <.> "shebanged"+        fname = if i == 0 then fnameBase else fnameBase <.> show i+        scriptContents = "#!/usr/bin/env -S shebanger exec " <> chunk+    ByteString.writeFile fname scriptContents+    makeExecutable fname++makeExecutable :: FilePath -> IO ()+makeExecutable path = do+    -- Get the current file permissions+    status <- getFileStatus path+    let currentMode = fileMode status+    -- Define the new mode with executable permissions+    let newMode = currentMode `unionFileModes` ownerExecuteMode `unionFileModes` groupExecuteMode `unionFileModes` otherExecuteMode+    -- Set the file mode to the new mode+    setFileMode path newMode++chunkByteString :: Int -> ByteString -> [ByteString]+chunkByteString n bs+    | ByteString.null bs = []+    | otherwise =+        let (chunk, rest) = ByteString.splitAt n bs+        in chunk : chunkByteString n rest++runCmdExec :: ExecArgs -> IO ()+runCmdExec execArgs = do+  maybeShebangerScriptContents <- getEnv "SHEBANGER_SCRIPT_CONTENTS"+  case getShebangedIndex execArgs.shebangScriptFilePath of+    Left badIndex ->+      error $+        "ERROR! Failed when trying to parse the current script's " <>+        "shebang index.  From file path \"" <>+        execArgs.shebangScriptFilePath <>+        "\", failed when trying to parse index: " <>+        badIndex+    Right idx -> do+      maybeNextScript <- findNextScript execArgs.shebangScriptFilePath idx+      case maybeNextScript of+        -- There is a next script that exists.  Update env var and execute the+        -- next script.+        Just nextScript -> do+          case maybeShebangerScriptContents of+            Nothing ->+              if idx == 0+              then+                -- This is the initial script (script.sh.shebanger), so we+                -- expect that there is not yet a SHEBANGER_SCRIPT_CONTENTS+                -- env var.  We create the env var with the initial part of+                -- the script.+                setEnv "SHEBANGER_SCRIPT_CONTENTS" execArgs.shebangScriptPart True+              else+                -- This is not the initial script (so it has a name like+                -- script.sh.shebanger.07), but there is no+                -- SHEBANGER_SCRIPT_CONTENTS env var.  This is unexpected.+                error $+                  "ERROR! This is not the first script, but no " <>+                  "SHEBANGER_SCRIPT_CONTENTS env var was found."+            Just envVarScriptContents ->+              setEnv+                "SHEBANGER_SCRIPT_CONTENTS"+                (envVarScriptContents <> execArgs.shebangScriptPart)+                True+          -- exec the next script+          executeFile nextScript True execArgs.additionalArgs Nothing+        -- There is not a next script.  This script is the last script.+        Nothing -> do+          let fullScript =+                fromMaybe "" maybeShebangerScriptContents <> execArgs.shebangScriptPart+              finalScriptName =+                if idx == 0+                then execArgs.shebangScriptFilePath <.> "final"+                else execArgs.shebangScriptFilePath -<.> "final"++          ByteString.writeFile finalScriptName fullScript+          makeExecutable finalScriptName++          -- unset the SHEBANGER_SCRIPT_CONTENTS env var, since we don't want+          -- it to be inherited by the child.+          unsetEnv "SHEBANGER_SCRIPT_CONTENTS"++          -- Call the new executable, making sure to unlink it afterwards.+          finally+            (callProcess finalScriptName execArgs.additionalArgs)+            (removeFile finalScriptName)++findNextScript :: FilePath -> Int -> IO (Maybe FilePath)+findNextScript fp currIdx = do+  let nextIdx = currIdx + 1+      nextScriptName =+        -- The initial script just ends with `.shebanged` (which we consider+        -- index 0), so in that case just add ".1".  Otherwise, increment the+        -- index by one.+        if currIdx == 0+        then fp <.> ".1"+        else fp -<.> show nextIdx+  exists <- doesFileExist nextScriptName+  pure $ if exists then Just nextScriptName else Nothing++-- | Returns 'Left' 'String' of the last part of the filename that it is trying+-- to parse a number on error.+--+-- Returns 'Right' 'Int' with the index if it is found correctly.+getShebangedIndex :: FilePath -> Either String Int+getShebangedIndex fname =+  if ".shebanged" `isSuffixOf` fname+  -- The .shebanged file is the initial file that represents index 0+  then Right 0+  else do+    -- The shebanged filenames are going to look like:+    -- my-script.sh.shebanged.XX, where XX is a number.+    let splitFname = split (== '.') fname+        -- pull out just the number+        strNum = NonEmpty.last splitFname+        -- try to parse it as a number+        maybeNum = readMaybe strNum+    case maybeNum of+      -- could not read file name part as number+      Nothing -> Left strNum+      Just num+        -- the file name part should never be less than 1+        | num < 1 -> Left strNum+        | otherwise -> Right num++-- | Split a list based on a predicate.+--+-- >>> split (== ' ') "hello world my name is bob"+-- "hello" :| ["world","my","name","is","bob"]+--+-- A predicate of @'const' 'True'@ splits on everything, leaving you with+-- a list of empty lists, with one more entry than your original list:+--+-- >>> split (const True) "bye"+-- "" :| ["","",""]+--+-- A predicate of @'const' 'False'@ produces no splits:+--+-- >>> split (const False) "bye"+-- "bye" :| []+--+-- An empty list doesn't get split, regardless of the predicate:+--+-- prop> \(Fun _ f) -> split f "" == ("" :| [])+split :: forall a. (a -> Bool) -> [a] -> NonEmpty [a]+split _ [] = [] :| []+split p t = loop t+  where+    loop :: [a] -> NonEmpty [a]+    loop s+      | null s'   = l :| []+      | otherwise = NonEmpty.cons l $ loop (tail s')+      where (l, s') = break p s
+ src/Shebanger/Cli.hs view
@@ -0,0 +1,127 @@+module Shebanger.Cli+  ( parseCliOpts+  , Command (..)+  , TranslateArgs (..)+  , ExecArgs (..)+  ) where++import Data.ByteString (ByteString)+import qualified Data.ByteString.Char8 as ByteString.Char8+import Data.ByteString.Base64 (decode)+import Data.Version (showVersion)+import Control.Applicative ((<**>), Alternative (many, (<|>)))+import Options.Applicative+    ( action, argument, command, eitherReader, fullDesc, header, help, info,+      metavar, progDesc, strArgument, execParser, helper, hsubparser,+      simpleVersioner, CommandFields, Mod, Parser, ParserInfo+    )+import Paths_shebanger (version)++data TranslateArgs = TranslateArgs { scriptFilePath :: FilePath }+  deriving stock Show++data ExecArgs = ExecArgs+  { shebangScriptPart :: ByteString+    -- ^ The base64-encoded part of the original script.+    --+    -- Example: @\"ICAgZWNobyAkaQpkb25lCmVjaG8KCiMgTGlzdCBzeXN0ZW0gaW5mb3JtYXRpb24KdW4=\"@+  , shebangScriptFilePath :: FilePath+    -- ^ The path of this shebanged script.  This is normally passed+    -- automatically by the kernel when calling a script with a shebang line.+    --+    -- Example: @\"./test.sh.shebanged.7\"@+  , additionalArgs :: [String]+    -- ^ Additional arguments that have been passed to the script on the command line.+    --+    -- Example @[\"-l\", \"-a\", \"somedirectory/\"]@+  }+  deriving stock Show++data Command+  = Translate TranslateArgs+  | Exec ExecArgs+  deriving stock Show++parseCliOpts :: IO Command+parseCliOpts = execParser cliCmdParserInfo++cliCmdParserInfo :: ParserInfo Command+cliCmdParserInfo =+  info+    ( cliCmdParser <**>+      helper <**>+      simpleVersioner (showVersion version)+    )+    ( fullDesc <>+      header "shebanger - translate a shell script into a series of shebang lines"+    )++cliCmdParser :: Parser Command+cliCmdParser = hsubparser (translateCommandMod <> execCommandMod) <|> translateCommandParser+  where+    translateCommandMod :: Mod CommandFields Command+    translateCommandMod =+      command+        "translate"+        ( info+            translateCommandParser+            (progDesc "Translate a shell script into a series of scripts with only shebang lines")+        )++    execCommandMod :: Mod CommandFields Command+    execCommandMod =+      command+        "exec"+        ( info+            (fmap Exec execArgsParser)+            (progDesc "Execute shebanged shell scripts")+        )++    translateCommandParser :: Parser Command+    translateCommandParser = fmap Translate translateArgsParser++translateArgsParser :: Parser TranslateArgs+translateArgsParser = TranslateArgs <$> inputFileParser++inputFileParser :: Parser FilePath+inputFileParser =+  strArgument+    ( metavar "FILE" <>+      help "Input shell scripts to shebang" <>+      action "file"+    )++execArgsParser :: Parser ExecArgs+execArgsParser =+  ExecArgs+    <$> shebangedScriptParser+    <*> shebangScriptFilePathParser+    <*> additionalArgsParser+++shebangedScriptParser :: Parser ByteString+shebangedScriptParser =+  argument+    (eitherReader b64Reader)+    ( metavar "SHEBANG_SCRIPT_PART" <>+      help "Part of a shebang translated script file.  Expected to be in base 64."+    )+  where+    b64Reader :: String -> Either String ByteString+    b64Reader b64Str = decode $ ByteString.Char8.pack b64Str++shebangScriptFilePathParser :: Parser FilePath+shebangScriptFilePathParser =+  strArgument+    ( metavar "FILE" <>+      help "Input shebanged script name.  Normally passed automatically by kernel." <>+      action "file"+    )++additionalArgsParser :: Parser [String]+additionalArgsParser =+  many $+    strArgument+      ( metavar "ARG" <>+        help "arguments to pass to the underlying script being called"+      )
+ test/DocTest.hs view
@@ -0,0 +1,12 @@++module Main where++import Build_doctests (flags, pkgs, module_sources)+import Test.DocTest (doctest)++main :: IO ()+main = do+  doctest args+  where+    args :: [String]+    args = flags ++ pkgs ++ module_sources
+ test/Test.hs view
@@ -0,0 +1,8 @@++module Main where++import Test.Shebanger (shebangerTests)+import Test.Tasty (defaultMain)++main :: IO ()+main = defaultMain shebangerTests
+ test/Test/Shebanger.hs view
@@ -0,0 +1,11 @@++module Test.Shebanger where++import Test.Tasty (TestTree, testGroup)++shebangerTests :: TestTree+shebangerTests =+  testGroup+    "Shebanger"+    [ -- foobarTest+    ]