#+Title: Shell scripts in Haskell & turtle? #+Author: Yann Esposito #+Date: <2018-11-07 Wed> * Script shell en Haskell: Pourquoi? *** Description #+BEGIN_QUOTE turtle is a reimplementation of the Unix command line environment in Haskell so that you can use Haskell as both a shell and a scripting language. Features include: * Batteries included: Command an extended suite of predefined utilities * Interoperability: You can still run external shell commands * Portability: Works on Windows, OS X, and Linux * Exception safety: Safely acquire and release resources * Streaming: Transform or fold command output in constant space * Patterns: Use typed regular expressions that can parse structured values * Formatting: Type-safe printf-style text formatting * Modern: Supports text and system-filepath #+END_QUOTE *** Pourquoi? - Facile à maintenir, à refactorer (fp + types) - programme type checké et interprété rapidement (<1s) - syntaxe "légère", distinction string, variables, etc.. - scripts shell sont seulement interprétés et lents; scripts Haskell peuvent être interpretés & lent ou compilés & rapide Parfait pour remplacer de *gros* (> 100 lignes) et *lents* scripts bash. *** Shell: /Good/ 1. commandes internes 2. commandes externes 3. composition: - redirection de la sortie standard - pipe - async *** Shell: *BAD* 1. variables (syntaxe $FOO, $foo, etc...) 2. gestion des string (for i in *; do ...; done) (files with ' ' or "\n", etc...) 3. modularité du code, fonctions 4. Vitesse d'exécution ** Haskell Turtle *** Commandes Internes =Turtle.Prelude= - =cd=, =ls=, =find=, =pwd=, =chmod=, etc... - =sleep=, =echo=, =err=, =arguments=, =realpath= - =mv=, =mkdir=, =mktree= (=mkdir -p=) - =cp=, =cptree=, =symlink= - =date=, =datefile= - =which=, =whichAll= (tous les PATHs) *** commandes externes - beaucoups de variantes plus ou moins "safes", "génériques" #+BEGIN_SRC haskell inshell :: Text -- ^ Command Line -> Shell Line -- ^ Lines of stdin -> Shell Line -- ^ Lines of stdout #+END_SRC - variantes avec separation des arguments =shell= vs =proc= - variantes strictes (stream) - avec sans, stderr - avec sans gestion des valeurs de retour *** commandes externes (2) La forme la plus générique: #+BEGIN_SRC haskell procStrictWithErr :: MonadIO io => Text -- ^ Command -> [Text] -- ^ Arguments -> Shell Line -- ^ Lines of standard input -> io (ExitCode, Text, Text) -- ^ (Exit code, stdout, stderr) #+END_SRC Run a command using execvp, retrieving the exit code, stdout, and stderr as a non-lazy blob of Text *** composition: redirections #+BEGIN_SRC bash cmd < input.txt > output.txt #+END_SRC Notion de stream de valeurs, generalement: =Shell Line= #+BEGIN_SRC haskell -- wc -l output.txt inshell "wc -l" (input "input.txt") (output "output.txt") #+END_SRC *** composition: pipes Bash: #+BEGIN_SRC bash ls | grep foo #+END_SRC Turtle: #+BEGIN_SRC haskell ls & grep "foo" #+END_SRC *** composition: async Bash: #+BEGIN_SRC bash sleep 1; echo "foo" & #+END_SRC Turtle: #+BEGIN_SRC haskell fork (do {shell "sleep 1" empty; echo "foo"}) #+END_SRC *** composition: async #+BEGIN_SRC haskell >>> :set +m >>> runManaged $ do fork $ do echo "start" sleep 1 echo "foo" sleep 2 echo "done" echo "waiting a bit" sleep 2 echo "stop waiting" waitsitnagr ta bit foo stop waiting #+END_SRC *** Securité des resources =Managed= - take a resource and think to free it: - =readonly= - =writeonly= - =appendonly= - =mktemp= create a temporary file, handle race condition - =fork=, =wait= - =pushd=; =view (pushd "/tmp" >> "pwd")= (=> =FilePath "/tmp"=) =pwd= (=FilePath "/"=) *** Fold =Foldl= pour faire des accumulations efficacement (plus vite qu'en C) et en un seul parcours. #+BEGIN_SRC haskell >>> fold (ls "/tmp") ((,,) <$> Fold.length <*> Fold.head <*> Fold.last) (21,Just (FilePath "/tmp/aaa"),Just (FilePath "/tmp/zzz")) #+END_SRC ** Gestion des arguments *** Example (1/2) =options :: MonadIO io => Description -> Parser a -> io a= #+BEGIN_SRC haskell parser :: Parser (Text, Int) parser = (,) <$> optText "name" 'n' "Your first name" <*> optInt "age" 'a' "Your current age" main = do (name, age) <- options "Greeting script" parser echo (repr (format ("Hello there, "%s) name)) echo (repr (format ("You are "%d%" years old") age)) #+END_SRC *** Example (2/2) #+BEGIN_SRC > ./options --name John --age 42 Hello there, John You are 42 years old #+END_SRC #+BEGIN_SRC > ./options --help Greeting script Usage: options (-n|--name NAME) (-a|--age AGE) Available options: -h,--help Show this help text --name NAME Your first name --age AGE Your current age #+END_SRC *** Real World™ Complex Example (1/4) #+BEGIN_SRC haskell gpm :: IO () gpm = do subcmd <- options "Git Project Manager" parser case subcmd of Init -> Init.init NewIssue issueOpt -> inGPM (Issue.handleNewIssue issueOpt) Review reviewCmd -> inGPM (Review.handleReview reviewCmd) Serve serveCmd -> Serve.handleServe serveCmd Hooks hooksCmd -> inGPM (Hooks.handleHooks hooksCmd) data Command = Init | NewIssue Issue.IssueOptions | Review Review.ReviewCommand | Serve Serve.ServeCommand | Hooks Hooks.HooksCommand parser :: Parser Command parser = subcommand "init" "Initialize gpm" (pure Init) <|> NewIssue <$> subcommand "new-issue" "Create a new Issue" Issue.parseIssueOptions <|> Review <$> subcommand "review" "Review (use current branch by default)" Review.parseReviewCmd <|> Serve <$> subcommand "serve" "Serve the git to the web" Serve.parseServeCommand <|> Hooks <$> subcommand "hooks" "Handle hooks for this git repository" Hooks.parseHooksCommand #+END_SRC *** Real World™ Complex Example (2/4) #+BEGIN_SRC bash > gpm Git Project Manager Usage: gpm (init | new-issue | review | serve | hooks) Available options: -h,--help Show this help text Available commands: init Initialize gpm new-issue Create a new Issue review Review (use current branch by default) serve Serve the git to the web hooks Handle hooks for this git repository #+END_SRC *** Real World™ Complex Example (3/4) #+BEGIN_SRC haskell data IssueOptions = IssueOptions { interactive :: Bool , newIssue :: NewIssue } parseIssueOptions :: Parser IssueOptions parseIssueOptions = IssueOptions <$> switch "interactive" 'i' "Interactive mode" <*> parseNewIssue parseNewIssue :: Parser NewIssue parseNewIssue = do isPriority <- optional $ optText "priority" 'p' "Priority A,B,C" isStatus <- optional $ optText "status" 's' "The status of the issue (TODO, QUESTION, ...)" isTitle <- optional $ optText "title" 't' "The status title" isUser <- optional $ optText "creator" 'c' "The user that created the issue" isBranch <- optional $ optText "branch" 'b' "The branch related to the issue" isTags <- optional $ optText "tags" 'g' "comma separated tags" isAssignee <- optional $ optText "assignee" 'a' "Assignee" isReviewers <- optional $ optText "reviewers" 'r' "comma separated reviewers" isDescription <- optional $ optText "descr" 'd' "Long issue description" pure NewIssue { priority = maybe PriorityB toPriority isPriority , status = fromMaybe "TODO" isStatus , title = fromMaybe "Issue Title" isTitle , user = isUser , branch = isBranch , tags = maybe [] (T.splitOn ",") isTags , assignee = isAssignee , reviewers = maybe [] (T.splitOn ",") isReviewers , description = isDescription } #+END_SRC *** Real World™ Complex Example (4/4) #+BEGIN_SRC text > gpm new-issue --help Usage: gpm new-issue [-i|--interactive] [-p|--priority PRIORITY] [-s|--status STATUS] [-t|--title TITLE] [-c|--creator CREATOR] [-b|--branch BRANCH] [-g|--tags TAGS] [-a|--assignee ASSIGNEE] [-r|--reviewers REVIEWERS] [-d|--descr DESCR] Create a new Issue Available options: -i,--interactive Interactive mode -p,--priority PRIORITY Priority A,B,C -s,--status STATUS The status of the issue (TODO, QUESTION, ...) -t,--title TITLE The status title -c,--creator CREATOR The user that created the issue -b,--branch BRANCH The branch related to the issue -g,--tags TAGS comma separated tags -a,--assignee ASSIGNEE Assignee -r,--reviewers REVIEWERS comma separated reviewers -d,--descr DESCR Long issue description -h,--help Show this help text #+END_SRC ** Regex? NO; Parsers! *** match #+BEGIN_SRC haskell >>> match ("can" <|> "cat") "cat" ["cat"] #+END_SRC Parser: #+BEGIN_SRC haskell ("can" <|> "cat") #+END_SRC *** Parser composition =Pattern a=: Polymorphique =a= - Functor - Applicative (=*>=) - Monad - Alternative (=<|>=) - MonadPlus - Monoid a => Monoid (Pattern a) - Monoid a => SemiGroup (Pattern a) - Monoid a => Num (Pattern a); =+= (<=> =<|>=), =*= (<=> =<>=) *** sed: Pattern Text #+BEGIN_SRC haskell sed :: Pattern Text -> Shell Line -> Shell Line #+END_SRC #+BEGIN_SRC haskell >>> stdout (input "file.txt") Test ABC 42 >>> -- sed 's/C/D/g' file.txt >>> stdout (sed ("C" *> return "D") (input "file.txt")) Test ABD 42 #+END_SRC *** sed: Apply fn on matches #+BEGIN_SRC haskell >>> -- sed 's/[[:digit:]]/!/g' file.txt >>> stdout (sed (digit *> return "!") (input "file.txt")) Test ABC !! >>> import qualified Data.Text as Text >>> -- rev file.txt >>> stdout (sed (fmap Text.reverse (plus dot)) (input "file.txt")) tseT CBA 24 #+END_SRC *** Helpers - =inplace=: =perl -pi -e '...' file= - =find :: Pattern a -> FilePath -> Shell FilePath= - =findtree :: Pattern a -> Shell FilePath -> Shell FilePath= (ie =ls | grep=) ** Conclusion /SAFE/ - /FAST/ - /AWESOME/ -> /CONFIDENCE/ * /Batteries included/: Command an extended suite of predefined utilities * /Interoperability/: You can still run external shell commands * /Portability/: Works on Windows, OS X, and Linux * /Exception safety/: Safely acquire and release resources * /Streaming/: Transform or fold command output in constant space * /Patterns/: Use typed regular expressions that can parse structured values * /Formatting/: Type-safe printf-style text formatting * /Modern/: Supports text and system-filepath