packages feed

sizes 2.4.3 → 2.4.4

raw patch · 4 files changed

+254/−49 lines, 4 filesdep +directorydep +filepathdep +processdep −dlistPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: directory, filepath, process

Dependencies removed: dlist

API changes (from Hackage documentation)

+ Sizes: combineEntryResults :: Bool -> (EntryInfo, ReportEntries) -> (EntryInfo, ReportEntries) -> (EntryInfo, ReportEntries)
+ Sizes: data ReportEntries
+ Sizes: emptyReportEntries :: ReportEntries
+ Sizes: reportEntriesToList :: ReportEntries -> [EntryInfo]
- Sizes: EntryInfo :: FilePath -> Int -> Int -> Bool -> EntryInfo
+ Sizes: EntryInfo :: FilePath -> !Int -> !Int -> Bool -> EntryInfo
- Sizes: SizesOpts :: Int -> Bool -> Bool -> Bool -> Bool -> String -> Int -> Int -> Int -> Bool -> Bool -> Bool -> Int -> [String] -> SizesOpts
+ Sizes: SizesOpts :: Int -> Bool -> Bool -> Bool -> Bool -> [String] -> Int -> Int -> Int -> Bool -> Bool -> Bool -> Int -> [String] -> SizesOpts
- Sizes: [_entryAllocSize] :: EntryInfo -> Int
+ Sizes: [_entryAllocSize] :: EntryInfo -> !Int
- Sizes: [_entryCount] :: EntryInfo -> Int
+ Sizes: [_entryCount] :: EntryInfo -> !Int
- Sizes: [exclude] :: SizesOpts -> String
+ Sizes: [exclude] :: SizesOpts -> [String]

Files

