find, findkeys, examinekey: escape output to terminal when --format is not used
authorJoey Hess <joeyh@joeyh.name>
Tue, 11 Apr 2023 18:57:09 +0000 (14:57 -0400)
committerJoey Hess <joeyh@joeyh.name>
Tue, 11 Apr 2023 19:27:07 +0000 (15:27 -0400)
Note that filenames are not quoted, only escaped. This is to match the
output of --format with escaping.

Sponsored-by: Lawrence Brogan on Patreon
CHANGELOG
Command/ExamineKey.hs
Command/Find.hs
Command/FindKeys.hs
Command/Whereis.hs
Utility/Format.hs
Utility/SafeOutput.hs
doc/git-annex-examinekey.mdwn
doc/git-annex-find.mdwn
doc/git-annex-findkeys.mdwn

index b52231418be0de8c3954eaa34d078012c48b896e..6de2c496f117f0d1ac76746e143db12cc1fa017d 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -7,6 +7,9 @@ git-annex (10.20230408) UNRELEASED; urgency=medium
   * Control characters in information coming from the repository or other
     possible untrusted sources are filtered out of the display of many
     commands.
+  * find, findkeys, examinekey: When outputting to a terminal and --format
+    is not used, quote unusual characters. 
+    (Similar to the behavior of GNU find.)
   * addurl --preserve-filename now rejects filenames that contain other
     control characters, besides the escape sequences it already rejected.
 
index bd91a8823e3991d5eb63afda63d6061fbf21b9c1..73dd77f7c5b9a990e9a5b35dab77c6ef8c4cedf9 100644 (file)
@@ -5,6 +5,8 @@
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE OverloadedStrings #-}
+
 module Command.ExamineKey where
 
 import Command
@@ -14,6 +16,7 @@ import Annex.Link
 import Backend
 import Types.Backend
 import Types.Key
+import Utility.SafeOutput
 
 import Data.Char
 import qualified Data.ByteString as B
@@ -54,7 +57,8 @@ run o _ input = do
        
        objectpath <- calcRepo $ gitAnnexLocation k
        let objectpointer = formatPointer k
