hwk 0.2.0 → 0.3
raw patch · 7 files changed
+174/−113 lines, 7 filesdep −processdep ~directory
Dependencies removed: process
Dependency ranges changed: directory
Files
- ChangeLog.md +5/−0
- Config.hs +56/−0
- Main.hs +64/−32
- README.md +34/−22
- data/Hwk.hs +0/−39
- hwk.cabal +8/−6
- install.sh +7/−14
ChangeLog.md view
@@ -1,5 +1,10 @@ # Version history for hwk +## 0.2.1 (2020-10-11)+- add --line mode: takes a function on a String - applied to every line+ - uses new ToString class (not ToList)+- add --type-check: prints the type of a given function+ ## 0.2 (2020-10-10) - first release by Jens Petersen - uses hint library and Hwk configuration module
+ Config.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE FlexibleInstances #-}++module Hwk where++import Data.List (intercalate)++-- | modules to be imported into hint+-- these modules must be from installed packages+userModules :: [String]+userModules = [ "Prelude", "Data.List", "Data.Char", "Data.Bool"+ -- , "Data.List.Extra", "Data.Tuple.Extra"+ , "System.FilePath"+ ]++-- you can put user hwk functions here:+++int :: String -> Int+int str = read str :: Int++ints :: [String] -> [Int]+ints = map int++------ below here all required internal machinery ------++-- | toString and toList are required by hwk+-- They determine the types of functions hwk can interpret,+-- allowing some simple polymorphism.++-- ToString allows handling functions of type: ToString a => String -> a+class ToString a where+ toString :: a -> String+instance ToString String where+ toString x = x+instance ToString [String] where+ toString = unwords+instance ToString [[String]] where+ toString = intercalate "\t" . map unwords+instance ToString Int where+ toString x = show x+instance ToString [Int] where+ toString lst = unwords $ map show lst++-- ToList allows handling functions of type: ToList a => [String] -> a+class ToList a where+ toList :: a -> [String]+instance ToList String where+ toList x = [x]+instance ToList [String] where+ toList = id+instance ToList [[String]] where+ toList = map (intercalate "\t")+instance ToList Int where+ toList x = [show x]+instance ToList [Int] where+ toList lst = map show lst
Main.hs view
@@ -1,31 +1,55 @@-import qualified Data.List as L+{-# LANGUAGE CPP #-}++#if !MIN_VERSION_simple_cmd_args(0,1,3)+import Control.Applicative ((<|>))+#endif+import Control.Monad.Extra+import qualified Data.List.Extra as L+import Data.Version (showVersion) import Language.Haskell.Interpreter import SimpleCmdArgs import System.Directory+import System.FilePath+import System.IO (hPutStrLn, stderr) import Paths_hwk (getDataDir, version) +data HwkMode = DefaultMode | WholeMode | LineMode | TypeMode+ deriving Eq+ main :: IO () main = simpleCmdArgs (Just version) "A Haskell awk/sed like tool" "Simple shell text processing with Haskell" $- runExpr <$> switchWith 'a' "all" "All input as a single string" <*> strArg "FUNCTION" {-<*> many (strArg "FILE...")-}+ runExpr <$> modeOpt <*> strArg "FUNCTION" {-<*> many (strArg "FILE...")-}+ where+ modeOpt :: Parser HwkMode+ modeOpt =+ flagWith' TypeMode 't' "type-check" "Print out the type of the given function" <|>+ flagWith' WholeMode 'a' "all" "Apply function once to the whole input" <|>+ flagWith DefaultMode LineMode 'l' "line" "Apply function to each line" -runExpr :: Bool -> String -> {-[FilePath] ->-} IO ()-runExpr allinput stmt {-files-} = do+runExpr :: HwkMode -> String -> {-[FilePath] ->-} IO ()+runExpr mode stmt {-files-} = do --mapM_ checkFileExists files input <- getContents- usercfg <- getXdgDirectory XdgConfig "hwk"+ userdir <- getXdgDirectory XdgConfig "hwk"+ let usercfg = userdir </> "Hwk.hs" datadir <- getDataDir--- copyFile (datadir </> "hwk.hs") usercfg- r <- runInterpreter (runHint [usercfg, datadir] input)+ let versionCfg = usercfg ++ "-" ++ showVersion version+ unlessM (doesFileExist versionCfg) $+ copyFile (datadir </> "Config.hs") versionCfg+ unlessM (doesFileExist usercfg) $ do+ copyFile (datadir </> "Config.hs") usercfg+ warn $ usercfg ++ " created"+ r <- runInterpreter (runHint userdir input) case r of Left err -> putStrLn $ errorString err Right () -> return () where- runHint :: [FilePath] -> String -> Interpreter ()- runHint cfgdirs input = do- set [searchPath := cfgdirs]+ runHint :: FilePath -> String -> Interpreter ()+ runHint cfgdir input = do+ set [searchPath := [cfgdir]] loadModules ["Hwk"] setHwkImports ["Prelude"] imports <- do@@ -34,25 +58,32 @@ then interpret "userModules" infer else return ["Prelude", "Data.List"] setHwkImports imports- -- -- Not sure which is better: typechecking manually or polymorph string- -- etypchk <- typeChecksWithDetails stmt- -- case etypchk of- -- Left err -> error' err- -- Right typ ->- -- case typ of- poly <- do- havePoly <- typeChecks "polymorph"- if havePoly then do- s <- interpret "polymorph" infer- return $ if null s then "" else s ++ " . "- else return ""- if allinput- then do- fn <- interpret (poly ++ stmt) (as :: String -> [String])- liftIO $ mapM_ putStrLn (fn input)+ if mode == TypeMode then do+#if MIN_VERSION_hint(0,8,0)+ etypchk <- typeChecksWithDetails stmt+ case etypchk of+ Left err -> liftIO $ mapM_ (putStrLn . errMsg) err+ Right typ -> liftIO $ putStrLn (cleanupType typ)+#else+ typeOf stmt >>= liftIO . putStrLn . cleanupType+#endif else do- fn <- interpret (poly ++ stmt) (as :: [String] -> [String])- liftIO $ mapM_ putStrLn (fn (lines input))+ let polyList = "toList . "+ polyString = "toString . "+ case mode of+ DefaultMode -> do+ fn <- interpret (polyList ++ stmt) (as :: [String] -> [String])+ liftIO $ mapM_ putStrLn (fn (lines input))+ LineMode -> do+ fn <- interpret (polyString ++ stmt) (as :: String -> String)+ liftIO $ mapM_ (putStrLn . fn) (lines input)+ WholeMode -> do+ fn <- interpret (polyList ++ stmt) (as :: String -> [String])+ liftIO $ mapM_ putStrLn (fn input)+ TypeMode -> error "already handled earlier"+ where+ cleanupType :: String -> String+ cleanupType = L.replace "[Char]" "String" -- FIXME use Set setHwkImports ms = setImports (L.nub (ms ++ ["Hwk"]))@@ -63,8 +94,9 @@ -- error' $ "file not found: " ++ file errorString :: InterpreterError -> String- errorString (WontCompile es) = L.intercalate "\n" (header : map unbox es)- where- header = "ERROR: Won't compile:"- unbox (GhcError e) = e+ errorString (WontCompile es) =+ unlines $ "ERROR: Won't compile:" : map errMsg es errorString e = show e++warn :: String -> IO ()+warn = hPutStrLn stderr
README.md view
@@ -5,19 +5,19 @@ <img align="right" alt="hwk" src="hwk.png" /> -**hwk** tries to demonstrate how a modern Haskell based stream manipulation tool could look like.-It is similar to tools like **awk** or **sed**.-`hwk` allows compact function sequences that operate on a list of strings. Because Haskell is lazy and has a powerful arsenal of functions, there is no need to invent another DSL and hopefully it encourages more people to think functionally.+**hwk** (pronounced "hawk") is a simple Haskell-based text stream manipulation tool, somewhat similar to tools like **awk** or **sed**.+`hwk` applies concisely composed pure functions to a list of strings from stdin. Because Haskell is lazy and has a powerful arsenal of functions, there is no need to invent another DSL. Hopefully this tool will also encourage more people to think functionally. ## Example -Prepend a string to each line:+Change and append a string to each line: ```bash-$ seq 1 3 | hwk 'map (++ ".txt")'+$ seq 0 2 | hwk --line '(++ ".txt") . show . (+1) . int' 1.txt 2.txt 3.txt ```+or without line-mode: `hwk 'map ((++ ".txt") . show . (+1) . int)'`. Sum all negative numbers: ```bash@@ -26,27 +26,44 @@ ``` The ints function transforms a list of strings into a list of ints +Factorials in your shell scripts!:+```bash+seq 10 12 | hwk --line 'let {fact 0 = 1; fact n = n * fact (n - 1)} in fact . int'+3628800+39916800+479001600+```+ Extract data from a file: ```bash-$ cat /etc/passwd | hwk 'take 3 . map (filter (/= "x") . take 3 . splitOn ":")'-root 0-bin 1-daemon 2+$ cat /etc/passwd | hwk --line 'reverse . filter (/= "x") . take 3 . splitOn ":"' | head -3+0 root+1 bin+2 daemon ``` (a module defining `splitOn` from the extra or split library needs to be added to the Hwk.hs config file). The argument passed to `hwk` must be a valid Haskell function: a function that takes a list of strings and returns a new list or a single value. -## Configuration-It uses a configuration module `Hwk` which provides the context for the hint evaluation of the supplied function.+Check where input contains a certain string:+```+$ cat /etc/passwd | hwk --all 'bool "no" "yes" . isInfixOf "1000"'+yes+``` -It searches for `Hwk.hs` in `~/.config/hwk`, then the package's installed data directory.+## Configuration+`hwk` uses a Haskell configuration file `~/.config/hwk/Hwk.hs` which provides the context for the hint evaluation of the supplied function. The default configuration [Hwk module](data/Hwk.hs) just sets-the `Prelude`, `Data.List`, and `Data.Char` modules to be imported by default into the hint interpreter.+the `Prelude`, `Data.List`, and `Data.Char` modules to be imported into the hint interpreter. -If you want to use other modules or define your own functions, you can copy the installed `Hwk.hs` or source `data/Hwk.hs` file to `~/.config/hwk/` to configure hwk.+The first time hwk is run it sets up `~/.config/hwk/Hwk.hs`. +You can add other modules to import or define your own functions in+`~/.config/hwk/Hwk.hs`.++After a hwk version update you may wish/have to update up your Hwk.hs file to take account of new changes: a copy of the latest default Hwk.hs is also put in `~/.config/hwk/` with version suffix.+ ## Install Either use the `install.sh` script, or install by cabal-install or stack as described below:@@ -57,25 +74,20 @@ Then go to the source directory and run the `install.sh` script, which - first runs `stack install`-- then moves the binary installed by `stack install` to `~/.local/bin/hwk-bin`, and sets up a wrapper script `~/.local/bin/hwk` which runs it.-- and also copies the Hwk.hs configuration module to `~/.config/hwk/Hwk.hs` (backing up any existing file).+- then moves the binary installed by `stack install` to `~/.local/lib/hwk`, and sets up a wrapper script `~/.local/bin/hwk` which runs it. -You may wish to change the resolver in stack.yaml first, which is also use to determine the resolver used by the created `hwk` wrapper script.+You may wish to change the resolver in stack.yaml first: it is also used to determine the resolver used by the created `hwk` wrapper script. ### cabal If you are on a Linux distro with a system installed ghc and Haskell libaries, you can install with `cabal install` to make use of them. -If you install with a recent cabal the Hwk.hs config module probably lives somewhere like `~/.cabal/store/ghc-*/hwk-*/share/data/Hwk.hs`, or you can copy it from the source `data/Hwk.hs`.- ### stack Installing by stack is better if you do not have a system ghc and/or global system Haskell libraries installed. Alternatively to install by hand: run `stack install`,-and then run it with `stack exec hwk ...` using the same resolver,-To customize hwk after a stack install it is probably easier just to copy-the `data/Hwk.hs` source file.+and then run it with `stack exec hwk ...` using the same resolver. ## How does `hwk` work?
− data/Hwk.hs
@@ -1,39 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}--module Hwk where--import Data.List (intercalate)---- | modules to be imported into hint--- these modules must be globally installed-userModules :: [String]-userModules = ["Prelude", "Data.List", "Data.Char"]---- this string is used by hwk--- | Determines the types of functions hwk can interpret--- "toList" allows some simple polymorphism--- use "id" or "" to allow only functions of type: [String] -> [String]-polymorph :: String-polymorph = "toList"---- ToList allows handling functions of type: ToList a => [String] -> a-class ToList a where- toList :: a -> [String]-instance ToList String where- toList x = [x]-instance ToList [String] where- toList = id-instance ToList [[String]] where- toList = map (intercalate "\t")-instance ToList Int where- toList x = [show x]-instance ToList [Int] where- toList lst = map show lst---- below here all user defined --------int :: String -> Int-int str = read str :: Int--ints :: [String] -> [Int]-ints = map int
hwk.cabal view
@@ -1,7 +1,10 @@ name: hwk-version: 0.2.0-synopsis: A modern Haskell based AWK replacement-description: A simple Haskell-based replacement for awk/sed.+version: 0.3+synopsis: Simple Haskell-based awk-like tool+description:+ A simple Haskell-based alternative to awk/sed.+ It uses Hint to apply the function given on the commandline+ to standard input and outputs the result. homepage: https://github.com/juhp/hwk license: MIT license-file: LICENSE@@ -11,7 +14,7 @@ 2020 Jens Petersen category: Development build-type: Simple-data-files: data/Hwk.hs+data-files: Config.hs extra-source-files: install.sh extra-doc-files: README.md ChangeLog.md@@ -26,11 +29,10 @@ other-modules: Paths_hwk autogen-modules: Paths_hwk build-depends: base <5,- directory,+ directory >= 1.2.3.0, extra, filepath, hint,- process, simple-cmd-args >= 0.1.2 default-language: Haskell2010 ghc-options: -Wall
install.sh view
@@ -4,24 +4,17 @@ stack install -RESOLVER=$(grep -i "resolver:" stack.yaml | sed -e "s/.*: *//")--CFGDIR=$HOME/.config/hwk-mkdir -p $CFGDIR+BINHWK=$HOME/.local/bin/hwk+LIBHWK=$HOME/.local/lib/hwk -if [ -f "$CFGDIR/Hwk.hs" ]; then- CFGHASH=$(cd $CFGDIR; md5sum Hwk.hs)-fi-if [ "$CFGHASH" != "$(cd data; md5sum Hwk.hs)" ]; then-cp -p -v --backup=numbered data/Hwk.hs $CFGDIR/-fi+mv -f $BINHWK $LIBHWK -mv -f ~/.local/bin/hwk ~/.local/bin/hwk-bin+RESOLVER=$(grep -i "resolver:" stack.yaml | sed -e "s/.*: *//") -cat > ~/.local/bin/hwk <<EOF+cat > $BINHWK <<EOF #!/bin/sh -stack --resolver $RESOLVER exec ~/.local/bin/hwk-bin "\$@"+stack --resolver $RESOLVER exec -- $LIBHWK "\$@" EOF -chmod u+x ~/.local/bin/hwk+chmod u+x $BINHWK