From cff831c3c0aad51c913667d66f92288956b8b220 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Wed, 16 Apr 2025 23:20:58 -1000 Subject: [PATCH 01/18] feat:setup: command skeleton --- hledger/Hledger/Cli/Commands.hs | 4 ++ hledger/Hledger/Cli/Commands/Setup.hs | 64 ++++++++++++++++++++++++ hledger/Hledger/Cli/Commands/Setup.md | 9 ++++ hledger/Hledger/Cli/Commands/Setup.txt | 6 +++ hledger/Hledger/Cli/Commands/commands.m4 | 1 + hledger/hledger.cabal | 2 + hledger/hledger.m4.md | 2 + hledger/package.yaml | 2 + 8 files changed, 90 insertions(+) create mode 100644 hledger/Hledger/Cli/Commands/Setup.hs create mode 100644 hledger/Hledger/Cli/Commands/Setup.md create mode 100644 hledger/Hledger/Cli/Commands/Setup.txt diff --git a/hledger/Hledger/Cli/Commands.hs b/hledger/Hledger/Cli/Commands.hs index 31a32f477..8cb6ea8e7 100644 --- a/hledger/Hledger/Cli/Commands.hs +++ b/hledger/Hledger/Cli/Commands.hs @@ -49,6 +49,7 @@ module Hledger.Cli.Commands ( ,module Hledger.Cli.Commands.Register ,module Hledger.Cli.Commands.Rewrite ,module Hledger.Cli.Commands.Run + ,module Hledger.Cli.Commands.Setup ,module Hledger.Cli.Commands.Stats ,module Hledger.Cli.Commands.Tags ) @@ -99,6 +100,7 @@ import Hledger.Cli.Commands.Register import Hledger.Cli.Commands.Rewrite import Hledger.Cli.Commands.Roi import Hledger.Cli.Commands.Run +import Hledger.Cli.Commands.Setup import Hledger.Cli.Commands.Stats import Hledger.Cli.Commands.Tags import Hledger.Cli.Utils (tests_Cli_Utils) @@ -138,6 +140,7 @@ builtinCommands = [ ,(roimode , roi) ,(runmode , runOrReplStub) ,(replmode , runOrReplStub) + ,(setupmode , setup) ,(statsmode , stats) ,(tagsmode , tags) ,(testmode , testcmd) @@ -292,6 +295,7 @@ commandsList progversion othercmds = ," diff compare an account's transactions in two journals" ,"+git save or view journal file history simply in git" -- hledger-git ,"+pijul save or view journal file history simply in pijul" -- hledger-pijul + ," setup check and help set up various installation things" ," test run some self tests" ,"" -----------------------------------------80------------------------------------- diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs new file mode 100644 index 000000000..67b5baf1a --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -0,0 +1,64 @@ +{-| + +Check and help set up various installation things. + +-} + +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE RecordWildCards #-} + +module Hledger.Cli.Commands.Setup ( + setupmode + ,setup +) +where + +-- import Data.Default (def) +-- import System.FilePath (takeFileName) +-- import Data.List (intercalate, nub, sortOn) +-- import Data.List.Extra (nubSort) +-- import qualified Data.Map as Map +-- import Data.Maybe (fromMaybe) +-- import Data.HashSet (size, fromList) +-- import qualified Data.Text as T +-- import qualified Data.Text.Lazy as TL +-- import qualified Data.Text.Lazy.Builder as TB +-- import Data.Time.Calendar (Day, addDays, diffDays) +-- import Data.Time.Clock.POSIX (getPOSIXTime) +-- import GHC.Stats +-- -- import System.Console.CmdArgs.Explicit hiding (Group) +-- import System.Mem (performMajorGC) +-- import Text.Printf (printf) +-- import Text.Tabular.AsciiWide + +import Hledger +import Hledger.Cli.CliOptions +-- import Hledger.Cli.Utils (writeOutputLazyText) + + +setupmode = hledgerCommandMode + $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") + [] + cligeneralflagsgroups1 + hiddenflags + ([], Just $ argsFlag "[QUERY]") + +-- like Register.summarisePostings +-- | Print various statistics for the journal. +setup :: CliOpts -> Journal -> IO () +setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _j = do + print "setup" + + + -- let today = _rsDay rspec + -- verbose = boolopt "verbose" rawopts + -- q = _rsQuery rspec + -- l = ledgerFromJournal q j + -- intervalspans = snd $ reportSpanBothDates j rspec + -- ismultiperiod = length intervalspans > 1 + -- (ls, txncounts) = unzip $ map (showLedgerStats verbose l today) intervalspans + -- numtxns = sum txncounts + -- txt = (if ismultiperiod then id else TL.init) $ TB.toLazyText $ unlinesB ls + -- writeOutputLazyText opts txt diff --git a/hledger/Hledger/Cli/Commands/Setup.md b/hledger/Hledger/Cli/Commands/Setup.md new file mode 100644 index 000000000..5a251b715 --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Setup.md @@ -0,0 +1,9 @@ +## setup + +Check and help set up various installation things. + +```flags +Flags: +no command-specific flags +``` + diff --git a/hledger/Hledger/Cli/Commands/Setup.txt b/hledger/Hledger/Cli/Commands/Setup.txt new file mode 100644 index 000000000..409c9b4c3 --- /dev/null +++ b/hledger/Hledger/Cli/Commands/Setup.txt @@ -0,0 +1,6 @@ +setup + +Check and help set up various installation things. + +Flags: +no command-specific flags diff --git a/hledger/Hledger/Cli/Commands/commands.m4 b/hledger/Hledger/Cli/Commands/commands.m4 index c8869f2fe..46e2cb6e0 100644 --- a/hledger/Hledger/Cli/Commands/commands.m4 +++ b/hledger/Hledger/Cli/Commands/commands.m4 @@ -70,6 +70,7 @@ _command_({{Rewrite}}) _command_({{Check}}) _command_({{Diff}}) +_command_({{Setup}}) _command_({{Test}}) }})m4_dnl diff --git a/hledger/hledger.cabal b/hledger/hledger.cabal index 50c9003d0..96ea9a3f3 100644 --- a/hledger/hledger.cabal +++ b/hledger/hledger.cabal @@ -87,6 +87,7 @@ extra-source-files: Hledger/Cli/Commands/Repl.txt Hledger/Cli/Commands/Roi.txt Hledger/Cli/Commands/Run.txt + Hledger/Cli/Commands/Setup.txt Hledger/Cli/Commands/Stats.txt Hledger/Cli/Commands/Tags.txt Hledger/Cli/Commands/Test.txt @@ -139,6 +140,7 @@ library Hledger.Cli.Commands.Rewrite Hledger.Cli.Commands.Roi Hledger.Cli.Commands.Run + Hledger.Cli.Commands.Setup Hledger.Cli.Commands.Stats Hledger.Cli.Commands.Tags Hledger.Cli.CompoundBalanceCommand diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 0fc9b7a6e..c364113a1 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -702,6 +702,7 @@ Here are those commands and the formats currently supported: | prices | | | | | | | rewrite | | | | | | | roi | | | | | | +| setup | | | | | | | stats | | | | | | | stockquotes | | | | | | | tags | | | | | | @@ -6514,6 +6515,7 @@ If you have installed more [add-on commands](../scripts.md), they also will be l - [check](#check) - check for various kinds of error in the data - [diff](#diff) - compare account transactions in two journal files +- [setup](#setup) - check and help set up various installation things - [test](#test) - run self tests diff --git a/hledger/package.yaml b/hledger/package.yaml index f03a66b88..38a1b2102 100644 --- a/hledger/package.yaml +++ b/hledger/package.yaml @@ -85,6 +85,7 @@ extra-source-files: - Hledger/Cli/Commands/Repl.txt - Hledger/Cli/Commands/Roi.txt - Hledger/Cli/Commands/Run.txt +- Hledger/Cli/Commands/Setup.txt - Hledger/Cli/Commands/Stats.txt - Hledger/Cli/Commands/Tags.txt - Hledger/Cli/Commands/Test.txt @@ -193,6 +194,7 @@ library: - Hledger.Cli.Commands.Rewrite - Hledger.Cli.Commands.Roi - Hledger.Cli.Commands.Run + - Hledger.Cli.Commands.Setup - Hledger.Cli.Commands.Stats - Hledger.Cli.Commands.Tags - Hledger.Cli.CompoundBalanceCommand From 69232cae7a0f970a8fa8a535208d8cb38ad29f18 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 01:26:11 -1000 Subject: [PATCH 02/18] feat: setup: first setup check: is hledger in PATH ? --- hledger/Hledger/Cli.hs | 2 +- hledger/Hledger/Cli/Commands.hs | 2 +- hledger/Hledger/Cli/Commands/Setup.hs | 168 ++++++++++++++++++++------ hledger/Hledger/Cli/Commands/Setup.md | 2 +- hledger/hledger.m4.md | 2 +- 5 files changed, 138 insertions(+), 38 deletions(-) diff --git a/hledger/Hledger/Cli.hs b/hledger/Hledger/Cli.hs index aec30c9bc..bb1f07ec0 100644 --- a/hledger/Hledger/Cli.hs +++ b/hledger/Hledger/Cli.hs @@ -421,7 +421,7 @@ main = exitOnExceptions $ withGhcDebug' $ do | manFlag -> runManForTopic "hledger" mmodecmdname -- 6.5.2. builtin command which should not require or read the journal - run it - | cmdname `elem` ["commands","demo","help","test"] -> + | cmdname `elem` ["commands","demo","help","setup","test"] -> cmdaction opts (ignoredjournal cmdname) -- 6.5.3. builtin command which should create the journal if missing - do that and run it diff --git a/hledger/Hledger/Cli/Commands.hs b/hledger/Hledger/Cli/Commands.hs index 8cb6ea8e7..4da675294 100644 --- a/hledger/Hledger/Cli/Commands.hs +++ b/hledger/Hledger/Cli/Commands.hs @@ -295,7 +295,7 @@ commandsList progversion othercmds = ," diff compare an account's transactions in two journals" ,"+git save or view journal file history simply in git" -- hledger-git ,"+pijul save or view journal file history simply in pijul" -- hledger-pijul - ," setup check and help set up various installation things" + ," setup check and show the status of the hledger installation" ," test run some self tests" ,"" -----------------------------------------80------------------------------------- diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 67b5baf1a..4e3b89ca7 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -1,6 +1,8 @@ {-| -Check and help set up various installation things. +Check and show the status of the hledger installation, +show extra info and hints, +and offer to fix problems where possible. -} @@ -15,27 +17,18 @@ module Hledger.Cli.Commands.Setup ( ) where --- import Data.Default (def) --- import System.FilePath (takeFileName) --- import Data.List (intercalate, nub, sortOn) --- import Data.List.Extra (nubSort) --- import qualified Data.Map as Map --- import Data.Maybe (fromMaybe) --- import Data.HashSet (size, fromList) --- import qualified Data.Text as T --- import qualified Data.Text.Lazy as TL --- import qualified Data.Text.Lazy.Builder as TB --- import Data.Time.Calendar (Day, addDays, diffDays) --- import Data.Time.Clock.POSIX (getPOSIXTime) --- import GHC.Stats --- -- import System.Console.CmdArgs.Explicit hiding (Group) --- import System.Mem (performMajorGC) --- import Text.Printf (printf) --- import Text.Tabular.AsciiWide - +import System.FilePath +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.IO as T import Hledger import Hledger.Cli.CliOptions --- import Hledger.Cli.Utils (writeOutputLazyText) +import Control.Monad +import System.Info +import System.Directory +import System.IO +import Safe +-- import Text.Printf (printf) setupmode = hledgerCommandMode @@ -45,20 +38,127 @@ setupmode = hledgerCommandMode hiddenflags ([], Just $ argsFlag "[QUERY]") --- like Register.summarisePostings --- | Print various statistics for the journal. +-- | 1. Check and show the status of various aspects of the hledger installation. +-- 2. Show extra info and hints on how to fix problems. +-- 3. When possible, offer to help fix problems, interactively. setup :: CliOpts -> Journal -> IO () -setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _j = do - print "setup" +setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do + -- This command is not given a journal and should not use _ignoredj; + -- instead detect it ourselves when we are ready. + let + p ok ymsg nmsg = + putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] + where + y = "yes ✅" + n = "no ❌" + putStrLn "hledger:" - -- let today = _rsDay rspec - -- verbose = boolopt "verbose" rawopts - -- q = _rsQuery rspec - -- l = ledgerFromJournal q j - -- intervalspans = snd $ reportSpanBothDates j rspec - -- ismultiperiod = length intervalspans > 1 - -- (ls, txncounts) = unzip $ map (showLedgerStats verbose l today) intervalspans - -- numtxns = sum txncounts - -- txt = (if ismultiperiod then id else TL.init) $ TB.toLazyText $ unlinesB ls - -- writeOutputLazyText opts txt + putStr "- in PATH ?" + pathexes <- findExecutables progname + home <- getHomeDirectory + appdata <- getXdgDirectory XdgData "" + otherexes <- flip findExecutablesInDirectories progname $ + [home ".local/bin" + ,home ".cabal/bin" + ,home ".nix-profile/bin" + ,"/opt/homebrew/bin" + ,"/usr/local/bin" + ,"/usr/bin" + ] + ++ [appdata "local/bin" | os == "mingw32"] + ++ [appdata "cabal/bin" | os == "mingw32"] + let + ok = not $ null pathexes + pathexe = quoteIfNeeded $ headDef (error' "showing nonexistent executable") pathexes + otherexe = quoteIfNeeded $ headDef (error' "showing nonexistent executable") otherexes + otherdir = takeDirectory otherexe + hint = if null otherexes + then ("Add " <> progname <> "'s directory to your shell's PATH.") + else unlines + ["Add " <> otherdir <> " to PATH in your shell config." + ," Eg on unix: echo 'export PATH=" <> otherdir <> ":$PATH' >> ~/.profile" + ," and start a new shell session." + ] + p ok pathexe hint + + -- putStr "- runnable ?" + + -- putStr "- up to date ?" + + -- putStr "- native binary ?" + + -- putStr "- eget installed ?" + + putStr "\n" + + -- putStrLn "config:" + -- putStr "- user config file exists ?" + -- putStr "\n" + -- putStr "- local config masking user config ?" + -- putStr "\n" + -- putStr "- config file readable ?" + -- putStr "\n" + -- putStr "- common general options configured ?" + -- putStr "\n" + -- putStr " --pretty --ignore-assertions --infer-costs" + -- putStr "\n" + -- putStr " print --explicit --show-costs" + -- putStr "\n" + -- putStr "\n" + + -- putStrLn "files:" + -- putStr "- default journal file exists ?" + -- putStr "\n" + -- putStr "- default journal file readable ?" + -- putStr "\n" + -- putStr "\n" + + -- putStrLn "accounts:" + -- putStr "- all account types declared or detected ?" + -- putStr "\n" + -- putStr " asset, liability, equity, revenue, expense, cash, conversion" + -- putStr "\n" + -- putStr "- untyped accounts ?" + -- putStr "\n" + -- putStr "- all used accounts declared ?" + -- putStr "\n" + -- putStr "\n" + + -- putStrLn "commodities:" + -- putStr "- all used commodities declared ?" + -- putStr "\n" + -- putStr "\n" + + -- putStrLn "tags:" + -- putStr "- all used tags declared ?" + -- putStr "\n" + -- putStr "\n" + +{- | Ensure there is a journal file at the given path, creating an empty one if needed. +On Windows, also ensure that the path contains no trailing dots +which could cause data loss (see 'isWindowsUnsafeDotPath'). +-} +_ensureJournalFileExists :: FilePath -> IO () +_ensureJournalFileExists f = do + when (os == "mingw32" && isWindowsUnsafeDotPath f) $ + error' $ + "Part of file path \"" <> show f <> "\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n" + exists <- doesFileExist f + unless exists $ do + hPutStrLn stderr $ "Creating hledger journal file " <> show f + -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, + -- we currently require unix line endings on all platforms. + newJournalContent >>= T.writeFile f + +{- | Does any part of this path contain non-. characters and end with a . ? +Such paths are not safe to use on Windows (cf #1056). +-} +isWindowsUnsafeDotPath :: FilePath -> Bool +isWindowsUnsafeDotPath = any (\x -> last x == '.' && any (/= '.') x) . splitDirectories + +-- | Give the content for a new auto-created journal file. +newJournalContent :: IO Text +newJournalContent = do + d <- getCurrentDay + return $ "; journal created " <> T.pack (show d) <> " by hledger\n" diff --git a/hledger/Hledger/Cli/Commands/Setup.md b/hledger/Hledger/Cli/Commands/Setup.md index 5a251b715..9f61e917e 100644 --- a/hledger/Hledger/Cli/Commands/Setup.md +++ b/hledger/Hledger/Cli/Commands/Setup.md @@ -1,6 +1,6 @@ ## setup -Check and help set up various installation things. +Check and show the status of the hledger installation. ```flags Flags: diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index c364113a1..65956a380 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -6515,7 +6515,7 @@ If you have installed more [add-on commands](../scripts.md), they also will be l - [check](#check) - check for various kinds of error in the data - [diff](#diff) - compare account transactions in two journal files -- [setup](#setup) - check and help set up various installation things +- [setup](#setup) - check and show the status of the hledger installation - [test](#test) - run self tests From 320d19773590905299e389cc86e2fabfcbd7acd9 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 04:16:13 -1000 Subject: [PATCH 03/18] imp:setup: more hledger setup tests --- hledger/Hledger/Cli/Commands/Setup.hs | 148 ++++++++++++++++++++------ 1 file changed, 115 insertions(+), 33 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 4e3b89ca7..32adfbdf2 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -1,15 +1,19 @@ {-| -Check and show the status of the hledger installation, -show extra info and hints, -and offer to fix problems where possible. +Check and show the status of the hledger installation. -} +-- {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +-- {-# LANGUAGE NamedFieldPuns #-} +-- {-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE RecordWildCards #-} + +{-# OPTIONS_GHC -Wno-name-shadowing #-} +{-# OPTIONS_GHC -Wno-unused-top-binds #-} +-- {-# OPTIONS_GHC -Wno-unused-matches #-} module Hledger.Cli.Commands.Setup ( setupmode @@ -17,19 +21,25 @@ module Hledger.Cli.Commands.Setup ( ) where -import System.FilePath +import Control.Exception +import Control.Monad import Data.Text (Text) import qualified Data.Text as T +import qualified Data.Text.Encoding as T import qualified Data.Text.IO as T +import Network.HTTP.Client +import Network.HTTP.Types (statusCode, hLocation) +import Network.HTTP.Req +import Safe +import System.Directory +import System.Exit +import System.FilePath +import System.Info +import System.IO +import System.Process + import Hledger import Hledger.Cli.CliOptions -import Control.Monad -import System.Info -import System.Directory -import System.IO -import Safe --- import Text.Printf (printf) - setupmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") @@ -38,23 +48,33 @@ setupmode = hledgerCommandMode hiddenflags ([], Just $ argsFlag "[QUERY]") --- | 1. Check and show the status of various aspects of the hledger installation. --- 2. Show extra info and hints on how to fix problems. --- 3. When possible, offer to help fix problems, interactively. +-- | Check and show the status of various aspects of the hledger installation. +-- Also show extra info and hints on how to fix problems. setup :: CliOpts -> Journal -> IO () setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- This command is not given a journal and should not use _ignoredj; -- instead detect it ourselves when we are ready. - let - p ok ymsg nmsg = - putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] - where - y = "yes ✅" - n = "no ❌" + putStrLn "checking setup..." + setupHledger + -- setupConfig + -- setupFiles + -- setupAccounts + -- setupCommodities + -- setupTags +-- | Print a check's status, and optional messages if it's ok or not ok. +p :: Bool -> String -> String -> IO () +p ok ymsg nmsg = + putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] + where + y = "yes ✅" + n = "no ❌" + +setupHledger :: IO () +setupHledger = do putStrLn "hledger:" - putStr "- in PATH ?" + putStr "- is in PATH ?" pathexes <- findExecutables progname home <- getHomeDirectory appdata <- getXdgDirectory XdgData "" @@ -82,17 +102,54 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do ] p ok pathexe hint - -- putStr "- runnable ?" + -- If hledger was found in PATH, run more checks + when ok $ do - -- putStr "- up to date ?" + putStr "- runs ?" + let arg = "--version" + (exit,out,err) <- readProcessWithExitCode progname [arg] "" + let + ok = exit == ExitSuccess + hint = "'" <> progname <> " " <> arg <> "' failed: \n" <> err + p ok "" hint + let verparts = words out -- use below - -- putStr "- native binary ?" + -- If hledger runs, run more checks + when ok $ do + putStr "- is a native binary ?" + let + exearch = case drop 2 verparts of + w:_ -> w + _ -> error' "couldn't parse arch from --version output" + os' -- keep synced: Version.hs + | os == "darwin" = "mac" + | os == "mingw32" = "windows" + | otherwise = os + sysarch = os' <> "-" <> arch + ok = exearch == sysarch + hint = "installed binary is for " <> exearch <> ", system is " <> sysarch + p ok "" hint + + putStr "- is up to date ?" + elatestver <- getLatestHledgerVersion + let + latestver = case elatestver of + Left e -> error' $ "failed to detect latest hledger version: " <> e + Right v -> v + exedetailedver = case drop 1 verparts of + w:_ -> w + _ -> error' "couldn't parse detailed version from --version output" + exever = takeWhile (`elem` ("0123456789."::String)) exedetailedver + ok = splitAtElement '.' exever >= splitAtElement '.' latestver + msg + | exever == latestver = exever + | otherwise = exever <> " installed, latest release is " <> latestver + p ok msg msg -- putStr "- eget installed ?" - putStr "\n" - - -- putStrLn "config:" +setupConfig = do + putStrLn "config:" -- putStr "- user config file exists ?" -- putStr "\n" -- putStr "- local config masking user config ?" @@ -107,14 +164,16 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- putStr "\n" -- putStr "\n" - -- putStrLn "files:" +setupFiles = do + putStrLn "files:" -- putStr "- default journal file exists ?" -- putStr "\n" -- putStr "- default journal file readable ?" -- putStr "\n" -- putStr "\n" - -- putStrLn "accounts:" +setupAccounts = do + putStrLn "accounts:" -- putStr "- all account types declared or detected ?" -- putStr "\n" -- putStr " asset, liability, equity, revenue, expense, cash, conversion" @@ -125,16 +184,39 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- putStr "\n" -- putStr "\n" - -- putStrLn "commodities:" +setupCommodities = do + putStrLn "commodities:" -- putStr "- all used commodities declared ?" -- putStr "\n" -- putStr "\n" - -- putStrLn "tags:" +setupTags = do + putStrLn "tags:" -- putStr "- all used tags declared ?" -- putStr "\n" -- putStr "\n" +getLatestHledgerVersion :: IO (Either String String) +getLatestHledgerVersion = do + result <- try $ runReq defaultHttpConfig{httpConfigRedirectCount=0} $ + req GET (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse mempty + case result of + Right _ -> return $ Left "no redirect" + Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException rsp _))) -> do + let status = statusCode $ responseStatus rsp + if status >= 300 && status < 400 + then do + let locationHeader = lookup hLocation (responseHeaders rsp) + case fmap T.decodeUtf8 locationHeader of + Nothing -> return $ Left "redirect response with no Location header" + Just location -> do + let packagename = take 1 $ drop 1 $ reverse $ T.splitOn "/" location + case packagename of + [n] -> return $ Right $ dropWhile (`notElem` ['0'..'9']) $ T.unpack n + _ -> return $ Left "failed to parse version from Location header" + else return $ Left $ "non-redirect status code: " ++ show status + Left err -> return $ Left $ "other exception: " ++ show err + {- | Ensure there is a journal file at the given path, creating an empty one if needed. On Windows, also ensure that the path contains no trailing dots which could cause data loss (see 'isWindowsUnsafeDotPath'). From daf550bbb0556f59cc4d747ac25de85740262c1a Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 17:18:13 -1000 Subject: [PATCH 04/18] imp: Hledger.Cli.Conf: refactor, expose more info about config files --- hledger/Hledger/Cli/Conf.hs | 82 ++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/hledger/Hledger/Cli/Conf.hs b/hledger/Hledger/Cli/Conf.hs index b58fe1aac..9e3124c98 100644 --- a/hledger/Hledger/Cli/Conf.hs +++ b/hledger/Hledger/Cli/Conf.hs @@ -8,7 +8,14 @@ Read extra CLI arguments from a hledger config file. module Hledger.Cli.Conf ( getConf + ,nullconf ,confLookup + ,activeConfFile + ,activeLocalConfFile + ,activeUserConfFile + ,confFiles + ,userConfFiles + ,parseConf ) where @@ -19,6 +26,7 @@ import qualified Data.Map as M import Data.Maybe (catMaybes) import Data.Text (Text) import qualified Data.Text as T (pack) +import Safe (headMay) import System.Directory (getHomeDirectory, getXdgDirectory, XdgDirectory (XdgConfig), doesFileExist, getCurrentDirectory) import System.FilePath ((), takeDirectory) import Text.Megaparsec @@ -99,12 +107,9 @@ getConf rawopts = do NoConfFile -> return $ traceAt 1 "ignoring config files" (nullconf, Nothing) SomeConfFile f -> getCurrentDirectory >>= flip expandPath f >>= readConfFile . dbg1 "using specified config file" AutoConfFile -> do - defconfpaths <- defaultConfFilePaths - conffiles <- fmap catMaybes $ forM defconfpaths $ \f -> do - exists <- doesFileExist f - return $ if exists then Just f else Nothing - case conffiles of - f:_ -> dbg8IO "found config files" conffiles >> dbg1IO "using nearest config file" f >> readConfFile f + fs <- confFiles + case fs of + f:_ -> dbg8IO "found config files" fs >> dbg1IO "using nearest config file" f >> readConfFile f [] -> return $ traceAt 1 "no config file found" (nullconf, Nothing) -- | Read this config file and parse its contents, or raise an error. @@ -113,7 +118,6 @@ readConfFile f = do -- avoid GHC 9.10.1's ugly stack trace when calling readFile on a nonexistent file exists <- doesFileExist f when (not exists) $ error' $ f <> " does not exist" - ecs <- readFile f <&> parseConf f . T.pack case ecs of Left err -> error' $ errorBundlePretty err -- customErrorBundlePretty err @@ -125,17 +129,67 @@ readConfFile f = do Just f ) --- | Get the possible paths for a hledger config file, depending on the current directory. -defaultConfFilePaths :: IO [FilePath] -defaultConfFilePaths = do - ds <- confDirs +-- | Get the highest precedence config file, based on the current directory. +activeConfFile :: IO (Maybe FilePath) +activeConfFile = headMay <$> confFiles + +-- | Get the highest precedence local config file: +-- a config file in the current directory or above, that is not a user-wide config file. +activeLocalConfFile :: IO (Maybe FilePath) +activeLocalConfFile = do + ufs <- userConfFiles + mf <- headMay <$> confFiles + return $ case mf of + Just f | f `notElem` ufs -> Just f + _ -> Nothing + +-- | Get the highest precedence user-wide config file, based on the current directory. +-- (This may not be the active config file.) +activeUserConfFile :: IO (Maybe FilePath) +activeUserConfFile = headMay <$> userConfFiles + +-- | Get the possibleConfFiles which exist, based on the current directory. +confFiles :: IO [FilePath] +confFiles = possibleConfFiles >>= existingFiles + +-- | Get the possibleUserConfFiles which exist, based on the current directory. +userConfFiles :: IO [FilePath] +userConfFiles = possibleUserConfFiles >>= existingFiles + +-- | Filter a list of paths to just the existing files. +existingFiles :: [FilePath] -> IO [FilePath] +existingFiles fs = + fmap catMaybes $ forM fs $ \f -> do + exists <- doesFileExist f + return $ if exists then Just f else Nothing + +-- | Get the possible paths for a hledger config file, highest precedence first: +-- hledger.conf in the current directory, +-- hledger.conf in any parent directory, +-- .hledger.conf in the home directory, +-- or hledger.conf in the XdgConfig directory. +possibleConfFiles :: IO [FilePath] +possibleConfFiles = do + ds <- possibleConfDirs home <- getHomeDirectory return $ dbg8 "possible config file paths" $ flip map ds $ \d -> d if d==home then ".hledger.conf" else "hledger.conf" --- | Get the directories to check for a hledger config file. -confDirs :: IO [FilePath] -confDirs = do +-- | Like possibleConfFiles, but consider only user-wide hledger config files: +-- .hledger.conf in the home directory, +-- or hledger.conf in the XdgConfig directory. +possibleUserConfFiles :: IO [FilePath] +possibleUserConfFiles = do + home <- getHomeDirectory + xdgc <- getXdgDirectory XdgConfig "hledger" + let ds = [home,xdgc] + return $ dbg8 "possible user config file paths" $ + flip map ds $ \d -> d if d==home then ".hledger.conf" else "hledger.conf" + +-- | Get the directories where a hledger config file could be, highest precedence first: +-- the current directory, any parent directory, the home directory, or the XdgConfig directory. +possibleConfDirs :: IO [FilePath] +possibleConfDirs = do xdgc <- getXdgDirectory XdgConfig "hledger" home <- getHomeDirectory here <- getCurrentDirectory From bb7510194e920a6fca662191e07842adbb5505d8 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 17:19:28 -1000 Subject: [PATCH 05/18] imp: setup: setup ignores the config file itself, for robustness --- hledger/Hledger/Cli.hs | 4 +++- hledger/hledger.m4.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/hledger/Hledger/Cli.hs b/hledger/Hledger/Cli.hs index bb1f07ec0..a0c968917 100644 --- a/hledger/Hledger/Cli.hs +++ b/hledger/Hledger/Cli.hs @@ -271,7 +271,9 @@ main = exitOnExceptions $ withGhcDebug' $ do -- Read extra general and command-specific args/opts from the config file, if any. (conf, mconffile) <- seq cliconfrawopts $ -- order debug output - getConf cliconfrawopts + if clicmdarg=="setup" -- the setup command checks config files, but never uses one itself + then return (nullconf,Nothing) + else getConf cliconfrawopts --------------------------------------------------------------- dbgIO "\n3. Identify a command name from config file or command line" () diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 65956a380..cc00235c1 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -615,6 +615,8 @@ This is useful when using hledger in scripts, or when troubleshooting. When both `--conf` and `--no-conf` options are used, the right-most wins. To inspect the processing of config files, use `--debug` or `--debug=8`. +Or, run the `setup` command, which will display any active config files. +(Apart from this, `setup` is not affected by config files, unlike other commands.) **Warning!** From 98da9709fc282577032fce6c500e11988449f9a7 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 17:20:05 -1000 Subject: [PATCH 06/18] imp:setup: config file tests; tidier more portable color-aware output --- hledger/Hledger/Cli/Commands/Setup.hs | 148 +++++++++++++++++--------- 1 file changed, 99 insertions(+), 49 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 32adfbdf2..064f9c4c5 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -21,8 +21,11 @@ module Hledger.Cli.Commands.Setup ( ) where +import Control.Applicative ((<|>)) import Control.Exception import Control.Monad +import Data.Functor ((<&>)) +import Data.Maybe import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -37,9 +40,12 @@ import System.FilePath import System.Info import System.IO import System.Process +import Text.Megaparsec.Error (errorBundlePretty) +import Text.Printf (printf) import Hledger import Hledger.Cli.CliOptions +import Hledger.Cli.Conf setupmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") @@ -48,7 +54,7 @@ setupmode = hledgerCommandMode hiddenflags ([], Just $ argsFlag "[QUERY]") --- | Check and show the status of various aspects of the hledger installation. +-- | Test and print the status of various aspects of the hledger installation. -- Also show extra info and hints on how to fix problems. setup :: CliOpts -> Journal -> IO () setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do @@ -56,25 +62,48 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- instead detect it ourselves when we are ready. putStrLn "checking setup..." setupHledger - -- setupConfig + setupConfig -- setupFiles -- setupAccounts -- setupCommodities -- setupTags --- | Print a check's status, and optional messages if it's ok or not ok. +-- | Print a test's pass or fail status, in green/red if supported, and optional messages if it's ok or not ok. p :: Bool -> String -> String -> IO () p ok ymsg nmsg = putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] where - y = "yes ✅" - n = "no ❌" + y = bold' $ brightGreen' "yes" -- ✅ apple emojis - won't work everywhere + n = bold' $ brightRed' "no" -- ❌ + +-- | Like p, but display both statuses as a warning message, in yellow if supported. +w :: Bool -> String -> String -> IO () +w ok ymsg nmsg = + putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] + where + y = bold' $ brightYellow' "yes" -- ⚠️ + n = bold' $ brightYellow' "no" -- ⚠️ + +-- | Like p, but display both statuses as an info message, in blue if supported. +i :: Bool -> String -> String -> IO () +i ok ymsg nmsg = + putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] + where + y = bold' $ brightBlue' "yes" -- ℹ️ + n = bold' $ brightBlue' "no" -- ℹ️ + +-- | Print a setup test group's heading. +pgroup s = putStr $ bold' $ "\n" <> s <> ":\n" + +-- | Print a setup test's description, formatting and padding it to a fixed width. +pdesc :: String -> IO () +pdesc s = printf "- %-38s" s setupHledger :: IO () setupHledger = do - putStrLn "hledger:" + pgroup "hledger" - putStr "- is in PATH ?" + pdesc "is in PATH ?" pathexes <- findExecutables progname home <- getHomeDirectory appdata <- getXdgDirectory XdgData "" @@ -105,7 +134,7 @@ setupHledger = do -- If hledger was found in PATH, run more checks when ok $ do - putStr "- runs ?" + pdesc "runs ?" let arg = "--version" (exit,out,err) <- readProcessWithExitCode progname [arg] "" let @@ -116,7 +145,7 @@ setupHledger = do -- If hledger runs, run more checks when ok $ do - putStr "- is a native binary ?" + pdesc "is a native binary ?" let exearch = case drop 2 verparts of w:_ -> w @@ -130,7 +159,7 @@ setupHledger = do hint = "installed binary is for " <> exearch <> ", system is " <> sysarch p ok "" hint - putStr "- is up to date ?" + pdesc "is up to date ?" elatestver <- getLatestHledgerVersion let latestver = case elatestver of @@ -146,55 +175,76 @@ setupHledger = do | otherwise = exever <> " installed, latest release is " <> latestver p ok msg msg - -- putStr "- eget installed ?" + -- pdesc "eget installed ?" setupConfig = do - putStrLn "config:" - -- putStr "- user config file exists ?" - -- putStr "\n" - -- putStr "- local config masking user config ?" - -- putStr "\n" - -- putStr "- config file readable ?" - -- putStr "\n" - -- putStr "- common general options configured ?" - -- putStr "\n" - -- putStr " --pretty --ignore-assertions --infer-costs" - -- putStr "\n" - -- putStr " print --explicit --show-costs" - -- putStr "\n" - -- putStr "\n" + pgroup "config" + + pdesc "user has a config file ?" + muf <- activeUserConfFile + let + (ok, msg) = case muf of + Just f -> (True, f) + Nothing -> (False, "") + i ok msg msg + + pdesc "a local config file exists ?" + mlf <- activeLocalConfFile + let + (ok, msg) = case mlf of + Just f -> (True, f) -- <> if isJust muf then " (masking user config)" else "") + Nothing -> (False, "") + i ok msg msg + + when (isJust muf && isJust mlf) $ do + pdesc "local config is masking user config ?" + i True "" "" + + let mf = mlf <|> muf + case mf of + Nothing -> return () + Just f -> do + pdesc "config file is readable ?" + ecs <- readFile f <&> parseConf f . T.pack + case ecs of + Right _ -> p True "" "" + Left e -> p False "" (errorBundlePretty e) + + -- pdesc "common general options configured ?" + -- --pretty --ignore-assertions --infer-costs" + -- print --explicit --show-costs" setupFiles = do - putStrLn "files:" - -- putStr "- default journal file exists ?" - -- putStr "\n" - -- putStr "- default journal file readable ?" - -- putStr "\n" - -- putStr "\n" + pgroup "files" + -- pdesc "default journal file exists ?" + -- pdesc "\n" + -- pdesc "default journal file readable ?" + -- pdesc "\n" + -- pdesc "\n" setupAccounts = do - putStrLn "accounts:" - -- putStr "- all account types declared or detected ?" - -- putStr "\n" - -- putStr " asset, liability, equity, revenue, expense, cash, conversion" - -- putStr "\n" - -- putStr "- untyped accounts ?" - -- putStr "\n" - -- putStr "- all used accounts declared ?" - -- putStr "\n" - -- putStr "\n" + pgroup "accounts" + -- pdesc "all account types declared or detected ?" + -- pdesc "\n" + -- pdesc " asset, liability, equity, revenue, expense, cash, conversion" + -- pdesc "\n" + -- pdesc "untyped accounts ?" + -- pdesc "\n" + -- pdesc "all used accounts declared ?" + -- pdesc "\n" + -- pdesc "\n" setupCommodities = do - putStrLn "commodities:" - -- putStr "- all used commodities declared ?" - -- putStr "\n" - -- putStr "\n" + pgroup "commodities" + -- pdesc "all used commodities declared ?" + -- pdesc "\n" + -- pdesc "\n" setupTags = do - putStrLn "tags:" - -- putStr "- all used tags declared ?" - -- putStr "\n" - -- putStr "\n" + pgroup "tags" + -- pdesc "all used tags declared ?" + -- pdesc "\n" + -- pdesc "\n" getLatestHledgerVersion :: IO (Either String String) getLatestHledgerVersion = do From d408f00b4223b9c5be8a34b5e5abaf4f4ead38cd Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 17 Apr 2025 17:36:20 -1000 Subject: [PATCH 07/18] imp:setup: use HEAD instead of GET to check current version --- hledger/Hledger/Cli/Commands/Setup.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 064f9c4c5..a8257ea5e 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -249,7 +249,7 @@ setupTags = do getLatestHledgerVersion :: IO (Either String String) getLatestHledgerVersion = do result <- try $ runReq defaultHttpConfig{httpConfigRedirectCount=0} $ - req GET (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse mempty + req HEAD (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse mempty case result of Right _ -> return $ Left "no redirect" Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException rsp _))) -> do From 2ef7434f479b2b7b8c75e94f4e53b74a7b1ea6a2 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Fri, 18 Apr 2025 11:16:19 -1000 Subject: [PATCH 08/18] imp:setup: check files setup (WIP) --- hledger/Hledger/Cli/Commands/Setup.hs | 70 +++++++++++++-------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index a8257ea5e..1a1a6e58b 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -26,10 +26,10 @@ import Control.Exception import Control.Monad import Data.Functor ((<&>)) import Data.Maybe -import Data.Text (Text) +-- import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as T -import qualified Data.Text.IO as T +-- import qualified Data.Text.IO as T import Network.HTTP.Client import Network.HTTP.Types (statusCode, hLocation) import Network.HTTP.Req @@ -38,7 +38,7 @@ import System.Directory import System.Exit import System.FilePath import System.Info -import System.IO +-- import System.IO import System.Process import Text.Megaparsec.Error (errorBundlePretty) import Text.Printf (printf) @@ -61,9 +61,9 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- This command is not given a journal and should not use _ignoredj; -- instead detect it ourselves when we are ready. putStrLn "checking setup..." - setupHledger - setupConfig - -- setupFiles + -- setupHledger + -- setupConfig + setupFiles -- setupAccounts -- setupCommodities -- setupTags @@ -216,11 +216,11 @@ setupConfig = do setupFiles = do pgroup "files" - -- pdesc "default journal file exists ?" - -- pdesc "\n" + + pdesc "default journal file exists ?" + -- pdesc "default journal file readable ?" - -- pdesc "\n" - -- pdesc "\n" + setupAccounts = do pgroup "accounts" @@ -267,30 +267,30 @@ getLatestHledgerVersion = do else return $ Left $ "non-redirect status code: " ++ show status Left err -> return $ Left $ "other exception: " ++ show err -{- | Ensure there is a journal file at the given path, creating an empty one if needed. -On Windows, also ensure that the path contains no trailing dots -which could cause data loss (see 'isWindowsUnsafeDotPath'). --} -_ensureJournalFileExists :: FilePath -> IO () -_ensureJournalFileExists f = do - when (os == "mingw32" && isWindowsUnsafeDotPath f) $ - error' $ - "Part of file path \"" <> show f <> "\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n" - exists <- doesFileExist f - unless exists $ do - hPutStrLn stderr $ "Creating hledger journal file " <> show f - -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, - -- we currently require unix line endings on all platforms. - newJournalContent >>= T.writeFile f +-- {- | Ensure there is a journal file at the given path, creating an empty one if needed. +-- On Windows, also ensure that the path contains no trailing dots +-- which could cause data loss (see 'isWindowsUnsafeDotPath'). +-- -} +-- _ensureJournalFileExists :: FilePath -> IO () +-- _ensureJournalFileExists f = do +-- when (os == "mingw32" && isWindowsUnsafeDotPath f) $ +-- error' $ +-- "Part of file path \"" <> show f <> "\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n" +-- exists <- doesFileExist f +-- unless exists $ do +-- hPutStrLn stderr $ "Creating hledger journal file " <> show f +-- -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, +-- -- we currently require unix line endings on all platforms. +-- newJournalContent >>= T.writeFile f -{- | Does any part of this path contain non-. characters and end with a . ? -Such paths are not safe to use on Windows (cf #1056). --} -isWindowsUnsafeDotPath :: FilePath -> Bool -isWindowsUnsafeDotPath = any (\x -> last x == '.' && any (/= '.') x) . splitDirectories +-- {- | Does any part of this path contain non-. characters and end with a . ? +-- Such paths are not safe to use on Windows (cf #1056). +-- -} +-- isWindowsUnsafeDotPath :: FilePath -> Bool +-- isWindowsUnsafeDotPath = any (\x -> last x == '.' && any (/= '.') x) . splitDirectories --- | Give the content for a new auto-created journal file. -newJournalContent :: IO Text -newJournalContent = do - d <- getCurrentDay - return $ "; journal created " <> T.pack (show d) <> " by hledger\n" +-- -- | Give the content for a new auto-created journal file. +-- newJournalContent :: IO Text +-- newJournalContent = do +-- d <- getCurrentDay +-- return $ "; journal created " <> T.pack (show d) <> " by hledger\n" From 75cd521ed78fcaf60bcc4c098404578d9c75c35a Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 04:51:01 -1000 Subject: [PATCH 09/18] imp:setup: drop unused options --- hledger/Hledger/Cli/Commands/Setup.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 1a1a6e58b..de52048a0 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -50,9 +50,9 @@ import Hledger.Cli.Conf setupmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") [] - cligeneralflagsgroups1 - hiddenflags - ([], Just $ argsFlag "[QUERY]") + [generalflagsgroup3] + [] + ([], Nothing) -- | Test and print the status of various aspects of the hledger installation. -- Also show extra info and hints on how to fix problems. From 4881a0deaacf90939a77e63b60e09d2ebf29a552 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 04:51:22 -1000 Subject: [PATCH 10/18] ;doc:setup --- hledger/Hledger/Cli/Commands/Setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.md b/hledger/Hledger/Cli/Commands/Setup.md index 9f61e917e..bf621a179 100644 --- a/hledger/Hledger/Cli/Commands/Setup.md +++ b/hledger/Hledger/Cli/Commands/Setup.md @@ -1,6 +1,6 @@ ## setup -Check and show the status of the hledger installation. +Check and show the status of various aspects of the hledger installation. ```flags Flags: From f37a4a7dcb1c070680439ee436f54b9021ab009a Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 14:09:42 -1000 Subject: [PATCH 11/18] lib: refactor, export getHomeSafe --- hledger-lib/Hledger/Read.hs | 8 ++++++-- hledger-lib/Hledger/Utils/IO.hs | 5 +++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/hledger-lib/Hledger/Read.hs b/hledger-lib/Hledger/Read.hs index 3028438ba..6f20677d0 100644 --- a/hledger-lib/Hledger/Read.hs +++ b/hledger-lib/Hledger/Read.hs @@ -92,6 +92,9 @@ module Hledger.Read ( defaultJournalPath, requireJournalFileExists, ensureJournalFileExists, + journalEnvVar, + -- journalEnvVar2, + journalDefaultFilename, -- * Journal parsing runExceptT, @@ -110,6 +113,7 @@ module Hledger.Read ( -- * Misc saveLatestDates, saveLatestDatesForFiles, + isWindowsUnsafeDotPath, -- * Re-exported JournalReader.tmpostingrulep, @@ -141,7 +145,7 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Time (Day) import Safe (headDef, headMay) -import System.Directory (doesFileExist, getHomeDirectory) +import System.Directory (doesFileExist) import System.Environment (getEnv) import System.FilePath ((<.>), (), splitDirectories, splitFileName, takeFileName) import System.Info (os) @@ -196,7 +200,7 @@ defaultJournalPath = do `C.catch` (\(_::C.IOException) -> getEnv journalEnvVar2 `C.catch` (\(_::C.IOException) -> return "")) defpath = do - home <- getHomeDirectory `C.catch` (\(_::C.IOException) -> return "") + home <- fromMaybe "" <$> getHomeSafe return $ home journalDefaultFilename -- | A file path optionally prefixed by a reader name and colon diff --git a/hledger-lib/Hledger/Utils/IO.hs b/hledger-lib/Hledger/Utils/IO.hs index c2b4446f1..d91e0096d 100644 --- a/hledger-lib/Hledger/Utils/IO.hs +++ b/hledger-lib/Hledger/Utils/IO.hs @@ -33,6 +33,7 @@ module Hledger.Utils.IO ( getCurrentZonedTime, -- * Files + getHomeSafe, embedFileRelative, expandHomePath, expandPath, @@ -294,6 +295,10 @@ getCurrentZonedTime = do -- Files +-- | Like getHomeDirectory, but in case of IO error (home directory not found, not understood, etc.), returns "". +getHomeSafe :: IO (Maybe FilePath) +getHomeSafe = fmap Just getHomeDirectory `catch` (\(_ :: IOException) -> return Nothing) + -- | Expand a tilde (representing home directory) at the start of a file path. -- ~username is not supported. Can raise an error. expandHomePath :: FilePath -> IO FilePath From 15173eeeb02e21bbf7a652a654ec43878e672475 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 14:10:18 -1000 Subject: [PATCH 12/18] imp: setup: test default file setup --- hledger/Hledger/Cli/Commands/Setup.hs | 81 +++++++++++++++++---------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index de52048a0..0a59cfc18 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -46,6 +46,7 @@ import Text.Printf (printf) import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.Conf +import System.Environment (lookupEnv) setupmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") @@ -61,8 +62,8 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- This command is not given a journal and should not use _ignoredj; -- instead detect it ourselves when we are ready. putStrLn "checking setup..." - -- setupHledger - -- setupConfig + setupHledger + setupConfig setupFiles -- setupAccounts -- setupCommodities @@ -217,10 +218,57 @@ setupConfig = do setupFiles = do pgroup "files" + pdesc "a home directory journal exists ?" + mh <- getHomeSafe + (ok,msg) <- case mh of + Just h -> do + let f = h journalDefaultFilename + e <- doesFileExist f + return (e, if e then f else "") + Nothing -> return (False, "") + i ok msg msg + + pdesc "LEDGER_FILE variable is defined ?" + mf <- lookupEnv journalEnvVar + let + (ok,msg) = case mf of + Just f -> (True, f) + Nothing -> (False, "") + i ok msg msg + + -- case mf of + -- Nothing -> return () + -- Just f -> do + -- pdesc "$LEDGER_FILE journal exists ?" + -- e <- doesFileExist f + -- i e "" "" + + -- when (isJust mh && isJust mf) $ do + -- pdesc "$LEDGER_FILE is masking home journal ?" + -- i True "" "" + pdesc "default journal file exists ?" + jfile <- defaultJournalPath + exists <- doesFileExist jfile + p exists jfile "" - -- pdesc "default journal file readable ?" + when exists $ do + + when (os == "mingw32") $ do + pdesc "default journal file path is safe for Windows ?" + let + (ok,msg) = + -- like ensureJournalFileExists: + if isWindowsUnsafeDotPath jfile + then (False, "the file name ends with a dot, this is unsafe on Windows") + else (True, "") + p ok msg msg + pdesc "default journal file is readable ?" + ej <- runExceptT $ readJournalFile definputopts jfile -- like defaultJournal + case ej of + Right _ -> p True "" "" + Left e -> p False "" e setupAccounts = do pgroup "accounts" @@ -267,30 +315,3 @@ getLatestHledgerVersion = do else return $ Left $ "non-redirect status code: " ++ show status Left err -> return $ Left $ "other exception: " ++ show err --- {- | Ensure there is a journal file at the given path, creating an empty one if needed. --- On Windows, also ensure that the path contains no trailing dots --- which could cause data loss (see 'isWindowsUnsafeDotPath'). --- -} --- _ensureJournalFileExists :: FilePath -> IO () --- _ensureJournalFileExists f = do --- when (os == "mingw32" && isWindowsUnsafeDotPath f) $ --- error' $ --- "Part of file path \"" <> show f <> "\"\n ends with a dot, which is unsafe on Windows; please use a different path.\n" --- exists <- doesFileExist f --- unless exists $ do --- hPutStrLn stderr $ "Creating hledger journal file " <> show f --- -- note Hledger.Utils.UTF8.* do no line ending conversion on windows, --- -- we currently require unix line endings on all platforms. --- newJournalContent >>= T.writeFile f - --- {- | Does any part of this path contain non-. characters and end with a . ? --- Such paths are not safe to use on Windows (cf #1056). --- -} --- isWindowsUnsafeDotPath :: FilePath -> Bool --- isWindowsUnsafeDotPath = any (\x -> last x == '.' && any (/= '.') x) . splitDirectories - --- -- | Give the content for a new auto-created journal file. --- newJournalContent :: IO Text --- newJournalContent = do --- d <- getCurrentDay --- return $ "; journal created " <> T.pack (show d) <> " by hledger\n" From 97e2e8572ffc91c52dd251018c12a4eb05a471ec Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 14:37:20 -1000 Subject: [PATCH 13/18] imp: setup: version check: add a timeout, improve error output --- hledger/Hledger/Cli/Commands/Setup.hs | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 0a59cfc18..ee402a54f 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -32,7 +32,7 @@ import qualified Data.Text.Encoding as T -- import qualified Data.Text.IO as T import Network.HTTP.Client import Network.HTTP.Types (statusCode, hLocation) -import Network.HTTP.Req +import Network.HTTP.Req as R import Safe import System.Directory import System.Exit @@ -163,18 +163,20 @@ setupHledger = do pdesc "is up to date ?" elatestver <- getLatestHledgerVersion let - latestver = case elatestver of - Left e -> error' $ "failed to detect latest hledger version: " <> e - Right v -> v - exedetailedver = case drop 1 verparts of - w:_ -> w - _ -> error' "couldn't parse detailed version from --version output" - exever = takeWhile (`elem` ("0123456789."::String)) exedetailedver - ok = splitAtElement '.' exever >= splitAtElement '.' latestver - msg - | exever == latestver = exever - | otherwise = exever <> " installed, latest release is " <> latestver - p ok msg msg + (ok,msg) = case elatestver of + Left e -> (False, "could not check latest version: " <> e) + Right latestver -> + case drop 1 verparts of + [] -> (False, "could not parse --version output") + w:_ -> (ok, msg) + where + exever = takeWhile (`elem` ("0123456789."::String)) w + ok = splitAtElement '.' exever >= splitAtElement '.' latestver + msg = + if exever == latestver + then exever + else exever <> " installed, latest release is " <> latestver + w ok msg msg -- pdesc "eget installed ?" @@ -294,10 +296,13 @@ setupTags = do -- pdesc "\n" -- pdesc "\n" +-- | Get the current hledger release version from the internet. +-- Currently requests the latest doc page from Hackage and inspects the redirect path. +-- Should catch all normal errors, and times out after 10 seconds. getLatestHledgerVersion :: IO (Either String String) getLatestHledgerVersion = do result <- try $ runReq defaultHttpConfig{httpConfigRedirectCount=0} $ - req HEAD (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse mempty + req HEAD (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse (R.responseTimeout 10000000) -- 10s case result of Right _ -> return $ Left "no redirect" Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException rsp _))) -> do @@ -312,6 +317,6 @@ getLatestHledgerVersion = do case packagename of [n] -> return $ Right $ dropWhile (`notElem` ['0'..'9']) $ T.unpack n _ -> return $ Left "failed to parse version from Location header" - else return $ Left $ "non-redirect status code: " ++ show status + else return $ Left $ "expected redirect, got HTTP status " ++ show status Left err -> return $ Left $ "other exception: " ++ show err From 8b48fc41bc0581178621f3eefa54ed25a679f69d Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 15:06:59 -1000 Subject: [PATCH 14/18] imp: setup: version check: improve error output when unknown --- hledger/Hledger/Cli/Commands/Setup.hs | 57 +++++++++++++++++++++------ 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index ee402a54f..b0aab3249 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -69,13 +69,14 @@ setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- setupCommodities -- setupTags + -- | Print a test's pass or fail status, in green/red if supported, and optional messages if it's ok or not ok. p :: Bool -> String -> String -> IO () p ok ymsg nmsg = putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] where y = bold' $ brightGreen' "yes" -- ✅ apple emojis - won't work everywhere - n = bold' $ brightRed' "no" -- ❌ + n = bold' $ brightRed' "no " -- ❌ -- | Like p, but display both statuses as a warning message, in yellow if supported. w :: Bool -> String -> String -> IO () @@ -83,7 +84,7 @@ w ok ymsg nmsg = putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] where y = bold' $ brightYellow' "yes" -- ⚠️ - n = bold' $ brightYellow' "no" -- ⚠️ + n = bold' $ brightYellow' "no " -- ⚠️ -- | Like p, but display both statuses as an info message, in blue if supported. i :: Bool -> String -> String -> IO () @@ -91,15 +92,42 @@ i ok ymsg nmsg = putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] where y = bold' $ brightBlue' "yes" -- ℹ️ - n = bold' $ brightBlue' "no" -- ℹ️ + n = bold' $ brightBlue' "no " -- ℹ️ + + -- | Print a setup test group's heading. +pgroup :: String -> IO () pgroup s = putStr $ bold' $ "\n" <> s <> ":\n" -- | Print a setup test's description, formatting and padding it to a fixed width. pdesc :: String -> IO () pdesc s = printf "- %-38s" s +-- yes, no, unknown +data YNU = Y | N | U deriving (Eq) + +-- Show status, in red/green/yellow if supported. +instance Show YNU where + show Y = bold' (brightGreen' "yes") -- ✅ apple emojis - won't work everywhere + show N = bold' (brightRed' "no ") -- ❌ + show U = bold' (brightYellow' " ? ") + +-- Show status, in blue/yellow if supported. +showInfo Y = bold' (brightBlue' "yes") -- ℹ️ +showInfo N = bold' (brightBlue' "no ") -- ℹ️ +showInfo U = bold' (brightYellow' " ? ") + +-- | Print a test's pass or fail status, as "yes" or "no" or "", +-- in green/red if supported, and the (possibly empty) provided message. +p' :: YNU -> String -> IO () +p' ok msg = putStrLn $ unwords ["", show ok, "", msg] + +-- | Like p, but display the status as info, in neutral blue. +i' :: YNU -> String -> IO () +i' ok msg = putStrLn $ unwords ["", showInfo ok, "", msg] + + setupHledger :: IO () setupHledger = do pgroup "hledger" @@ -164,19 +192,19 @@ setupHledger = do elatestver <- getLatestHledgerVersion let (ok,msg) = case elatestver of - Left e -> (False, "could not check latest version: " <> e) + Left e -> (U, "could not get " <> latestHledgerVersionUrlStr <> " : " <> e) Right latestver -> case drop 1 verparts of - [] -> (False, "could not parse --version output") + [] -> (U, "could not parse --version output") w:_ -> (ok, msg) where exever = takeWhile (`elem` ("0123456789."::String)) w - ok = splitAtElement '.' exever >= splitAtElement '.' latestver + ok = if splitAtElement '.' exever >= splitAtElement '.' latestver then Y else N msg = if exever == latestver then exever else exever <> " installed, latest release is " <> latestver - w ok msg msg + p' ok msg -- pdesc "eget installed ?" @@ -218,7 +246,7 @@ setupConfig = do -- print --explicit --show-costs" setupFiles = do - pgroup "files" + pgroup "file" pdesc "a home directory journal exists ?" mh <- getHomeSafe @@ -296,27 +324,30 @@ setupTags = do -- pdesc "\n" -- pdesc "\n" +latestHledgerVersionUrl = https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "" +latestHledgerVersionUrlStr = "https://hackage.haskell.org/package/hledger/docs" + -- | Get the current hledger release version from the internet. -- Currently requests the latest doc page from Hackage and inspects the redirect path. -- Should catch all normal errors, and times out after 10 seconds. getLatestHledgerVersion :: IO (Either String String) getLatestHledgerVersion = do result <- try $ runReq defaultHttpConfig{httpConfigRedirectCount=0} $ - req HEAD (https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "") NoReqBody bsResponse (R.responseTimeout 10000000) -- 10s + req HEAD latestHledgerVersionUrl NoReqBody bsResponse (R.responseTimeout 10000000) -- 10s case result of - Right _ -> return $ Left "no redirect" + Right _ -> return $ Left "expected a redirect" Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException rsp _))) -> do let status = statusCode $ responseStatus rsp if status >= 300 && status < 400 then do let locationHeader = lookup hLocation (responseHeaders rsp) case fmap T.decodeUtf8 locationHeader of - Nothing -> return $ Left "redirect response with no Location header" + Nothing -> return $ Left "no Location header" Just location -> do let packagename = take 1 $ drop 1 $ reverse $ T.splitOn "/" location case packagename of [n] -> return $ Right $ dropWhile (`notElem` ['0'..'9']) $ T.unpack n - _ -> return $ Left "failed to parse version from Location header" - else return $ Left $ "expected redirect, got HTTP status " ++ show status + _ -> return $ Left "could not parse Location" + else return $ Left $ "HTTP status " ++ show status Left err -> return $ Left $ "other exception: " ++ show err From 580bc0a8f836b38498ca642940cb96661e47660f Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 15:39:57 -1000 Subject: [PATCH 15/18] dev: setup: refactor output helpers --- hledger/Hledger/Cli/Commands/Setup.hs | 127 +++++++++++--------------- 1 file changed, 52 insertions(+), 75 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index b0aab3249..262c897f4 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -61,42 +61,16 @@ setup :: CliOpts -> Journal -> IO () setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- This command is not given a journal and should not use _ignoredj; -- instead detect it ourselves when we are ready. - putStrLn "checking setup..." + putStrLn "checking..." setupHledger setupConfig setupFiles -- setupAccounts -- setupCommodities -- setupTags + putStr "\n" - --- | Print a test's pass or fail status, in green/red if supported, and optional messages if it's ok or not ok. -p :: Bool -> String -> String -> IO () -p ok ymsg nmsg = - putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] - where - y = bold' $ brightGreen' "yes" -- ✅ apple emojis - won't work everywhere - n = bold' $ brightRed' "no " -- ❌ - --- | Like p, but display both statuses as a warning message, in yellow if supported. -w :: Bool -> String -> String -> IO () -w ok ymsg nmsg = - putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] - where - y = bold' $ brightYellow' "yes" -- ⚠️ - n = bold' $ brightYellow' "no " -- ⚠️ - --- | Like p, but display both statuses as an info message, in blue if supported. -i :: Bool -> String -> String -> IO () -i ok ymsg nmsg = - putStrLn $ unwords $ if ok then ["", y, "", ymsg] else ["", n, "", nmsg] - where - y = bold' $ brightBlue' "yes" -- ℹ️ - n = bold' $ brightBlue' "no " -- ℹ️ - - - --- | Print a setup test group's heading. +-- | Print a setup test groups heading. pgroup :: String -> IO () pgroup s = putStr $ bold' $ "\n" <> s <> ":\n" @@ -120,12 +94,12 @@ showInfo U = bold' (brightYellow' " ? ") -- | Print a test's pass or fail status, as "yes" or "no" or "", -- in green/red if supported, and the (possibly empty) provided message. -p' :: YNU -> String -> IO () -p' ok msg = putStrLn $ unwords ["", show ok, "", msg] +p :: YNU -> String -> IO () +p ok msg = putStrLn $ unwords ["", show ok, "", msg] -- | Like p, but display the status as info, in neutral blue. -i' :: YNU -> String -> IO () -i' ok msg = putStrLn $ unwords ["", showInfo ok, "", msg] +i :: YNU -> String -> IO () +i ok msg = putStrLn $ unwords ["", showInfo ok, "", msg] setupHledger :: IO () @@ -147,33 +121,34 @@ setupHledger = do ++ [appdata "local/bin" | os == "mingw32"] ++ [appdata "cabal/bin" | os == "mingw32"] let - ok = not $ null pathexes + ok = if null pathexes then N else Y pathexe = quoteIfNeeded $ headDef (error' "showing nonexistent executable") pathexes otherexe = quoteIfNeeded $ headDef (error' "showing nonexistent executable") otherexes otherdir = takeDirectory otherexe - hint = if null otherexes - then ("Add " <> progname <> "'s directory to your shell's PATH.") - else unlines - ["Add " <> otherdir <> " to PATH in your shell config." - ," Eg on unix: echo 'export PATH=" <> otherdir <> ":$PATH' >> ~/.profile" - ," and start a new shell session." - ] - p ok pathexe hint + msg + | ok == Y = pathexe + | null otherexes = "Add " <> progname <> "'s directory to your shell's PATH." + | otherwise = unlines + ["Add " <> otherdir <> " to PATH in your shell config." + ," Eg on unix: echo 'export PATH=" <> otherdir <> ":$PATH' >> ~/.profile" + ," and start a new shell session." + ] + p ok msg -- If hledger was found in PATH, run more checks - when ok $ do + when (ok==Y) $ do pdesc "runs ?" let arg = "--version" (exit,out,err) <- readProcessWithExitCode progname [arg] "" let - ok = exit == ExitSuccess - hint = "'" <> progname <> " " <> arg <> "' failed: \n" <> err - p ok "" hint + ok = if exit == ExitSuccess then Y else N + msg = if ok==Y then "" else "'" <> progname <> " " <> arg <> "' failed: \n" <> err + p ok msg let verparts = words out -- use below -- If hledger runs, run more checks - when ok $ do + when (ok==Y) $ do pdesc "is a native binary ?" let exearch = case drop 2 verparts of @@ -184,14 +159,15 @@ setupHledger = do | os == "mingw32" = "windows" | otherwise = os sysarch = os' <> "-" <> arch - ok = exearch == sysarch - hint = "installed binary is for " <> exearch <> ", system is " <> sysarch - p ok "" hint + (ok, msg) + | exearch == sysarch = (Y, "") + | otherwise = (N, "installed binary is for " <> exearch <> ", system is " <> sysarch) + p ok msg pdesc "is up to date ?" elatestver <- getLatestHledgerVersion let - (ok,msg) = case elatestver of + (ok, msg) = case elatestver of Left e -> (U, "could not get " <> latestHledgerVersionUrlStr <> " : " <> e) Right latestver -> case drop 1 verparts of @@ -204,7 +180,7 @@ setupHledger = do if exever == latestver then exever else exever <> " installed, latest release is " <> latestver - p' ok msg + p ok msg -- pdesc "eget installed ?" @@ -215,21 +191,21 @@ setupConfig = do muf <- activeUserConfFile let (ok, msg) = case muf of - Just f -> (True, f) - Nothing -> (False, "") - i ok msg msg + Just f -> (Y, f) + Nothing -> (N, "") + i ok msg pdesc "a local config file exists ?" mlf <- activeLocalConfFile let (ok, msg) = case mlf of - Just f -> (True, f) -- <> if isJust muf then " (masking user config)" else "") - Nothing -> (False, "") - i ok msg msg + Just f -> (Y, f) -- <> if isJust muf then " (masking user config)" else "") + Nothing -> (N, "") + i ok msg when (isJust muf && isJust mlf) $ do pdesc "local config is masking user config ?" - i True "" "" + i Y "" let mf = mlf <|> muf case mf of @@ -238,8 +214,8 @@ setupConfig = do pdesc "config file is readable ?" ecs <- readFile f <&> parseConf f . T.pack case ecs of - Right _ -> p True "" "" - Left e -> p False "" (errorBundlePretty e) + Right _ -> p Y "" + Left e -> p N (errorBundlePretty e) -- pdesc "common general options configured ?" -- --pretty --ignore-assertions --infer-costs" @@ -254,17 +230,17 @@ setupFiles = do Just h -> do let f = h journalDefaultFilename e <- doesFileExist f - return (e, if e then f else "") - Nothing -> return (False, "") - i ok msg msg + return (if e then Y else N, if e then f else "") + Nothing -> return (N, "") + i ok msg pdesc "LEDGER_FILE variable is defined ?" mf <- lookupEnv journalEnvVar let - (ok,msg) = case mf of - Just f -> (True, f) - Nothing -> (False, "") - i ok msg msg + (ok, msg) = case mf of + Just f -> (Y, f) + Nothing -> (N, "") + i ok msg -- case mf of -- Nothing -> return () @@ -280,25 +256,26 @@ setupFiles = do pdesc "default journal file exists ?" jfile <- defaultJournalPath exists <- doesFileExist jfile - p exists jfile "" + let (ok, msg) = (if exists then Y else N, if exists then jfile else "") + p ok msg when exists $ do when (os == "mingw32") $ do pdesc "default journal file path is safe for Windows ?" let - (ok,msg) = + (ok, msg) = -- like ensureJournalFileExists: if isWindowsUnsafeDotPath jfile - then (False, "the file name ends with a dot, this is unsafe on Windows") - else (True, "") - p ok msg msg + then (N, "the file name ends with a dot, this is unsafe on Windows") + else (Y, "") + p ok msg pdesc "default journal file is readable ?" ej <- runExceptT $ readJournalFile definputopts jfile -- like defaultJournal case ej of - Right _ -> p True "" "" - Left e -> p False "" e + Right _ -> p Y "" + Left e -> p N e setupAccounts = do pgroup "accounts" From a18a4c612d220ea8021b20b33b9f3376c79914af Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 16:54:45 -1000 Subject: [PATCH 16/18] imp: setup: get version from hledger.org, hackage is down --- hledger/Hledger/Cli/Commands/Setup.hs | 36 +++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 262c897f4..7939eee92 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -47,6 +47,8 @@ import Hledger import Hledger.Cli.CliOptions import Hledger.Cli.Conf import System.Environment (lookupEnv) +import Data.Char +import Data.List setupmode = hledgerCommandMode $(embedFileRelative "Hledger/Cli/Commands/Setup.txt") @@ -168,7 +170,7 @@ setupHledger = do elatestver <- getLatestHledgerVersion let (ok, msg) = case elatestver of - Left e -> (U, "could not get " <> latestHledgerVersionUrlStr <> " : " <> e) + Left e -> (U, "could not read " <> latestHledgerVersionUrlStr <> " : " <> e) Right latestver -> case drop 1 verparts of [] -> (U, "could not parse --version output") @@ -301,16 +303,20 @@ setupTags = do -- pdesc "\n" -- pdesc "\n" -latestHledgerVersionUrl = https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "" -latestHledgerVersionUrlStr = "https://hackage.haskell.org/package/hledger/docs" +(getLatestHledgerVersion, latestHledgerVersionUrlStr) = + -- (getLatestHledgerVersionFromHackage, "https://hackage.haskell.org/package/hledger/docs") + (getLatestHledgerVersionFromHledgerOrg, "https://hledger.org/install.html") + +httptimeout = 10000000 -- 10s -- | Get the current hledger release version from the internet. -- Currently requests the latest doc page from Hackage and inspects the redirect path. --- Should catch all normal errors, and times out after 10 seconds. -getLatestHledgerVersion :: IO (Either String String) -getLatestHledgerVersion = do +-- Should catch all normal errors, and time out after 10 seconds. +getLatestHledgerVersionFromHackage :: IO (Either String String) +getLatestHledgerVersionFromHackage = do + let url = https "hackage.haskell.org" /: "package" /: "hledger" /: "docs" /: "" result <- try $ runReq defaultHttpConfig{httpConfigRedirectCount=0} $ - req HEAD latestHledgerVersionUrl NoReqBody bsResponse (R.responseTimeout 10000000) -- 10s + req HEAD url NoReqBody bsResponse (R.responseTimeout httptimeout) case result of Right _ -> return $ Left "expected a redirect" Left (VanillaHttpException (HttpExceptionRequest _ (StatusCodeException rsp _))) -> do @@ -328,3 +334,19 @@ getLatestHledgerVersion = do else return $ Left $ "HTTP status " ++ show status Left err -> return $ Left $ "other exception: " ++ show err +-- | Like the above, but get the version from the first number on the hledger.org Install page. +getLatestHledgerVersionFromHledgerOrg :: IO (Either String String) +getLatestHledgerVersionFromHledgerOrg = do + let url = https "hledger.org" /: "install.html" + result <- try $ runReq defaultHttpConfig $ + req GET url NoReqBody bsResponse (R.responseTimeout httptimeout) + case result of + Left (e :: R.HttpException) -> return $ Left $ show e + Right rsp -> case T.decodeUtf8' $ R.responseBody rsp of + Left e -> return $ Left $ show e + Right t -> return $ + if null version then Left "could not parse version" else Right version + where + -- keep synced + versionline = take 1 $ dropWhile (not . ("current hledger release" `isInfixOf`)) $ lines $ T.unpack t + version = takeWhile (`elem` ("0123456789."::[Char])) $ dropWhile (not . isDigit) $ headDef "" $ versionline From c3c91448a74ba73f475c6c0e0dace5d3b464007b Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sat, 19 Apr 2025 17:54:03 -1000 Subject: [PATCH 17/18] imp: setup: output tweaks --- hledger/Hledger/Cli/Commands/Setup.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index 7939eee92..a6e77b79e 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -63,7 +63,7 @@ setup :: CliOpts -> Journal -> IO () setup _opts@CliOpts{rawopts_=_rawopts, reportspec_=_rspec} _ignoredj = do -- This command is not given a journal and should not use _ignoredj; -- instead detect it ourselves when we are ready. - putStrLn "checking..." + putStrLn "Checking your hledger setup.." setupHledger setupConfig setupFiles @@ -78,7 +78,7 @@ pgroup s = putStr $ bold' $ "\n" <> s <> ":\n" -- | Print a setup test's description, formatting and padding it to a fixed width. pdesc :: String -> IO () -pdesc s = printf "- %-38s" s +pdesc s = printf "* %-38s" s -- yes, no, unknown data YNU = Y | N | U deriving (Eq) @@ -181,7 +181,7 @@ setupHledger = do msg = if exever == latestver then exever - else exever <> " installed, latest release is " <> latestver + else exever <> " installed, latest is " <> latestver p ok msg -- pdesc "eget installed ?" @@ -189,7 +189,7 @@ setupHledger = do setupConfig = do pgroup "config" - pdesc "user has a config file ?" + pdesc "a user config file exists ? (optional)" muf <- activeUserConfFile let (ok, msg) = case muf of @@ -226,7 +226,7 @@ setupConfig = do setupFiles = do pgroup "file" - pdesc "a home directory journal exists ?" + pdesc "a home directory journal file exists ?" mh <- getHomeSafe (ok,msg) <- case mh of Just h -> do @@ -253,7 +253,7 @@ setupFiles = do -- when (isJust mh && isJust mf) $ do -- pdesc "$LEDGER_FILE is masking home journal ?" - -- i True "" "" + -- i Y "" pdesc "default journal file exists ?" jfile <- defaultJournalPath From f0ff2aa34eac39b617b42daf8ae85c5a40df9699 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sun, 20 Apr 2025 09:25:35 -1000 Subject: [PATCH 18/18] imp: setup: show full --version output --- hledger/Hledger/Cli/Commands/Setup.hs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/hledger/Hledger/Cli/Commands/Setup.hs b/hledger/Hledger/Cli/Commands/Setup.hs index a6e77b79e..78000416c 100644 --- a/hledger/Hledger/Cli/Commands/Setup.hs +++ b/hledger/Hledger/Cli/Commands/Setup.hs @@ -147,13 +147,16 @@ setupHledger = do ok = if exit == ExitSuccess then Y else N msg = if ok==Y then "" else "'" <> progname <> " " <> arg <> "' failed: \n" <> err p ok msg - let verparts = words out -- use below + -- save output, used below + let + versionstr = rstrip out + versionstrparts = words versionstr -- If hledger runs, run more checks when (ok==Y) $ do pdesc "is a native binary ?" let - exearch = case drop 2 verparts of + exearch = case drop 2 versionstrparts of w:_ -> w _ -> error' "couldn't parse arch from --version output" os' -- keep synced: Version.hs @@ -162,7 +165,7 @@ setupHledger = do | otherwise = os sysarch = os' <> "-" <> arch (ok, msg) - | exearch == sysarch = (Y, "") + | exearch == sysarch = (Y, versionstr) | otherwise = (N, "installed binary is for " <> exearch <> ", system is " <> sysarch) p ok msg @@ -172,7 +175,7 @@ setupHledger = do (ok, msg) = case elatestver of Left e -> (U, "could not read " <> latestHledgerVersionUrlStr <> " : " <> e) Right latestver -> - case drop 1 verparts of + case drop 1 versionstrparts of [] -> (U, "could not parse --version output") w:_ -> (ok, msg) where