packages feed

mdoc 0.2.0.0 → 0.3.0.0

raw patch · 14 files changed

+371/−55 lines, 14 files

Files

+ examples/pass.1 view
@@ -0,0 +1,137 @@+.\" vim: ft=nroff.mustache+.Dd $Mdocdate: September 20 2026 $+.Dt PASS 1+.Os+.Sh NAME+.Nm pass+.Nd stores, retrieves, generates, and synchronizes passwords securely+.Sh SYNOPSIS+.Nm+.Bk -words+.Ar command+.Ek+.Pp+.Nm+.Cm init+.Bk -words+.Op Fl p Ar sub\-folder+.Ar gpg\-id+.Ek+.Pp+.Nm+.Cm ls+.Bk -words+.Ar subfolder+.Ek+.Pp+.Nm+.Cm grep+.Bk -words+.Op Ar GREPOPTIONS+.Ar search\-string+.Ek+.Pp+.Nm+.Cm find+.Bk -words+.Ar pass\-names+.Op Ar pass\-names ...+.Ek+.Pp+.Nm+.Cm show+.Bk -words+.Op Fl c Ar line\-number+.Op Fl q Ar line\-number+.Ar pass\-name+.Ek+.Pp+.Nm+.Cm help+.Pp+.Nm+.Cm version+.Sh DESCRIPTION+.Nm+is a very simple password store that keeps passwords inside+.Xr gpg2 1+encrypted files inside a simple directory tree residing at+.Pa ~/.password-store .+The pass utility provides a series of commands for manipulating the password+store, allowing the user to add, remove, edit, synchronize, generate, and+manipulate passwords.+.Pp+If no COMMAND is specified, COMMAND defaults to either+.Cm show+or+.Cm ls ,+depending on the type of specifier in ARGS. Alternatively, if+.Ar PASSWORD_STORE_ENABLE_EXTENSIONS+is set to "true", and the file+.Pa .extensions/COMMAND.bash+exists inside the password store and is executable, then it is sourced into the+environment, passing any arguments and environment variables. Extensions+existing in a system- wide directory, only installable by the administrator, are+always enabled.+.Pp+Otherwise COMMAND must be one of the valid commands listed below.+.Pp+Several of the commands below rely on or provide additional functionality if the+password store directory is also a git repository. If the password store+directory is a git repository, all password store modification commands will+cause a corresponding git commit. Sub- directories may be separate nested git+repositories, and pass will use the inner-most directory relative to the current+password. See the+.Ar EXTENDED GIT EXAMPLE+section for a detailed description using init and+.Xr git 1+.Pp+The+.Cm init+command must be run before other commands in order to initialize the password+store with the correct gpg key id. Passwords are encrypted using the gpg key set+with+.Cm init .+.Pp+There is a corresponding bash completion script for use with tab completing+password names in+.Xr bash 1 .+.Ss COMMANDS+.Bl -tag -width indent+.It Cm init+Initialize new password storage and use gpg\-id for encryption. Multiple+gpg\-ids may be specified, in order to encrypt each password with multiple ids.+This command must be run first before a password store can be used. If the+specified gpg\-id is different from the key used in any existing files, these+files will be reencrypted to use the new id. Note that use of gpg\-agent(1) is+recommended so that the batch decryption does not require as much user+intervention. If \-\-path or \-p is specified, along with an argument, a+specific gpg\-id or set of gpg\-ids is assigned for that specific sub folder of+the password store. If only one gpg\-id is given, and it is an empty string,+then the current .gpg\-id file for the specified sub\-folder (or root if+unspecified) is removed.+.It Cm ls+List names of passwords inside the tree at subfolder by using the tree(1)+program. This command is alternatively named list.+.It Cm grep+Searches inside each decrypted password file for search\-string, and displays+line containing matched string along with filename. Uses grep(1) for matching.+GREPOPTIONS are passed to grep(1) as\-is. (Note: the GREP_OPTIONS environment+variable functions as well.)+.It Cm find+List names of passwords inside the tree that match pass\-names by using the+tree(1) program. This command is alternatively named search.+.It Cm show+Decrypt and print a password named pass\-name. If \-\-clip or \-c is specified,+do not print the password but instead copy the first (or otherwise specified)+line to the clipboard using xclip(1) or wl\-clipboard(1) and then restore the+clipboard after 45 (or PASSWORD_STORE_CLIP_TIME) seconds. If \-\-qrcode or \-q+is specified, do not print the password but instead display a QR code using+qrencode(1) either to the terminal or graphically if supported.+.It Cm help+Show usage message.+.It Cm version+Show version information.+.El+.Sh EXIT STATUS+.Ex -std
mdoc.cabal view
@@ -1,6 +1,6 @@ cabal-version:      1.18 name:               mdoc-version:            0.2.0.0+version:            0.3.0.0 license:            AGPL-3 license-file:       COPYING maintainer:         Pat Brisbin@@ -37,6 +37,7 @@     examples/grep.1     examples/mdoc-dump.1     examples/mdoc-dump.1.html+    examples/pass.1     examples/person.5     examples/style.css @@ -54,6 +55,7 @@         Env.Mdoc         Mdoc         Mdoc.Data.Argument+        Mdoc.Data.Command         Mdoc.Data.Config         Mdoc.Data.Described         Mdoc.Data.EnvVar@@ -72,6 +74,7 @@         Mdoc.Dump.Options         Mdoc.Examples.Grep         Mdoc.Examples.OptEnvConf+        Mdoc.Examples.Pass         Mdoc.Examples.Person         Mdoc.Input         Mdoc.Optics
src/Mdoc/Data/Argument.hs view
@@ -37,8 +37,8 @@  instance Pretty Argument where   pretty Argument {schema, optionality} = case optionality of-    Required -> "Ar" <+> pretty schema-    _ -> "Op Ar" <+> pretty schema+    Required -> "Ar" <+> pretty (esc $ pack schema)+    _ -> "Op Ar" <+> pretty (esc $ pack schema)  data ShortArg = ShortArg   { name :: Char@@ -49,7 +49,7 @@   pretty (ShortArg {name, argument}) =     prefix (pretty $ esc $ T.singleton name)       <+> pretty Ar-      <+> pretty argument.schema+      <+> pretty (esc $ pack argument.schema)    where     prefix :: Doc ann -> Doc ann     prefix = case argument.optionality of@@ -66,7 +66,7 @@     prefix (pretty (esc $ pack name))       <+> pretty Ns       <+> pretty Ar-      <+> pretty argument.schema+      <+> pretty (esc $ pack argument.schema)    where     prefix :: Doc ann -> Doc ann     prefix = case argument.optionality of
+ src/Mdoc/Data/Command.hs view
@@ -0,0 +1,20 @@+module Mdoc.Data.Command+  ( Command (..)+  ) where++import Mdoc.Prelude++import Mdoc.Data.Synopsis (Synopsis)+import Mdoc.Syntax.Mdoc++data Command = Command+  { index :: Int+  , name :: String+  , synopsis :: Synopsis+  , description :: Maybe Mdoc+  }+  deriving stock (Eq, Generic, Show)+  deriving anyclass (ToJSON)++instance Ord Command where+  compare = comparing (.index)
src/Mdoc/Data/Described.hs view
@@ -18,7 +18,6 @@  import Data.Aeson (object, (.=)) import Data.Function (on)-import Data.Text qualified as T import Mdoc.Data.Optionality import Mdoc.Optics import Mdoc.Pretty@@ -77,14 +76,3 @@ -- | Set a 'Described's 'helpLines' from a 'Text' setHelpText :: Text -> Described a -> Described a setHelpText = maybe id setHelp . textToMdoc--textToMdoc :: Text -> Maybe Mdoc-textToMdoc =-  fmap-    ( Mdoc-        . intersperse (MacroLine Pp [])-        . map (TextLine . esc)-        . toList-    )-    . nonEmpty-    . T.lines
src/Mdoc/Data/List.hs view
@@ -19,17 +19,16 @@  import Mdoc.Prelude -import Data.Aeson (object, (.=))+import Data.Aeson (Value (..), object, (.=))+import Data.Aeson.KeyMap qualified as KeyMap import Data.Kind (Constraint, Type) import Data.List.NonEmpty qualified as NE import Data.Text qualified as T-import Mdoc.Data.Described-import Mdoc.Pretty import Mdoc.Syntax  type List :: forall {k}. k -> Type -> Type newtype List t a = List-  { items :: NonEmpty (Described a)+  { items :: NonEmpty a   }   deriving stock (Eq, Generic, Show)   deriving (Semigroup) via Generically (List t a)@@ -41,14 +40,14 @@       , "items" .= items       ]    where-    items :: NonEmpty (Described a)+    items :: NonEmpty a     items = NE.sort $ NE.nub l.items -fromNonEmpty :: NonEmpty (Described a) -> List t a+fromNonEmpty :: NonEmpty a -> List t a fromNonEmpty = foldMap1 singleton -singleton :: Described a -> List t a-singleton described = List {items = pure described}+singleton :: a -> List t a+singleton a = List {items = pure a}  data Indent @@ -56,21 +55,27 @@  type Width :: forall {k}. k -> Type -> Constraint class Width t a where-  renderedWidth :: Proxy t -> NonEmpty (Described a) -> MacroArg+  renderedWidth :: Proxy t -> NonEmpty a -> MacroArg  instance Width Indent a where   renderedWidth _ _ = "indent" -instance Pretty a => Width ByItem a where+instance ToJSON a => Width ByItem a where   renderedWidth _ =     Quoted       . esc       . maximumBy (comparing T.length)       . fmap headWidth -headWidth :: Pretty a => Described a -> Text-headWidth = go . T.words . renderPlain . pretty . (.item)+headWidth :: ToJSON a => a -> Text+headWidth = go . T.words . getHead . toJSON  where+  getHead :: Value -> Text+  getHead v = fromMaybe "" $ do+    Object km <- pure v+    String t <- KeyMap.lookup "head" km+    pure t+   go :: [Text] -> Text   go = \case     [] -> ""
src/Mdoc/Data/Page.hs view
@@ -13,12 +13,15 @@   , addSwitch   , addOption   , addArgument+  , addCommand+  , addCommands   , addConfig      -- * @ENVIRONMENT@   , addEnvVar -    -- * Other, rarel-used elements+    -- * Other, rarely-used elements+  , getSynopsis   , setSynopsis   , setPrologue   , setEpilogue@@ -27,6 +30,7 @@ import Mdoc.Prelude  import Mdoc.Data.Argument+import Mdoc.Data.Command import Mdoc.Data.Config import Mdoc.Data.Described import Mdoc.Data.EnvVar@@ -41,9 +45,10 @@ data Page = Page   { synopsis :: Maybe Synopsis   , prologue :: Maybe Mdoc-  , options :: Maybe (List Indent Positional)-  , configs :: Maybe (List Indent Config)-  , environment :: Maybe (List ByItem EnvVar)+  , options :: Maybe (List Indent (Described Positional))+  , commands :: Maybe (List Indent Command)+  , configs :: Maybe (List Indent (Described Config))+  , environment :: Maybe (List ByItem (Described EnvVar))   , epilogue :: Maybe Mdoc   }   deriving stock (Eq, Generic, Show)@@ -65,6 +70,12 @@     & field @"synopsis" <?>~ Synopsis.singleton described     & field @"options" <?>~ List.singleton described +addCommand :: Command -> Page -> Page+addCommand c m = m & field @"commands" <?>~ List.singleton c++addCommands :: [Command] -> Page -> Page+addCommands = appEndo . foldMap (Endo . addCommand)+ addConfig :: Described Config -> Page -> Page addConfig described m =   m & field @"configs" <?>~ List.singleton described@@ -72,6 +83,9 @@ addEnvVar :: Described EnvVar -> Page -> Page addEnvVar described m =   m & field @"environment" <?>~ List.singleton described++getSynopsis :: Page -> Synopsis+getSynopsis p = fromMaybe mempty p.synopsis  setSynopsis :: Mdoc -> Page -> Page setSynopsis s = field @"synopsis" ?~ SynopsisCustom s
src/Mdoc/Data/Synopsis.hs view
@@ -64,11 +64,8 @@     unAnnotate       $ vsep       $ catMaybes-        [ Just ".Nm"-        , Just ".Bk -words"-        , (".Op Fl" <+>) . pretty . esc . pack . toList <$> nonEmpty u.chars+        [ (".Op Fl" <+>) . pretty . esc . pack . toList <$> nonEmpty u.chars         , vsep . toList <$> nonEmpty (u.shorts <> u.longs <> u.args)-        , Just ".Ek"         ]  buildUsage :: [Described Positional] -> Usage ann
+ src/Mdoc/Examples/Pass.hs view
@@ -0,0 +1,100 @@+-- |+--+-- Module      : Mdoc.Examples.Pass+-- Copyright   : (c) 2026 Patrick Brisbin+-- License     : AGPL-3+-- Maintainer  : pbrisbin@gmail.com+-- Stability   : experimental+-- Portability : POSIX+module Mdoc.Examples.Pass+  ( pass1+  ) where++import Mdoc.Prelude++import Mdoc.Data.Named+import Mdoc.Data.Page (setPrologue)+import Mdoc.Syntax+import Options.Applicative+import Options.Applicative.Mdoc++pass1 :: Named+pass1 =+  mempty+    & (<> getPage passOpt)+    & setPrologue+      ( Mdoc+          [ MacroLine Nm []+          , TextLine "is a very simple password store that keeps passwords inside"+          , MacroLine Xr ["gpg2", "1"]+          , TextLine "encrypted files inside a simple directory tree residing at"+          , MacroLine Pa ["~/.password-store", "."]+          , TextLine+              "The pass utility provides a series of commands for manipulating the password store, allowing the user to add, remove, edit, synchronize, generate, and manipulate passwords."+          , MacroLine Pp []+          , TextLine "If no COMMAND is specified, COMMAND defaults to either"+          , MacroLine Cm ["show"]+          , TextLine "or"+          , MacroLine Cm ["ls", ","]+          , TextLine "depending on the type of specifier in ARGS. Alternatively, if"+          , MacroLine Ar ["PASSWORD_STORE_ENABLE_EXTENSIONS"]+          , TextLine "is set to \"true\", and the file"+          , MacroLine Pa [".extensions/COMMAND.bash"]+          , TextLine+              "exists inside the password store and is executable, then it is sourced into the environment, passing any arguments and environment variables. Extensions existing in a system- wide directory, only installable by the administrator, are always enabled."+          , MacroLine Pp []+          , TextLine "Otherwise COMMAND must be one of the valid commands listed below."+          , MacroLine Pp []+          , TextLine+              "Several of the commands below rely on or provide additional functionality if the password store directory is also a git repository. If the password store directory is a git repository, all password store modification commands will cause a corresponding git commit. Sub- directories may be separate nested git repositories, and pass will use the inner-most directory relative to the current password. See the"+          , MacroLine Ar ["EXTENDED", "GIT", "EXAMPLE"]+          , TextLine "section for a detailed description using init and"+          , MacroLine Xr ["git", "1"]+          , MacroLine Pp []+          , TextLine "The"+          , MacroLine Cm ["init"]+          , TextLine+              "command must be run before other commands in order to initialize the password store with the correct gpg key id. Passwords are encrypted using the gpg key set with"+          , MacroLine Cm ["init", "."]+          , MacroLine Pp []+          , TextLine+              "There is a corresponding bash completion script for use with tab completing password names in"+          , MacroLine Xr ["bash", "1", "."]+          ]+      )+    & name "pass" "stores, retrieves, generates, and synchronizes passwords securely"++{- FOURMOLU_DISABLE -}+passOpt :: Parser ()+passOpt = void $ subparser (mconcat+  [ command "init"    $ info initOpt   $ progDesc "Initialize new password storage and use gpg-id for encryption. Multiple gpg-ids may be specified, in order to encrypt each password with multiple ids. This command must be run first before a password store can be used. If the specified gpg-id is different from the key used in any existing files, these files will be reencrypted to use the new id. Note that use of gpg-agent(1) is recommended so that the batch decryption does not require as much user intervention. If --path or -p is specified, along with an argument, a specific gpg-id or set of gpg-ids is assigned for that specific sub folder of the password store. If only one gpg-id is given, and it is an empty string, then the current .gpg-id file for the specified sub-folder (or root if unspecified) is removed."+  , command "ls"      $ info lsOpt     $ progDesc "List names of passwords inside the tree at subfolder by using the tree(1) program. This command is alternatively named list."+  , command "grep"    $ info grepOpt   $ progDesc "Searches inside each decrypted password file for search-string, and displays line containing matched string along with filename. Uses grep(1) for matching. GREPOPTIONS are passed to grep(1) as-is. (Note: the GREP_OPTIONS environment variable functions as well.)"+  , command "find"    $ info findOpt   $ progDesc "List names of passwords inside the tree that match pass-names by using the tree(1) program. This command is alternatively named search."+  , command "show"    $ info showOpt   $ progDesc "Decrypt and print a password named pass-name. If --clip or -c is specified, do not print the password but instead copy the first (or otherwise specified) line to the clipboard using xclip(1) or wl-clipboard(1) and then restore the clipboard after 45 (or PASSWORD_STORE_CLIP_TIME) seconds. If --qrcode or -q is specified, do not print the password but instead display a QR code using qrencode(1) either to the terminal or graphically if supported."+  , command "help"    $ info (pure ()) $ progDesc "Show usage message."+  , command "version" $ info (pure ()) $ progDesc "Show version information."+  ])++initOpt :: Parser ()+initOpt = void $ (,)+  <$> optional (option (str @Text) (short 'p' <> long "path" <> metavar "sub-folder"))+  <*> argument (str @Text) (metavar "gpg-id")++lsOpt :: Parser ()+lsOpt = void $ argument (str @Text) (metavar "subfolder") -- sic++grepOpt :: Parser ()+grepOpt = void $ (,)+  <$> optional (argument (str @Text) (metavar "GREPOPTIONS"))+  <*> argument (str @Text) (metavar "search-string")++findOpt :: Parser ()+findOpt = void $ some1 $ argument (str @Text) (metavar "pass-names")++showOpt :: Parser ()+showOpt = void $ (,,)+  <$> optional (option (str @Text) (short 'c' <> long "clip" <> metavar "line-number"))+  <*> optional (option (str @Text) (short 'q' <> long "qrcode" <> metavar "line-number"))+  <*> argument (str @Text) (metavar "pass-name")+{- FOURMOLU_ENABLE -}
src/Mdoc/Prelude.hs view
@@ -24,7 +24,7 @@ import Data.Aeson as X (ToJSON (..)) import Data.Bifunctor as X (bimap, first, second) import Data.Foldable as X (for_, toList, traverse_)-import Data.Foldable1 as X (foldMap1, maximumBy)+import Data.Foldable1 as X (Foldable1, foldMap1, maximumBy, toNonEmpty) import Data.Function as X ((&)) import Data.Functor as X ((<&>)) import Data.Functor.Identity as X (Identity)
src/Mdoc/Syntax/Mdoc.hs view
@@ -11,11 +11,16 @@      -- * Pretty   , prettyMdoc++    -- * Util+  , textToMdoc   ) where  import Mdoc.Prelude +import Data.Text qualified as T import Mdoc.Pretty+import Mdoc.Syntax.MacroName import Mdoc.Syntax.MdocLine  newtype Mdoc = Mdoc@@ -30,3 +35,14 @@  instance Pretty Mdoc where   pretty = unAnnotate . prettyMdoc++textToMdoc :: Text -> Maybe Mdoc+textToMdoc =+  fmap+    ( Mdoc+        . intersperse (MacroLine Pp [])+        . map (TextLine . esc)+        . toList+    )+    . nonEmpty+    . T.lines
src/Options/Applicative/Mdoc.hs view
@@ -16,8 +16,11 @@  import Mdoc.Prelude +import Control.Monad (foldM)+import Control.Monad.State (MonadState (..), evalState, modify) import Data.List (sort) import Mdoc.Data.Argument+import Mdoc.Data.Command import Mdoc.Data.Described import Mdoc.Data.Flag import Mdoc.Data.Option@@ -32,8 +35,24 @@ import Prettyprinter.Render.Text qualified as Pretty  getPage :: Parser a -> Page-getPage p = foldOptTree 0 mempty optionToMan1 $ treeMapParser (const void) p+getPage p = foldOptTree optionToMan1 $ treeMapParser (const void) p +getCommand :: Int -> String -> O.ParserInfo x -> Command+getCommand index name pinfo =+  Command+    { index+    , name+    , description = textToMdoc =<< docToText doc+    , synopsis = getSynopsis $ getPage $ O.infoParser pinfo+    }+ where+  doc =+    mconcat+      [ O.infoProgDesc pinfo+      , O.infoHeader pinfo+      , O.infoFooter pinfo+      ]+ optionToMan1   :: Int   -> Page@@ -54,7 +73,12 @@     schema <- metavar     let argument = Argument {index, schema, optionality = Required}     pure $ addArgument (argument <$ d) acc-  O.CmdReader {} -> acc -- TODO+  O.CmdReader _mGroup cmds ->+    let+      commands :: [Command]+      commands = zipWith (uncurry . getCommand) [1 ..] $ reverse cmds+    in+      addCommands commands acc  where   o = d.item   metavar = guarded (not . null) $ O.optMetaVar o@@ -73,30 +97,44 @@     O.OptLong x -> GNUFlag x xs  foldOptTree-  :: Int-  -> Page-  -> (Int -> Page -> Described (O.Option x) -> Page)+  :: (Int -> Page -> Described (O.Option x) -> Page)   -> O.OptTree (O.Option x)   -> Page-foldOptTree index acc f = \case+foldOptTree f = flip evalState 0 . foldOptTreeM f mempty++foldOptTreeM+  :: MonadState Int m+  => (Int -> Page -> Described (O.Option x) -> Page)+  -> Page+  -> O.OptTree (O.Option x)+  -> m Page+foldOptTreeM f acc = \case   O.Leaf o ->     case O.optVisibility o of-      O.Visible ->-        f index acc+      O.Visible -> do+        index <- get+        modify (+ 1)+        pure+          $ f index acc           $ Described             { item = o             , optionality = maybe Required Defaulted (O.optShowDefault o)             , multiple = False             , help = textToMdoc =<< docToText (O.optHelp o)             }-      O.Internal -> acc-      O.Hidden -> acc+      O.Internal -> pure acc+      O.Hidden -> pure acc   O.MultNode ts -> recur f ts   O.AltNode O.MarkDefault ts -> recur fAsOptional ts   O.AltNode O.NoDefault ts -> recur fAsRequired ts-  O.BindNode t -> foldOptTree (index + 1) acc fAsMultiple t+  O.BindNode t -> foldOptTreeM fAsMultiple acc t  where-  recur g = maybe acc (foldMap1 $ foldOptTree (index + 1) acc g) . nonEmpty+  recur+    :: MonadState Int m+    => (Int -> Page -> Described (O.Option x) -> Page)+    -> [O.OptTree (O.Option x)]+    -> m Page+  recur g = foldM (foldOptTreeM g) acc    fAsOptional i m d = f i m $ d {optionality = Optional}   fAsRequired i m d = f i m $ d {optionality = Required}
test/Mdoc/Data/SynopsisSpec.hs view
@@ -21,8 +21,4 @@   it "renders an empty synopsis" $ do     let synopsis = Synopsis.fromList [] -    synopsis-      `shouldRender` [ ".Nm"-                     , ".Bk -words"-                     , ".Ek"-                     ]+    synopsis `shouldRender` [""]
test/Mdoc/ExamplesSpec.hs view
@@ -21,6 +21,7 @@ import Mdoc.Data.Named import Mdoc.Examples.Grep import Mdoc.Examples.OptEnvConf+import Mdoc.Examples.Pass import Mdoc.Examples.Person import Mdoc.Input (parseMdocBytes) import Mdoc.Parse (errorBundlePretty, exitParseError)@@ -36,6 +37,7 @@ spec = do   it "grep.1" $ exampleGolden man1 grep1 1   it "example.1" $ exampleGolden man1 example1 1+  it "pass.1" $ exampleGolden man1 pass1 1   it "conf.5" $ exampleGolden man5 conf5 5   it "example.5" $ exampleGolden man5 example5 5   it "person.5" $ exampleGolden man5 person5 5