1
0
mirror of https://github.com/stedolan/jq.git synced 2024-05-11 05:55:39 +00:00

Ancient Haskell version of jq. Might be useful someday. Maybe.

This commit is contained in:
Stephen Dolan
2012-09-18 17:29:56 +01:00
parent eca89acee0
commit 3622810ea7
7 changed files with 269 additions and 46 deletions

213
JQ.hs
View File

@@ -2,55 +2,93 @@ module JQ where
import Text.JSON import Text.JSON
import Text.JSON.String import Text.JSON.String
import Data.Maybe import Data.Maybe
import Data.List (sortBy,sort,groupBy) import Data.Char
import Data.List (sortBy,sort,groupBy,partition,intercalate)
import Data.Function (on) import Data.Function (on)
import Data.Ord (comparing) import Data.Ord (comparing)
import Control.Monad import Control.Monad
import Control.Monad.Writer import Control.Monad.Writer
import Control.Monad.List import Control.Monad.List
import Control.Monad.Reader import Control.Monad.Reader
import qualified Data.Map as M
import qualified Data.Set as S
import Data.Map ((!))
import Debug.Trace
type Path = [Either Int String] type Path = [Either Int String]
type Program = JSValue -> [(JSValue, Path)] type Program = JSValue -> [(JSValue, Path)]
type JQ = ReaderT JSValue (WriterT Path []) type Filter = JQ JSValue
newtype Operator = Operator {runOperator:: [Filter] -> Filter}
type JQ = ReaderT (JSValue, M.Map String JSValue, M.Map (String, Int) Operator) (WriterT Path [])
runJQ :: JQ a -> JSValue -> [a] runJQ :: JQ a -> JSValue -> [a]
runJQ prog val = map fst $ runWriterT $ runReaderT prog val runJQ prog val = map fst $ runWriterT $ runReaderT prog (val,M.empty,primitives)
(>|) :: JQ JSValue -> JQ a -> JQ a (>|) :: JQ JSValue -> JQ a -> JQ a
a >| b = do a >| b = do
val <- a val <- a
local (const val) b local (\(v,s,d) -> (val,s,d)) b
setvar name val prog =
local (\(v,s,d) -> (v, M.insert name val s, d)) prog
getvar name = liftM (! name) $ asks (\(v,s,d) -> s)
input = asks (\(v,s,d) -> v)
collect :: JQ a -> JQ [a] collect :: JQ a -> JQ [a]
collect prog = do collect prog = liftM (map fst) $ collectPaths prog
arg <- ask
return $ runJQ prog arg
collectPaths :: JQ a -> JQ [(a,Path)] collectPaths :: JQ a -> JQ [(a,Path)]
collectPaths prog = do collectPaths prog = do
arg <- ask rd <- ask
return $ runWriterT $ runReaderT prog arg return $ runWriterT $ runReaderT prog rd
insert :: JSValue -> (JSValue, Path) -> JSValue withDefn :: (String, [String], Filter) -> Filter -> Filter
insert base (replace, []) = replace withDefn (name, formals, body) subexp =
insert (JSArray values) (replace, ((Left n):rest)) = JSArray values' local (\(v,s,d) -> (v,s,M.insert (name,length formals) (Operator func) d)) subexp
where where
(left, (_:right)) = splitAt n values func args = local (\(v,s,d) -> (v,M.empty,M.fromList (zip (zip formals (repeat 0)) (map (Operator . const) args)) `M.union` d)) body
values' = left ++ [replace] ++ right
insert (JSObject obj) (replace, ((Right k):rest))= JSObject $ toJSObject obj' subexp :: JQ a -> JQ a
where subexp = censor $ const []
withoutK = filter ((/= k) . fst) $ fromJSObject obj
obj' = (k, replace):withoutK
yieldPaths :: [(a,Path)] -> JQ a
yieldPaths ps = ReaderT $ const $ WriterT ps
insert :: JQ JSValue -> JSValue -> Path -> JQ JSValue
insert replace base [] = replace
insert replace (JSArray values) ((Left n):rest) = do
let array = take (max (n+1) (length values)) (values ++ repeat JSNull)
replacement <- insert replace (array !! n) rest
let (left, (_:right)) = splitAt n array
return $ JSArray $ left ++ [replacement] ++ right
insert replace (JSObject obj) ((Right k):rest) = do
let oldval = maybe JSNull id (lookup k $ fromJSObject obj)
replacement <- insert replace oldval rest
let withoutK = filter ((/= k) . fst) $ fromJSObject obj
return $ JSObject $ toJSObject $ (k, replacement):withoutK
insert replace JSNull p@((Right k):rest) = insert replace (JSObject $ toJSObject []) p
insert replace JSNull p@((Left n):rest) = insert replace (JSArray []) p
insert _ base p = error $ "Cannot insert into " ++ intercalate ", " (map (either show show) p) ++ " of " ++ encode base
eqj a b = JSBool $ a == b eqj a b = JSBool $ a == b
boolj (JSBool false) = False
boolj (JSNull) = False
boolj _ = True
andj a b = JSBool $ boolj a && boolj b
orj a b = JSBool $ boolj a || boolj b
liftp :: (JSValue -> JSValue) -> JQ JSValue liftp :: (JSValue -> JSValue) -> JQ JSValue
liftp f = liftM f ask liftp f = liftM f input
idp = undefined idp = undefined
failp t = [] failp t = []
@@ -68,7 +106,7 @@ anyj values = any isTrue values
selectp prog = do selectp prog = do
match <- collect prog match <- collect prog
guard $ anyj match guard $ anyj match
ask input
constStr :: String -> JQ JSValue constStr :: String -> JQ JSValue
constStr = return . JSString . toJSString constStr = return . JSString . toJSString
@@ -76,9 +114,13 @@ constStr = return . JSString . toJSString
constInt :: Int -> JQ JSValue constInt :: Int -> JQ JSValue
constInt = return . JSRational False . toRational constInt = return . JSRational False . toRational
updatep p = do tr x = trace (show x) x
t <- ask
liftM (foldl insert t) $ collectPaths p
assignp sel replace = do
paths <- collectPaths sel
t <- input
foldM (\base (val,path) -> insert (return val >| replace) base path) t paths
arrayp prog = liftM JSArray $ collect prog arrayp prog = liftM JSArray $ collect prog
@@ -88,15 +130,15 @@ childp' (JSArray values) = msum [tell [Left i] >> return v | (v,i) <- zip values
childp' (JSObject obj) = msum [tell [Right k] >> return v | (k,v) <- fromJSObject obj] childp' (JSObject obj) = msum [tell [Right k] >> return v | (k,v) <- fromJSObject obj]
childp' _ = mzero childp' _ = mzero
childp = ask >>= childp' childp = input >>= childp'
--findp :: Program -> Program --findp :: Program -> Program
findp prog = do findp prog = do
found <- collect prog found <- collect prog
if anyj found then ask else childp >| findp prog if anyj found then input else childp >| findp prog
groupp prog = do groupp prog = do
list <- ask list <- input
case list of case list of
JSArray values -> do JSArray values -> do
marked <- forM values $ \v -> do marked <- forM values $ \v -> do
@@ -109,17 +151,72 @@ groupp prog = do
marked marked
_ -> return JSNull _ -> return JSNull
recp prog = do
found <- collectPaths prog
let (roots,subs) = partition (null . snd) found
msum $
[tell p >> return x | (x,p) <- roots] ++
[tell p >> (return x >| recp prog) | (x,p) <- subs]
elsep p1 p2 = do
p1' <- collectPaths p1
if null p1' then p2 else yieldPaths p1'
fullresultp prog = do
res <- collectPaths prog
msum [return $ JSObject $ toJSObject $ [("val",a),("path",JSArray $ map fromPath p)] | (a,p) <- res]
where
fromPath (Left n) = js n
fromPath (Right s) = js s
withArray f (JSArray values) = JSArray $ f values withArray f (JSArray values) = JSArray $ f values
withArray f x = x withArray f x = x
withString f (JSString str) = JSString $ toJSString $ f $ fromJSString str
withString f x = x
{-
callp "select" [p] = selectp p callp "select" [p] = selectp p
callp "find" [p] = findp p callp "find" [p] = findp p
callp "set" [p] = updatep p
callp "sort" [] = liftp (withArray sort) callp "sort" [] = liftp (withArray sort)
callp "group" [p] = groupp p callp "group" [p] = groupp p
callp "rec" [p] = recp p
callp "empty" [] = mzero
callp "true" [] = return $ JSBool True
callp "false" [] = return $ JSBool False
callp "null" [] = return $ JSNull
callp "count" [] = liftp countj
callp "fullresult" [p] = fullresultp p
callp "uppercase" [] = liftp $ withString $ map toUpper
callp "lowercase" [] = liftp $ withString $ map toLower
-}
primitives = M.fromList [((name,arglen),Operator func) |
(name,arglen,func) <- prim]
where
prim = [("if",1,\[p] -> selectp p),
("find", 1, \[p] -> findp p),
("group", 1, \[p] -> groupp p),
("rec", 1, \[p] -> recp p),
("true", 0, const $ return $ JSBool True),
("false", 0, const $ return $ JSBool False),
("null", 0, const $ return $ JSNull),
("count", 0, const $ liftp countj),
("fullresult", 1, \[p] -> fullresultp p),
("zip", 0, const $ liftp zipj),
("keys", 0, const $ liftp keysj)
]
callp :: String -> [Filter] -> Filter
callp name args = do
(v,s,d) <- ask
runOperator (d ! (name, length args)) args
countj (JSArray v) = js$ length v
countj (JSObject o) = js$ length $ fromJSObject o
countj _ = js$ (1::Int)
lookupj :: JSValue -> JSValue -> JQ JSValue lookupj :: JSValue -> JSValue -> JQ JSValue
lookupj (JSArray values) (JSRational _ n) = do lookupj (JSArray values) (JSRational _ n) = do
@@ -133,25 +230,75 @@ lookupj (JSObject obj) (JSString s) = do
case (lookup (fromJSString s) (fromJSObject obj)) of case (lookup (fromJSString s) (fromJSObject obj)) of
Just x -> return x Just x -> return x
Nothing -> return JSNull Nothing -> return JSNull
lookupj JSNull (JSRational _ n) = do
tell [Left $ round n]
return JSNull
lookupj JSNull (JSString s) = do
tell [Right (fromJSString s)]
return JSNull
--lookupj v i = error $ "Cannot get element " ++ encode i ++ " of " ++ encode v
lookupj _ _ = mzero lookupj _ _ = mzero
plusj (JSRational _ n1) (JSRational _ n2) = JSRational True (n1 + n2) plusj (JSRational f1 n1) (JSRational f2 n2) = JSRational (f1 || f2) (n1 + n2)
plusj (JSString s1) (JSString s2) = JSString $ toJSString (fromJSString s1 ++ fromJSString s2) plusj (JSString s1) (JSString s2) = JSString $ toJSString (fromJSString s1 ++ fromJSString s2)
plusj (JSArray a1) (JSArray a2) = JSArray $ a1 ++ a2 plusj (JSArray a1) (JSArray a2) = JSArray $ a1 ++ a2
plusj (JSObject o1) (JSObject o2) = JSObject $ toJSObject $ o1' ++ fromJSObject o2
where
newkeys = map fst $ fromJSObject o2
o1' = filter (not . (`elem` newkeys) . fst) $ fromJSObject o1
keysj (JSArray v) = js [0..length v - 1]
keysj (JSObject obj) = js (map fst $ fromJSObject obj)
keysj _ = JSArray []
zipj jsonValue = result $ tx values ctor
where
(result, values) = extract packed
(packed, ctor) = case jsonValue of
JSArray values -> (values, (\vs' -> JSArray $ [v' | Just v' <- vs']))
JSObject jsObject ->
let object = fromJSObject jsObject
keys = map fst object
values = map snd object
build vs' = [(k,v') | (k,Just v') <- zip keys vs']
in (values, JSObject . toJSObject . build)
_ -> error "only arrays and objects may be zipped"
extract values | all isArray values = (JSArray, [map Just arr | JSArray arr <- values])
| all isObject values =
let objects = [fromJSObject o | JSObject o <- values]
keys = S.toList $ S.fromList [k | obj <- objects, (k,_) <- obj]
values' :: [[Maybe JSValue]]
values' = [[lookup k object | k <- keys] | object <- objects]
result r = JSObject $ toJSObject $ zip keys r
in (result, values')
| otherwise = error "elements of zipped value must be all objects or all arrays"
where
isArray (JSArray a) = True
isArray _ = False
isObject (JSObject o) = True
isObject _ = False
head' [] = Nothing
head' (x:xs) = x
tail' [] = []
tail' (x:xs) = xs
tx values ctor | all null values = []
| otherwise = ctor (map head' values):tx (map tail' values) ctor
js :: JSON a => a -> JSValue js :: JSON a => a -> JSValue
js = showJSON js = showJSON
index s = do index s = do
v <- ask v <- input
lookupj v (js s) lookupj v (js s)
dictp progs = do dictp progs = do
liftM (JSObject . toJSObject) $ forM progs $ \(k,v) -> do liftM (JSObject . toJSObject) $ forM progs $ \(k,v) -> do
JSString k' <- k JSString k' <- subexp k
v' <- v v' <- subexp v
return (fromJSString k', v') return (fromJSString k', v')

View File

@@ -7,13 +7,14 @@ import Control.Monad.Error
$digit = 0-9 $digit = 0-9
$alpha = [a-zA-Z_] $alpha = [a-zA-Z_]
@reserved = "."|"["|"]"|","|":"|"("|")"|"{"|"}"|"|"|"=="|"+" @reserved = "."|"["|"]"|","|":"|"("|")"|"{"|"}"|"|"|"=="|"+"|"="|"$"|"def"|";"|"else"|"and"|"or"|"as"
@ident = $alpha [$alpha $digit]* @ident = $alpha [$alpha $digit]*
@string = \" ($printable)* \" @string = \" ($printable)* \"
tokens :- tokens :-
<0> "#" ($printable # [\n\r])* ;
<0> $white+ ; <0> $white+ ;
<0> @reserved { tok TRes } <0> @reserved { tok TRes }
<0> @ident { tok TIdent } <0> @ident { tok TIdent }

View File

@@ -3,11 +3,11 @@ import Lexer
import JQ import JQ
import Text.JSON import Text.JSON
import Text.JSON.String import Text.JSON.String
import PrettyJSON
import System.Environment import System.Environment
import Control.Monad import Control.Monad
import System.IO import System.IO
parseJS :: String -> JSValue parseJS :: String -> JSValue
parseJS s = case runGetJSON readJSValue s of parseJS s = case runGetJSON readJSValue s of
Left err -> error err Left err -> error err
@@ -16,7 +16,8 @@ parseJS s = case runGetJSON readJSValue s of
main = do main = do
[program] <- getArgs [program] <- getArgs
stdlib <- openFile "stdlib.jq" ReadMode >>= hGetContents
json <- liftM parseJS $ hGetContents stdin json <- liftM parseJS $ hGetContents stdin
case runLexer program >>= runParser of case runLexer (stdlib ++ program) >>= runParser of
Left err -> putStrLn err Left err -> putStrLn err
Right program -> mapM_ (putStrLn . encode) (runJQ program json) Right program -> mapM_ (putStrLn . show . renderJSON) (runJQ program json)

8
Makefile Normal file
View File

@@ -0,0 +1,8 @@
jq: *.hs Parser.hs Lexer.hs
ghc *.hs -o jq
Parser.hs: Parser.y
happy -i Parser.y
Lexer.hs: Lexer.x
alex Lexer.x

View File

@@ -7,13 +7,21 @@ import Debug.Trace
import Data.List import Data.List
import Control.Monad.Error import Control.Monad.Error
import Control.Monad.Reader import Control.Monad.Reader
instance Error (Maybe a) where
noMsg = Nothing
strMsg = const Nothing
instance (Error a, Error b) => Error (a, b) where
noMsg = (noMsg, noMsg)
strMsg s = (strMsg s, strMsg s)
} }
%name runParser Exp %name runParser TopLevel
%tokentype { Token } %tokentype { Token }
%monad { Either String } %monad { Either String }
%error { \t -> fail $ "parse error: unexpected " ++ if null t then [] else (show $ head t) } %error { \t -> throwError $ "parse error: unexpected " ++ if null t then [] else (show $ t) }
%token %token
'|' { TRes "|" } '|' { TRes "|" }
@@ -24,28 +32,61 @@ import Control.Monad.Reader
'}' { TRes "}" } '}' { TRes "}" }
'(' { TRes "(" } '(' { TRes "(" }
')' { TRes ")" } ')' { TRes ")" }
'$' { TRes "$" }
'as' { TRes "as" }
',' { TRes "," } ',' { TRes "," }
':' { TRes ":" } ':' { TRes ":" }
'==' { TRes "==" } '==' { TRes "==" }
'=' { TRes "=" }
'+' { TRes "+" } '+' { TRes "+" }
'def' { TRes "def" }
';' { TRes ";" }
'else' { TRes "else" }
'and' { TRes "and" }
'or' { TRes "or" }
Ident { TIdent $$ } Ident { TIdent $$ }
String { TString $$ } String { TString $$ }
Int { TInt $$ } Int { TInt $$ }
%left '|'
%left 'else'
%right '|'
%left '='
%left ',' %left ','
%left 'and' 'or'
%nonassoc '==' %nonassoc '=='
%left '+' %left '+'
%% %%
TopLevel
: Defn TopLevel { withDefn $1 $2 }
| Exp { $1 }
Exp Exp
: Exp '|' Exp { $1 >| $3 } : Exp 'else' Exp { $1 `elsep` $3 }
| Assign '|' Exp { do { v <- snd $1; setvar (fst $1) v $3 } }
| Exp '|' Exp { $1 >| $3 }
| Exp ',' Exp { $1 `mplus` $3 } | Exp ',' Exp { $1 `mplus` $3 }
| Exp '==' Exp { liftM2 eqj $1 $3 } | Exp 'and' Exp { liftM2 andj $1 $3 }
| Exp '+' Exp { liftM2 plusj $1 $3 } | Exp 'or' Exp { liftM2 orj $1 $3 }
| Exp '=' Exp { assignp $1 $3 }
| Exp '==' Exp { liftM2 eqj (subexp $1) (subexp $3) }
| Exp '+' Exp { liftM2 plusj (subexp $1) (subexp $3) }
| Term { $1 } | Term { $1 }
Assign
: Term 'as' '$' Ident { ($4, $1) }
Defn
: 'def' Ident '=' Exp ';' { ($2, [], $4) }
| 'def' Ident '(' ParamList ')' '=' Exp ';' { ($2, $4, $7) }
ParamList
: { [] }
| Ident { [$1] }
| Ident ';' ParamList { $1:$3 }
ExpD ExpD
: ExpD '|' ExpD { $1 >| $3 } : ExpD '|' ExpD { $1 >| $3 }
| ExpD '==' ExpD { liftM2 eqj $1 $3 } | ExpD '==' ExpD { liftM2 eqj $1 $3 }
@@ -53,24 +94,26 @@ ExpD
Term Term
: '.' { ask } : '.' { input }
| Term '.' Ident { $1 >| index $3 } | Term '.' Ident { $1 >| index $3 }
| '.' Ident { index $2 } | '.' Ident { index $2 }
| String { constStr $1 } | String { constStr $1 }
| Term '[' Exp ']' { do {t <- $1; i <- $3; lookupj t i} } | Term '[' Exp ']' { do {t <- $1; i <- subexp $3; lookupj t i} }
| Term '[' ']' { $1 >| childp } | Term '[' ']' { $1 >| childp }
| '(' Exp ')' { $2 } | '(' Exp ')' { $2 }
| '[' Exp ']' { arrayp $2 } | '[' Exp ']' { arrayp $2 }
| '[' ']' { arrayp (callp "empty" []) }
| Int { constInt $1 } | Int { constInt $1 }
| '{' MkDict '}' { dictp $2 } | '{' MkDict '}' { dictp $2 }
| Ident '(' Exp ')' { callp $1 [$3] } | Ident '(' Exp ')' { callp $1 [$3] }
| Ident { callp $1 [] } | Ident { callp $1 [] }
| '$' Ident { getvar $2 }
MkDict MkDict
: { [] } : { [] }
| MkDictPair { [$1] } | MkDictPair { [$1] }
| MkDictPair ',' MkDict { $1:$3 } | MkDictPair ',' MkDict { $1:$3 }
MkDictPair MkDictPair
: Ident ':' ExpD { (constStr $1, $3) } : Ident ':' ExpD { (constStr $1, $3) }
| Ident { (constStr $1, index $1) } | Ident { (constStr $1, index $1) }

12
PrettyJSON.hs Normal file
View File

@@ -0,0 +1,12 @@
module PrettyJSON where
import Text.JSON
import Text.PrettyPrint
renderJSON (JSArray vals) = brackets $ fsep $ punctuate comma $ map renderJSON vals
renderJSON (JSObject jsObject) =
let object = fromJSObject jsObject
in braces $ fsep $ punctuate comma $
[hang (renderJSON (JSString $ toJSString $ k) <> colon) 2 (renderJSON v)
| (k,v) <- object]
renderJSON x = text $ encode x

11
stdlib.jq Normal file
View File

@@ -0,0 +1,11 @@
def map(f) = [.[] | f];
def first = .[0];
# def last = .[count-1];
def next = .[count];
# ([])[] would be a decent definition of "empty"
# except ([]) is defined as syntactic sugar for empty
def empty = {}[];
def sort = [group(.) | .[]];