Skip to content

Commit

Permalink
Show full help text when CLI is invoked without arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
ad-si committed Jan 16, 2024
1 parent ebe5c16 commit d615ef6
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 83 deletions.
4 changes: 3 additions & 1 deletion cli-spec.ncl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
name = "transity",
description = "transity <command> <path/to/journal.yaml>",
description = m%"
Transity is a full fledged, CLI based plain text accounting tool.
"%,
version = "0.8.0",
funcName = "runApp",
commands = [
Expand Down
2 changes: 0 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ docs-dev: output index.js | node_modules
define JsonEmbedStart
module CliSpec.JsonEmbed where

import Prelude

fileContent :: String
fileContent = """
endef
Expand Down
67 changes: 41 additions & 26 deletions src/CliSpec.purs
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
module CliSpec where

import Prelude (Unit, bind, (#), ($), (/=), (<>))
import Prelude (Unit, bind, (#), ($), (/=), (<>), (-), (>))

import Ansi.Codes (Color(..))
import Ansi.Output (withGraphics, foreground)
import Data.Argonaut.Decode (decodeJson)
import Data.Argonaut.Decode.Error (printJsonDecodeError)
import Data.Argonaut.Parser (jsonParser)
import Data.Array (concat, cons, difference, filter, null, zip, (!!))
import Data.Array (head, tail, drop, take, reverse, foldl)
import Data.Array (drop, head, tail, replicate, foldl, foldMap, fold, find, length)
import Data.Bifunctor (lmap)
import Data.String as Str
import Data.Eq ((==))
import Data.Foldable (foldMap, find, length)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Newtype (over)
import Data.Result (Result(..), result, note, isOk, fromEither)
import Data.Show (show)
import Data.String (Pattern(..), indexOf, split)
import Data.Traversable (for_, sequence)
import Data.Tuple (Tuple(..), fst, snd)
import Data.Result (Result(..), fromEither)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class.Console (log)
import Effect.Exception (throw)
import Foreign.Object as Obj
import Node.Encoding (Encoding(UTF8))
import Node.FS.Async (stat)
import Node.FS.Stats (isFile, isDirectory)
import Node.FS.Sync as Sync
import Node.Path as Path
import Node.Process (argv, cwd)
import Node.Process (argv)

import CliSpec.Types

import Debug as Debug

-- TODO: Automatically disable colors if not supported
makeRed :: String -> String
Expand Down Expand Up @@ -68,7 +54,13 @@ callCommand
callCommand cliSpec usageString args executor = do
case args # head of
Just (CmdArg "help") -> log usageString
Just (FlagLong "help") -> log usageString
Just (FlagShort 'h') -> log usageString

Just (CmdArg "version") -> log cliSpec.version
Just (FlagLong "version") -> log cliSpec.version
Just (FlagShort 'v') -> log cliSpec.version

Just (CmdArg cmdName) -> do
let
commandMb = cliSpec.commands
Expand Down Expand Up @@ -102,11 +94,13 @@ callCommand cliSpec usageString args executor = do
<> usageString

Nothing -> do
log $
makeRed $
"ERROR: No command specified"
<> "\n\n"
<> usageString
log usageString


-- | Function to repeat a string n times
repeatString :: String -> Int -> String
repeatString str n =
fold $ replicate n str


callCliApp
Expand All @@ -115,8 +109,29 @@ callCliApp
-> Effect Unit
callCliApp cliSpec executor = do
let
lengthLongestCmd :: Int
lengthLongestCmd =
cliSpec.commands
# foldl (\acc cmd ->
if acc > Str.length cmd.name
then acc
else Str.length cmd.name
) 0

usageString =
"Usage: " <> cliSpec.name <> " <command> [options]"
"USAGE: " <> cliSpec.name <> " <command> [options]"
<> "\n\n"
<> cliSpec.description
<> "\n\n"
<> "COMMANDS:"
<> "\n\n"
<> (cliSpec.commands
# foldMap (\cmd ->
cmd.name
<> (repeatString " " (lengthLongestCmd - Str.length cmd.name))
<> " " <> cmd.description <> "\n"
)
)

arguments <- argv

Expand All @@ -142,5 +157,5 @@ callCliApp cliSpec executor = do
callCommand
cliSpec
usageString
(parseCliArguments toolArgs)
(parseCliArguments 0 toolArgs)
executor
48 changes: 17 additions & 31 deletions src/CliSpec/Types.purs
Original file line number Diff line number Diff line change
@@ -1,24 +1,10 @@
module CliSpec.Types where

import Prelude (
class Eq, class Show, bind, max, pure, map,
($), (+), (<>), (==), (||), (&&), (>>=), (<$>), (#), (<#>)
)

import Ansi.Codes (Color(..))
import Ansi.Output (withGraphics, foreground)
import Data.Argonaut.Core (Json, toObject)
import Data.Argonaut.Decode.Class (class DecodeJson)
import Data.Argonaut.Encode.Class (class EncodeJson)
import Data.Argonaut.Encode.Generic (genericEncodeJson)
import Data.Argonaut.Encode.Generic (genericEncodeJson)
import Data.Array ((:), length, drop, take, null, concat)
import Data.Generic.Rep (class Generic)
import Prelude (class Eq, class Show, map, (#), ($), (+), (==))

import Data.Array ((:), drop, take, null, concat, mapWithIndex)
import Data.Generic.Rep (class Generic)
import Data.Maybe (Maybe(..))
import Data.Newtype (class Newtype)
import Data.Result (Result(Ok, Error), toEither)
import Data.Show.Generic (genericShow)
import Data.Maybe (Maybe)
import Data.Show.Generic (genericShow)
import Data.String.CodeUnits (toCharArray, fromCharArray)

Expand Down Expand Up @@ -52,8 +38,8 @@ instance showCliArgument :: Show CliArgument where

-- | One argument can lead to multiple `CliArgument`s
-- | e.g. `-ab` -> `[FlagShort 'a', FlagShort 'b']`
parseCliArgument :: String -> Array CliArgument
parseCliArgument arg = do
parseCliArgument :: Int -> String -> Array CliArgument
parseCliArgument index arg = do
let
chars = arg # toCharArray :: Array Char
charsRest = chars # drop 2 :: Array Char
Expand All @@ -66,19 +52,19 @@ parseCliArgument arg = do
then []
else charsRest # map FlagShort

_ -> [ValArg $ StringArg arg]
_ -> if index == 0
then [CmdArg arg]
else [ValArg $ StringArg arg]


parseCliArguments :: Array String -> Array CliArgument
parseCliArguments arguments =
let
firstArg = arguments # take 1
restArgs = arguments # drop 1
in
(firstArg <#> CmdArg)
<> (restArgs
# map parseCliArgument
# concat)
parseCliArguments :: Int -> Array String -> Array CliArgument
parseCliArguments subcmdLevels arguments = do
arguments
# mapWithIndex (\index ->
parseCliArgument
(if subcmdLevels == 0 then index else index + 1)
)
# concat


type Command = {
Expand Down
33 changes: 11 additions & 22 deletions src/Main.purs
Original file line number Diff line number Diff line change
@@ -1,52 +1,41 @@
module Main where

import Prelude (
Unit, bind, discard, pure, unit, identity,
(#), ($), (<#>), (<>), (/=), (>), (>>=)
)
import Prelude
( Unit, bind, discard, pure, unit
, (#), ($), (/=), (<#>), (<>), (>)
)

import Ansi.Codes (Color(..))
import Ansi.Output (withGraphics, foreground)
import Control.Plus (empty)
import Data.Argonaut.Core as JSON
import Data.Argonaut.Decode (decodeJson)
import Data.Argonaut.Decode.Error (printJsonDecodeError)
import Data.Argonaut.Parser (jsonParser)
import Data.Array (concat, cons, difference, filter, null, zip, (!!))
import Data.Array (head, tail)
import Data.Bifunctor (lmap)
import Data.Array (concat, cons, difference, filter, null, zip)
import Data.Eq ((==))
import Data.Foldable (foldMap)
import Data.Maybe (Maybe(..), fromMaybe)
import Data.Maybe (Maybe(..))
import Data.Newtype (over)
import Data.Result (Result(..), result, note, isOk, fromEither)
import Data.Show (show)
import Data.String (Pattern(..), indexOf, length, split)
import Data.Result (Result(..), note, isOk, fromEither)
import Data.String (Pattern(..), indexOf, length)
import Data.Traversable (for_, sequence)
import Data.Tuple (Tuple(..), fst, snd)
import Effect (Effect)
import Effect.Aff (launchAff_)
import Effect.Class.Console (log, warn)
import Effect.Exception (throw)
import Foreign.Object as Obj
import Node.Encoding (Encoding(UTF8))
import Node.FS.Async (stat)
import Node.FS.Stats (isFile, isDirectory)
import Node.FS.Sync as Sync
import Node.Path as Path
import Node.Process (argv, cwd)
import Node.Process (cwd)

import CliSpec.JsonEmbed as CliSpec.JsonEmbed
import CliSpec.Types ( CliSpec(..), CliArgument(..), CliArgPrim(..))
import CliSpec.Types (CliArgument(..), CliArgPrim(..))
import CliSpec (parseCliSpec, callCliApp)
import Transity.Data.Ledger (Ledger(..), BalanceFilter(..))
import Transity.Data.Ledger as Ledger
import Transity.Data.Config (Config, ColorFlag(..), config)
import Transity.Data.Config (ColorFlag(..), config)
import Transity.Data.Transaction (Transaction(..))
import Transity.Plot as Plot
import Transity.Utils (SortOrder(..), makeRed, errorAndExit)
import Transity.Xlsx (writeToZip, entriesAsXlsx)
import Debug as Debug


-- TODO: Move validation to parsing
Expand Down
10 changes: 9 additions & 1 deletion test/CliSpec.purs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@ parseCliStr :: String -> Array CliArgument
parseCliStr str =
str
# split (Pattern " ")
# parseCliArguments
# parseCliArguments 0


tests :: Spec Unit
tests = do
describe "CliSpec" do
describe "Parser" do
it "parses a CLI invocation" do
(parseCliStr "xx")
`shouldEqual` [CmdArg "xx"]

it "parses a standalone flag (for subcommands)" do
(parseCliStr "--help")
`shouldEqual` [FlagLong "help"]

it "parses a CLI with an argument" do
(parseCliStr "xx dir")
`shouldEqual` [CmdArg "xx", ValArg (StringArg "dir")]

Expand Down

0 comments on commit d615ef6

Please sign in to comment.