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 +4/−0
- LICENSE +28/−0
- README.md +134/−0
- Setup.hs +6/−0
- app/Main.hs +7/−0
- default.nix +3/−0
- nix/default.nix +15/−0
- nix/overlay.nix +121/−0
- shebanger.cabal +117/−0
- shell.nix +3/−0
- src/Shebanger.hs +208/−0
- src/Shebanger/Cli.hs +127/−0
- test/DocTest.hs +12/−0
- test/Test.hs +8/−0
- test/Test/Shebanger.hs +11/−0
+ 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+ ]