-       showFormatted (format o) (serializeKey' k) $
+       isterminal <- liftIO $ checkIsTerminal stdout
+       showFormatted isterminal (format o) (serializeKey' k) $
                [ ("objectpath", fromRawFilePath objectpath)
                , ("objectpointer", fromRawFilePath objectpointer)
                ] ++ formatVars k af
index 8a7fbb6a11a3e71dd3d0e8f5f990e2dd1fe8bd61..05bd17f76f3d2389d284b195143b45c1f6c7b49f 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010-2018 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2023 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -19,6 +19,7 @@ import Types.Key
 import Git.FilePath
 import qualified Utility.Format
 import Utility.DataUnits
+import Utility.SafeOutput
 
 cmd :: Command
 cmd = withAnnexOptions [annexedMatchingOptions] $ mkCommand $
@@ -60,14 +61,15 @@ seek :: FindOptions -> CommandSeek
 seek o = do
        unless (isJust (keyOptions o)) $
                checkNotBareRepo
+       isterminal <- liftIO $ checkIsTerminal stdout
        seeker <- contentPresentUnlessLimited $ AnnexedFileSeeker
-               { startAction = start o
+               { startAction = start o isterminal
                , checkContentPresent = Nothing
                , usesLocationLog = False
                }
        case batchOption o of
                NoBatch -> withKeyOptions (keyOptions o) False seeker
-                       (commandAction . startKeys o)
+                       (commandAction . startKeys o isterminal)
                        (withFilesInGitAnnex ww seeker)
                        =<< workTreeItems ww (findThese o)
                Batch fmt -> batchOnly (keyOptions o) (findThese o) $
@@ -86,22 +88,25 @@ contentPresentUnlessLimited s = do
                        else Just True
                }
 
-start :: FindOptions -> SeekInput -> RawFilePath -> Key -> CommandStart
-start o _ file key = startingCustomOutput key $ do
-       showFormatted (formatOption o) file 
+start :: FindOptions -> IsTerminal -> SeekInput -> RawFilePath -> Key -> CommandStart
+start o isterminal _ file key = startingCustomOutput key $ do
+       showFormatted isterminal (formatOption o) file
                (formatVars key (AssociatedFile (Just file)))
        next $ return True
 
-startKeys :: FindOptions -> (SeekInput, Key, ActionItem) -> CommandStart
-startKeys o (si, key, ActionItemBranchFilePath (BranchFilePath _ topf) _) = 
-       start o si (getTopFilePath topf) key
-startKeys _ _ = stop
+startKeys :: FindOptions -> IsTerminal -> (SeekInput, Key, ActionItem) -> CommandStart
+startKeys o isterminal (si, key, ActionItemBranchFilePath (BranchFilePath _ topf) _) = 
+       start o isterminal si (getTopFilePath topf) key
+startKeys _ _ = stop
 
-showFormatted :: Maybe Utility.Format.Format -> S.ByteString -> [(String, String)] -> Annex ()
-showFormatted format unformatted vars =
+showFormatted :: IsTerminal -> Maybe Utility.Format.Format -> S.ByteString -> [(String, String)] -> Annex ()
+showFormatted (IsTerminal isterminal) format unformatted vars =
        unlessM (showFullJSON $ JSONChunk vars) $
                case format of
-                       Nothing -> liftIO $ S8.putStrLn unformatted
+                       Nothing -> do
+                               liftIO $ S8.putStrLn $ if isterminal
+                                       then Utility.Format.escapedFormat unformatted
+                                       else unformatted
                        Just formatter -> liftIO $ putStr $
                                Utility.Format.format formatter $
                                        M.fromList vars
index f6b892c91d26202a830544601319c53b16d096e8..f2a86cde5052b7664f2ef437b8d722f74d34fbc9 100644 (file)
@@ -8,8 +8,9 @@
 module Command.FindKeys where
 
 import Command
-import qualified Utility.Format
 import qualified Command.Find
+import qualified Utility.Format
+import Utility.SafeOutput
 
 cmd :: Command
 cmd = withAnnexOptions [keyMatchingOptions] $ Command.Find.mkCommand $
@@ -26,22 +27,23 @@ optParser _ = FindKeysOptions
 
 seek :: FindKeysOptions -> CommandSeek
 seek o = do
+       isterminal <- liftIO $ checkIsTerminal stdout
        seeker <- Command.Find.contentPresentUnlessLimited $ AnnexedFileSeeker
                { checkContentPresent = Nothing
                , usesLocationLog = False
                -- startAction is not actually used since this
                -- is not used to seek files
-               , startAction = \_ _ key -> start' o key
+               , startAction = \_ _ key -> start' o isterminal key
                }
        withKeyOptions (Just WantAllKeys) False seeker
-               (commandAction . start o)
+               (commandAction . start o isterminal)
                (const noop) (WorkTreeItems [])
 
-start :: FindKeysOptions -> (SeekInput, Key, ActionItem) -> CommandStart
-start o (_si, key, _ai) = start' o key
+start :: FindKeysOptions -> IsTerminal -> (SeekInput, Key, ActionItem) -> CommandStart
+start o isterminal (_si, key, _ai) = start' o isterminal key
 
-start' :: FindKeysOptions -> Key -> CommandStart
-start' o key = startingCustomOutput key $ do
-       Command.Find.showFormatted (formatOption o) (serializeKey' key)
+start' :: FindKeysOptions -> IsTerminal -> Key -> CommandStart
+start' o isterminal key = startingCustomOutput key $ do
+       Command.Find.showFormatted isterminal (formatOption o) (serializeKey' key)
                (Command.Find.formatVars key (AssociatedFile Nothing))
        next $ return True
index 5f9c9b51db3ca6a77e8637483beb1fc1cb9782e6..9052147249e965ed00491651dc17bf9614353549 100644 (file)
@@ -17,7 +17,6 @@ import Remote.Web (getWebUrls)
 import Annex.UUID
 import qualified Utility.Format
 import qualified Command.Find
-import Utility.SafeOutput
 
 import qualified Data.Map as M
 import qualified Data.Vector as V
index a85ab12fb1bb5ce0ec49a45389cba2d6872a01e9..930b7ee226b37281871f88bb661fff4004b7f731 100644 (file)
@@ -9,6 +9,7 @@ module Utility.Format (
        Format,
        gen,
        format,
+       escapedFormat,
        formatContainsVar,
        decode_c,
        encode_c,
@@ -53,7 +54,7 @@ format f vars = concatMap expand f
   where
        expand (Const s) = s
        expand (Var name j esc)
-               | esc = justify j $ decodeBS $ encode_c needescape $
+               | esc = justify j $ decodeBS $ escapedFormat $
                        encodeBS $ getvar name
                | otherwise = justify j $ getvar name
        getvar name = fromMaybe "" $ M.lookup name vars
@@ -62,6 +63,10 @@ format f vars = concatMap expand f
        justify (RightJustified i) s = pad i s ++ s
        pad i s = take (i - length s) spaces
        spaces = repeat ' '
+
+escapedFormat :: S.ByteString -> S.ByteString
+escapedFormat = encode_c needescape
+  where
        needescape c = isUtf8Byte c ||
                isSpace (chr (fromIntegral c)) ||
                c == fromIntegral (ord '"')
index 0ca2d87549794ff7a55aa0c57f064060ad19902a..fae36c274e88333acbf7aff7f3c212983ebc5344 100644 (file)
@@ -6,13 +6,25 @@
  - License: BSD-2-clause
  -}
 
-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
+{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, CPP #-}
 {-# OPTIONS_GHC -fno-warn-tabs #-}
 
-module Utility.SafeOutput (safeOutput) where
+module Utility.SafeOutput (
+       safeOutput,
+       IsTerminal(..),
+       checkIsTerminal,
+) where
 
 import Data.Char
 import qualified Data.ByteString as S
+import System.IO
+#ifdef mingw32_HOST_OS
+import System.Win32.MinTTY (isMinTTYHandle)
+import System.Win32.File
+import System.Win32.Types
+import Graphics.Win32.Misc
+import Control.Exception
+#endif
 
 class SafeOutputtable t where
        safeOutput :: t -> t
@@ -22,3 +34,25 @@ instance SafeOutputtable String where
 
 instance SafeOutputtable S.ByteString where
        safeOutput = S.filter (not . isControl . chr . fromIntegral)
+
+newtype IsTerminal = IsTerminal Bool
+
+checkIsTerminal :: Handle -> IO IsTerminal
+checkIsTerminal h = do
+#ifndef mingw32_HOST_OS
+       b <- hIsTerminalDevice h
+       return (IsTerminal b)
+#else
+       b <- hIsTerminalDevice h
+       if b
+               then return (IsTerminal b)
+               else do
+                       h' <- getStdHandle sTD_OUTPUT_HANDLE
+                               `catch` \(_ :: IOError) ->
+                                       return nullHANDLE
+                       if h == nullHANDLE
+                               then return (IsTerminal False)
+                               else do
+                                       b' <- isMinTTYHandle h'
+                                       return (IsTerminal b)
+#endif
index f2c7a095fbdfae4e36d38d14abe3d70bd1a52ee6..a35f34121668b1f20cfcf5216ba16cacfd0c5aa0 100644 (file)
@@ -33,6 +33,9 @@ that can be determined purely by looking at the key.
   provided to examinekey).
 
   Also, '\\n' is a newline, '\\000' is a NULL, etc.
+  
+  The default output format is the same as `--format='${escapedkey}\\n'`
+  when outputting to the terminal, and otherwise `--format='${key}\\n'`
 
 * `--json`
 
index dde58ff61b12639294830f565b7dd44dac8336c1..c16240d4e7bf0022d7b3ff3a9080150958140977 100644 (file)
@@ -50,7 +50,8 @@ finds files in the current directory and its subdirectories.
 
   Also, '\\n' is a newline, '\\000' is a NULL, etc.
 
-  The default output format is the same as `--format='${file}\\n'`
+  The default output format is the same as `--format='${escaped_file}\\n'`
+  when outputting to the terminal, and otherwise `--format='${file}\\n'`
 
 * `--json`
 
index 45419e8f1a574a73e7ba2f069c787fb10a3a04dd..819206e77dbdf7b98ad8469b64d2fe4362fb75fc 100644 (file)
@@ -45,7 +45,8 @@ Outputs a list of keys known to git-annex.
 
   Also, '\\n' is a newline, '\\000' is a NULL, etc.
 
-  The default output format is the same as `--format='${key}\\n'`
+  The default output format is the same as `--format='${escapedkey}\\n'`
+  when outputting to the terminal, and otherwise `--format='${key}\\n'`
 
 * `--json`