Skip to content

Commit 7e54972

Browse files
committed
fix: cli,ui,web: quote spaced file paths properly on Windows
hledger-ui's A key (running hledger-iadd) and E key (running $EDITOR) now properly handle file paths containing spaces, on Windows machines. The same fix is applied to running info, man, tldr, or a pager, eg when displaying help. Added shellQuoteIfNeeded in Hledger.Utils.String, which uses double-quote escaping on mingw32 and single quotes elsewhere. RulesReader's runCommand/runCommandAsFilter are not changed; they run user-authored commands from CSV rules files, where quoting is the user's responsibility.
1 parent efc282a commit 7e54972

3 files changed

Lines changed: 21 additions & 7 deletions

File tree

hledger-lib/Hledger/Utils/String.hs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module Hledger.Utils.String (
1111
-- quoting
1212
quoteIfNeeded,
1313
singleQuoteIfNeeded,
14+
shellQuoteIfNeeded,
1415
quoteForCommandLine,
1516
-- quotechars,
1617
-- whitespacechars,
@@ -41,6 +42,7 @@ import Data.Char (isSpace, toLower, toUpper)
4142
import Data.List (intercalate, dropWhileEnd)
4243
import Data.Text qualified as T
4344
import Safe (headErr, tailErr)
45+
import System.Info (os)
4446
import Text.Megaparsec ((<|>), between, many, noneOf, sepBy)
4547
import Text.Megaparsec.Char (char)
4648
import Text.Printf (printf)
@@ -155,6 +157,18 @@ singleQuoteIfNeeded s | any (`elem` s) (quotechars++whitespacechars) = singleQuo
155157
singleQuote :: String -> String
156158
singleQuote s = "'"++s++"'"
157159

160+
-- | Quote a string if needed for use as one argument in a shell command line
161+
-- on the current platform. Uses double-quote escaping on Windows (cmd.exe does
162+
-- not recognise single quotes as a quoting character); elsewhere uses single
163+
-- quotes, which are literal in POSIX shells. Use this whenever interpolating
164+
-- a path or other string into a command that will be passed to
165+
-- 'System.Process.runCommand', 'System.Process.callCommand', or
166+
-- 'System.Process.shell'.
167+
shellQuoteIfNeeded :: String -> String
168+
shellQuoteIfNeeded
169+
| os == "mingw32" = quoteIfNeeded
170+
| otherwise = singleQuoteIfNeeded
171+
158172
-- | Try to single- and backslash-quote a string as needed to make it usable
159173
-- as an argument on a (sh/bash) shell command line. At least, well enough
160174
-- to handle common currency symbols, like $. Probably broken in many ways.

hledger-ui/Hledger/UI/Editor.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ endPosition = Just (-1, Nothing)
3333
-- and return the exit code; or raise an error.
3434
-- hledger-iadd is an alternative to the built-in add command.
3535
runIadd :: FilePath -> IO ExitCode
36-
runIadd f = runCommand ("hledger-iadd -f " ++ f) >>= waitForProcess
36+
runIadd f = runCommand ("hledger-iadd -f " ++ shellQuoteIfNeeded f) >>= waitForProcess
3737

3838
-- | Run the user's preferred text editor (or try a default editor),
3939
-- on the given file, blocking until it exits, and return the exit
@@ -99,7 +99,7 @@ editFileAtPositionCommand :: Maybe TextPosition -> FilePath -> IO String
9999
editFileAtPositionCommand mpos f = do
100100
cmd <- getEditCommand
101101
let editor = lowercase $ takeBaseName $ headDef "" $ words' cmd
102-
f' = singleQuoteIfNeeded f
102+
f' = shellQuoteIfNeeded f
103103
mpos' = Just . bimap show (fmap show) =<< mpos
104104
join sep = intercalate sep . catMaybes
105105
args = case editor of

hledger/Hledger/Cli/DocFiles.hs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import System.IO
2929
import System.IO.Temp
3030
import System.Process
3131

32-
import Hledger.Utils (first3, second3, third3, embedFileRelative, error')
32+
import Hledger.Utils (first3, second3, third3, embedFileRelative, error', shellQuoteIfNeeded)
3333
import Text.Printf (printf)
3434
import System.Environment (lookupEnv)
3535
import Hledger.Utils.Debug
@@ -107,7 +107,7 @@ runInfoForTopic tool mtopic =
107107
BC.hPutStrLn h $ manualInfo tool
108108
hClose h
109109
callCommand $ dbg1 "info command" $
110-
"info -f " ++ f ++ maybe "" (printf " -n '%s'") mtopic
110+
"info -f " ++ shellQuoteIfNeeded f ++ maybe "" (printf " -n '%s'") mtopic
111111

112112
-- less with any vertical whitespace squashed, case-insensitive searching, the $ regex metacharacter accessible as \$.
113113
less = "less -s -i --use-backslash"
@@ -129,7 +129,7 @@ runPagerForTopic tool mtopic = do
129129
case mtopic of
130130
Nothing -> (envpager, "")
131131
Just t -> (less, "-p'^( )?" ++ t ++ if exactmatch then "\\$'" else "")
132-
callCommand $ dbg1 "pager command" $ unwords [pager, searcharg, f]
132+
callCommand $ dbg1 "pager command" $ unwords [pager, searcharg, shellQuoteIfNeeded f]
133133

134134
-- | Display a man page for this tool, scrolled to the given topic if provided, using "man".
135135
-- When a topic is provided we force man to use "less", ignoring $MANPAGER and $PAGER.
@@ -145,7 +145,7 @@ runManForTopic tool mtopic =
145145
case mtopic of
146146
Nothing -> ""
147147
Just t -> "-P \"" ++ less ++ " -p'^( )?" ++ t ++ (if exactmatch then "\\\\$" else "") ++ "'\""
148-
callCommand $ dbg1 "man command" $ unwords ["man", pagerarg, f]
148+
callCommand $ dbg1 "man command" $ unwords ["man", pagerarg, shellQuoteIfNeeded f]
149149

150150
-- | Get the named tldr page's source, if we know it.
151151
tldr :: TldrPage -> Maybe ByteString
@@ -165,7 +165,7 @@ runTldrForPage name =
165165
-- tlrc - ?
166166
-- tldr-node-client - undocumented env var suggested in output
167167
setEnv "TLDR_AUTO_UPDATE_DISABLED" "1"
168-
callCommand $ dbg1 "tldr command" $ "tldr --render " <> f
168+
callCommand $ dbg1 "tldr command" $ "tldr --render " <> shellQuoteIfNeeded f
169169
) `catch` (\(_e::IOException) -> do
170170
hPutStrLn stderr $ "Warning: could not run tldr --render, using fallback viewer instead.\n"
171171
BC.putStrLn b

0 commit comments

Comments
 (0)