├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── Setup.hs ├── app └── Main.hs ├── examples ├── env.yaml ├── favourites.py ├── footer.md ├── gitprofile.md ├── simple.md └── use-scripts.md ├── src └── Tempered │ ├── Options.hs │ ├── Parser.hs │ └── Template.hs ├── stack.yaml ├── tempered.cabal ├── test └── Spec.hs └── tools ├── attach-binary.sh ├── install-ghr.sh └── install-stack.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /.stack-work 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | addons: 2 | apt: 3 | packages: 4 | - libgmp-dev 5 | language: c 6 | sudo: false 7 | cache: 8 | directories: 9 | - $HOME/.local/bin 10 | - $HOME/.stack 11 | os: 12 | - linux 13 | - osx 14 | before_install: 15 | - sh tools/install-stack.sh 16 | - sh tools/install-ghr.sh 17 | script: 18 | - stack setup 19 | - stack build --ghc-options -O2 --pedantic 20 | after_success: 21 | - sh tools/attach-binary.sh 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Chris Penner (c) 2017 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Chris Penner nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tempered 2 | ====== 3 | 4 | [![Hackage](https://img.shields.io/badge/hackage-latest-green.svg)](https://hackage.haskell.org/package/tempered) 5 | 6 | A dead-simple templating utility for simple shell interpolation. 7 | Use at your own risk and only on trusted templates. 8 | 9 | ```bash 10 | $ tempered --help 11 | Tempered - Templating engine based on shell interpolation 12 | 13 | Usage: tempered [templates] 14 | Interpolate templates to stdout 15 | 16 | Available options: 17 | templates The templates to interpolate 18 | -h,--help Show this help text 19 | ``` 20 | 21 | Here's a simple use-case: 22 | 23 | ```md 24 | {{ # inside "post.md" }} 25 | # My Blog Post 26 | 27 | by {{ echo $AUTHOR }} 28 | Published on {{ date +'%B %d, %Y' }} 29 | 30 | Here's my blog post! 31 | 32 | --- 33 | {{ cat ./footer.md | tr 'a-z' 'A-Z' }} 34 | ``` 35 | 36 | ```md 37 | {{ # inside "footer.md" }} 38 | Copyright 2017 Chris Penner 39 | Check me out on twitter @chrislpenner! 40 | See you next time! 41 | ``` 42 | 43 | Then we can render the template with 44 | 45 | ``` 46 | tempered ./post.md 47 | 48 | # OR 49 | 50 | cat ./post.md | tempered 51 | ``` 52 | 53 | to get: 54 | 55 | ``` 56 | # My Blog Post 57 | 58 | by Chris Penner 59 | Published on April 22, 2017 60 | 61 | Here's my blog post! 62 | 63 | --- 64 | COPYRIGHT 2017 CHRIS PENNER 65 | CHECK ME OUT ON TWITTER @CHRISLPENNER! 66 | SEE YOU NEXT TIME! 67 | ``` 68 | 69 | If you want you can add a shebang to the top of your template and just run it 70 | as an executable, tempered will strip the shebang for you automagically: 71 | 72 | > test.txt 73 | ``` 74 | #!/path/to/tempered 75 | interpolate {{ echo $THIS }} 76 | ``` 77 | 78 | ```bash 79 | $ chmod +x test.txt 80 | $ export THIS="that" 81 | $ ./test.txt 82 | interpolate that 83 | ``` 84 | 85 | Shell not powerful enough for you? No problem! Use all your favourite languages and 86 | tools; See the FAQ or examples to see how to integrate with scripts (spoilers, just call them like you do in bash!) 87 | 88 | 89 | [**Examples Here**](https://github.com/ChrisPenner/tempered/tree/master/examples) 90 | 91 | Installation 92 | ============ 93 | 94 | Mac and Linux binaries are provided [HERE](https://github.com/ChrisPenner/tempered/releases/latest); 95 | 96 | ### Homebrew On Mac 97 | 98 | ``` 99 | brew update 100 | brew install ChrisPenner/tools/tempered 101 | ``` 102 | 103 | ### Stack 104 | If you're familiar with Haskell Stack: 105 | 106 | ``` 107 | stack update && stack install tempered 108 | ``` 109 | 110 | FAQ 111 | ===== 112 | 113 | There's really not much to it; parses the file and runs anything 114 | inside `{{ }}` as a shell expression and pipes stdout into its spot. 115 | If you're clever you can do pretty much anything you want with this. 116 | 117 | ### Variables? 118 | 119 | Sure; It's bash. 120 | 121 | ```bash 122 | Hello, my name is {{echo $USER}} 123 | ``` 124 | 125 | You can set up environment overrides in `env.yaml`, tempered looks up through 126 | the file-system to find an `env.yaml` from the cwd NOT the template location. 127 | 128 | Here's an example env.yaml; we can do simple strings or commands here; just make 129 | sure to quote any entries that start with `{{` or the YAML parser gets mad. 130 | 131 | > env.yaml 132 | ```yaml 133 | PROJECT: Tempered 134 | DATE: "{{ date +'%B %d, %Y' }}" 135 | ``` 136 | 137 | Then you can use them just like normal variables. 138 | 139 | ### For Loops? 140 | 141 | It's bash; go for it: 142 | ```bash 143 | {{ for i in 99 98 97 ; do 144 | cat < output: 151 | ``` 152 | 99 bottles of beer on the wall 153 | 98 bottles of beer on the wall 154 | 97 bottles of beer on the wall 155 | ``` 156 | 157 | ### Other Scripts/Tools? 158 | 159 | Duh! Interpolation works like a shell, just call the scripts or binaries you want! 160 | Here we'll use a simple python script to spice things up! 161 | 162 | ```python 163 | # favourites.py 164 | import sys 165 | 166 | print(" and ".join(sys.argv[1:]) + ",") 167 | print("These are a few of my favourite things") 168 | ``` 169 | 170 | ``` 171 | # My Favourite Things 172 | 173 | {{ python favourites.py Gumdrops Roses Whiskers Kittens }} 174 | 175 | When the dog bites 176 | When the bee stings 177 | When I'm feeling sad 178 | I simply remember my favourite things 179 | And then I don't feel so bad 180 | ``` 181 | 182 | Output: 183 | 184 | ``` 185 | # My Favourite Things 186 | 187 | Gumdrops and Roses and Whiskers and Kittens, 188 | These are a few of my favourite things 189 | 190 | When the dog bites 191 | When the bee stings 192 | When I'm feeling sad 193 | I simply remember my favourite things 194 | And then I don't feel so bad 195 | ``` 196 | 197 | ### \_\_: command not found? 198 | 199 | Chances are you're forgetting to echo an env-var; 200 | `{{ $TITLE }}` will try to run the contents of `$TITLE` as a command, you want 201 | `{{ echo "$TITLE" }}`. 202 | 203 | ### Isn't this whole thing a security risk? 204 | 205 | Probably. 206 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /app/Main.hs: -------------------------------------------------------------------------------- 1 | {-# language OverloadedStrings #-} 2 | module Main where 3 | 4 | import System.Environment 5 | import System.Directory 6 | import System.Exit 7 | 8 | import Paths_tempered (version) 9 | import Data.Version (showVersion) 10 | 11 | import Data.Foldable 12 | import Data.Monoid 13 | import qualified Data.Map as M 14 | import Control.Monad.Reader 15 | 16 | import Options.Applicative 17 | 18 | import Tempered.Options 19 | import Tempered.Parser 20 | import Tempered.Template 21 | 22 | data Options = Options 23 | { files :: [String] 24 | , versionFlag :: Bool 25 | } deriving (Show) 26 | 27 | 28 | options :: Parser Options 29 | options = Options <$> many (argument str meta) <*> switch vers 30 | where meta = help "The templates to interpolate" <> metavar "templates" 31 | vers = long "version" <> short 'v' <> help "Show version" 32 | 33 | argParser :: ParserInfo Options 34 | argParser = info (options <**> helper) 35 | ( fullDesc <> 36 | progDesc "Interpolate templates to stdout" <> 37 | header "Tempered - Templating engine based on shell interpolation") 38 | 39 | -- | Run tempered on cmdline args. 40 | main :: IO () 41 | main = do 42 | Options filenames versionF <- execParser argParser 43 | when versionF $ putStrLn (showVersion version) >> exitSuccess 44 | envVars <- getEnvVars 45 | templates <- getTemplates filenames 46 | runReaderT (renderOutput templates) envVars 47 | where 48 | -- Choose stdin if we're not given any files 49 | getTemplates [] = (:[]) <$> readStdInTemplate 50 | getTemplates filenames = traverse (templateFromFile >=> handleTemplateError) filenames 51 | 52 | readStdInTemplate = getContents >>= handleTemplateError . parseTemplate "stdin" 53 | renderOutput = traverse_ (interpTemplate >=> liftIO . putStr) 54 | 55 | -- | Combine local and global environment variables 56 | getEnvVars :: IO EnvVars 57 | getEnvVars = do 58 | cwd <- getCurrentDirectory 59 | globalEnvVars <- getEnvironment 60 | localEnvVars <- getProjectOptions cwd 61 | envTemplates <- traverse (handleTemplateError . parseTemplate "env.yaml") localEnvVars 62 | interpolatedLocalEnvVars <- runReaderT (traverse interpTemplate envTemplates) mempty 63 | -- Precedence to local env vars; last in list has precedence 64 | return (globalEnvVars ++ M.toList interpolatedLocalEnvVars) 65 | -------------------------------------------------------------------------------- /examples/env.yaml: -------------------------------------------------------------------------------- 1 | DATE: "{{ date +'%B %d, %Y' }}" 2 | TITLE: Basic Templating 3 | -------------------------------------------------------------------------------- /examples/favourites.py: -------------------------------------------------------------------------------- 1 | # You can write scripts in any language you like to level up your templates! 2 | import sys 3 | 4 | print(" and ".join(sys.argv[1:]) + ",") 5 | print("These are a few of my favourite things") 6 | -------------------------------------------------------------------------------- /examples/footer.md: -------------------------------------------------------------------------------- 1 | Copyright 2017 Chris Penner 2 | Check me out on twitter @chrislpenner! 3 | See you next time! 4 | -------------------------------------------------------------------------------- /examples/gitprofile.md: -------------------------------------------------------------------------------- 1 | #!/Users/chris/.local/bin/tempered 2 | Chris's github account has {{ 3 | account_info=`curl https://api.github.com/users/chrispenner` 4 | followers=`json followers << $account_info` 5 | echo $account_info | json followers }} followers! 6 | 7 | -------------------------------------------------------------------------------- /examples/simple.md: -------------------------------------------------------------------------------- 1 | {{ # inside "post.md" }} 2 | # {{ echo $TITLE | tr 'a-z' 'A-Z' }} 3 | by {{ echo $USER }} 4 | Published on {{ echo $DATE }} 5 | 6 | Here's my blog post! 7 | 8 | {{ for i in 99 98 97 ; do 9 | cat < IO EnvMap 24 | getProjectOptions path = do 25 | mProjSettingsFile <- findProjSettings path 26 | mOptions <- traverse optionsFromFilename mProjSettingsFile 27 | return $ fromMaybe mempty mOptions 28 | 29 | -- Retrieve an EnvMap from a yaml file 30 | optionsFromFilename :: FilePath -> IO EnvMap 31 | optionsFromFilename = Y.decodeFileEither >=> 32 | \case 33 | Left err -> die . prettyPrintParseException $ err 34 | Right options -> return options 35 | 36 | -- Try to find an 'env.yaml' file. 37 | findProjSettings :: FilePath -> IO (Maybe FilePath) 38 | findProjSettings fpath = do 39 | absPath <- makeAbsolute fpath 40 | let searchPaths = ( "env.yaml") <$> recurseUp absPath 41 | listToMaybe <$> filterM doesFileExist searchPaths 42 | 43 | -- Get all parent directories of a directory path. 44 | recurseUp :: FilePath -> [FilePath] 45 | recurseUp = unfoldr go 46 | where 47 | go "/" = Nothing 48 | go path = Just (path, takeDirectory path) 49 | -------------------------------------------------------------------------------- /src/Tempered/Parser.hs: -------------------------------------------------------------------------------- 1 | {-# language OverloadedStrings #-} 2 | module Tempered.Parser 3 | ( templateFromFile 4 | , parseTemplate 5 | , handleTemplateError 6 | ) where 7 | 8 | import Tempered.Template 9 | import System.Exit 10 | 11 | import Control.Applicative (liftA2) 12 | 13 | import Text.Parsec 14 | import Text.Parsec.String 15 | 16 | infix 0 ?> 17 | (?>) :: String -> ParsecT s u m a -> ParsecT s u m a 18 | (?>) = flip () 19 | 20 | -- | Parse a template from a file. 21 | templateFromFile :: FilePath -> IO (Either ParseError (Template Command)) 22 | templateFromFile fname = do 23 | file <- readFile fname 24 | return $ parseTemplate fname file 25 | 26 | -- | Parse a template from a string with a given filename for errors. 27 | parseTemplate :: FilePath -> String -> Either ParseError (Template Command) 28 | parseTemplate = runP templateP () 29 | 30 | -- | Fail if parsing errors occurred, otherwise return the template. 31 | handleTemplateError :: Either ParseError (Template a) -> IO (Template a) 32 | handleTemplateError (Left err) = print err >> exitFailure 33 | handleTemplateError (Right temp) = return temp 34 | 35 | -- | Template Parser 36 | templateP :: Parser (Template Command) 37 | templateP = "template" ?> do 38 | optional (try shebangP) 39 | contents <- many (cmd <|> txt) 40 | eof 41 | return $ Template contents 42 | where 43 | cmd = Right <$> commandP 44 | txt = Left <$> many1 (notFollowedBy (string "{{") *> anyChar) 45 | 46 | -- | Shebang Parser 47 | shebangP :: Parser String 48 | shebangP = "shebang" ?> 49 | liftA2 (++) (lookAhead (string "#!") *> string "#!") (manyTill anyChar (char '\n')) 50 | 51 | -- | Command Parser 52 | commandP :: Parser Command 53 | commandP = "command" ?> do 54 | _ <- string "{{" 55 | cmdString <- manyTill anyChar (string "}}") 56 | optional newline 57 | return $ Command cmdString 58 | -------------------------------------------------------------------------------- /src/Tempered/Template.hs: -------------------------------------------------------------------------------- 1 | {-# language OverloadedStrings #-} 2 | {-# language FlexibleContexts #-} 3 | module Tempered.Template 4 | ( Template(..) 5 | , Command(..) 6 | , interpTemplate 7 | ) where 8 | 9 | import System.Process 10 | import Control.Monad.Reader 11 | 12 | import Data.Foldable 13 | 14 | import Tempered.Options 15 | 16 | -- | Represents values interspersed with text. 17 | data Template a = 18 | Template [Either String a] 19 | deriving Show 20 | 21 | -- | Given an execution environment render a template into a string. 22 | interpTemplate :: (MonadReader EnvVars m, MonadIO m) => Template Command -> m String 23 | interpTemplate (Template elems) = fold <$> mapM toText elems 24 | where 25 | toText = either return interpCommand 26 | 27 | -- | Represents a command to be run by the system. 28 | data Command = 29 | Command String 30 | deriving Show 31 | 32 | -- | Run a command in an environment returning the result. 33 | interpCommand :: (MonadReader EnvVars m, MonadIO m) => Command -> m String 34 | interpCommand (Command cmd) = do 35 | envVars <- ask 36 | let process = (shell cmd){env=Just envVars} 37 | liftIO $ readCreateProcess process "" 38 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # http://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # resolver: ghcjs-0.1.0_ghc-7.10.2 15 | # resolver: 16 | # name: custom-snapshot 17 | # location: "./custom-snapshot.yaml" 18 | resolver: lts-8.11 19 | 20 | # User packages to be built. 21 | # Various formats can be used as shown in the example below. 22 | # 23 | # packages: 24 | # - some-directory 25 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 26 | # - location: 27 | # git: https://github.com/commercialhaskell/stack.git 28 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 29 | # - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a 30 | # extra-dep: true 31 | # subdirs: 32 | # - auto-update 33 | # - wai 34 | # 35 | # A package marked 'extra-dep: true' will only be built if demanded by a 36 | # non-dependency (i.e. a user package), and its test suites and benchmarks 37 | # will not be run. This is useful for tweaking upstream packages. 38 | packages: 39 | - '.' 40 | # Dependency packages to be pulled from upstream that are not in the resolver 41 | # (e.g., acme-missiles-0.3) 42 | extra-deps: [] 43 | 44 | # Override default flag values for local packages and extra-deps 45 | flags: {} 46 | 47 | # Extra package databases containing global packages 48 | extra-package-dbs: [] 49 | 50 | # Control whether we use the GHC we find on the path 51 | # system-ghc: true 52 | # 53 | # Require a specific version of stack, using version ranges 54 | # require-stack-version: -any # Default 55 | # require-stack-version: ">=1.2" 56 | # 57 | # Override the architecture used by stack, especially useful on Windows 58 | # arch: i386 59 | # arch: x86_64 60 | # 61 | # Extra directories used by stack for building 62 | # extra-include-dirs: [/path/to/dir] 63 | # extra-lib-dirs: [/path/to/dir] 64 | # 65 | # Allow a newer minor version of GHC than the snapshot specifies 66 | # compiler-check: newer-minor -------------------------------------------------------------------------------- /tempered.cabal: -------------------------------------------------------------------------------- 1 | name: tempered 2 | version: 0.2.0 3 | synopsis: A dead-simple shell interpolation templating utility 4 | -- description: A dead-simple shell interpolation templating utility 5 | homepage: https://github.com/ChrisPenner/tempered#readme 6 | license: BSD3 7 | license-file: LICENSE 8 | author: Chris Penner 9 | maintainer: christopher.penner@gmail.com 10 | copyright: 2017 Chris Penner 11 | category: Templating 12 | build-type: Simple 13 | extra-source-files: README.md 14 | cabal-version: >=1.10 15 | 16 | library 17 | hs-source-dirs: src 18 | exposed-modules: Tempered.Template 19 | , Tempered.Parser 20 | , Tempered.Options 21 | build-depends: base >= 4.9 && < 5 22 | , containers 23 | , directory 24 | , filepath 25 | , mtl 26 | , parsec 27 | , process 28 | , yaml 29 | default-language: Haskell2010 30 | 31 | executable tempered 32 | hs-source-dirs: app 33 | main-is: Main.hs 34 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 35 | build-depends: base >= 4.9 && < 5 36 | , directory 37 | , mtl 38 | , tempered 39 | , containers 40 | , optparse-applicative 41 | -- Required for --version flag 42 | other-modules: Paths_tempered 43 | default-language: Haskell2010 44 | 45 | test-suite tempered-test 46 | type: exitcode-stdio-1.0 47 | hs-source-dirs: test 48 | main-is: Spec.hs 49 | build-depends: base 50 | , tempered 51 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 52 | default-language: Haskell2010 53 | 54 | source-repository head 55 | type: git 56 | location: https://github.com/ChrisPenner/tempered 57 | -------------------------------------------------------------------------------- /test/Spec.hs: -------------------------------------------------------------------------------- 1 | main :: IO () 2 | main = putStrLn "Test suite not yet implemented" 3 | -------------------------------------------------------------------------------- /tools/attach-binary.sh: -------------------------------------------------------------------------------- 1 | set -o errexit -o verbose 2 | 3 | if test ! "$TRAVIS_TAG" 4 | then 5 | echo 'This is not a release build.' 6 | elif test ! "$GITHUB_TOKEN" 7 | then 8 | echo 'The GITHUB_TOKEN environment variable is not set!' 9 | exit 1 10 | else 11 | echo "Attaching binary for $TRAVIS_OS_NAME to $TRAVIS_TAG..." 12 | OWNER="$(echo "$TRAVIS_REPO_SLUG" | cut -f1 -d/)" 13 | REPO="$(echo "$TRAVIS_REPO_SLUG" | cut -f2 -d/)" 14 | BIN="$(stack path --local-install-root)/bin/$REPO" 15 | BUNDLE_NAME="$REPO-$TRAVIS_TAG-$TRAVIS_OS_NAME.tar.gz" 16 | cp "$BIN" "./$REPO" 17 | chmod +x "./$REPO" 18 | tar -czf "$BUNDLE_NAME" "$REPO" 19 | echo "SHA256:" 20 | shasum -a 256 "$BUNDLE_NAME" 21 | ghr -t "$GITHUB_TOKEN" -u "$OWNER" -r "$REPO" --replace "$(git describe --tags)" "$BUNDLE_NAME" 22 | fi 23 | -------------------------------------------------------------------------------- /tools/install-ghr.sh: -------------------------------------------------------------------------------- 1 | set -o errexit -o verbose 2 | 3 | if test ! "$TRAVIS_TAG" 4 | then 5 | echo 'This is not a release build.' 6 | else 7 | if [ "$TRAVIS_OS_NAME" = "linux" ] 8 | then 9 | ARCH="linux" 10 | else 11 | ARCH="darwin" 12 | fi 13 | echo "Installing ghr" 14 | URL="https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_${ARCH}_386.zip" 15 | curl -L ${URL} > ghr.zip 16 | mkdir -p "$HOME/bin" 17 | export PATH="$HOME/bin:$PATH" 18 | unzip ghr.zip -d "$HOME/bin" 19 | rm ghr.zip 20 | fi 21 | -------------------------------------------------------------------------------- /tools/install-stack.sh: -------------------------------------------------------------------------------- 1 | set -o errexit -o verbose 2 | 3 | if test -f "$HOME/.local/bin/stack" 4 | then 5 | echo 'Stack is already installed.' 6 | else 7 | echo "Installing Stack for $TRAVIS_OS_NAME..." 8 | URL="https://www.stackage.org/stack/$TRAVIS_OS_NAME-x86_64" 9 | curl --location "$URL" > stack.tar.gz 10 | gunzip stack.tar.gz 11 | tar -x -f stack.tar --strip-components 1 12 | mkdir -p "$HOME/.local/bin" 13 | mv stack "$HOME/.local/bin/" 14 | rm stack.tar 15 | fi 16 | 17 | stack --version 18 | --------------------------------------------------------------------------------