From afa5b883dc9a05c11cd729ad16f58208584e9f70 Mon Sep 17 00:00:00 2001 From: Joey Hess Date: Tue, 11 Apr 2023 14:57:09 -0400 Subject: [PATCH] find, findkeys, examinekey: escape output to terminal when --format is not used 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 | 3 +++ Command/ExamineKey.hs | 6 +++++- Command/Find.hs | 31 ++++++++++++++++------------ Command/FindKeys.hs | 18 +++++++++-------- Command/Whereis.hs | 1 - Utility/Format.hs | 7 ++++++- Utility/SafeOutput.hs | 38 +++++++++++++++++++++++++++++++++-- doc/git-annex-examinekey.mdwn | 3 +++ doc/git-annex-find.mdwn | 3 ++- doc/git-annex-findkeys.mdwn | 3 ++- 10 files changed, 85 insertions(+), 28 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b52231418b..6de2c496f1 100644 --- 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. diff --git a/Command/ExamineKey.hs b/Command/ExamineKey.hs index bd91a8823e..73dd77f7c5 100644 --- a/Command/ExamineKey.hs +++ b/Command/ExamineKey.hs @@ -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 diff --git a/Command/Find.hs b/Command/Find.hs index 8a7fbb6a11..05bd17f76f 100644 --- a/Command/Find.hs +++ b/Command/Find.hs @@ -1,6 +1,6 @@ {- git-annex command - - - Copyright 2010-2018 Joey Hess + - Copyright 2010-2023 Joey Hess - - 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 diff --git a/Command/FindKeys.hs b/Command/FindKeys.hs index f6b892c91d..f2a86cde50 100644 --- a/Command/FindKeys.hs +++ b/Command/FindKeys.hs @@ -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 diff --git a/Command/Whereis.hs b/Command/Whereis.hs index 5f9c9b51db..9052147249 100644 --- a/Command/Whereis.hs +++ b/Command/Whereis.hs @@ -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 diff --git a/Utility/Format.hs b/Utility/Format.hs index a85ab12fb1..930b7ee226 100644 --- a/Utility/Format.hs +++ b/Utility/Format.hs @@ -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 '"') diff --git a/Utility/SafeOutput.hs b/Utility/SafeOutput.hs index 0ca2d87549..fae36c274e 100644 --- a/Utility/SafeOutput.hs +++ b/Utility/SafeOutput.hs @@ -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 diff --git a/doc/git-annex-examinekey.mdwn b/doc/git-annex-examinekey.mdwn index f2c7a095fb..a35f341216 100644 --- a/doc/git-annex-examinekey.mdwn +++ b/doc/git-annex-examinekey.mdwn @@ -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` diff --git a/doc/git-annex-find.mdwn b/doc/git-annex-find.mdwn index dde58ff61b..c16240d4e7 100644 --- a/doc/git-annex-find.mdwn +++ b/doc/git-annex-find.mdwn @@ -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` diff --git a/doc/git-annex-findkeys.mdwn b/doc/git-annex-findkeys.mdwn index 45419e8f1a..819206e77d 100644 --- a/doc/git-annex-findkeys.mdwn +++ b/doc/git-annex-findkeys.mdwn @@ -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`