README.md view
@@ -36,6 +36,9 @@ # Exclude paths matching a regex sizes -x '\.cache' /path/to/dir +# Exclude paths matching any of several regexes (repeat -x)+sizes -x '\.cache' -x '\.git' /path/to/dir+ # Stay on one filesystem (skip mounted volumes, NFS/SMB shares, etc.) sizes -X /path/to/dir @@ -70,7 +73,7 @@ | `-A` | Git-annex aware | | `--apparent` | Apparent sizes, not disk blocks | | `-H` | Base-10 (MB/GB) |-| `-x REGEX` | Exclude matching paths |+| `-x REGEX` | Exclude matching paths; repeatable | | `-m INT` | Minimum size in MB (default: 10) | | `-M INT` | Minimum file count (default: 100) | | `-B INT` | Block size in bytes (default: 512) |
Sizes.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-}@@ -10,6 +11,7 @@     -- * Types     SizesOpts (..),     EntryInfo (..),+    ReportEntries,     SeenInodes,      -- * Lenses@@ -22,11 +24,12 @@     humanReadable,     reportEntryP,     newEntry,+    emptyReportEntries,+    reportEntriesToList,+    combineEntryResults,     crossesFileSystemBoundary, ) where --- jww (2013-08-23): Still need to deal with hard-links.- import Control.Concurrent.ParallelIO import Control.DeepSeq import Control.Exception hiding (catch)@@ -34,10 +37,11 @@ import Control.Monad import Control.Monad.Catch (catch) import Control.Monad.State.Strict-import Data.DList (DList)-import qualified Data.DList as DL+import qualified Data.Foldable as Foldable import Data.Function import qualified Data.List as L+import Data.Sequence (Seq)+import qualified Data.Sequence as Seq import qualified Data.Set as Set import Data.Text as T hiding (chunksOf, filter, map) import Filesystem (isFile, listDirectory)@@ -46,6 +50,8 @@ import Stat import System.Console.CmdArgs import System.Environment (getArgs, withArgs)+import System.Exit (ExitCode (ExitFailure), exitWith)+import System.IO (hPutStrLn, stderr) import System.Posix.Files hiding (fileBlockSize) import System.Posix.Types (DeviceID, FileID) import Text.Printf@@ -56,7 +62,7 @@ default (Integer, Text)  version :: String-version = "2.4.3"+version = "2.4.4"  copyright :: String copyright = "2012-2026"@@ -70,7 +76,7 @@     , annex :: Bool     , apparent :: Bool     , baseTen :: Bool-    , exclude :: String+    , exclude :: [String]     , minSize :: Int     , minCount :: Int     , blockSize :: Int@@ -114,7 +120,7 @@             def                 &= name "x"                 &= typ "REGEX"-                &= help "Exclude files whose path matches the REGEX"+                &= help "Exclude files whose path matches any of the REGEXes (repeatable)"         , minSize =             def                 &= name "m"@@ -161,8 +167,8 @@  data EntryInfo = EntryInfo     { _entryPath :: FilePath-    , _entryCount :: Int-    , _entryAllocSize :: Int+    , _entryCount :: !Int+    , _entryAllocSize :: !Int     , _entryIsDir :: Bool     }     deriving (Show, Eq)@@ -172,23 +178,48 @@ -- Track (DeviceID, FileID) pairs to detect hard links type SeenInodes = Set.Set (DeviceID, FileID) +-- | Stack-safe collection of entries retained for the final report.+newtype ReportEntries = ReportEntries (Seq EntryInfo)++-- | An empty report-entry collection.+emptyReportEntries :: ReportEntries+emptyReportEntries = ReportEntries Seq.empty++-- | Materialize report entries in traversal order.+reportEntriesToList :: ReportEntries -> [EntryInfo]+reportEntriesToList (ReportEntries entries) = Foldable.toList entries+ newEntry :: FilePath -> Bool -> EntryInfo newEntry p = EntryInfo p 0 0  instance Semigroup EntryInfo where     x <> y =-        seq x $-            seq y $-                entryCount +~ y ^. entryCount $-                    entryAllocSize +~ y ^. entryAllocSize $-                        x+        let !count' = x ^. entryCount + y ^. entryCount+            !allocSize' = x ^. entryAllocSize + y ^. entryAllocSize+         in x+                & entryCount .~ count'+                & entryAllocSize .~ allocSize'  instance Monoid EntryInfo where     mempty = newEntry "" False  instance NFData EntryInfo where-    rnf a = a `seq` ()+    rnf entry =+        rnf (toTextIgnore (entry ^. entryPath)) `seq`+            rnf (entry ^. entryCount) `seq`+                rnf (entry ^. entryAllocSize) `seq`+                    rnf (entry ^. entryIsDir) +-- | Add one child aggregate and its retained descendants to a directory.+combineEntryResults :: Bool -> (EntryInfo, ReportEntries) -> (EntryInfo, ReportEntries) -> (EntryInfo, ReportEntries)+combineEntryResults keepReports (total, ReportEntries reports) (child, ReportEntries childReports) =+    let total' = total <> child+        reports' =+            if keepReports+                then ReportEntries ((reports Seq.|> child) Seq.>< childReports)+                else emptyReportEntries+     in total' `seq` reports' `seq` (total', reports')+ sizesMain :: IO () sizesMain = do     mainArgs <- getArgs@@ -219,10 +250,11 @@  reportSizes :: SizesOpts -> [FilePath] -> IO () reportSizes opts xs = do-    entryInfos <- parallel $ Prelude.map reportSizesForDir xs+    excludes <- compileExcludes (exclude opts)+    entryInfos <- parallel $ Prelude.map (reportSizesForDir excludes) xs     let infos =             Prelude.map fst entryInfos-                ++ DL.toList (DL.concat (Prelude.map snd entryInfos))+                ++ Prelude.concatMap (reportEntriesToList . snd) entryInfos         sorted =             L.sortBy                 ( (compare `on`) $@@ -235,15 +267,29 @@         (reportEntry (baseTen opts))         (Prelude.filter (reportEntryP opts) sorted)   where-    reportSizesForDir dir =+    reportSizesForDir excludes dir =         -- fsStatus <- getFilesystemStatus (E.encodeUtf8 (toTextIgnore dir))         let fsBlkSize = statBlockSize -- filesystemBlockSize fsStatus             opts' =                 if blockSize opts == 0                     then opts{blockSize = fromIntegral fsBlkSize}                     else opts-         in fst <$> runStateT (gatherSizes opts' Nothing 0 dir) Set.empty+         in fst <$> runStateT (gatherSizes opts' excludes Nothing 0 dir) Set.empty +{- | Compile the exclusion patterns once, up front.  An invalid pattern is a+fatal startup error rather than a silent per-path failure.+-}+compileExcludes :: [String] -> IO [Regex]+compileExcludes = mapM compileOne+  where+    compileOne regex = do+        compiled <- try (makeRegexM regex)+        case compiled of+            Left ex -> do+                hPutStrLn stderr $ "invalid exclusion regex " ++ Prelude.show regex ++ ": " ++ Prelude.show (ex :: SomeException)+                exitWith (ExitFailure 1)+            Right r -> pure r+ humanReadable :: Int -> Int -> String humanReadable x d     | x < d = printf "%db" x@@ -272,8 +318,8 @@ toTextIgnore :: FilePath -> Text toTextIgnore = either id id . toText -returnEmpty :: FilePath -> StateT SeenInodes IO (EntryInfo, DList EntryInfo)-returnEmpty path = return (newEntry path False, DL.empty)+returnEmpty :: FilePath -> StateT SeenInodes IO (EntryInfo, ReportEntries)+returnEmpty path = return (newEntry path False, emptyReportEntries)  {- | Decide whether an entry should be skipped because it lies on a different filesystem than the traversal root.  The second argument is the device of@@ -285,16 +331,15 @@ crossesFileSystemBoundary True Nothing _ = False crossesFileSystemBoundary True (Just rootDev) dev = dev /= rootDev -gatherSizes :: SizesOpts -> Maybe DeviceID -> Int -> FilePath -> StateT SeenInodes IO (EntryInfo, DList EntryInfo)-gatherSizes opts mRootDev curDepth path = do-    excl <--        if L.null (exclude opts)-            then return $ Right False-            else liftIO $ try $ return $ path' =~ exclude opts -- jww (2013-08-15): poor-    case excl of-        Left (_ :: SomeException) -> returnEmpty path-        Right True -> returnEmpty path-        _ ->+-- | Decide whether a path matches any of the compiled exclusion patterns.+excludePath :: [Regex] -> String -> Bool+excludePath patterns path = L.any (`match` path) patterns++gatherSizes :: SizesOpts -> [Regex] -> Maybe DeviceID -> Int -> FilePath -> StateT SeenInodes IO (EntryInfo, ReportEntries)+gatherSizes opts excludes mRootDev curDepth path =+    if excludePath excludes path'+        then returnEmpty path+        else             ( do                 status <-                     liftIO@@ -318,15 +363,10 @@         | isDirectory status =             foldM                 ( \(y, ys) x -> do-                    (x', xs') <- gatherSizes opts (Just (deviceID status)) (curDepth + 1) (collapse x)-                    let x'' = y <> x'-                        xs'' =-                            if curDepth < depth opts-                                then ys <> DL.singleton x' <> xs'-                                else DL.empty-                    return $! x'' `seq` xs'' `seq` (x'', xs'')+                    child <- gatherSizes opts excludes (Just (deviceID status)) (curDepth + 1) (collapse x)+                    return $! combineEntryResults (curDepth < depth opts) (y, ys) child                 )-                (newEntry path True, DL.empty)+                (newEntry path True, emptyReportEntries)                 =<< liftIO (listDirectory path)         | ( isRegularFile status                 && not (annex opts && ".git/annex/" `isInfixOf` pathT)@@ -386,7 +426,7 @@                     , _entryAllocSize = allocSize                     , _entryIsDir = False                     }-                , DL.empty+                , emptyReportEntries                 )         | otherwise = returnEmpty path 
sizes.cabal view
@@ -1,6 +1,6 @@ cabal-version:   2.4 name:            sizes-version:         2.4.3+version:         2.4.4 synopsis:        Recursively show space (size and i-nodes) used in subdirectories description:   A command-line utility that recursively analyzes directory trees to display@@ -37,7 +37,6 @@     , cmdargs         >= 0.10  && < 0.11     , containers      >= 0.5   && < 0.8     , deepseq         >= 1.3   && < 1.6-    , dlist           >= 0.5   && < 1.1     , exceptions      >= 0.8   && < 0.11     , lens            >= 2.8   && < 6     , mtl             >= 2.2   && < 2.4@@ -53,7 +52,7 @@   main-is:          Main.hs   hs-source-dirs:   app   default-language: Haskell2010-  ghc-options:      -Wall -Wcompat -threaded -with-rtsopts=-K64M+  ghc-options:      -Wall -Wcompat -threaded    build-depends:     , base  >= 4 && < 5@@ -64,12 +63,19 @@   main-is:          Spec.hs   hs-source-dirs:   test   default-language: Haskell2010-  ghc-options:      -Wall -Wcompat+  -- Fault-inject a small stack so the wide-aggregation regression stays meaningful.+  ghc-options:      -Wall -Wcompat -with-rtsopts=-K1M    build-depends:-    , base     >= 4   && < 5-    , hedgehog >= 1.0 && < 1.6+    , base      >= 4   && < 5+    , directory >= 1.3 && < 1.4+    , filepath  >= 1.4 && < 1.6+    , hedgehog  >= 1.0 && < 1.6+    , process   >= 1.6 && < 1.7     , sizes++  build-tool-depends:+    sizes:sizes  source-repository head   type:     git
test/Spec.hs view
@@ -2,19 +2,44 @@  module Main where +import Control.Exception (bracket) import Control.Monad (unless)+import qualified Data.List as List import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range-import Sizes (EntryInfo (..), crossesFileSystemBoundary, humanReadable)-import System.Exit (exitFailure)+import Sizes (+    EntryInfo (..),+    combineEntryResults,+    crossesFileSystemBoundary,+    emptyReportEntries,+    humanReadable,+    reportEntriesToList,+ )+import System.Directory (createDirectory, getTemporaryDirectory, removeFile, removePathForcibly)+import System.Exit (ExitCode (ExitSuccess), exitFailure)+import System.FilePath ((</>))+import System.IO (hClose, openTempFile) import System.Posix.Types (DeviceID)+import System.Process (readProcessWithExitCode)  main :: IO () main = do     passed <- checkParallel $$(discover)     unless passed exitFailure +-- | Allocate a unique scratch directory and remove it after the test.+withScratchDirectory :: (FilePath -> IO a) -> IO a+withScratchDirectory = bracket create removePathForcibly+  where+    create = do+        temporaryRoot <- getTemporaryDirectory+        (path, handle) <- openTempFile temporaryRoot "sizes-test"+        hClose handle+        removeFile path+        createDirectory path+        pure path+ -- | Generate an EntryInfo with random count and size. genEntryInfo :: Gen EntryInfo genEntryInfo = do@@ -80,6 +105,137 @@ prop_monoid_right_identity_size = property $ do     e <- forAll genEntryInfo     _entryAllocSize (e <> mempty) === _entryAllocSize e++-- Wide directory aggregation must not require stack proportional to entry count.+prop_wide_directory_aggregation_stack_safe :: Property+prop_wide_directory_aggregation_stack_safe =+    withTests 1 . property $ do+        let entryCount = 200000+            leaf = mempty{_entryCount = 1, _entryAllocSize = 2}+            step aggregate _ =+                combineEntryResults+                    True+                    aggregate+                    (leaf, emptyReportEntries)+            (total, reports) =+                List.foldl'+                    step+                    (mempty, emptyReportEntries)+                    [1 .. entryCount]+        _entryCount total === entryCount+        _entryAllocSize total === 2 * entryCount+        length (reportEntriesToList reports) === entryCount++-- Report accumulation preserves sibling/preorder content and drops it past depth.+prop_report_entries_preserve_preorder :: Property+prop_report_entries_preserve_preorder =+    withTests 1 . property $ do+        let first = mempty{_entryCount = 1, _entryAllocSize = 10}+            child = mempty{_entryCount = 2, _entryAllocSize = 20}+            grandchild = mempty{_entryCount = 3, _entryAllocSize = 30}+            firstResult =+                combineEntryResults+                    True+                    (mempty, emptyReportEntries)+                    (first, emptyReportEntries)+            (_, grandchildReports) =+                combineEntryResults+                    True+                    (mempty, emptyReportEntries)+                    (grandchild, emptyReportEntries)+            (_, retained) =+                combineEntryResults+                    True+                    firstResult+                    (child, grandchildReports)+            (_, dropped) =+                combineEntryResults+                    False+                    firstResult+                    (child, grandchildReports)+            entryIdentity entry = (_entryCount entry, _entryAllocSize entry)+        fmap entryIdentity (reportEntriesToList retained)+            === [(1, 10), (2, 20), (3, 30)]+        reportEntriesToList dropped === []++-- The packaged executable traverses a real nested tree in report preorder.+prop_cli_traverses_nested_directory :: Property+prop_cli_traverses_nested_directory =+    withTests 1 . property $ do+        (root, child, file, exitCode, stdout, stderr) <- evalIO $+            withScratchDirectory $ \root -> do+                let child = root </> "child"+                    file = child </> "file"+                createDirectory child+                writeFile file "abc"+                (exitCode, stdout, stderr) <-+                    readProcessWithExitCode+                        "sizes"+                        ["-j1", "-a", "-s", "-d3", root]+                        ""+                pure (root, child, file, exitCode, stdout, stderr)+        annotate stderr+        exitCode === ExitSuccess+        fmap (last . words) (lines stdout)+            === [root ++ "/", child ++ "/", file]++-- A single -x pattern excludes matching paths but keeps the rest.+prop_cli_single_exclude_filters :: Property+prop_cli_single_exclude_filters =+    withTests 1 . property $ do+        (exitCode, stdout, stderr) <- evalIO $+            withScratchDirectory $ \root -> do+                let keepDir = root </> "keep"+                    dropDir = root </> "drop-me"+                mapM_+                    (\dir -> createDirectory dir >> writeFile (dir </> "marker") "x")+                    [keepDir, dropDir]+                readProcessWithExitCode+                    "sizes"+                    ["-j1", "-a", "-s", "-d3", "-x", "drop-me", root]+                    ""+        annotate stderr+        exitCode === ExitSuccess+        let reported = map (last . words) (lines stdout)+        annotate (unlines reported)+        assert $ any (List.isSuffixOf "keep/marker") reported+        assert $ not $ any (List.isSuffixOf "drop-me/marker") reported++-- Repeated -x flags accumulate: every provided pattern excludes its matches,+-- not just the last one.+prop_cli_multiple_excludes_accumulate :: Property+prop_cli_multiple_excludes_accumulate =+    withTests 1 . property $ do+        (exitCode, stdout, stderr) <- evalIO $+            withScratchDirectory $ \root -> do+                let keepDir = root </> "keep"+                    firstDir = root </> "drop-first"+                    secondDir = root </> "drop-second"+                mapM_+                    (\dir -> createDirectory dir >> writeFile (dir </> "marker") "x")+                    [keepDir, firstDir, secondDir]+                readProcessWithExitCode+                    "sizes"+                    [ "-j1"+                    , "-a"+                    , "-s"+                    , "-d3"+                    , "-x"+                    , "drop-first"+                    , "-x"+                    , "drop-second"+                    , root+                    ]+                    ""+        annotate stderr+        exitCode === ExitSuccess+        let reported = map (last . words) (lines stdout)+        annotate (unlines reported)+        -- The first -x pattern must still exclude: both flags accumulate+        -- rather than the last one winning.+        assert $ any (List.isSuffixOf "keep/marker") reported+        assert $ not $ any (List.isSuffixOf "drop-first/marker") reported+        assert $ not $ any (List.isSuffixOf "drop-second/marker") reported  -- humanReadable always returns a non-empty string prop_humanReadable_nonempty :: Property