-
Notifications
You must be signed in to change notification settings - Fork 5
/
FixWhitespace.hs
235 lines (193 loc) · 7.7 KB
/
FixWhitespace.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
-- | Program to enforce a whitespace policy.
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Monad ( unless, when )
import Data.List.Extra ( nubOrd )
import qualified Data.Text as Text
import qualified Data.Text.IO as Text {- Strict IO -}
import Data.Version ( showVersion )
import System.Console.GetOpt ( OptDescr(Option), ArgDescr(NoArg, ReqArg), ArgOrder(Permute), getOpt, usageInfo )
import System.Directory ( getCurrentDirectory, doesFileExist )
import System.Environment ( getArgs, getProgName )
import System.Exit ( die, exitFailure, exitSuccess )
import System.FilePattern.Directory ( getDirectoryFiles, getDirectoryFilesIgnore )
import System.IO ( IOMode(WriteMode), hPutStr, hPutStrLn, hSetEncoding, stderr, utf8, withFile )
import Text.Read ( readMaybe )
import Data.Text.FixWhitespace ( CheckResult(CheckOK, CheckViolation, CheckIOError), checkFile, displayLineError
, TabSize, Verbose, defaultTabSize )
import ParseConfig ( Config(Config), parseConfig )
import qualified Paths_fix_whitespace as PFW ( version )
-- | Default configuration file.
defaultConfigFile :: String
defaultConfigFile = "fix-whitespace.yaml"
-- Modes.
data Mode
= Fix -- ^ Fix whitespace issues.
| Check -- ^ Check if there are any whitespace issues.
deriving (Show, Eq)
data Options = Options
{ optVerbose :: Verbose
-- ^ Display the location of a file being checked or not.
, optHelp :: Bool
-- ^ Display the help information.
, optVersion :: Bool
-- ^ Display the program's version.
, optMode :: Mode
, optConfig :: FilePath
-- ^ The location to the configuration file.
, optTabSize :: String
-- ^ The number of spaces to expand a tab character to. @"0"@ for keeping tabs.
}
defaultOptions :: Options
defaultOptions = Options
{ optVerbose = False
, optHelp = False
, optVersion = False
, optMode = Fix
, optConfig = defaultConfigFile
, optTabSize = show defaultTabSize
}
options :: [OptDescr (Options -> Options)]
options =
[ Option ['h'] ["help"]
(NoArg (\opts -> opts { optHelp = True }))
"Show this help information."
, Option ['V'] ["version"]
(NoArg (\opts -> opts { optVersion = True }))
"Show the program's version."
, Option ['v'] ["verbose"]
(NoArg (\opts -> opts { optVerbose = True }))
(unlines
[ "Show files as they are being checked."
, "Display location of detected whitespace violations."
])
, Option ['t'] ["tab"]
(ReqArg (\ts opts -> opts { optTabSize = ts }) "TABSIZE")
(unlines
[ "Expand tab characters to TABSIZE (default: " ++ show defaultTabSize ++ ") many spaces."
, "Keep tabs if 0 is given as TABSIZE."
])
, Option [] ["config"]
(ReqArg (\loc opts -> opts { optConfig = loc }) "CONFIG")
(concat ["Override the project configuration ", defaultConfigFile, "."])
, Option [] ["check"]
(NoArg (\opts -> opts { optMode = Check }))
(unlines
[ "With --check the program does not change any files,"
, "it just checks if any files would have been changed."
, "In the latter case it returns with a non-zero exit code."
])
]
programOpts :: String -> IO (Options, [String])
programOpts progName = do
argv <- getArgs
case getOpt Permute options argv of
(o, n, [] ) -> return (foldl (flip id) defaultOptions o, n)
(_, _, errs) -> ioError $ userError $ concat errs ++ "\n" ++ shortUsageHeader progName
shortUsageHeader :: String -> String
shortUsageHeader progName = unwords
[ "Usage:"
, progName
, "[-h|--help] [-v|--verbose] [--check] [--config CONFIG] [-t|--tab TABSIZE] [FILES]"
]
usageHeader :: String -> String
usageHeader progName = unlines
[ shortUsageHeader progName
, ""
, "The program does the following"
, ""
, " * Removes trailing whitespace."
, " * Removes trailing lines containing nothing but whitespace."
, " * Ensures that the file ends in a newline character."
, " * Convert tabs to TABSIZE (default: " ++ show defaultTabSize ++ ") spaces, unless TABSIZE is set to 0."
, ""
, "for files specified in [FILES] or"
, ""
, "\t" ++ defaultConfigFile
, ""
, "under the current directory."
, ""
, "Available options:"
]
usage :: String -> String
usage progName = usageInfo (usageHeader progName) options
main :: IO ()
main = do
progName <- getProgName
(opts, nonOpts) <- programOpts progName
-- check if the user asks for help
when (optHelp opts) $ putStr (usage progName) >> exitSuccess
-- check if the user asks for the program's version
when (optVersion opts) $ putStrLn (showVersion PFW.version) >> exitSuccess
-- check if the configuration file exists
configExist <- doesFileExist $ optConfig opts
unless (configExist || not (null nonOpts)) $ do
hPutStr stderr $ unlines
[ unwords [defaultConfigFile, "is not found and there are no files specified as arguments."]
, ""
, shortUsageHeader progName
]
exitFailure
let mode = optMode opts
verbose = optVerbose opts
config = optConfig opts
tabSize <- maybe (die "Error: Illegal TABSIZE, must be an integer.") return $
readMaybe $ optTabSize opts
base <- getCurrentDirectory
files <- if not $ null nonOpts
then getDirectoryFiles base nonOpts
else do
Config incDirs0 excDirs0 incFiles excFiles <- parseConfig config
let incDirs = map (++ "/**/") incDirs0
let excDirs = map (++ "/**/") excDirs0
-- File patterns to always include
-- when not matching an excluded file pattern
let incWhitelistPatterns = concatMap (\d -> map (d ++) incFiles) incDirs
-- File patterns to always exclude
let excBlacklistPatterns = map ("**/" ++) excFiles
-- Files to include when not in an excluded directory
-- and when not matching an excluded file pattern
let incPatterns = map ("**/" ++) incFiles
-- Directory and file patterns to exclude
let excPatterns = (map (++ "*") excDirs)
++ (map ("**/" ++) excFiles)
when verbose $ do
putStrLn "Include whitelist:"
putStrLn (unlines incWhitelistPatterns)
putStrLn "Exclude blacklist:"
putStrLn (unlines excBlacklistPatterns)
putStrLn "Include:"
putStrLn (unlines incPatterns)
putStrLn "Exclude:"
putStrLn (unlines excPatterns)
files0 <- getDirectoryFilesIgnore base incWhitelistPatterns excBlacklistPatterns
files1 <- getDirectoryFilesIgnore base incPatterns excPatterns
return (nubOrd (files0 ++ files1))
changes <- mapM (fix mode verbose tabSize) files
when (or changes && mode == Check) exitFailure
fix :: Mode -> Verbose -> TabSize -> FilePath -> IO Bool
fix mode verbose tabSize f =
checkFile tabSize verbose f >>= \case
CheckOK -> do
when verbose $
putStrLn $ "[ Checked ] " ++ f
return False
CheckViolation s vs -> do
hPutStrLn stderr (msg vs)
when (mode == Fix) $
withFile f WriteMode $ \h -> do
hSetEncoding h utf8
Text.hPutStr h s
return True
CheckIOError _e -> do
hPutStrLn stderr $
"[ Read error ] " ++ f
return False
where
msg vs
| mode == Fix =
"[ Violation fixed ] " ++ f
| otherwise =
"[ Violation detected ] " ++ f ++
(if not verbose then "" else
":\n" ++ unlines (map (Text.unpack . displayLineError f) vs))