diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Henning Thielemann
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Henning Thielemann nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,3 @@
+#! /usr/bin/env runhaskell
+> import Distribution.Simple
+> main = defaultMain
diff --git a/ods2csv.cabal b/ods2csv.cabal
new file mode 100644
--- /dev/null
+++ b/ods2csv.cabal
@@ -0,0 +1,51 @@
+Name:                ods2csv
+Version:             0.0
+Synopsis:            Convert Open Document Spreadsheet ODS to CSV
+Description:
+  Usually you would convert ODS to CSV via @libreoffice --headless@
+  but this conflicts with running GUI instances of @libreoffice@
+  and requires a full libreoffice installation
+  and has incomprehensible CSV export command-line options
+  and selection of individual tables is inferior.
+  .
+  This program quickly scans through a FODS document using a XML parser
+  and watches only the necessary data.
+  It does not interpret or evaluate the formula data,
+  instead it assumes that the contained evaluated values are correct.
+  This is true, if the file was saved from LibreOffice
+  but might not be true if generated or manipulated by other tools.
+  .
+  ToDo: Provide FODS parser as library function, maybe in @spreadsheet@.
+Homepage:            https://hub.darcs.net/thielema/ods2csv
+License:             BSD3
+License-File:        LICENSE
+Author:              Henning Thielemann
+Maintainer:          haskell@henning-thielemann.de
+Category:            Text, CSV
+Build-Type:          Simple
+Cabal-Version:       >=1.10
+
+Source-Repository this
+  Tag:         0.0
+  Type:        darcs
+  Location:    https://hub.darcs.net/thielema/ods2csv
+
+Source-Repository head
+  Type:        darcs
+  Location:    https://hub.darcs.net/thielema/ods2csv
+
+Executable ods2csv
+  Build-Depends:
+    spreadsheet >=0.1.3 && <0.2,
+    tagchup >=0.4 && <0.5,
+    xml-basic >=0.1.1 && <0.2,
+    shell-utility >=0.1 && <0.2,
+    optparse-applicative >=0.11 && <0.19,
+    non-empty >=0.3.4 && <0.4,
+    utility-ht >=0.0.10 && <0.1,
+    base >=4.5 && <5
+  Hs-Source-Dirs:      src
+  Main-Is:             Main.hs
+  Default-Language:    Haskell2010
+  GHC-Options:
+    -Wall -fwarn-incomplete-uni-patterns -fwarn-tabs
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,155 @@
+module Main where
+
+import Shell.Utility.ParseArgument (parseNumber)
+import Shell.Utility.Exit (exitFailureMsg)
+
+import qualified Options.Applicative as OP
+
+import qualified Text.HTML.Tagchup.Parser as TagParser
+import qualified Text.HTML.Tagchup.Tag.Match as TagMatch
+import qualified Text.HTML.Tagchup.Tag as Tag
+import qualified Text.XML.Basic.Attribute as Attr
+import qualified Text.XML.Basic.Name.MixedCase as Name
+import qualified Text.XML.Basic.Name as NameC
+
+import qualified Data.Spreadsheet as Spreadsheet
+import qualified Data.NonEmpty.Mixed as NonEmptyM
+import qualified Data.NonEmpty as NonEmpty
+import qualified Data.List.HT as ListHT
+import Data.Tuple.HT (mapSnd)
+import Data.Maybe (mapMaybe)
+import Data.Monoid ((<>))
+
+import Control.Monad (join, guard)
+import Control.Applicative (pure, (<*>), (<|>))
+
+import Text.Printf (printf)
+
+
+
+maybeTableName :: Tag.T Name.T String -> Maybe String
+maybeTableName tag = do
+   (foundName, attrs) <- Tag.maybeOpen tag
+   guard $ NameC.match "table:table" foundName
+   Attr.lookupLit "table:name" attrs
+
+maybeCell :: Tag.T Name.T String -> Maybe Int
+maybeCell tag = do
+   (foundName, attrs) <- Tag.maybeOpen tag
+   guard $ NameC.match "table:table-cell" foundName
+   case fmap reads $ Attr.lookupLit "table:number-columns-repeated" attrs of
+      Just [(n,_)] -> return n
+      _ -> return 1
+
+{-
+<config:config-item-map-entry config:name="BlaBlaBla">
+<table:table table:name="BlaBlaBla" table:style-name="ta1">
+-}
+extractTablesNames :: [Tag.T Name.T String] -> [String]
+extractTablesNames = mapMaybe maybeTableName
+
+listTables :: FilePath -> IO ()
+listTables input =
+   mapM_ putStrLn . extractTablesNames . TagParser.runSoup =<< readFile input
+
+
+extractTablesContents :: [Tag.T Name.T String] -> [(String, [[String]])]
+extractTablesContents =
+   map (mapSnd (
+         map
+            (concatMap
+               (uncurry $ \n ->
+                  (\texts ->
+                     replicate n $
+                     case texts of
+                        text:_ -> text
+                        [] -> "")
+                   .
+                   mapMaybe Tag.maybeText
+                   .
+                   dropWhile (not . TagMatch.openNameLit "text:p"))
+             .
+             snd
+             .
+             ListHT.segmentBeforeJust maybeCell
+             .
+             NonEmpty.tail)
+         .
+         snd
+         .
+         NonEmptyM.segmentBefore
+            (TagMatch.openNameLit "table:table-row"))) .
+   snd . ListHT.segmentBeforeJust maybeTableName
+
+contentFromTables :: Char -> FilePath -> IO ()
+contentFromTables separator input =
+   mapM_
+      (\(tableName, content) -> do
+         putStrLn ""
+         putStrLn tableName
+         putStrLn (Spreadsheet.toString '"' separator content)) .
+   extractTablesContents .
+   TagParser.runSoup
+      =<< readFile input
+
+contentFromTable :: Char -> Either String Int -> FilePath -> IO ()
+contentFromTable separator tableSelector input = do
+   tables <- extractTablesContents . TagParser.runSoup <$> readFile input
+   putStr . Spreadsheet.toString '"' separator =<<
+      case filter (\(tableId, (tableName, _content)) ->
+                     either (tableName==) (tableId==) tableSelector) $
+           zip [1..] tables of
+         (_,(_,found)):_ -> return found
+         _ ->
+            exitFailureMsg $
+               "table with " ++
+               either (printf "number %d") (printf "name %s") tableSelector ++
+               " not found"
+
+
+parser :: OP.Parser (IO ())
+parser =
+   ((OP.flag' listTables $
+         OP.long "list-tables" <>
+         OP.help "List all tables in an ODS document")
+      <|>
+      (pure contentFromTable
+         <*>
+         (OP.option
+            (OP.eitherReader (\str ->
+               case str of
+                  "TAB" -> Right '\t'
+                  [c] -> Right c
+                  _ -> Left "separator must be one character")) $
+            OP.long "separator" <>
+            OP.metavar "CHAR" <>
+            OP.value ',' <>
+            OP.help "CSV separator, TAB for tabulator")
+         <*>
+         (
+            (fmap Left $
+             OP.strOption $
+               OP.long "sheetname" <>
+               OP.metavar "NAME" <>
+               OP.help "Select table by name")
+            <|>
+            (fmap Right $
+             OP.option (OP.eitherReader $ parseNumber "page" (0<) "positive") $
+               OP.long "sheetnumber" <>
+               OP.metavar "ONEBASED" <>
+               OP.help "Select table by number")
+         )))
+   <*>
+      OP.strArgument
+         (OP.metavar "INPUT" <>
+          OP.help "Input Document")
+
+info :: OP.Parser a -> OP.ParserInfo a
+info p =
+   OP.info
+      (OP.helper <*> p)
+      (OP.fullDesc <>
+       OP.progDesc "Convert Open Document Spreadsheet ODS to CSV.")
+
+main :: IO ()
+main = join $ OP.execParser $ info parser
