packages feed

okf-cli-0.10.0.0: src/Okf/Cli/Aliases.hs

-- | Pure command-alias validation and presentation, independent of config IO.
module Okf.Cli.Aliases
  ( validateAliases,
    renderAliases,
    isAliasCandidate,
    expandAlias,
    AliasCommand (..),
    aliasCommandParser,
  )
where

import Data.Char (isSpace)
import Data.Foldable (traverse_)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text qualified as Text
import Okf.Prelude
import Options.Applicative

data AliasCommand = AliasList
  deriving stock (Show, Eq)

-- | Bare @alias@ defaults to listing; unknown subcommands remain parse errors.
aliasCommandParser :: Parser AliasCommand
aliasCommandParser =
  hsubparser (command "list" (info (pure AliasList <**> helper) (progDesc "List configured command aliases")))
    <|> pure AliasList

validateAliases :: Map Text Text -> Either Text ()
validateAliases = traverse_ validate . Map.toAscList
  where
    validate (name, expansion)
      | Text.null name = invalid "has empty name"
      | Text.any isSpace name = invalid "has whitespace in name"
      | Text.isPrefixOf "-" name = invalid "has dash-prefixed name"
      | null (Text.words expansion) = invalid "has empty expansion"
      | otherwise = Right ()
      where
        invalid problem = Left ("alias " <> Text.pack (show name) <> " " <> problem)

-- | Include a final newline so callers can print the result verbatim.
renderAliases :: Map Text Text -> Text
renderAliases aliases
  | Map.null aliases = "No aliases configured.\n"
  | otherwise =
      Text.unlines
        [ Text.justifyLeft width ' ' name <> "  = " <> expansion
        | (name, expansion) <- Map.toAscList aliases
        ]
  where
    width = maximum (map Text.length (Map.keys aliases))

-- | Only the first argument can be an alias. Protected paths avoid config IO.
isAliasCandidate :: [Text] -> [String] -> Bool
isAliasCandidate _ [] = False
isAliasCandidate builtins (firstArg : _) =
  not (Text.isPrefixOf "-" name) && name `notElem` builtins
  where
    name = Text.pack firstArg

-- | Expand once using whitespace splitting, preserving every trailing argument.
-- Quotes are literal text; no shell or second alias lookup is involved.
expandAlias :: [Text] -> Map Text Text -> [String] -> [String]
expandAlias builtins aliases args@(firstArg : rest)
  | isAliasCandidate builtins args,
    Just expansion <- Map.lookup (Text.pack firstArg) aliases =
      map Text.unpack (Text.words expansion) <> rest
expandAlias _ _ args = args