table (empty) → 0.1.0.0
raw patch · 6 files changed
+320/−0 lines, 6 filesdep +basedep +csvdep +optparse-applicativesetup-changed
Dependencies added: base, csv, optparse-applicative, process, split
Files
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Daniel Choi++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Main.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE RecordWildCards, ScopedTypeVariables #-} +module Main where+import Data.List +import Data.Monoid+import Text.Printf+import Options.Applicative+import Data.List.Split (splitOn)+import Control.Applicative+import Data.Char (isDigit, isSpace)+import Control.Monad (when)+import System.Process (readProcess)+import qualified Text.CSV as CSV+++data Options = Options {+ splitter :: String -> [String] -- delimiter splitting function+ , csvInput :: Bool+ , suppressRowDividers :: Bool + , printHeaderDivider :: Bool + , maxWidth :: Int+ } ++parseOptions :: Parser Options+parseOptions = Options+ <$> (setDelimiter <|> whiteSpaceDelimiter <|> pure (splitOn "\t"))+ <*> switch (short 'c' <> help "Parse input as CSV")+ <*> switch (short 'R' <> help "Don't print row dividers")+ <*> switch (short 'H' <> help "Print header row divider")+ <*> parseMaxWidth++setDelimiter = + splitOn <$> strOption (metavar "DELIM" <> short 'd' <> help "Input field delimiter. Default is TAB (\\t).")++whiteSpaceDelimiter = flag' + words+ (short 's' <> help "Use any run of whitespace as input field delimiter")++parseMaxWidth = read <$> strOption (+ value "0" <> short 'w' <> metavar "WIDTH" + <> help "Max table width. Defaults to value of `tput cols` command.")++opts = info (helper <*> parseOptions)+ (fullDesc + <> progDesc "Pretty format TSV input into table with aligned and wrapped cells"+ <> header "table"+ <> footer "https://github.com/danchoi/table")++main = do + Options {..} <- execParser opts+ s <- getContents + let rawRows = if csvInput + then case CSV.parseCSV "" s of+ Left err -> error $ "Error: " ++ show err+ Right xs' -> stripBlankRows xs'+ else map splitter . lines $ s + let initialWidths = getCellWidths rawRows+ -- Adjust the max width subtract padding for gutters on side and ' | ' between cells+ maxWidth' <- do + if maxWidth == 0+ then (read <$> readProcess "tput" ["cols"] [])+ else (return maxWidth)+ let adjMaxWidth = maxWidth' - 2 - ((length initialWidths - 1) * 3)+ let adjustedWidths = adjustWidths adjMaxWidth initialWidths+ let rows = mkCells adjustedWidths rawRows+ mapM_ (\(n, row) -> do+ when (not suppressRowDividers || (printHeaderDivider && n == 1)) $ + putStrLn $ printDivider 1 $ map width row+ putStrLn . printRow 1 $ row + ) $ zip [0..] rows+ when (not suppressRowDividers) $ + putStrLn $ printDivider 1 $ map width (head rows)+++stripBlankRows :: [[String]] -> [[String]]+stripBlankRows xs = [x | x <- xs, all (> 0) $ map length x ]++adjustWidths :: Int -> [Int] -> [Int]+adjustWidths maxWidth xs | sum xs <= maxWidth = xs+ | otherwise = adjustWidths maxWidth $ reduceWidest xs++reduceWidest :: [Int] -> [Int]+reduceWidest xs = let m = maximum xs+ in [ if x == m then x - 1 else x | x <- xs ]++data Cell = Cell {+ content :: [String]+ , width :: Int+ , height :: Int + , isNumeric :: Bool+ } deriving (Show)++-- | get initial column widths+getCellWidths :: [[String]] -> [Int]+getCellWidths rows = map (maximum . map length) . transpose $ rows++{- Each row is represented as Cell, which contains dimension information. The+first value is the text content; the second is the normalized of the column+width for that cell. -}++mkCells :: [Int] -> [[String]] -> [[Cell]]+mkCells columnWidths rows = + let cols = transpose rows+ colCells = map (\(width, cell) -> addCellDimensions width cell) $ zip columnWidths cols + in transpose colCells++{- Input is a column of strings. Wraps data in a Cell, which adds width and+height to each cell in a column, and also a flag if the column looks numeric,+which determines the alignment. Also wraps stings to max cell width -} ++addCellDimensions :: Int -> [String] -> [Cell]+addCellDimensions maxWidth xs = + let w = min (maximum . map length $ xs) maxWidth+ xs' = map (wrapString w) xs -- wrapped string content+ numeric = all (all isDigit) (if length xs > 1 then (tail xs) else xs)+ in map (\lines -> Cell lines w (length lines) numeric) xs'++wrapString :: Int -> String -> [String]+wrapString maxWidth x = map trim . wrapLine maxWidth $ x++-- | prints a row of cells with dividers+-- gutter is the width of the blank space at left and right of table+printRow :: Int -> [Cell] -> String+printRow gutter xs = + let rowHeight = maximum $ map height xs+ subcells :: [[String]]+ subcells = map content xs+ lines = map (\n -> + let ss :: [String] + ss = map (cellLine n) xs+ in formatRow ss)+ [0..(rowHeight - 1)] + in mconcat $ intersperse "\n" $ lines+ where formatRow :: [String] -> String+ formatRow ss = + mconcat [margin gutter ' ' , (intercalate " | " ss) , margin gutter ' '] ++-- prints the nth line of a cell +cellLine :: Int -> Cell -> String+cellLine n Cell {..} = + if n < length content + then printf fmt (content !! n) + else printf fmt ""+ where fmt | isNumeric = "%" ++ show width ++ "s"+ | otherwise = "%-" ++ show width ++ "s"++margin :: Int -> Char -> String+margin w c = take w $ repeat c++printDivider :: Int -> [Int] -> String+printDivider gutter widths = + mconcat [margin gutter '-'+ , (intercalate "-+-" + $ map (\w -> take w $ repeat '-') widths)+ , margin gutter '-']+++------------------------------------------------------------------------+-- Word wrapping+-- taken from http://moreindirection.blogspot.com/2010/08/blog-post.html+++trim :: String -> String+trim = trimAndReverse . trimAndReverse+ where trimAndReverse = reverse . dropWhile isSpace++reverseBreak :: (a -> Bool) -> [a] -> ([a], [a])+reverseBreak f xs = (reverse before, reverse after)+ where (after, before) = break f $ reverse xs++wrapLine :: Int -> String -> [String]+wrapLine maxLen line + | length line <= maxLen = [line]+ | any isSpace beforeMax = beforeSpace : (wrapLine maxLen $ afterSpace ++ afterMax)+ | otherwise = beforeMax : wrapLine maxLen afterMax+ where (beforeMax, afterMax) = splitAt maxLen line+ (beforeSpace, afterSpace) = reverseBreak isSpace beforeMax+
+ README.md view
@@ -0,0 +1,88 @@+# table++Formats lines of TSV, CSV, or DSV (delimiter-separated values) into a pretty+plain text table, wrappings cells with long content to try to fit the table in+the screen.++## Usage ++```+table++Usage: table ([-d DELIM] | [-s]) [-c] [-R] [-H] [-w WIDTH]+ Pretty format TSV input into table with aligned and wrapped cells++Available options:+ -h,--help Show this help text+ -d DELIM Input field delimiter. Default is TAB (\t).+ -s Use any run of whitespace as input field delimiter+ -c Parse input as CSV+ -R Don't print row dividers+ -H Print header row divider+ -w WIDTH Max table width. Defaults to value of `tput cols`+ command.++https://github.com/danchoi/table+```++## Example++Format Rails issues from the GitHub API into a table. This example uses+[jq](http://stedolan.github.io/jq/) and+[jsontsv](https://github.com/danchoi/jsontsv) to transform the GitHub API JSON+into tab separated values before piping that data into `table`:++```bash+curl -s "https://api.github.com/repos/rails/rails/issues" | +jq -M '.[]' | +jsontsv -H 'number title user.login:user state' |+table+```++Outputs this when the terminal width is 72 characters: ++```+--------+--------------------------------------+----------------+-------+ number | title | user | state +--------+--------------------------------------+----------------+-------+ 18290 | Updating guides for Rails 5.0 | lucascaton | open +--------+--------------------------------------+----------------+-------+ 18288 | Add --skip-action-mailer (or -M) to | claudiob | open + | rails generate | | +--------+--------------------------------------+----------------+-------+ 18287 | Rails 4.2.0: Scaffold generator | dldinternet | open + | with --helper=false produced an | | + | error | | +--------+--------------------------------------+----------------+-------+ 18285 | undefined method 'clear' for | nPn- | open + | nil:NilClass in | | + | actionpack-4.2.0/lib/action_controll | | + | er/test_case.rb | | +--------+--------------------------------------+----------------+-------+ 18283 | Prefer `array?` rather than `array` | kamipo | open +--------+--------------------------------------+----------------+-------+ 18279 | Refactoring of add_constraints in | eileencodes | open + | AssociationScope | | +--------+--------------------------------------+----------------+-------+ 18273 | Rails 3.2.21 (4.x too?): | jensb | open + | attribute_will_change! with symbol | | + | parameter causes "multiple | | + | assignment" error in Postgres | | +--------+--------------------------------------+----------------+-------+ 18257 | ActionView::Helpers::TranslationHelp | jcoyne | open + | er.translate makes unsafe values | | + | html_safe | | +...+```++You can use `table` to pretty-format TSV output for `mysql`, `psql`, or+`sqlite3` as well, especially when you need row cell content to wrap to fit+everything nicely on the screen.++## Author++* Daniel Choi <https://github.com/danchoi>++## License++MIT License
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ input view
@@ -0,0 +1,4 @@+id fruit+1 apple+2 banana+3 yogurt
+ table.cabal view
@@ -0,0 +1,29 @@+-- Initial table.cabal generated by cabal init. For further documentation,+-- see http://haskell.org/cabal/users-guide/++name: table+version: 0.1.0.0+synopsis: Simple tool to generate tables from DSV input+-- description: +homepage: https://github.com/danchoi/table+license: MIT+license-file: LICENSE+author: Daniel Choi+maintainer: dhchoi@gmail.com+-- copyright: +category: Text+build-type: Simple+-- extra-source-files: +cabal-version: >=1.10++executable table+ main-is: Main.hs+ -- other-modules: + -- other-extensions: + build-depends: base >=4.6 && <4.8+ , optparse-applicative+ , split >=0.2+ , process+ , csv >= 0.1.0+ -- hs-source-dirs: + default-language: Haskell2010