+--
+-- hfusk is a Haskell implementation of a "Fusker". For
+-- more information, see http://en.wikipedia.org/wiki/Fusker.
+--
+-- Copyright Michael Orlitzky
+--
+-- http://michael.orlitzky.com/
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+-- GNU General Public License for more details.
+--
+-- http://www.fsf.org/licensing/licenses/gpl.html
+--
+--
+-- Valid patterns involve (possibly nested) combinations of the following:
+-- Text : Everything except the patterns below will be used literally.
+-- [x-y] : Produces all integers between x and y inclusive.
+-- {x,y,z..} : Produces x, y, and z.
+--
+--
+-- Examples:
+--
+-- hfusk "http://mail[1-3].google.com/"
+-- -> "http://mail1.google.com/"
+-- -> "http://mail2.google.com/"
+-- -> "http://mail3.google.com/"
+--
+-- hfusk "http://{mail1,mail2}.google.com/"
+-- -> "http://mail1.google.com/"
+-- -> "http://mail2.google.com/"
+--
+
+
+import Data.List (nub)
+import System.Console.GetOpt
+import System.Environment (getArgs)
+import System.Exit
+import System.IO (hPutStrLn, stderr)
+import Text.ParserCombinators.Parsec
+
+
+-----------------------
+-- Command-line flags -
+-----------------------
+
+data Flag
+ = Help -- -h
+ | Quiet -- -q
+ deriving (Eq, Ord, Enum, Show, Bounded)
+
+
+-- The list of flags we accept
+flags :: [OptDescr Flag]
+flags = [ Option ['h'][] (NoArg Help) "Prints this help message.",
+ Option ['q'][] (NoArg Quiet) "Quiet mode: produce no standard output." ]
+
+
+-- The usage header
+usage :: String
+usage = "Usage: hfusk [-hq] <pattern>"
+
+
+-- The usage header, and all available flags (as generated by GetOpt)
+help :: String
+help = usageInfo usage flags
+
+
+-- Return a list of options and a list of patterns if everything
+-- goes well. Otherwise, bail and show the help.
+parse_command_line :: [String] -> IO ([Flag], [String])
+parse_command_line argv =
+ case (getOpt Permute flags argv) of
+ (options, pattern, []) ->
+ return (options, pattern)
+ (_, _, errors) -> do
+ hPutStrLn stderr (concat errors)
+ hPutStrLn stderr help
+ exitFailure
+
+
+-----------------------
+-- Cartesian Product --
+-----------------------
+
+cartesian_product :: [[a]] -> [[a]] -> [[a]]
+cartesian_product set1 set2 =
+ [ concat [x, y] | x <- set1, y <- set2 ]
+
+
+----------------
+-- Parser junk -
+----------------
+
+data Pattern
+ = Range (Pattern, Pattern)
+ | Set [Pattern]
+ | LiteralInt Integer
+ | Url String
+ deriving (Show)
+
+
+class Listable a where
+ to_list :: a -> [[String]]
+
+
+first_readable_int :: [[String]] -> Integer
+first_readable_int x = (read (head (head x)))
+
+to_integer_range :: Pattern -> Pattern -> [Integer]
+to_integer_range x y =
+ [(first_readable_int (to_list x)) .. (first_readable_int (to_list y))]
+
+
+instance Listable Pattern where
+ to_list (Range (x, y)) = map (\n -> [show n]) (to_integer_range x y)
+ to_list (Set x) = concat (map to_list x)
+ to_list (LiteralInt x) = [[show x]]
+ to_list (Url x) = [[x]]
+
+
+
+-- An integer is one or more digits, as far as I can tell.
+int_parser :: Parser Pattern
+int_parser = do
+ val <- many1 digit
+ return $ LiteralInt (read val)
+
+
+-- Parse a traditional Unix shell-style range,
+-- i.e. [1-100]
+range_parser :: Parser Pattern
+range_parser = do
+ char '['
+ inf <- int_parser
+ char '-'
+ sup <- int_parser
+ char ']'
+ return $ Range (inf, sup)
+
+
+-- Parse sets of the form {a, b, c..., z}
+set_parser :: Parser Pattern
+set_parser = do
+ char '{'
+ elements <- sepBy1 value_parser set_separator_parser
+ char '}'
+ return $ Set elements
+
+
+-- Parse past a comma surrounded by zero or more spaces.
+set_separator_parser :: Parser ()
+set_separator_parser = do
+ skipMany space
+ char ','
+ skipMany space
+
+
+-- Ad-hoc compilation of characters that I think
+-- are valid in URLs
+url_characters :: GenParser Char st Char
+url_characters = alphaNum <|>
+ char '.' <|>
+ char ':' <|>
+ char '_' <|>
+ char '-' <|>
+ char '%' <|>
+ char '~' <|>
+ char '?' <|>
+ char '/' <|>
+ char '#' <|>
+ char '&' <|>
+ char '+'
+
+
+-- Will parse one or more url_characters
+url_parser :: Parser Pattern
+url_parser = do
+ val <- many1 url_characters
+ return $ Url val
+
+
+-- Will parse any of the defined patterns.
+value_parser :: Parser Pattern
+value_parser =
+ range_parser <|>
+ set_parser <|>
+ url_parser <|>
+ int_parser
+
+
+-- Uses value_parser to walk the input string,
+-- returning the list of parsed Patterns.
+main_parser :: Parser [Pattern]
+main_parser = do
+ val <- many1 value_parser
+ return val
+
+
+
+---------
+-- Main -
+---------
+
+collapse_ast :: [Pattern] -> [String]
+collapse_ast ast =
+ nub (map concat (foldr1 cartesian_product (map to_list ast)))
+
+
+print_list :: Show a => [a] -> IO ()
+print_list xs = mapM_ (putStrLn . show) xs
+
+
+
+main :: IO ()
+main = do
+ argv <- getArgs
+ (options, pattern) <- parse_command_line argv
+
+ -- If the user asked for help, show it and exit successfully.
+ if (Help `elem` options)
+ then do
+ putStrLn help
+ exitWith ExitSuccess
+ else return ()
+
+ -- If the user didn't supply a pattern we also show the
+ -- help, but then exit with a failure code.
+ if (pattern == [])
+ then do
+ putStrLn help
+ exitFailure
+ else return ()
+
+
+ case (parse main_parser "" (head pattern)) of
+ Left err -> do
+ putStrLn ("Error " ++ (show err))
+ exitFailure
+ Right ast -> do
+ print_list (collapse_ast ast)
+ exitWith ExitSuccess