packages feed

denominate (empty) → 0.4.1

raw patch · 5 files changed

+409/−0 lines, 5 filesdep +basedep +directorydep +filepathbuild-type:Customsetup-changed

Dependencies added: base, directory, filepath

Files

+ LICENSE.txt view
@@ -0,0 +1,27 @@+Copyright (c) Calvin Smith++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.+2. 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.+3. Neither the name of the author nor the names of his contributors+   may be used to endorse or promote products derived from this software+   without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 AUTHORS 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.
+ Main.hs view
@@ -0,0 +1,34 @@+import System.Environment(getArgs, getProgName)+import System.Exit(exitFailure)+import Control.Monad(when)+import System.Denominate++usage = getProgName >>= \name ->+          return ("Usage: " ++ name ++ " base_directory") >>=+          putStrLn >> exitFailure++main = do+  getArgs >>= \args ->+    when (doUsageAndExit args) usage >>+      (fileToTypedFilePath . base) args >>=+         renameAll defaultFilenameConverter >>=+            mapM_ handle++-- Get base dir to recurse in and ensure there is no terminal '/'.+base [] = error "Main.base"+base (d:_) =+  case d == "/" of+    True    -> d+    False   -> if (head revD) /= '/' +                  then d+                  else reverse $ tail revD+  where revD = reverse d++handle result =+  case result of+    (Failure (_, fName) msg) -> putStr msg >> putStr " [" >>+                                    putStr fName >> putStrLn "]"+    (Success  _         _  )  -> return ()++doUsageAndExit args = null args || arg0 == "-h" || arg0 == "--help"+  where arg0 = head args
+ Setup.hs view
@@ -0,0 +1,13 @@+import Distribution.Simple+import Distribution.PackageDescription (PackageDescription)+import Distribution.Simple.LocalBuildInfo (LocalBuildInfo, buildDir)+import System.Exit+import Control.Monad+import System.Process++main :: IO ()+main = defaultMainWithHooks (defaultUserHooks { runTests = test })++test :: Args -> Bool -> PackageDescription -> LocalBuildInfo -> IO ()+test _ _ _ _ = runCommand commandStr >>= waitForProcess >> return ()+  where commandStr = "./quickcheck +names System/Denominate_Test.hs"
+ System/Denominate.hs view
@@ -0,0 +1,295 @@+{- |+Module      :  $Header$+Description :  Supports bulk renaming of directories and files into a+               standard, normalized format.+Copyright   :  (c) Calvin Smith+License     :  BSD3, see LICENSE.txt++Maintainer  :  Calvin Smith <cs-haskell@protempore.net>+Stability   :  Experimental+Portability :  portable++Functions to assist in renaming of directories and files into a+standard, normalized format.++This module defines several functions supporting renaming of files+and directories, and is especially useful for doing a bulk renaming+of all files and directories, recursively, in a given base directory.++The primary functions of interest are 'rename' and 'renameAll', both+of which accept a function for creating the new filename based on its+current name. The user may supply a custom filename converter function,+or may use the pre-defined function that this module defines.++The standard pre-defined converter determines the new name for a file+or directory using the following rules:++  1. all letters are converted to lowercase;++  2. all non-alphanumeric characters at the beginning of a file or+     directory name are removed (with the exception of an initial '.');++  3. all non-alphanumeric characters at the end of a directory name+     or the end of a filename (before the extension) are removed;++  4. all other blocks of one or more non-alphanumeric characters are+     converted to a single hyphen.++See the documentation of the exported functions for more information.+-}++module System.Denominate (FileType(Directory, File),+                   RenameResult(Success, Failure),+                    TypedFilePath, FilenameConverter,+                    normalizeFilename, allFilepaths,+                    rename, renameAll,+                    fileToTypedFilePath, defaultFilenameConverter)+where++import System.Directory+import System.FilePath+import System.IO+import Data.Char+import Data.List+import Control.Monad+import Control.Exception++-- |Represents the type of a file or directory. These are exhaustive,+-- as for purposes of this module, we consider everything that isn't+-- a directory to be a file.+data FileType = Directory | File+  deriving (Eq, Show, Enum, Ord)++-- |Represents the result of a rename attempt, which either fails or+-- succeeds. In both cases, the typed file path is that of the file+-- for which a rename attempt was made. Upon failure, the string+-- parameter contains information about the error (may have been an+-- os-level error or user error); upon success, the string parameter+-- is the name to which the file was renamed (which includes the case+-- that no change was performed because old and new names were equal).+data RenameResult = Failure TypedFilePath String |+                    Success TypedFilePath String+  deriving (Eq, Show)++-- |Represents a filepath together with the type of file to which +-- the path refers.+type TypedFilePath = (FileType, FilePath)++-- |A filename converter maps an old filename to a new filename. A converter+-- takes a typed file representing a directory name or a filename without+-- extension, and only needs to determine the new name based on the old name.+-- It does not need to worry about extracting the file path from an absolute+-- path or determining the file extension, as all functions in this module+-- that use a FilenameConverter will only pass in typed files containing+-- the last directory (if a directory) or the filename without extension+-- if a file.+type FilenameConverter = TypedFilePath -> FilePath++-- |Rename a single file or directory using the supplied filename converter,+-- which will be passed just the directory name (without any parent+-- directories or a terminal slash) in the case of a directory or just+-- the filename without the extension in the case of a file. If there+-- already exists a file with the intended new name, nothing is done. If+-- the new name is the same as the old name (not considering file extension),+-- then the function successfully returns without touching the filesystem.+-- In all cases where a file is renamed, the extension of a file will be+-- automatically converted to lowercase but otherwise remains the+-- same (no characters are ever removed from the extension).+rename :: FilenameConverter -> TypedFilePath -> IO (RenameResult)+rename convFunc f@(_, oldPath) =+  let+      newPath     =  normalizeFilename convFunc f+      fail f msg  =  return (Failure f msg)+  in+      case (oldPath == newPath) of+        True -> return (Success f oldPath)+        _    -> do exists <- fileExists newPath+                   if exists+                      then fail f "WARN: File already exists with new name."+                      else doRenameSafe f newPath++-- |Renames the old file or directory to the new path, returning+-- a result that indicates success or failure. If successful,+-- the name of the new file is the success message; if unsuccessful,+-- the failure message gives more information.+doRenameSafe :: TypedFilePath -> FilePath -> IO RenameResult+doRenameSafe f newPath =+  handle (\exc -> return (Failure f ("ERROR: " ++ show exc)))+         (doRename f newPath >> (return (Success f newPath)))+  where+    doRename :: TypedFilePath -> FilePath -> IO()+    doRename (fileType, oldPath) newPath =+      case fileType of+        Directory -> renameDirectory oldPath newPath+        File      -> renameFile      oldPath newPath++-- |Rename all files and directories, recursively, in the given directory,+-- using the supplied filename converter to determine the new name of each+-- file or directory. The converter function will be called once for each+-- file or directory, and will be passed just the directory name (without+-- the parent directories) in the case of a directory or just the filename+-- without the extension in the case of a file. The extension of files (but+-- not directories if they seem to have an extension) will be converted to+-- lower case, but is not otherwise changed. There will be one RenameResult+-- for each success or failure, and an indication of the reason for failure+-- for failures, or the new name in case of success.+renameAll :: FilenameConverter -> TypedFilePath -> IO ([RenameResult])+renameAll fn baseDir = (allFilepaths . snd) baseDir >>= mapM (rename fn)++-- |Determine if the filename does not represent a dot file ("." or "..").+isNotDotFile :: FilePath -> Bool+isNotDotFile = not . flip elem [".", ".."]++-- |Determine whether there exists a file or directory with the given path.+fileExists :: FilePath -> IO Bool+fileExists path = do+  b1 <- doesDirectoryExist path+  b2 <- doesFileExist path+  return (b1 || b2)++-- |Generate a list of all files and directories below the given directory,+-- in depth-first order such that all files in a given directory appear+-- before the directory or any of its parent directories.+allFilepaths :: FilePath -> IO [TypedFilePath]+allFilepaths dir = do+  -- get [TypedFilePath] for files in this dir, dirs sorted before files+  dirTypedFilePaths <- getFilesAndDirectories dir+  -- get [TypedFilePath] for each subdir in this dir+  subDirTypedFilePaths <- mapM+                        (allFilepaths . snd) $+                        filter (isDirectoryFileType . fst) dirTypedFilePaths+  -- concatenate all the subdir paths with the paths from this dir+  return (prepend subDirTypedFilePaths dirTypedFilePaths)++-- |Get a list of files and directories, as TypedFilePath, for the given+-- directory, with directories sorted before files.+getFilesAndDirectories :: String -> IO [TypedFilePath]+getFilesAndDirectories dir =+    getDirectoryContents dir >>=+        filterM (return . isNotDotFile) >>=+        return . map (joinFileName dir) >>=+        mapM fileToTypedFilePath >>=+        return . sortPaths++-- |Converts a list of lists and a list into a single list,+-- ensuring that all items of the single list are *after*+-- all items from the list of lists. E.g.,+-- prepend [[1,2], [3,4], [5,6,7]] [66,67] == [1,2,3,4,5,6,7,66,67]+prepend :: [[a]] -> [a] -> [a]+prepend []     out = out+prepend (x:xs) out = x ++ prepend xs out++-- |Determine whether the given FileType is Directory.+isDirectoryFileType :: FileType -> Bool+isDirectoryFileType Directory = True+isDirectoryFileType _         = False++-- |Convert a filepath to a TypedFilePath. A Directory is a file+-- for which Directory.doesDirectoryExist returns true. If the path+-- does not represent a directory, it is considered a file, and there+-- is no further testing to verify that a file with that path actually+-- exists.+fileToTypedFilePath :: FilePath -> IO TypedFilePath+fileToTypedFilePath filepath =+  doesDirectoryExist filepath >>= \b ->+    case b of+      True   ->  return (Directory, filepath)+      False  ->  return (File, filepath)++-- |Sort the paths in a given directory by putting all directories first+-- and then sorting by path within each type. It is designed for sorting+-- all files and directories in the top level of a directory, and will+-- probably not provide a useful ordering if full paths are used.+sortPaths :: [TypedFilePath] -> [TypedFilePath]+sortPaths = sortBy compareTypedFilePaths++-- |Compare two typed file paths, using the built-in ordering of+-- FileType (dir before file) and path.+compareTypedFilePaths :: TypedFilePath -> TypedFilePath -> Ordering+compareTypedFilePaths (f1type, f1path) (f2type, f2path) =+  case typeComp of+    EQ          -> compare f1path f2path+    _           -> typeComp+  where typeComp = compare f1type f2type++-- |Normalize the filename of the given typed file path using the+-- supplied FilenameConverter function, which will be passed the+-- directory name (without parent directories) in case of a directory+-- or the filename (without any parent directories or the extension)+-- in case of a file. This function takes care of extracting the part+-- of the path that is to be normalized, calling the user-supplied+-- function with only that part, and then reassembling the+-- result of the filename converter into a full path again.+normalizeFilename :: FilenameConverter -> TypedFilePath -> String+normalizeFilename fn (fileType, origPath) =+  let (dir, filenameWithExt)   =  dirAndFile origPath+      (filenameNoExt, ext)     =  if fileType == Directory+                                     then (filenameWithExt, "")+                                     else fileAndExt filenameWithExt+      newFilenameNoExt = fn (fileType, filenameNoExt)+      result =  joinFileName dir $ joinFileExt (if null newFilenameNoExt +                                                  then filenameNoExt +                                                  else newFilenameNoExt) (map toLower ext)+  in if null result then origPath else result++-- |The default filename converter, which normalizes a filename by+-- converting letters to lowercase and converting one or more undesirable+-- characters into a single hyphen (or removing altogether if at the+-- beginning or the end of the name). The only exception to these rules+-- is that an initial dot of a filename is not removed.+defaultFilenameConverter :: FilenameConverter+defaultFilenameConverter (_, path) = if isDotFile then ('.':result) else result+  where result = convert' Initial path+        isDotFile = not (null path) && head path == '.'++convert' :: State -> String -> String+convert' _      []      = []+convert' currState  (i:is)  =+  case (currState, nextState) of+    (HyphenBlock, NormalBlock) -> '-' : chr : rest+    (_          , NormalBlock) ->       chr : rest+    (_          , _          ) ->             rest+  where+    rest = convert' nextState is+    (emitted, nextState) = transition currState i+    chr = toLower $ maybe (error "FilenameNormalizer.normalize'") id emitted+++-- |The state of finite state machine for normalizing.+data State =   Initial     -- initial state, until enter normal+             | HyphenBlock -- in block of chars to hyphenate+             | NormalBlock -- in block of normal chars to lowercase+  deriving (Eq, Show, Enum)++-- |The transition function of the fsm. The transition rules are+-- very simple: if we see a normal character, we always emit it+-- and transition to normal state. If we see a non-normal character,+-- we never emit it, and we stay in initial state if currently+-- in initial state, or else transition to hyphen state.+-- The normalize' function above takes care of emitting the+-- hyphen when we transition from hyphen to normal.+transition :: State -> Char -> (Maybe Char, State)+transition currState c+  | isAlphaNum c = (Just c, NormalBlock)+  | otherwise    = (Nothing, nonNormalState)+  where nonNormalState = case currState of+                           Initial   -> Initial+                           _         -> HyphenBlock++joinFileName :: String -> String -> String+joinFileName dirpath filename = joinPath [dirpath, filename]++joinFileExt :: String -> String -> String+joinFileExt filename ext = addExtension filename ext++-- |Split path into directory part (without slash) and file part.+dirAndFile :: FilePath -> (String, String)+dirAndFile path = splitFileName path++-- |Split file path into filename and ext. If +fileAndExt :: FilePath -> (String, String)+fileAndExt filename = +  case splitExtension filename of+    ([],   ext) -> (ext, [])+    (file, ext) -> (file, ext)+
+ denominate.cabal view
@@ -0,0 +1,40 @@+name:              denominate+version:           0.4.1+cabal-version:     >= 1.2+license:           BSD3+license-file:      LICENSE.txt+author:            Calvin Smith+copyright:         (c) Calvin Smith+maintainer:        cs-haskell@protempore.net+homepage:          http://protempore.net/denominate/+category:          System+stability:         Experimental+synopsis:          Functions supporting bulk file and directory name +                   normalization.+description:       Denominate provides a main program for performing bulk +                   file and directory renaming, using a built-in filename+                   converter or user-defined converters.+                   .+                   Additionally, it provides a module that exposes some +                   functions related to renaming and name normalization +                   that might be otherwise useful.++flag small_base+  description:     Choose the new smaller, split-up base package.++library+  exposed-modules: System.Denominate+  if flag(small_base)+    build-depends: base >= 3, directory, filepath+  else+    build-depends: base < 3, filepath+  ghc-options:     -O -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-orphans++executable denominate+  main-is:         Main.hs+  if flag(small_base)+    build-depends: base >= 3, directory, filepath+  else+    build-depends: base < 3, filepath+  ghc-options:     -O -Wall -fno-warn-name-shadowing -fno-warn-missing-signatures -fno-warn-orphans+