1+ {-# LANGUAGE RecordWildCards #-}
2+ {-# LANGUAGE ScopedTypeVariables #-}
3+
14-- | Extra functions for optparse-applicative.
25
36module Options.Applicative.Builder.Extra
@@ -17,19 +20,28 @@ module Options.Applicative.Builder.Extra
1720 ,absDirOption
1821 ,relDirOption
1922 ,eitherReader'
23+ ,fileCompleter
24+ ,fileExtCompleter
25+ ,dirCompleter
26+ ,PathCompleterOpts (.. )
27+ ,defaultPathCompleterOpts
28+ ,pathCompleterWith
2029 ) where
2130
22- import Control.Monad (when )
31+ import Control.Exception (IOException , catch )
32+ import Control.Monad (when , forM )
2333import Data.Either.Combinators
34+ import Data.List (isPrefixOf )
2435import Data.Maybe
2536import Data.Monoid
37+ import Data.Text (Text )
38+ import qualified Data.Text as T
2639import Options.Applicative
2740import Options.Applicative.Types (readerAsk , Completer (.. ))
28- import Path
41+ import Path hiding ((</>) )
42+ import System.Directory (getCurrentDirectory , getDirectoryContents , doesDirectoryExist )
2943import System.Environment (withArgs )
30- import System.FilePath (takeBaseName )
31- import Data.Text (Text )
32- import qualified Data.Text as T
44+ import System.FilePath (takeBaseName , (</>) , splitFileName , isRelative , takeExtension )
3345
3446-- | Enable/disable flags for a 'Bool'.
3547boolFlags :: Bool -- ^ Default value
@@ -147,23 +159,98 @@ optionalFirst = fmap First . optional
147159
148160absFileOption :: Mod OptionFields (Path Abs File ) -> Parser (Path Abs File )
149161absFileOption mods = option (eitherReader' parseAbsFile) $
150- completer (listCompleter [ " / " ] <> mapCompleter ( filter (isJust . parseAbsFile)) (bashCompleter " file " ) ) <> mods
162+ completer (pathCompleterWith defaultPathCompleterOpts { pcoRelative = False } ) <> mods
151163
152164relFileOption :: Mod OptionFields (Path Rel File ) -> Parser (Path Rel File )
153165relFileOption mods = option (eitherReader' parseRelFile) $
154- completer (mapCompleter ( filter (isJust . parseRelFile)) (bashCompleter " file " ) ) <> mods
166+ completer (pathCompleterWith defaultPathCompleterOpts { pcoAbsolute = False } ) <> mods
155167
156168absDirOption :: Mod OptionFields (Path Abs Dir ) -> Parser (Path Abs Dir )
157169absDirOption mods = option (eitherReader' parseAbsDir) $
158- completer (listCompleter [ " / " ] <> mapCompleter ( filter (isJust . parseAbsDir)) (bashCompleter " directory " ) ) <> mods
170+ completer (pathCompleterWith defaultPathCompleterOpts { pcoRelative = False , pcoFileFilter = const False } ) <> mods
159171
160172relDirOption :: Mod OptionFields (Path Rel Dir ) -> Parser (Path Rel Dir )
161173relDirOption mods = option (eitherReader' parseRelDir) $
162- completer (mapCompleter ( filter (isJust . parseRelDir)) (bashCompleter " directory " ) ) <> mods
174+ completer (pathCompleterWith defaultPathCompleterOpts { pcoAbsolute = False , pcoFileFilter = const False } ) <> mods
163175
164176-- | Like 'eitherReader', but accepting any @'Show' e@ on the 'Left'.
165177eitherReader' :: Show e => (String -> Either e a ) -> ReadM a
166178eitherReader' f = eitherReader (mapLeft show . f)
167179
168- mapCompleter :: ([String ] -> [String ]) -> Completer -> Completer
169- mapCompleter f (Completer g) = Completer (\ x -> fmap f (g x))
180+ data PathCompleterOpts = PathCompleterOpts
181+ { pcoAbsolute :: Bool
182+ , pcoRelative :: Bool
183+ , pcoRootDir :: Maybe FilePath
184+ , pcoFileFilter :: FilePath -> Bool
185+ , pcoDirFilter :: FilePath -> Bool
186+ }
187+
188+ defaultPathCompleterOpts :: PathCompleterOpts
189+ defaultPathCompleterOpts = PathCompleterOpts
190+ { pcoAbsolute = True
191+ , pcoRelative = True
192+ , pcoRootDir = Nothing
193+ , pcoFileFilter = const True
194+ , pcoDirFilter = const True
195+ }
196+
197+ fileCompleter :: Completer
198+ fileCompleter = pathCompleterWith defaultPathCompleterOpts
199+
200+ fileExtCompleter :: [String ] -> Completer
201+ fileExtCompleter exts = pathCompleterWith defaultPathCompleterOpts { pcoFileFilter = (`elem` exts) . takeExtension }
202+
203+ dirCompleter :: Completer
204+ dirCompleter = pathCompleterWith defaultPathCompleterOpts { pcoFileFilter = const False }
205+
206+ pathCompleterWith :: PathCompleterOpts -> Completer
207+ pathCompleterWith PathCompleterOpts {.. } = mkCompleter $ \ inputRaw -> do
208+ -- Unescape input, to handle single and double quotes. Note that the
209+ -- results do not need to be re-escaped, due to some fiddly bash
210+ -- magic.
211+ let input = unescapeBashArg inputRaw
212+ let (inputSearchDir0, searchPrefix) = splitFileName input
213+ inputSearchDir = if inputSearchDir0 == " ./" then " " else inputSearchDir0
214+ msearchDir <-
215+ case (isRelative inputSearchDir, pcoAbsolute, pcoRelative) of
216+ (True , _, True ) -> do
217+ rootDir <- maybe getCurrentDirectory return pcoRootDir
218+ return $ Just (rootDir </> inputSearchDir)
219+ (False , True , _) -> return $ Just inputSearchDir
220+ _ -> return Nothing
221+ case msearchDir of
222+ Nothing
223+ | input == " " && pcoAbsolute -> return [" /" ]
224+ | otherwise -> return []
225+ Just searchDir -> do
226+ entries <- getDirectoryContents searchDir `catch` \ (_ :: IOException ) -> return []
227+ results <- fmap catMaybes $ forM entries $ \ entry ->
228+ -- Skip . and .. unless user is typing . or ..
229+ if entry `elem` [" .." , " ." ] && searchPrefix `notElem` [" .." , " ." ] then return Nothing else
230+ if searchPrefix `isPrefixOf` entry
231+ then do
232+ let path = searchDir </> entry
233+ case (pcoFileFilter path, pcoDirFilter path) of
234+ (True , True ) -> return $ Just (inputSearchDir </> entry)
235+ (fileAllowed, dirAllowed) -> do
236+ isDir <- doesDirectoryExist path
237+ if (if isDir then dirAllowed else fileAllowed)
238+ then return $ Just (inputSearchDir </> entry)
239+ else return Nothing
240+ else return Nothing
241+ return results
242+
243+ unescapeBashArg :: String -> String
244+ unescapeBashArg (' \' ' : rest) = rest
245+ unescapeBashArg (' \" ' : rest) = go rest
246+ where
247+ go [] = []
248+ go (' \\ ' : x : xs)
249+ | x `elem` " $`\"\\\n " = x : xs
250+ | otherwise = ' \\ ' : x : go xs
251+ go (x : xs) = x : go xs
252+ unescapeBashArg input = go input
253+ where
254+ go [] = []
255+ go (' \\ ' : x : xs) = x : go xs
256+ go (x : xs) = x : go xs
0 commit comments