11 KiB
11 KiB
Shell scripts in Haskell & turtle?
- Script shell en Haskell: Pourquoi?
Script shell en Haskell: Pourquoi?
Description
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
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
- commandes internes
- commandes externes
-
composition:
- redirection de la sortie standard
- pipe
- async
Shell: BAD
- variables (syntaxe $FOO, $foo, etc…)
- gestion des string (for i in *; do …; done) (files with ' ' or "\n", etc…)
- modularité du code, fonctions
- 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"
inshell :: Text -- ^ Command Line
-> Shell Line -- ^ Lines of stdin
-> Shell Line -- ^ Lines of stdout
- variantes avec separation des arguments
shell
vsproc
- variantes strictes (stream)
- avec sans, stderr
- avec sans gestion des valeurs de retour
commandes externes (2)
La forme la plus générique:
procStrictWithErr :: MonadIO io
=> Text -- ^ Command
-> [Text] -- ^ Arguments
-> Shell Line -- ^ Lines of standard input
-> io (ExitCode, Text, Text) -- ^ (Exit code, stdout, stderr)
Run a command using execvp, retrieving the exit code, stdout, and stderr as a non-lazy blob of Text
composition: redirections
cmd < input.txt > output.txt
Notion de stream de valeurs, generalement: Shell Line
-- wc -l <input.txt >output.txt
inshell "wc -l" (input "input.txt") (output "output.txt")
composition: pipes
Bash:
ls | grep foo
Turtle:
ls & grep "foo"
composition: async
Bash:
sleep 1; echo "foo" &
Turtle:
fork (do {shell "sleep 1" empty; echo "foo"})
composition: async
>>> :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
Securité des resources Managed
-
take a resource and think to free it:
readonly
writeonly
appendonly
mktemp
create a temporary file, handle race conditionfork
,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.
>>> fold (ls "/tmp") ((,,) <$> Fold.length <*> Fold.head <*> Fold.last)
(21,Just (FilePath "/tmp/aaa"),Just (FilePath "/tmp/zzz"))
Gestion des arguments
Example (1/2)
options :: MonadIO io => Description -> Parser a -> io a
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))
Example (2/2)
> ./options --name John --age 42
Hello there, John
You are 42 years old
> ./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
Real World™ Complex Example (1/4)
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
Real World™ Complex Example (2/4)
> 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
Real World™ Complex Example (3/4)
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
}
Real World™ Complex Example (4/4)
> 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
Regex? NO; Parsers!
match
>>> match ("can" <|> "cat") "cat"
["cat"]
Parser:
("can" <|> "cat")
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
sed :: Pattern Text -> Shell Line -> Shell Line
>>> 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
sed: Apply fn on matches
>>> -- 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
Helpers
inplace
:perl -pi -e '...' file
find :: Pattern a -> FilePath -> Shell FilePath
findtree :: Pattern a -> Shell FilePath -> Shell FilePath
(iels | 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