Skip to content

Commit 24ba0ac

Browse files
committed
Use a haskell implementation of path completion rather than bash
1 parent 9f794e1 commit 24ba0ac

9 files changed

Lines changed: 114 additions & 28 deletions

File tree

src/Options/Applicative/Builder/Extra.hs

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
{-# LANGUAGE ScopedTypeVariables #-}
3+
14
-- | Extra functions for optparse-applicative.
25

36
module 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)
2333
import Data.Either.Combinators
34+
import Data.List (isPrefixOf)
2435
import Data.Maybe
2536
import Data.Monoid
37+
import Data.Text (Text)
38+
import qualified Data.Text as T
2639
import Options.Applicative
2740
import Options.Applicative.Types (readerAsk, Completer(..))
28-
import Path
41+
import Path hiding ((</>))
42+
import System.Directory (getCurrentDirectory, getDirectoryContents, doesDirectoryExist)
2943
import 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'.
3547
boolFlags :: Bool -- ^ Default value
@@ -147,23 +159,98 @@ optionalFirst = fmap First . optional
147159

148160
absFileOption :: Mod OptionFields (Path Abs File) -> Parser (Path Abs File)
149161
absFileOption mods = option (eitherReader' parseAbsFile) $
150-
completer (listCompleter ["/"] <> mapCompleter (filter (isJust . parseAbsFile)) (bashCompleter "file")) <> mods
162+
completer (pathCompleterWith defaultPathCompleterOpts { pcoRelative = False }) <> mods
151163

152164
relFileOption :: Mod OptionFields (Path Rel File) -> Parser (Path Rel File)
153165
relFileOption mods = option (eitherReader' parseRelFile) $
154-
completer (mapCompleter (filter (isJust . parseRelFile)) (bashCompleter "file")) <> mods
166+
completer (pathCompleterWith defaultPathCompleterOpts { pcoAbsolute = False }) <> mods
155167

156168
absDirOption :: Mod OptionFields (Path Abs Dir) -> Parser (Path Abs Dir)
157169
absDirOption 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

160172
relDirOption :: Mod OptionFields (Path Rel Dir) -> Parser (Path Rel Dir)
161173
relDirOption 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'.
165177
eitherReader' :: Show e => (String -> Either e a) -> ReadM a
166178
eitherReader' 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

src/Stack/Options/ConfigParser.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ configOptsParser currentDir hide0 =
5252
<*> optionalFirst (option (eitherReader (mapLeft showWorkDirError . parseRelDir))
5353
( long "work-dir"
5454
<> metavar "WORK-DIR"
55+
<> completer (pathCompleterWith (defaultPathCompleterOpts { pcoAbsolute = False, pcoFileFilter = const False }))
5556
<> help ("Relative path of work directory " ++
5657
"(Overrides any STACK_WORK environment variable, default is '.stack-work')")
5758
<> hide
@@ -85,21 +86,20 @@ configOptsParser currentDir hide0 =
8586
<*> fmap Set.fromList (many ((currentDir FilePath.</>) <$> strOption
8687
( long "extra-include-dirs"
8788
<> metavar "DIR"
88-
<> action "directory"
89+
<> completer dirCompleter
8990
<> help "Extra directories to check for C header files"
9091
<> hide
9192
)))
9293
<*> fmap Set.fromList (many ((currentDir FilePath.</>) <$> strOption
9394
( long "extra-lib-dirs"
9495
<> metavar "DIR"
95-
<> action "directory"
96+
<> completer dirCompleter
9697
<> help "Extra directories to check for libraries"
9798
<> hide
9899
)))
99100
<*> optionalFirst (absFileOption
100101
( long "with-gcc"
101102
<> metavar "PATH-TO-GCC"
102-
<> action "file"
103103
<> help "Use gcc found at PATH-TO-GCC"
104104
<> hide
105105
))
@@ -114,7 +114,7 @@ configOptsParser currentDir hide0 =
114114
<*> optionalFirst (strOption
115115
( long "local-bin-path"
116116
<> metavar "DIR"
117-
<> action "directory"
117+
<> completer dirCompleter
118118
<> help "Install binaries to DIR"
119119
<> hide
120120
))

src/Stack/Options/DockerParser.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ dockerOptsParser hide0 =
6565
<*> many (option auto (long (dockerOptName dockerMountArgName) <>
6666
hide <>
6767
metavar "(PATH | HOST-PATH:CONTAINER-PATH)" <>
68-
action "directory" <>
68+
completer dirCompleter <>
6969
help ("Mount volumes from host in container " ++
7070
"(may specify multiple times)")))
7171
<*> many (option str (long (dockerOptName dockerEnvArgName) <>
@@ -87,7 +87,7 @@ dockerOptsParser hide0 =
8787
long(dockerOptName dockerStackExeArgName) <>
8888
hide <>
8989
metavar (intercalate "|" (specialOpts ++ ["PATH"])) <>
90-
completer (listCompleter specialOpts <> bashCompleter "file") <>
90+
completer (listCompleter specialOpts <> fileCompleter) <>
9191
help (concat [ "Location of "
9292
, stackProgName
9393
, " executable used in container" ])))

src/Stack/Options/GhciParser.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ghciOptsParser = GhciOpts
1616
<$> many
1717
(textArgument
1818
(metavar "TARGET/FILE" <>
19-
action "file" <>
19+
completer (fileExtCompleter [".hs", ".lhs"]) <>
2020
help ("If none specified, use all local packages. " <>
2121
"See https://docs.haskellstack.org/en/v" <>
2222
showVersion Meta.version <>

src/Stack/Options/GlobalParser.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,7 @@ globalOptsParser currentDir kind defLogLevel =
4343
(strOption
4444
(long "stack-yaml" <>
4545
metavar "STACK-YAML" <>
46-
-- TODO: ideally we'd have a completer that filtered files
47-
-- to "*.yaml", but still completed paths
48-
action "file" <>
46+
completer (fileExtCompleter [".yaml"]) <>
4947
help ("Override project stack.yaml file " <>
5048
"(overrides any STACK_YAML environment variable)") <>
5149
hide))
@@ -75,8 +73,8 @@ initOptsParser =
7573
where
7674
searchDirs =
7775
many (textArgument
78-
(metavar "DIRS" <>
79-
action "directory" <>
76+
(metavar "DIR" <>
77+
completer dirCompleter <>
8078
help "Directories to include, default is current directory."))
8179
ignoreSubDirs = switch (long "ignore-subdirs" <>
8280
help "Do not search for .cabal files in sub directories")

src/Stack/Options/HpcReportParser.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ hpcReportOptsParser = HpcReportOpts
1515
<*> switch (long "all" <> help "Use results from all packages and components involved in previous --coverage run")
1616
<*> optional (strOption (long "destdir" <>
1717
metavar "DIR" <>
18-
action "directory" <>
18+
completer dirCompleter <>
1919
help "Output directory for HTML report"))
2020
<*> switch (long "open" <> help "Open the report in the browser")
2121

src/Stack/Options/NixParser.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ nixOptsParser hide0 = overrideActivation <$>
3030
str
3131
(long "nix-shell-file" <>
3232
metavar "FILE" <>
33-
action "file" <>
33+
completer (fileExtCompleter [".nix"]) <>
3434
help "Nix file to be used to launch a nix-shell (for regular Nix users)" <>
3535
hide))
3636
<*> optionalFirst

src/Stack/Options/ScriptParser.hs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module Stack.Options.ScriptParser where
22

33
import Data.Monoid ((<>))
44
import Options.Applicative
5+
import Options.Applicative.Builder.Extra
56

67
data ScriptOpts = ScriptOpts
78
{ soPackages :: ![String]
@@ -20,7 +21,7 @@ data ScriptExecute
2021
scriptOptsParser :: Parser ScriptOpts
2122
scriptOptsParser = ScriptOpts
2223
<$> many (strOption (long "package" <> help "Additional packages that must be installed"))
23-
<*> strArgument (metavar "FILE" <> action "file")
24+
<*> strArgument (metavar "FILE" <> completer (fileExtCompleter [".hs", ".lhs"]))
2425
<*> many (strArgument (metavar "-- ARGS (e.g. stack ghc -- X.hs -o x)"))
2526
<*> (flag' SECompile
2627
( long "compile"

src/main/Main.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ commandLineHandler currentDir progName isInterpreter = complicatedOptions
297297
"upload"
298298
"Upload a package to Hackage"
299299
uploadCmd
300-
((,,,,) <$> many (strArgument $ metavar "TARBALL/DIR") <*>
300+
((,,,,) <$> many (strArgument $ metavar "TARBALL/DIR" <> completer fileCompleter) <*>
301301
optional pvpBoundsOption <*>
302302
ignoreCheckSwitch <*>
303303
switch (long "no-signature" <> help "Do not sign & upload signatures") <*>
@@ -309,7 +309,7 @@ commandLineHandler currentDir progName isInterpreter = complicatedOptions
309309
"sdist"
310310
"Create source distribution tarballs"
311311
sdistCmd
312-
((,,,,) <$> many (strArgument $ metavar "DIR") <*>
312+
((,,,,) <$> many (strArgument $ metavar "DIR" <> completer dirCompleter) <*>
313313
optional pvpBoundsOption <*>
314314
ignoreCheckSwitch <*>
315315
switch (long "sign" <> help "Sign & upload signatures") <*>

0 commit comments

Comments
 (0)