From e1d16438b44ecd962565756a828c9ed8014cf894 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 7 Jun 2013 18:18:10 -0400 Subject: [PATCH] Add more Haddock comments. Replace a few custom functions with library ones. Allow input CIDRs to be separated by any whitespace, not just newlines. Split the exit codes out into their own module. Remove now-unused ListUtils modules. Update deps in cabal file. Add a man page. --- doc/man/hath.1 | 99 ++++++++++++++++++++++++++++++++++++++++++++++ hath.cabal | 10 ++--- src/Bit.hs | 2 +- src/Cidr.hs | 39 +++++++++++++----- src/CommandLine.hs | 63 ++++++++++++++--------------- src/ExitCodes.hs | 11 ++++++ src/ListUtils.hs | 37 ----------------- src/Main.hs | 73 +++++++++++++++------------------- src/Maskbits.hs | 5 ++- 9 files changed, 212 insertions(+), 127 deletions(-) create mode 100644 doc/man/hath.1 create mode 100644 src/ExitCodes.hs delete mode 100644 src/ListUtils.hs diff --git a/doc/man/hath.1 b/doc/man/hath.1 new file mode 100644 index 0000000..7006ac3 --- /dev/null +++ b/doc/man/hath.1 @@ -0,0 +1,99 @@ +.TH hath 1 + +.SH NAME +hath \- Manipulate network blocks in CIDR notation + +.SH SYNOPSIS + +\fBhath\fR [\fBregexed|reduced|duped|diffed\fR] [\fB\-h\fR] [\fB-i \fIFILE\fR] \fI\fR + +.SH INPUT + +.P +The \fIinput\fR (default: stdin) should be a list of CIDR blocks, +separated by whitespace. Empty lines will be ignored, but otherwise, +malformed entries will cause an error to be displayed. + +.SH DESCRIPTION + +.P +Hath is a Haskell program for working with network blocks in CIDR +notation. When dealing with blocks of network addresses, there are a +few things that one usually wants to do with them: + +.IP \[bu] 2 +Create a regular expression matching the CIDR block(s). This is +because grep will throw up if you feed it CIDR. + +.IP \[bu] +Combine small blocks into larger ones. For example, if you have two +consecutive /24s, they might combine into a larger /23. + +.IP \[bu] +View the result of block combination in a useful way. + +.P +Hath does just that. It takes as its input (via stdin, or a file with +the -i parameter) a list of CIDR blocks. + +.SH MODES + +.P +Hath currently has four modes: + +.IP \[bu] 2 +\fBRegexed\fR + +This computes a (Perl-compatible) regular expression matching +the input CIDR blocks. It's the default mode of operation. + +.nf +.B $ hath <<< \(dq10.0.0.0/24 10.0.1.0/24\(dq +([^\.0-9](10)\.(0)\.(0)\.(0)[^\.0-9]|[^\.0-9](10)\.(0)\.(1) +\.(0)[^\.0-9]) + +.IP \[bu] +\fBReduced\fR + +This combines small blocks into larger ones where possible, and +eliminates redundant blocks. The output should be equivalent to +the input, though. + +.nf +.B $ hath reduced <<< \(dq10.0.0.0/24 10.0.1.0/24\(dq +10.0.0.0/23 + +.IP \[bu] +\fBDuped\fR + +Shows only the blocks that would be removed by reduce; that is, it +shows the ones that would get combined into larger blocks or are +simply redundant. + +.nf +.B $ hath duped <<< \(dq10.0.0.0/24 10.0.1.0/24\(dq +10.0.0.0/24 +10.0.1.0/24 + +.IP \[bu] +\fBDiffed\fR + +Shows what would change if you used reduce. Uses diff-like +notation. + +.nf +.B $ hath diffed <<< \(dq10.0.0.0/24 10.0.1.0/24\(dq +-10.0.0.0/24 +-10.0.1.0/24 ++10.0.0.0/23 + +.P +Each of the modes also supports a present-tense flavor; the following +are equivalent to their counterparts: \fBregex\fR, \fBreduce\fR, +\fBdupe\fR, \fBdiff\fR. + +.SH OPTIONS + +.IP \fB\-\-input\fR,\ \fB\-i\fR +Specify the input file containing a list of CIDRs, rather than using +stdin (the default). diff --git a/hath.cabal b/hath.cabal index 23f6cc4..12f811a 100644 --- a/hath.cabal +++ b/hath.cabal @@ -13,6 +13,8 @@ executable hath base == 4.*, HUnit == 1.2.*, QuickCheck == 2.6.*, + MissingH == 1.2.*, + split == 0.2.*, test-framework == 0.8.*, test-framework-hunit == 0.3.*, test-framework-quickcheck2 == 0.3.* @@ -34,9 +36,6 @@ executable hath -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unused-do-bind - -funbox-strict-fields - -fexcess-precision - -fno-spec-constr-count -rtsopts -threaded -optc-O3 @@ -57,6 +56,8 @@ test-suite testsuite base == 4.*, HUnit == 1.2.*, QuickCheck == 2.6.*, + MissingH == 1.2.*, + split == 0.2.*, test-framework == 0.8.*, test-framework-hunit == 0.3.*, test-framework-quickcheck2 == 0.3.* @@ -73,9 +74,6 @@ test-suite testsuite -fwarn-incomplete-record-updates -fwarn-monomorphism-restriction -fwarn-unused-do-bind - -funbox-strict-fields - -fexcess-precision - -fno-spec-constr-count -rtsopts -threaded -optc-O3 diff --git a/src/Bit.hs b/src/Bit.hs index 128b6f6..5c8c5aa 100644 --- a/src/Bit.hs +++ b/src/Bit.hs @@ -28,7 +28,7 @@ bit_to_int Zero = 0 bit_to_int One = 1 -- | If we are passed a '0' or '1', convert it --- appropriately. Otherwise, default to Nothing. +-- appropriately. Otherwise, return Nothing. bit_from_char :: Char -> Maybe Bit bit_from_char '0' = Just Zero bit_from_char '1' = Just One diff --git a/src/Cidr.hs b/src/Cidr.hs index d41ba11..002ec0b 100644 --- a/src/Cidr.hs +++ b/src/Cidr.hs @@ -1,3 +1,5 @@ +-- | The CIDR modules contains most of the functions used for working +-- with the CIDR type. module Cidr ( Cidr(..), cidr_from_string, @@ -19,6 +21,7 @@ module Cidr ) where import Data.List (nubBy) +import Data.List.Split (splitOneOf) import Data.Maybe (catMaybes, fromJust) import Test.HUnit (assertEqual) @@ -29,7 +32,6 @@ import Test.QuickCheck (Arbitrary(..), Gen, Property, (==>)) import qualified Bit as B import IPv4Address -import ListUtils import Maskable import Maskbits import Octet @@ -55,27 +57,27 @@ instance Eq Cidr where cidr1 == cidr2 = (cidr1 `equivalent` cidr2) --- Two CIDR ranges are equivalent if they have the same network bits --- and the masks are the same. +-- | Two CIDR ranges are equivalent if they have the same network bits +-- and the masks are the same. equivalent :: Cidr -> Cidr -> Bool equivalent (Cidr addr1 mbits1) (Cidr addr2 mbits2) = (mbits1 == mbits2) && ((apply_mask addr1 mbits1 B.Zero) == (apply_mask addr2 mbits2 B.Zero)) --- Returns the mask portion of a CIDR address. That is, everything --- after the trailing slash. +-- | Returns the mask portion of a CIDR address. That is, everything +-- after the trailing slash. maskbits_from_cidr_string :: String -> Maybe Maskbits maskbits_from_cidr_string s | length partlist == 2 = maskbits_from_string (partlist !! 1) | otherwise = Nothing where - partlist = (splitWith (`elem` "/") s) + partlist = splitOneOf "/" s -- | Takes an IP address String in CIDR notation, and returns a list -- of its octets (as Ints). octets_from_cidr_string :: String -> [Octet] octets_from_cidr_string s = - catMaybes $ map octet_from_string (take 4 (splitWith (`elem` "./") s)) + catMaybes $ map octet_from_string (take 4 (splitOneOf "./" s)) -- | Return Nothing if we can't parse both maskbits and octets from @@ -92,35 +94,53 @@ cidr_from_string s = +-- | Given a CIDR, return the minimum valid IPv4 address contained +-- within it. min_host :: Cidr -> IPv4Address min_host (Cidr addr mask) = apply_mask addr mask B.Zero - +-- | Given a CIDR, return the maximum valid IPv4 address contained +-- within it. max_host :: Cidr -> IPv4Address max_host (Cidr addr mask) = apply_mask addr mask B.One - +-- | Given a CIDR, return the first octet of the minimum valid IPv4 +-- address contained within it. min_octet1 :: Cidr -> Octet min_octet1 cidr = octet1 (min_host cidr) +-- | Given a CIDR, return the second octet of the minimum valid IPv4 +-- address contained within it. min_octet2 :: Cidr -> Octet min_octet2 cidr = octet2 (min_host cidr) +-- | Given a CIDR, return the third octet of the minimum valid IPv4 +-- address contained within it. min_octet3 :: Cidr -> Octet min_octet3 cidr = octet3 (min_host cidr) +-- | Given a CIDR, return the fourth octet of the minimum valid IPv4 +-- address contained within it. min_octet4 :: Cidr -> Octet min_octet4 cidr = octet4 (min_host cidr) +-- | Given a CIDR, return the first octet of the maximum valid IPv4 +-- address contained within it. max_octet1 :: Cidr -> Octet max_octet1 cidr = octet1 (max_host cidr) +-- | Given a CIDR, return the second octet of the maximum valid IPv4 +-- address contained within it. max_octet2 :: Cidr -> Octet max_octet2 cidr = octet2 (max_host cidr) +-- | Given a CIDR, return the third octet of the maximum valid IPv4 +-- address contained within it. max_octet3 :: Cidr -> Octet max_octet3 cidr = octet3 (max_host cidr) +-- | Given a CIDR, return the fourth octet of the maximum valid IPv4 +-- address contained within it. max_octet4 :: Cidr -> Octet max_octet4 cidr = octet4 (max_host cidr) @@ -167,6 +187,7 @@ contains (Cidr addr1 mbits1) (Cidr addr2 mbits2) addr2masked = apply_mask addr2 mbits1 B.Zero +-- | Contains but is not equal to. contains_proper :: Cidr -> Cidr -> Bool contains_proper cidr1 cidr2 = (cidr1 `contains` cidr2) && (not (cidr2 `contains` cidr1)) diff --git a/src/CommandLine.hs b/src/CommandLine.hs index 68957b1..c2bbe0c 100644 --- a/src/CommandLine.hs +++ b/src/CommandLine.hs @@ -16,71 +16,72 @@ import System.Console.GetOpt import System.Environment (getArgs) --- Dark magic. +-- | Lowercase an entire string. lowercase :: String -> String lowercase = map toLower --- The application currently has four modes. The default, Regex, will --- compute a regular expression matching the input CIDRs. Reduce, on --- the other hand, will combine any redundant/adjacent CIDR blocks --- into one. Dupe will show you what would be removed by Reduce, and --- Diff will show both additions and deletions in a diff-like format. +-- | The application currently has four modes. The default, Regex, +-- will compute a regular expression matching the input +-- CIDRs. Reduce, on the other hand, will combine any +-- redundant/adjacent CIDR blocks into one. Dupe will show you what +-- would be removed by Reduce, and Diff will show both additions and +-- deletions in a diff-like format. data Mode = Regex | Reduce | Dupe | Diff --- A record containing values for all available options. +-- | A record containing values for all available options. data Options = Options { opt_help :: Bool, opt_input :: IO String } --- This constructs an instance of Options, with each of its members --- set to default values. +-- | This constructs an instance of Options, with each of its members +-- set to default values. default_options :: Options default_options = Options { opt_help = False, opt_input = getContents } --- The options list that we construct associates a function with each --- option. This function is responsible for updating an Options record --- with the appropriate value. +-- | The options list that we construct associates a function with +-- each option. This function is responsible for updating an Options +-- record with the appropriate value. -- --- For more information and an example of this idiom, see, +-- For more information and an example of this idiom, see, -- --- http://www.haskell.org/haskellwiki/High-level_option_handling_with_GetOpt +-- http://www.haskell.org/haskellwiki/High-level_option_handling_with_GetOpt -- options :: [OptDescr (Options -> IO Options)] options = - [ Option ['h'][] (NoArg set_help) "Prints this help message.", - Option ['i'][] (ReqArg set_input "FILE") "Read FILE instead of stdin." ] + [ Option ['h']["help"] (NoArg set_help) "Prints this help message.", + Option ['i']["input"] (ReqArg set_input "FILE") "Read FILE instead of stdin." ] --- Takes an Options as an argument, and sets its opt_help member to --- True. +-- | Takes an Options as an argument, and sets its opt_help member to +-- True. set_help :: Options -> IO Options set_help opts = do return opts { opt_help = True } --- If the input file option is set, this function will update the --- passed Options record with a new function for opt_input. The --- default opt_input is to read from stdin, but if this option is set, --- we replace that with readFile. +-- | If the input file option is set, this function will update the +-- passed Options record with a new function for opt_input. The +-- default opt_input is to read from stdin, but if this option is +-- set, we replace that with readFile. set_input :: String -> Options -> IO Options set_input arg opts = do return opts { opt_input = readFile arg } --- The usage header +-- | The usage header. usage :: String -usage = "Usage: hath [regexed|reduced|duped|diffed] [-h] [-i FILE]" +usage = "Usage: hath [regexed|reduced|duped|diffed] [-h] [-i FILE] " --- The usage header, and all available flags (as generated by GetOpt) +-- | The usage header, and all available flags (as generated by GetOpt). help_text :: String help_text = usageInfo usage options --- Return a list of options. +-- | Return a list of options. parse_options :: IO Options parse_options = do argv <- getArgs @@ -95,7 +96,7 @@ parse_options = do return opts --- Return the mode if one was given. +-- | Return the mode if one was given. parse_mode :: IO Mode parse_mode = do argv <- getArgs @@ -120,7 +121,7 @@ parse_mode = do --- Return a list of errors. +-- | Return a list of errors. parse_errors :: IO [String] parse_errors = do argv <- getArgs @@ -129,15 +130,15 @@ parse_errors = do --- Is the help option set? +-- | Is the help option set? help_set :: IO Bool help_set = do opts <- parse_options return (opt_help opts) --- Return our input function, getContents by default, or readFile if --- the input file option was set. +-- | Return our input function, getContents by default, or readFile if +-- the input file option was set. input_function :: IO (IO String) input_function = do opts <- parse_options diff --git a/src/ExitCodes.hs b/src/ExitCodes.hs new file mode 100644 index 0000000..4257191 --- /dev/null +++ b/src/ExitCodes.hs @@ -0,0 +1,11 @@ +-- | Some exit codes, used in the ExitFailure constructor. +module ExitCodes +where + +-- | One of the CIDRs is invalid (malformed, not a CIDR at all, etc). +exit_invalid_cidr :: Int +exit_invalid_cidr = 1 + +-- | We were unable to parse the command-line arguments. +exit_args_parse_failed :: Int +exit_args_parse_failed = 2 diff --git a/src/ListUtils.hs b/src/ListUtils.hs deleted file mode 100644 index 5ec9120..0000000 --- a/src/ListUtils.hs +++ /dev/null @@ -1,37 +0,0 @@ -module ListUtils -( pad_left_to, - pad_right_to, - splitWith -) where - - --- Stolen from ByteString. Splits a list at each element satisfying --- the predicate p. -splitWith :: (a -> Bool) -> [a] -> [[a]] -splitWith p xs = - ys : case zs of - [] -> [] - _:ws -> splitWith p ws - where (ys,zs) = break p xs - - --- Pads a list (on the left) to length len by prepending pad_elem. -pad_left_to :: Int -> a -> [a] -> [a] -pad_left_to len pad_elem xs = - if (length xs) >= len then - xs - else - (replicate padcount pad_elem) ++ xs - where - padcount = len - (length xs) - - --- Pads a list (on the right) to length len by appending pad_elem. -pad_right_to :: Int -> a -> [a] -> [a] -pad_right_to len pad_elem xs = - if (length xs) >= len then - xs - else - xs ++ (replicate padcount pad_elem) - where - padcount = len - (length xs) diff --git a/src/Main.hs b/src/Main.hs index b2fc64b..dd7eefe 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,5 +1,7 @@ +import Control.Monad (when) import Data.List ((\\), intercalate, intersperse) import Data.Maybe (catMaybes, isNothing) +import Data.String.Utils (splitWs) import System.Exit (ExitCode(..), exitWith) import System.IO (stderr, hPutStrLn) @@ -21,39 +23,32 @@ import CommandLine (help_set, Mode(..), parse_errors, parse_mode) - + +import ExitCodes import Octet --- Some exit codes, used in the ExitFailure constructor. -exit_invalid_cidr :: Int -exit_invalid_cidr = 1 - -exit_args_parse_failed :: Int -exit_args_parse_failed = 2 - --- A regular expression that matches a non-address character. +-- | A regular expression that matches a non-address character. non_addr_char :: String non_addr_char = "[^\\.0-9]" --- Add non_addr_chars on either side of the given String. This --- prevents (for example) the regex '127.0.0.1' from matching --- '127.0.0.100'. +-- | Add non_addr_chars on either side of the given String. This +-- prevents (for example) the regex '127.0.0.1' from matching +-- '127.0.0.100'. addr_barrier :: String -> String addr_barrier x = non_addr_char ++ x ++ non_addr_char --- The magic happens here. We take a CIDR String as an argument, and --- return the equivalent regular expression. We do this as follows: +-- | The magic happens here. We take a CIDR String as an argument, and +-- return the equivalent regular expression. We do this as follows: -- --- 1. Compute the minimum possible value of each octet. --- 2. Compute the maximum possible value of each octet. --- 3. Generate a regex matching every value between those min and --- max values. --- 4. Join the regexes from step 3 with regexes matching periods. --- 5. Stick an address boundary on either side of the result. ---cidr_to_regex :: String -> String +-- 1. Compute the minimum possible value of each octet. +-- 2. Compute the maximum possible value of each octet. +-- 3. Generate a regex matching every value between those min and +-- max values. +-- 4. Join the regexes from step 3 with regexes matching periods. +-- 5. Stick an address boundary on either side of the result. cidr_to_regex :: Cidr.Cidr -> String cidr_to_regex cidr = addr_barrier (intercalate "\\." [range1, range2, range3, range4]) @@ -73,14 +68,14 @@ cidr_to_regex cidr = --- Take a list of Strings, and return a regular expression matching --- any of them. +-- | Take a list of Strings, and return a regular expression matching +-- any of them. alternate :: [String] -> String alternate terms = "(" ++ (concat (intersperse "|" terms)) ++ ")" --- Take two Ints as parameters, and return a regex matching any --- integer between them (inclusive). +-- | Take two Ints as parameters, and return a regex matching any +-- integer between them (inclusive). numeric_range :: Int -> Int -> String numeric_range x y = alternate (map show [lower..upper]) @@ -94,34 +89,28 @@ main = do -- First, check for any errors that occurred while parsing -- the command line options. errors <- CommandLine.parse_errors - if not (null errors) - then do - hPutStrLn stderr (concat errors) - putStrLn CommandLine.help_text - exitWith (ExitFailure exit_args_parse_failed) - else do -- Nothing + when ((not . null) errors) $ do + hPutStrLn stderr (concat errors) + putStrLn CommandLine.help_text + exitWith (ExitFailure exit_args_parse_failed) -- Next, check to see if the 'help' option was passed to the -- program. If it was, display the help, and exit successfully. help_opt_set <- CommandLine.help_set - if help_opt_set - then do - putStrLn CommandLine.help_text - exitWith ExitSuccess - else do -- Nothing + when help_opt_set $ do + putStrLn CommandLine.help_text + exitWith ExitSuccess -- The input function we receive here should know what to read. inputfunc <- (CommandLine.input_function) input <- inputfunc - let cidr_strings = lines input + let cidr_strings = splitWs input let cidrs = map cidr_from_string cidr_strings - if (any isNothing cidrs) - then do - putStrLn "Error: not valid CIDR notation." - exitWith (ExitFailure exit_invalid_cidr) - else do -- Nothing + when (any isNothing cidrs) $ do + putStrLn "Error: not valid CIDR notation." + exitWith (ExitFailure exit_invalid_cidr) -- Filter out only the valid ones. let valid_cidrs = catMaybes cidrs diff --git a/src/Maskbits.hs b/src/Maskbits.hs index d7064d8..ac46ccf 100644 --- a/src/Maskbits.hs +++ b/src/Maskbits.hs @@ -6,7 +6,7 @@ module Maskbits import Test.QuickCheck --- A type representing the number of bits in a CIDR netmask. +-- | A type representing the number of bits in a CIDR netmask. data Maskbits = Zero | One @@ -156,6 +156,8 @@ maskbits_from_int 31 = Just ThirtyOne maskbits_from_int 32 = Just ThirtyTwo maskbits_from_int _ = Nothing + +-- | Convert a String to Maskbits, if possible. maskbits_from_string :: String -> Maybe Maskbits maskbits_from_string s = case (reads s :: [(Int, String)]) of @@ -164,6 +166,7 @@ maskbits_from_string s = +-- | Maskbits are just natural numbers, this returns the previous one. decrement :: Maskbits -> Maskbits decrement Zero = Zero decrement One = Zero -- 2.43.2