opt-env-conf 0.15.0.0 → 0.15.0.1
raw patch · 3 files changed
+79/−23 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +7/−0
- opt-env-conf.cabal +1/−1
- src/OptEnvConf/Completer.hs +71/−22
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog +## [0.15.0.1] - 2026-03-18++### Changed++* Fixed autocompletion for paths containing `..` components.+ Paths like `../`, `../foo`, and `bar/../` now complete correctly.+ ## [0.15.0.0] - 2026-03-17 ### Added
opt-env-conf.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: opt-env-conf-version: 0.15.0.0+version: 0.15.0.1 synopsis: Settings parsing for Haskell: command-line arguments, environment variables, and configuration values. homepage: https://github.com/NorfairKing/opt-env-conf#readme bug-reports: https://github.com/NorfairKing/opt-env-conf/issues
src/OptEnvConf/Completer.hs view
@@ -63,18 +63,17 @@ filePath :: Completer filePath = Completer $ \fp' -> do here <- getCurrentDir+ let (prefix, fp, baseDir) = splitDotDot here fp'+ filePathFromDir baseDir prefix fp fp' - -- An empty string is not a valid relative file or dir, but it is the most- -- common option so we special case it here- let (prefix, fp) = stripCurDir fp'+filePathFromDir :: Path Abs Dir -> String -> FilePath -> FilePath -> IO [CompletionResult]+filePathFromDir baseDir prefix fp fp' = do fmap (filterPrefix fp' . map (addPrefix prefix)) $ do let listDirForgiving d = fromMaybe ([], []) <$> forgivingAbsence (listDirRel d) (dirsFromParentListing, filesFromParentListing) <- case parseSomeDir fp of Nothing -> case fp of [] -> do- -- This is not a valid rel dir but still a prefix of a valid rel dir:- -- the current dir- (ds, fs) <- listDirRel here+ (ds, fs) <- listDirRel baseDir pure ( map fromRelDir $ filter (not . hiddenRel) ds, map fromRelFile $ filter (not . hiddenRel) fs@@ -87,7 +86,8 @@ map (fromAbsFile . (ad </>)) $ filter (not . hiddenRel) fs ) Just (Rel rd) -> do- (ds, fs) <- listDirForgiving rd+ let ad = baseDir </> rd+ (ds, fs) <- listDirForgiving ad pure ( map (fromRelDir . (rd </>)) $ filter (not . hiddenRel) ds, map (fromRelFile . (rd </>)) $ filter (not . hiddenRel) fs@@ -95,11 +95,9 @@ (dirsFromPartialListing, filesFromPartialListing) <- case parseSomeFile fp of Nothing ->- -- This is not a valid rel file but still a prefix of a valid- -- (hidden) rel file. if fp == "." then do- (ds, fs) <- listDirRel here+ (ds, fs) <- listDirRel baseDir pure ( map fromRelDir ds, map fromRelFile fs@@ -115,8 +113,9 @@ ) Just (Rel rf) -> do let dir = parent rf+ let ad = baseDir </> dir let filterHidden = if hiddenRel rf then id else filter (not . hiddenRel)- (ds, fs) <- listDirForgiving dir+ (ds, fs) <- listDirForgiving ad pure ( map (fromRelDir . (dir </>)) $ filterHidden ds, map (fromRelFile . (dir </>)) $ filterHidden fs@@ -148,25 +147,25 @@ directoryPath :: Completer directoryPath = Completer $ \fp' -> do here <- getCurrentDir+ let (prefix, fp, baseDir) = splitDotDot here fp'+ directoryPathFromDir baseDir prefix fp fp' - -- An empty string is not a valid relative file or dir, but it is the most- -- common option so we special case it here- let (prefix, fp) = stripCurDir fp'+directoryPathFromDir :: Path Abs Dir -> String -> FilePath -> FilePath -> IO [CompletionResult]+directoryPathFromDir baseDir prefix fp fp' = do fmap (filterPrefix fp' . map (addPrefix prefix . dirResult)) $ do let listDirForgiving d = fromMaybe ([], []) <$> forgivingAbsence (listDirRel d) dirsFromParentListing <- case parseSomeDir fp of Nothing -> case fp of [] -> do- -- This is not a valid rel dir but still a prefix of a valid rel dir:- -- the current dir- (ds, _) <- listDirRel here+ (ds, _) <- listDirRel baseDir pure (map fromRelDir $ filter (not . hiddenRel) ds) _ -> pure [] Just (Abs ad) -> do (ds, _) <- listDirForgiving ad pure (map (fromAbsDir . (ad </>)) $ filter (not . hiddenRel) ds) Just (Rel rd) -> do- (ds, _) <- listDirForgiving rd+ let ad = baseDir </> rd+ (ds, _) <- listDirForgiving ad pure (map (fromRelDir . (rd </>)) $ filter (not . hiddenRel) ds) dirsFromPartialListing <- case parseSomeDir fp of@@ -177,16 +176,15 @@ (ds, _) <- listDirForgiving dir pure (map (fromAbsDir . (dir </>)) $ filterHidden ds) Just (Rel rf) ->- -- This is not a valid rel dir but still a prefix of a valid- -- (hidden) rel dir. if fp == "." then do- (ds, _) <- listDirRel here+ (ds, _) <- listDirRel baseDir pure (map fromRelDir ds) else do let dir = parent rf+ let ad = baseDir </> dir let filterHidden = if hiddenRel rf then id else filter (not . hiddenRel)- (ds, _) <- listDirForgiving dir+ (ds, _) <- listDirForgiving ad pure (map (fromRelDir . (dir </>)) $ filterHidden ds) pure $@@ -223,6 +221,57 @@ let (pf, rest) = stripCurDir rest' in ("./" <> pf, rest) p -> ("", p)++-- | Split a path at @..@ components.+--+-- Returns @(prefix, remainder, baseDir)@ where:+--+-- * @prefix@ is the literal text up to and including the last @..\/@ (to+-- prepend to completion results)+-- * @remainder@ is the part after that last @..\/@ (to pass to+-- @parseSomeDir@ \/ @parseSomeFile@, which reject @..@)+-- * @baseDir@ is the absolute directory that @remainder@ should be resolved+-- relative to+--+-- If there are no @..@ components, @prefix@ is just the stripped @.\/@ prefix,+-- @remainder@ is the rest, and @baseDir@ is the current directory.+splitDotDot :: Path Abs Dir -> FilePath -> (FilePath, FilePath, Path Abs Dir)+splitDotDot here fp' =+ let (curDirPrefix, afterCurDir) = stripCurDir fp'+ components = splitOnSlash afterCurDir+ -- Find the index after the last "../" component+ lastDotDotIdx = case [i | (i, c) <- zip [1 ..] components, c == ".."] of+ [] -> 0+ ixs -> maximum ixs+ in if lastDotDotIdx == 0+ then (curDirPrefix, afterCurDir, here)+ else+ let prefixComponents = take lastDotDotIdx components+ remainderComponents = drop lastDotDotIdx components+ prefix = curDirPrefix <> concatMap (<> "/") prefixComponents+ -- Preserve trailing slash from the original input+ trailingSlash+ | "/" `isSuffixOf` afterCurDir, not (null remainderComponents) = "/"+ | otherwise = ""+ remainder = intercalate "/" remainderComponents <> trailingSlash+ baseDir = foldl' applyComponent here prefixComponents+ in (prefix, remainder, baseDir)++-- | Split a filepath on @\/@ separators, dropping empty segments.+splitOnSlash :: FilePath -> [String]+splitOnSlash [] = []+splitOnSlash s =+ let (seg, rest) = break (== '/') s+ in case rest of+ [] -> [seg | not (null seg)]+ _ : rest' -> [seg | not (null seg)] <> splitOnSlash rest'++-- | Apply a single path component to an absolute directory.+applyComponent :: Path Abs Dir -> String -> Path Abs Dir+applyComponent d ".." = parent d+applyComponent d c = case parseRelDir c of+ Nothing -> d+ Just rd -> d </> rd filterPrefix :: String -> [CompletionResult] -> [CompletionResult] filterPrefix s = filter ((s `isPrefixOf`) . completionResultValue)