packages feed

lsfrom-2.0: src/Main.hs

module Main (main) where

import Control.Monad.Extra (filterM, unless, when, whenJust)
import Data.List.Extra (dropWhileEnd, unsnoc)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (isNothing, maybeToList)
import SimpleCmd (cmdLines, error', warning, (+-+))
import SimpleCmdArgs
import System.Directory (doesDirectoryExist, doesFileExist)
import System.FilePath
import System.Locale.SetLocale (setLocale, Category(LC_COLLATE))

import Collate (beforeUnsafe)
import Paths_lsfrom (version)

data IncludeExclude a = Include a | Exclude a

unInclExcl :: IncludeExclude a -> a
unInclExcl (Include x) = x
unInclExcl (Exclude x) = x

type NEString = NE.NonEmpty Char

showFile :: IncludeExclude NEString -> String
showFile = NE.toList . unInclExcl

data Only = OnlyDirs | OnlyFiles

main :: IO ()
main =
  simpleCmdArgs (Just version) "List directories files starting from file"
  "lsfrom lists the files in a directory that follow from the given file" $
  lsfrom
  <$> switchWith 's' "strict" "fail if specified file(s) do not exist"
  <*> switchWith 'A' "all" "include hidden (dot) files"
  <*> optional
  (flagLongWith' OnlyDirs "dirs" "Only list directories" <|>
   flagLongWith' OnlyFiles "files" "Only list files")
  <*> optional
  (Include <$> optNonEmpty 'f' "from" "STARTFILE" "files from STARTFILE" <|>
   Exclude <$> optNonEmpty 'a' "after" "STARTFILE" "files after STARTFILE")
  <*> optional
  (Include <$> optNonEmpty 'u' "until" "LASTFILE" "files until LASTFILE" <|>
   Exclude <$> optNonEmpty 'b' "before" "LASTFILE" "files before LASTFILE")
  where
    optNonEmpty =
      let readNonEmpty = NE.nonEmpty . removeTrailing
          removeTrailing "" = error' "empty filename not allowed"
          removeTrailing f =
            if f == "/"
            then "/"
            else dropWhileEnd (== '/') f
      in optionWith (maybeReader readNonEmpty)

lsfrom :: Bool -> Bool -> Maybe Only -> Maybe (IncludeExclude NEString)
       -> Maybe (IncludeExclude NEString) -> IO ()
lsfrom strict hidden monly mstart mlast = do
  let dirarg = maybe [] (maybeToList . fst . mdirfile) mstart
      showhidden = hidden || fmap (NE.head . unInclExcl) mstart == Just '.'
  listing <- cmdLines "ls" (["-A" | showhidden] ++ dirarg) >>= filterTypes
  when strict $ do
    whenJust mstart $ \start ->
      unless (showFile start `elem` listing) $
      error' $ showFile start +-+ "does not exist"
    whenJust mlast $ \lst ->
      unless (showFile lst `elem` listing) $
      error' $ showFile lst +-+ "does not exist"
  -- set collation for current locale
  mlocale <- setLocale LC_COLLATE $ Just ""
  when (isNothing mlocale) $ warning "setlocale failed"
  let result = takeLast $ dropStart listing -- uses LC_COLLATE
  mapM_ (putStrLn . (renderDir </>)) result
  where
    mdirfile :: IncludeExclude NEString -> (Maybe FilePath, FilePath)
    mdirfile ie =
        case splitFileName $ showFile ie of
          (d,f) ->
            let md = if d == "./" then Nothing else Just d
            in (md,f)

    filterTypes :: [String] -> IO [String]
    filterTypes =
      case monly of
        Nothing -> return
        Just o ->
          case o of
            OnlyDirs -> filterM doesDirectoryExist
            OnlyFiles -> filterM doesFileExist

    dropStart :: [FilePath] -> [FilePath]
    dropStart files =
      case mstart of
        Nothing -> files
        Just start ->
          case mdirfile start of
            (_,file) ->
              case dropWhile (`beforeUnsafe` file) files of
                [] -> []
                sf@(f:fs) ->
                  case start of
                    Include _ -> sf
                    Exclude e -> if f == NE.toList e then fs else sf

    takeLast files =
      case mlast of
        Nothing -> files
        Just ie ->
          case dropWhileEnd (showFile ie `beforeUnsafe`) files of
            [] -> []
            lf ->
              case ie of
                Include _ -> lf
                Exclude e ->
                  case unsnoc lf of
                    Nothing -> []
                    Just (ls,l) ->
                      if l == NE.toList e then ls else lf

    renderDir =
      case fst . mdirfile <$> mstart of
        Just (Just dir) -> dir
        _ -> ""