diff --git a/Changelog.md b/Changelog.md index d3f48e2368..4e468d89a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,8 @@ # FOSSA CLI Changelog +## v3.8.34 +- Add color and update formatting in cli help commands ([#1367](https://github.com/fossas/fossa-cli/pull/1367)) + ## v3.8.33 - Removes warnings and tracebacks to stderr [#1358](https://github.com/fossas/fossa-cli/pull/1358) diff --git a/spectrometer.cabal b/spectrometer.cabal index bbf76f06f9..989e58bc2b 100644 --- a/spectrometer.cabal +++ b/spectrometer.cabal @@ -485,6 +485,7 @@ library Strategy.Swift.Xcode.Pbxproj Strategy.Swift.Xcode.PbxprojParser Strategy.SwiftPM + Style System.Args Text.URI.Builder Type.Operator diff --git a/src/App/Fossa/Config/Analyze.hs b/src/App/Fossa/Config/Analyze.hs index 6827aca708..bf63df26c3 100644 --- a/src/App/Fossa/Config/Analyze.hs +++ b/src/App/Fossa/Config/Analyze.hs @@ -26,6 +26,7 @@ module App.Fossa.Config.Analyze ( loadConfig, cliParser, mergeOpts, + branchHelp, ) where import App.Fossa.Config.Common ( @@ -97,13 +98,13 @@ import Options.Applicative ( InfoMod, Parser, eitherReader, - help, + helpDoc, hidden, long, metavar, option, optional, - progDesc, + progDescDoc, short, strOption, switch, @@ -111,14 +112,11 @@ import Options.Applicative ( ) import Path (Abs, Dir, Path, Rel) import Path.Extra (SomePath) -import Prettyprinter (Doc, annotate, defaultLayoutOptions, layoutPretty) -import Prettyprinter.Render.Terminal (AnsiStyle, Color (Red), color, renderStrict) +import Prettyprinter (Doc, annotate, indent) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green, Red), color) +import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, stringToHelpDoc) import Types (ArchiveUploadType (..), LicenseScanPathFilters (..), TargetFilter) --- Utility functions -coloredText :: Color -> Doc AnsiStyle -> String -coloredText clr str = toString . renderStrict . layoutPretty defaultLayoutOptions $ annotate (color clr) str - -- CLI flags, for use with 'Data.Flag' data DeprecatedAllowNativeLicenseScan = DeprecatedAllowNativeLicenseScan deriving (Generic) data ForceVendoredDependencyRescans = ForceVendoredDependencyRescans deriving (Generic) @@ -265,43 +263,72 @@ instance ToJSON ExperimentalAnalyzeConfig where toEncoding = genericToEncoding defaultOptions mkSubCommand :: (AnalyzeConfig -> EffStack ()) -> SubCommand AnalyzeCliOpts AnalyzeConfig -mkSubCommand = SubCommand "analyze" analyzeInfo cliParser loadConfig mergeOpts +mkSubCommand = SubCommand ("analyze") analyzeInfo cliParser loadConfig mergeOpts analyzeInfo :: InfoMod a -analyzeInfo = progDesc "Scan for projects and their dependencies" +analyzeInfo = + progDescDoc $ + Just . formatDoc $ + vsep + [ "Scan for projects and their dependencies" + , "" + , "Examples:" + , indent 2 $ + vsep + [ "- " <> coloredBoldItalicized Green "fossa analyze" <> " # Analyze current directory" + , "- " <> coloredBoldItalicized Green "fossa analyze {path/to/specific/directory}" <> " # Analyze specific directory" + , "- " <> coloredBoldItalicized Green "fossa analyze --detect-vendored" <> " # Analyze and detect vendored open source libraries in current directory" + , "- " <> coloredBoldItalicized Green "fossa analyze --debug" <> " # Analyze current directory in debug mode" + ] + ] cliParser :: Parser AnalyzeCliOpts cliParser = AnalyzeCliOpts <$> commonOpts - <*> switch (long "output" <> short 'o' <> help "Output results to stdout instead of uploading to fossa") - <*> flagOpt UnpackArchives (long "unpack-archives" <> help "Recursively unpack and analyze discovered archives") - <*> flagOpt JsonOutput (long "json" <> help "Output project metadata as json to the console. Useful for communicating with the FOSSA API") - <*> flagOpt IncludeAll (long "include-unused-deps" <> help "Include all deps found, instead of filtering non-production deps. Ignored by VSI.") - <*> flagOpt NoDiscoveryExclusion (long "debug-no-discovery-exclusion" <> help "Ignore filters during discovery phase. This is for debugging only and may be removed without warning." <> hidden) + <*> switch (applyFossaStyle <> long "output" <> short 'o' <> stringToHelpDoc "Output results to stdout instead of uploading to FOSSA") + <*> flagOpt UnpackArchives (applyFossaStyle <> long "unpack-archives" <> stringToHelpDoc "Recursively unpack and analyze discovered archives") + <*> flagOpt JsonOutput (applyFossaStyle <> long "json" <> stringToHelpDoc "Output project metadata as JSON to the console. This is useful for communicating with the FOSSA API.") + <*> flagOpt IncludeAll (applyFossaStyle <> long "include-unused-deps" <> stringToHelpDoc "Include all deps found, instead of filtering non-production deps. Ignored by VSI.") + <*> flagOpt NoDiscoveryExclusion (applyFossaStyle <> long "debug-no-discovery-exclusion" <> stringToHelpDoc "Ignore filters during discovery phase. This is for debugging only and may be removed without warning." <> hidden) -- AllowNativeLicenseScan is no longer used. We started emitting a warning if it was used in https://github.com/fossas/fossa-cli/pull/1113 - <*> flagOpt DeprecatedAllowNativeLicenseScan (long "experimental-native-license-scan" <> hidden) + <*> flagOpt DeprecatedAllowNativeLicenseScan (applyFossaStyle <> long "experimental-native-license-scan" <> hidden) <*> optional vendoredDependencyModeOpt - <*> flagOpt ForceVendoredDependencyRescans (long "force-vendored-dependency-rescans" <> help "Force vendored dependencies to be rescanned even if the revision has been previously analyzed by FOSSA. This currently only works for CLI-side license scans.") - <*> optional (strOption (long "branch" <> short 'b' <> help "this repository's current branch (default: current VCS branch)")) + <*> flagOpt ForceVendoredDependencyRescans (applyFossaStyle <> long "force-vendored-dependency-rescans" <> stringToHelpDoc "Force vendored dependencies to be rescanned even if the revision has been previously analyzed by FOSSA. This currently only works for CLI-side license scans.") + <*> optional (strOption (applyFossaStyle <> long "branch" <> short 'b' <> helpDoc branchHelp)) <*> metadataOpts - <*> many (option (eitherReader targetOpt) (long "only-target" <> help "Only scan these targets. See targets.only in the fossa.yml spec." <> metavar "PATH")) - <*> many (option (eitherReader targetOpt) (long "exclude-target" <> help "Exclude these targets from scanning. See targets.exclude in the fossa.yml spec." <> metavar "PATH")) - <*> many (option (eitherReader pathOpt) (long "only-path" <> help "Only scan these paths. See paths.only in the fossa.yml spec." <> metavar "PATH")) - <*> many (option (eitherReader pathOpt) (long "exclude-path" <> help "Exclude these paths from scanning. See paths.exclude in the fossa.yml spec." <> metavar "PATH")) + <*> many (option (eitherReader targetOpt) (applyFossaStyle <> long "only-target" <> stringToHelpDoc "Only scan these targets. See `targets.only` in the fossa.yml spec." <> metavar "PATH")) + <*> many (option (eitherReader targetOpt) (applyFossaStyle <> long "exclude-target" <> stringToHelpDoc "Exclude these targets from scanning. See `targets.exclude` in the fossa.yml spec." <> metavar "PATH")) + <*> many (option (eitherReader pathOpt) (applyFossaStyle <> long "only-path" <> stringToHelpDoc "Only scan these paths. See `paths.only` in the fossa.yml spec." <> metavar "PATH")) + <*> many (option (eitherReader pathOpt) (applyFossaStyle <> long "exclude-path" <> stringToHelpDoc "Exclude these paths from scanning. See `paths.exclude` in the fossa.yml spec." <> metavar "PATH")) <*> vsiEnableOpt - <*> flagOpt BinaryDiscovery (long "experimental-enable-binary-discovery" <> help "Reports binary files as unlicensed dependencies") - <*> optional (strOption (long "experimental-link-project-binary" <> metavar "DIR" <> help "Links output binary files to this project in FOSSA")) + <*> flagOpt BinaryDiscovery (applyFossaStyle <> long "experimental-enable-binary-discovery" <> stringToHelpDoc "Reports binary files as unlicensed dependencies") + <*> optional (strOption (applyFossaStyle <> long "experimental-link-project-binary" <> metavar "DIR" <> stringToHelpDoc "Links output binary files to this project in FOSSA")) <*> optional dynamicLinkInspectOpt <*> many skipVSIGraphResolutionOpt <*> baseDirArg <*> experimentalUseV3GoResolver <*> experimentalAnalyzePathDependencies - <*> flagOpt ForceFirstPartyScans (long "experimental-force-first-party-scans" <> help "Force first party scans") - <*> flagOpt ForceNoFirstPartyScans (long "experimental-block-first-party-scans" <> help "Block first party scans. This can be used to forcibly turn off first-party scans if your organization defaults to first-party scans.") - <*> flagOpt IgnoreOrgWideCustomLicenseScanConfigs (long "ignore-org-wide-custom-license-scan-configs" <> help "Ignore custom-license scan configurations for your organization. These configurations are defined in the \"Integrations\" section of the Admin settings in the FOSSA web app") - <*> optional (strOption (long "fossa-deps-file" <> help "Path to fossa-deps file including filename (default: fossa-deps.{yaml|yml|json})")) - <*> flagOpt StaticOnlyTactics (long "static-only-analysis" <> help "Only analyze the project using static strategies.") + <*> flagOpt ForceFirstPartyScans (applyFossaStyle <> long "experimental-force-first-party-scans" <> stringToHelpDoc "Force first party scans") + <*> flagOpt ForceNoFirstPartyScans (applyFossaStyle <> long "experimental-block-first-party-scans" <> stringToHelpDoc "Block first party scans. This can be used to forcibly turn off first-party scans if your organization defaults to first-party scans.") + <*> flagOpt IgnoreOrgWideCustomLicenseScanConfigs (applyFossaStyle <> long "ignore-org-wide-custom-license-scan-configs" <> stringToHelpDoc "Ignore custom-license scan configurations for your organization. These configurations are defined in the `Integrations` section of the Admin settings in the FOSSA web app") + <*> optional (strOption (applyFossaStyle <> long "fossa-deps-file" <> helpDoc fossaDepsFileHelp <> metavar "FILEPATH")) + <*> flagOpt StaticOnlyTactics (applyFossaStyle <> long "static-only-analysis" <> stringToHelpDoc "Only analyze the project using static strategies.") + where + fossaDepsFileHelp :: Maybe (Doc AnsiStyle) + fossaDepsFileHelp = + Just . formatDoc $ + vsep + [ "Path to fossa-deps file including filename" + , boldItalicized "Default:" <> " fossa-deps.{yaml|yml|json}" + ] +branchHelp :: Maybe (Doc AnsiStyle) +branchHelp = + Just . formatDoc $ + vsep + [ "This repository's current branch" + , boldItalicized "Default: " <> "Current VCS branch" + ] data GoDynamicTactic = GoModulesBasedTactic @@ -320,21 +347,26 @@ experimentalUseV3GoResolver = ) . switch $ long "experimental-use-v3-go-resolver" - <> help - ( coloredText Red "DEPRECATED: This is now default and will be removed in the future." - <> " For Go: generate a graph of module deps based on package deps. This will be the default in the future." - ) + <> applyFossaStyle + <> helpDoc helpMsg + where + helpMsg :: Maybe (Doc AnsiStyle) + helpMsg = + Just . formatDoc $ + vsep + [ annotate (color Red) "DEPRECATED: This is now default and will be removed in the future" + , boldItalicized "For Go: " <> "Generate a graph of module deps based on package deps. This will be the default in the future." + ] experimentalAnalyzePathDependencies :: Parser Bool experimentalAnalyzePathDependencies = switch $ long "experimental-analyze-path-dependencies" - <> help - ( "License scan dependencies sourced from file system, as indicated in manifest files. This will be enabled by default in the future." - ) + <> applyFossaStyle + <> stringToHelpDoc "License scan dependencies sourced from file system, as indicated in manifest files. This will be enabled by default in the future." vendoredDependencyModeOpt :: Parser ArchiveUploadType -vendoredDependencyModeOpt = option (eitherReader parseType) (long "force-vendored-dependency-scan-method" <> metavar "METHOD" <> help "Force the vendored dependency scan method. The options are 'CLILicenseScan' or 'ArchiveUpload'. 'CLILicenseScan' is usually the default unless your organization has overridden this.") +vendoredDependencyModeOpt = option (eitherReader parseType) (applyFossaStyle <> long "force-vendored-dependency-scan-method" <> metavar "METHOD" <> helpDoc vendoredDependencyScanMethodHelp) where parseType :: String -> Either String ArchiveUploadType parseType = \case @@ -342,18 +374,27 @@ vendoredDependencyModeOpt = option (eitherReader parseType) (long "force-vendore "CLILicenseScan" -> Right CLILicenseScan val -> Left ("must be either 'CLILicenseScan' or 'ArchiveUpload'. Found " <> val) + vendoredDependencyScanMethodHelp :: Maybe (Doc AnsiStyle) + vendoredDependencyScanMethodHelp = + Just . formatDoc $ + vsep + [ "Force the vendored dependency scan method" + , boldItalicized "Options: " <> coloredBoldItalicized Green "CLILicenseScan" <> boldItalicized "|" <> coloredBoldItalicized Green "ArchiveUpload" + , boldItalicized "Note: " <> coloredBoldItalicized Green "CLILicenseScan" <> " is usually the default unless your organization has overridden this" + ] + dynamicLinkInspectOpt :: Parser FilePath dynamicLinkInspectOpt = visible <|> legacy where - visible = strOption (long "detect-dynamic" <> metavar "BINARY" <> help "Analyzes dynamically linked libraries in the target binary and reports them as dependencies") - legacy = strOption (long "experimental-analyze-dynamic-deps" <> hidden) + visible = strOption (applyFossaStyle <> long "detect-dynamic" <> metavar "BINARY" <> stringToHelpDoc "Analyzes dynamically linked libraries in the target binary and reports them as dependencies") + legacy = strOption (applyFossaStyle <> long "experimental-analyze-dynamic-deps" <> hidden) vsiEnableOpt :: Parser (Flag VSIAnalysis) vsiEnableOpt = visible <|> legacyExperimental <|> legacy where - visible = flagOpt VSIAnalysis (long "detect-vendored" <> help "Analyzes project files on disk to detect vendored open source libraries") - legacyExperimental = flagOpt VSIAnalysis (long "experimental-enable-vsi" <> hidden) - legacy = flagOpt VSIAnalysis (long "enable-vsi" <> hidden) + visible = flagOpt VSIAnalysis (applyFossaStyle <> long "detect-vendored" <> stringToHelpDoc "Analyzes project files on disk to detect vendored open source libraries") + legacyExperimental = flagOpt VSIAnalysis (applyFossaStyle <> long "experimental-enable-vsi" <> hidden) + legacy = flagOpt VSIAnalysis (applyFossaStyle <> long "enable-vsi" <> hidden) skipVSIGraphResolutionOpt :: Parser VSI.Locator skipVSIGraphResolutionOpt = (option (eitherReader parseLocator) details) @@ -362,8 +403,9 @@ skipVSIGraphResolutionOpt = (option (eitherReader parseLocator) details) mconcat [ long "experimental-skip-vsi-graph" , metavar "LOCATOR" - , help "Skip resolving the dependencies of the given project in FOSSA" + , stringToHelpDoc "Skip resolving the dependencies of the given project in FOSSA" ] + <> applyFossaStyle parseLocator :: String -> Either String VSI.Locator parseLocator s = case VSI.parseLocator (toText s) of Left err -> Left $ toString (toText err) diff --git a/src/App/Fossa/Config/Common.hs b/src/App/Fossa/Config/Common.hs index 3a7bfd9217..89dbab1b7f 100644 --- a/src/App/Fossa/Config/Common.hs +++ b/src/App/Fossa/Config/Common.hs @@ -35,6 +35,10 @@ module App.Fossa.Config.Common ( defaultTimeoutDuration, collectRevisionData', fossaApiKeyCmdText, + + -- * Global Parser Help Message + endpointHelp, + fossaApiKeyHelp, ) where import App.Fossa.Config.ConfigFile ( @@ -101,7 +105,7 @@ import Data.Text (Text, null, strip, toLower) import Diag.Result (Result (Failure, Success), renderFailure) import Discovery.Filters (AllFilters (AllFilters), MavenScopeFilters (..), comboExclude, comboInclude, setExclude, setInclude, targetFilterParser) import Effect.Exec (Exec) -import Effect.Logger (Logger, logDebug, logInfo) +import Effect.Logger (Logger, logDebug, logInfo, vsep) import Effect.ReadFS (ReadFS, doesDirExist, doesFileExist) import Fossa.API.Types (ApiKey (ApiKey), ApiOpts (ApiOpts), defaultApiPollDelay) import GHC.Generics (Generic) @@ -112,7 +116,6 @@ import Options.Applicative ( argument, auto, eitherReader, - help, long, metavar, option, @@ -125,9 +128,14 @@ import Options.Applicative ( value, (<|>), ) +import Options.Applicative.Builder (helpDoc) +import Options.Applicative.Help (AnsiStyle) import Path (Abs, Dir, File, Path, Rel, SomeBase (..), parseRelDir) import Path.Extra (SomePath (..)) import Path.IO (resolveDir', resolveFile') +import Prettyprinter (Doc) +import Prettyprinter.Render.Terminal (Color (Green)) +import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, stringToHelpDoc) import Text.Megaparsec (errorBundlePretty, runParser) import Text.URI (URI, mkURI) import Types (TargetFilter) @@ -155,34 +163,54 @@ readMWithError errMsg = auto <|> readerError errMsg metadataOpts :: Parser ProjectMetadata metadataOpts = ProjectMetadata - <$> optional (strOption (long "title" <> short 't' <> help "the title of the FOSSA project. (default: the project name)")) - <*> optional (strOption (long "project-url" <> short 'P' <> help "this repository's home page")) - <*> optional (strOption (long "jira-project-key" <> short 'j' <> help "this repository's JIRA project key")) - <*> optional (strOption (long "link" <> short 'L' <> help "a link to attach to the current build")) - <*> optional (strOption (long "team" <> short 'T' <> help "this repository's team inside your organization")) + <$> optional (strOption (applyFossaStyle <> long "title" <> short 't' <> helpDoc titleHelp)) + <*> optional (strOption (applyFossaStyle <> long "project-url" <> short 'P' <> stringToHelpDoc "This repository's home page")) + <*> optional (strOption (applyFossaStyle <> long "jira-project-key" <> short 'j' <> stringToHelpDoc "This repository's JIRA project key")) + <*> optional (strOption (applyFossaStyle <> long "link" <> short 'L' <> stringToHelpDoc "A link to attach to the current build")) + <*> optional (strOption (applyFossaStyle <> long "team" <> short 'T' <> stringToHelpDoc "This repository's team inside your organization")) <*> parsePolicyOptions - <*> many (strOption (long "project-label" <> help "assign up to 5 labels to the project")) + <*> many (strOption (applyFossaStyle <> long "project-label" <> stringToHelpDoc "Assign up to 5 labels to the project")) <*> optional releaseGroupMetadataOpts where policy :: Parser Policy - policy = PolicyName <$> (strOption (long "policy" <> help "The name of the policy to assign to this project in FOSSA. Mutually excludes --policy-id.")) + policy = PolicyName <$> (strOption (applyFossaStyle <> long "policy" <> helpDoc policyHelp)) policyId :: Parser Policy policyId = PolicyId <$> ( option (readMWithError "failed to parse --policy-id, expecting int") - (long "policy-id" <> help "The id of the policy to assign to this project in FOSSA. Mutually excludes --policy.") + (applyFossaStyle <> long "policy-id" <> helpDoc policyIdHelp) ) parsePolicyOptions :: Parser (Maybe Policy) parsePolicyOptions = optional (policy <|> policyId) -- For Parsers '<|>' tries every alternative and fails if they all succeed. + titleHelp :: Maybe (Doc AnsiStyle) + titleHelp = + Just . formatDoc $ + vsep + [ "The title of the FOSSA project" + , boldItalicized "Default: " <> "The project name" + ] + + policyHelp :: Maybe (Doc AnsiStyle) + policyHelp = + Just . formatDoc $ + vsep + [ "The name of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy-id" <> "." + ] + policyIdHelp :: Maybe (Doc AnsiStyle) + policyIdHelp = + Just . formatDoc $ + vsep + [ "The id of the policy to assign to this project in FOSSA. Mutually excludes " <> coloredBoldItalicized Green "--policy" <> "." + ] releaseGroupMetadataOpts :: Parser ReleaseGroupMetadata releaseGroupMetadataOpts = ReleaseGroupMetadata - <$> strOption (long "release-group-name" <> help "the name of the release group to add this project to") - <*> strOption (long "release-group-release" <> help "the release of the release group to add this project to") + <$> strOption (applyFossaStyle <> long "release-group-name" <> stringToHelpDoc "The name of the release group to add this project to") + <*> strOption (applyFossaStyle <> long "release-group-release" <> stringToHelpDoc "The release of the release group to add this project to") pathOpt :: String -> Either String (Path Rel Dir) pathOpt = first show . parseRelDir @@ -191,7 +219,14 @@ targetOpt :: String -> Either String TargetFilter targetOpt = first errorBundlePretty . runParser targetFilterParser "(Command-line arguments)" . toText baseDirArg :: Parser String -baseDirArg = argument str (metavar "DIR" <> help "Set the base directory for scanning (default: current directory)" <> value ".") +baseDirArg = argument str (applyFossaStyle <> metavar "DIR" <> helpDoc baseDirDoc <> value ".") + where + baseDirDoc = + Just . formatDoc $ + vsep + [ "Set the base directory for scanning" + , boldItalicized "Default: " <> "Current directory" + ] collectBaseDir :: ( Has Diagnostics sig m @@ -409,13 +444,59 @@ fossaApiKeyCmdText = "fossa-api-key" commonOpts :: Parser CommonOpts commonOpts = CommonOpts - <$> switch (long "debug" <> help "Enable debug logging, and write detailed debug information to `fossa.debug.json`") - <*> optional (uriOption (long "endpoint" <> short 'e' <> metavar "URL" <> help "The FOSSA API server base URL (default: https://app.fossa.com)")) - <*> optional (strOption (long "project" <> short 'p' <> help "this repository's URL or VCS endpoint (default: VCS remote 'origin')")) - <*> optional (strOption (long "revision" <> short 'r' <> help "this repository's current revision hash (default: VCS hash HEAD)")) - <*> optional (strOption (long fossaApiKeyCmdText <> help "the FOSSA API server authentication key (default: FOSSA_API_KEY from env)")) - <*> optional (strOption (long "config" <> short 'c' <> help "Path to configuration file including filename (default: .fossa.yml)")) - <*> optional (option parseTelemetryScope (long "with-telemetry-scope" <> help "Scope of telemetry to use, the options are 'full' or 'off'. (default: 'full')")) + <$> switch (applyFossaStyle <> long "debug" <> stringToHelpDoc "Enable debug logging, and write detailed debug information to `fossa.debug.json`") + <*> optional (uriOption (applyFossaStyle <> long "endpoint" <> short 'e' <> metavar "URL" <> helpDoc endpointHelp)) + <*> optional (strOption (applyFossaStyle <> long "project" <> short 'p' <> helpDoc projectHelp)) + <*> optional (strOption (applyFossaStyle <> long "revision" <> short 'r' <> helpDoc revisionHelp)) + <*> optional (strOption (applyFossaStyle <> long fossaApiKeyCmdText <> helpDoc fossaApiKeyHelp)) + <*> optional (strOption (applyFossaStyle <> long "config" <> short 'c' <> helpDoc configHelp)) + <*> optional (option parseTelemetryScope (applyFossaStyle <> long "with-telemetry-scope" <> helpDoc telemtryScopeHelp)) + where + projectHelp :: Maybe (Doc AnsiStyle) + projectHelp = + Just . formatDoc $ + vsep + [ "This repository's URL or VCS endpoint" + , boldItalicized "Default: " <> "VCS remote 'origin'" + ] + revisionHelp :: Maybe (Doc AnsiStyle) + revisionHelp = + Just . formatDoc $ + vsep + [ "This repository's current revision hash" + , boldItalicized "Default: " <> "VCS hash HEAD" + ] + configHelp :: Maybe (Doc AnsiStyle) + configHelp = + Just . formatDoc $ + vsep + [ "Path to configuration file including filename" + , boldItalicized "Default: " <> ".fossa.yml" + ] + telemtryScopeHelp :: Maybe (Doc AnsiStyle) + telemtryScopeHelp = + Just . formatDoc $ + vsep + [ "Scope of telemetry to use" + , boldItalicized "Options: " <> coloredBoldItalicized Green "full" <> boldItalicized "|" <> coloredBoldItalicized Green "off" + , boldItalicized "Default: " <> coloredBoldItalicized Green "full" + ] + +endpointHelp :: Maybe (Doc AnsiStyle) +endpointHelp = + Just . formatDoc $ + vsep + [ "The FOSSA API server base URL" + , boldItalicized "Default: " <> "https://app.fossa.com" + ] + +fossaApiKeyHelp :: Maybe (Doc AnsiStyle) +fossaApiKeyHelp = + Just . formatDoc $ + vsep + [ "The FOSSA API server authentication key" + , boldItalicized "Default: " <> "FOSSA_API_KEY from env" + ] collectConfigFileFilters :: ConfigFile -> AllFilters collectConfigFileFilters configFile = do diff --git a/src/App/Fossa/Config/Container.hs b/src/App/Fossa/Config/Container.hs index f2b2832adf..a77e3c2b68 100644 --- a/src/App/Fossa/Config/Container.hs +++ b/src/App/Fossa/Config/Container.hs @@ -33,12 +33,13 @@ import GHC.Generics (Generic) import Options.Applicative ( InfoMod, Parser, - hsubparser, - progDesc, + progDescDoc, + subparser, ) +import Style (formatStringToDoc) containerCmdInfo :: InfoMod a -containerCmdInfo = progDesc "Run in container-scanning mode" +containerCmdInfo = progDescDoc $ formatStringToDoc "Run in container-scanning mode" mkSubCommand :: (ContainerScanConfig -> EffStack ()) -> SubCommand ContainerCommand ContainerScanConfig mkSubCommand = SubCommand "container" containerCmdInfo parser loadConfig mergeOpts @@ -106,4 +107,4 @@ instance GetCommonOpts ContainerCommand where parser :: Parser ContainerCommand parser = public where - public = hsubparser $ Analyze.subcommand ContainerAnalyze <> Test.subcommand ContainerTest <> ListTargets.subcommand ContainerListTargets + public = subparser $ Analyze.subcommand ContainerAnalyze <> Test.subcommand ContainerTest <> ListTargets.subcommand ContainerListTargets diff --git a/src/App/Fossa/Config/Container/Analyze.hs b/src/App/Fossa/Config/Container/Analyze.hs index 2953dee48c..8ec7c329ae 100644 --- a/src/App/Fossa/Config/Container/Analyze.hs +++ b/src/App/Fossa/Config/Container/Analyze.hs @@ -10,6 +10,7 @@ module App.Fossa.Config.Container.Analyze ( subcommand, ) where +import App.Fossa.Config.Analyze (branchHelp) import App.Fossa.Config.Common ( CommonOpts (CommonOpts, optDebug, optProjectName, optProjectRevision), ScanDestination (..), @@ -47,16 +48,16 @@ import Options.Applicative ( Mod, Parser, command, - help, hidden, info, long, optional, - progDesc, short, strOption, switch, ) +import Options.Applicative.Builder (helpDoc, progDescDoc) +import Style (applyFossaStyle, formatStringToDoc, stringToHelpDoc) data NoUpload = NoUpload data JsonOutput = JsonOutput deriving (Generic) @@ -98,7 +99,7 @@ subcommand f = command "analyze" ( info (f <$> cliParser) $ - progDesc "Scan an image for vulnerabilities" + progDescDoc (formatStringToDoc "Scan an image for vulnerabilities") ) cliParser :: Parser ContainerAnalyzeOptions @@ -107,22 +108,24 @@ cliParser = <$> commonOpts <*> flagOpt NoUpload - ( long "output" + ( applyFossaStyle + <> long "output" <> short 'o' - <> help "Output results to stdout instead of uploading to fossa" + <> stringToHelpDoc "Output results to stdout instead of uploading to FOSSA" ) - <*> flagOpt JsonOutput (long "json" <> help "Output project metadata as json to the console. Useful for communicating with the FOSSA API") + <*> flagOpt JsonOutput (applyFossaStyle <> long "json" <> stringToHelpDoc "Output project metadata as JSON to the console. This is useful for communicating with the FOSSA API.") <*> optional ( strOption - ( long "branch" + ( applyFossaStyle + <> long "branch" <> short 'b' - <> help "this repository's current branch (default: current VCS branch)" + <> helpDoc branchHelp ) ) <*> metadataOpts <*> imageTextArg - <*> switch (long "experimental-scanner" <> help "Uses experimental fossa native container scanner." <> hidden) - <*> switch (long "only-system-deps" <> help "Only analyzes system dependencies (e.g. apk, dep, rpm).") + <*> switch (applyFossaStyle <> long "experimental-scanner" <> stringToHelpDoc "Uses experimental fossa native container scanner" <> hidden) + <*> switch (applyFossaStyle <> long "only-system-deps" <> stringToHelpDoc "Only analyzes system dependencies (e.g. apk, dep, rpm)") mergeOpts :: (Has Diagnostics sig m) => diff --git a/src/App/Fossa/Config/Container/Common.hs b/src/App/Fossa/Config/Container/Common.hs index 9a8bd6548a..4c6d5ccfc1 100644 --- a/src/App/Fossa/Config/Container/Common.hs +++ b/src/App/Fossa/Config/Container/Common.hs @@ -12,8 +12,9 @@ import Data.String.Conversion (toText) import Data.Text (Text) import Data.Text qualified as Text import GHC.Generics (Generic) -import Options.Applicative (Parser, argument, help, metavar, str) +import Options.Applicative (Parser, argument, metavar, str) import Prettyprinter (pretty, vsep) +import Style (applyFossaStyle, stringToHelpDoc) import System.Info (arch) newtype ImageText = ImageText @@ -25,7 +26,7 @@ instance ToJSON ImageText where toEncoding = genericToEncoding defaultOptions imageTextArg :: Parser ImageText -imageTextArg = ImageText <$> argument str (metavar "IMAGE" <> help "The image to scan") +imageTextArg = ImageText <$> argument str (applyFossaStyle <> metavar "IMAGE" <> stringToHelpDoc "The image to scan") -- | Get current runtime arch, We use this to find suitable image, -- if multi-platform image is discovered. This is similar to diff --git a/src/App/Fossa/Config/Container/ListTargets.hs b/src/App/Fossa/Config/Container/ListTargets.hs index 3b3c074963..47635c47f5 100644 --- a/src/App/Fossa/Config/Container/ListTargets.hs +++ b/src/App/Fossa/Config/Container/ListTargets.hs @@ -27,8 +27,9 @@ import Options.Applicative ( Parser, command, info, - progDesc, + progDescDoc, ) +import Style (formatStringToDoc) newtype ContainerListTargetsOptions = ContainerListTargetsOptions { imageLocator :: ImageText @@ -53,7 +54,8 @@ subcommand f = command "list-targets" ( info (f <$> listTargetParser) $ - progDesc "Lists target with container image" + progDescDoc $ + formatStringToDoc "Lists target with container image" ) listTargetParser :: Parser ContainerListTargetsOptions diff --git a/src/App/Fossa/Config/Container/Test.hs b/src/App/Fossa/Config/Container/Test.hs index 99ed2e31d7..aa9ba876e0 100644 --- a/src/App/Fossa/Config/Container/Test.hs +++ b/src/App/Fossa/Config/Container/Test.hs @@ -24,7 +24,7 @@ import App.Fossa.Config.Container.Common ( imageTextArg, ) import App.Fossa.Config.EnvironmentVars (EnvVars) -import App.Fossa.Config.Test (TestOutputFormat (TestOutputJson, TestOutputPretty), defaultOutputFmt, testOutputFormatList, validateOutputFormat) +import App.Fossa.Config.Test (TestOutputFormat (TestOutputJson, TestOutputPretty), defaultOutputFmt, testFormatHelp, validateOutputFormat) import App.Types (OverrideProject (OverrideProject)) import Control.Effect.Diagnostics (Diagnostics, Has) import Control.Monad (when) @@ -41,22 +41,24 @@ import Options.Applicative ( auto, command, flag, - help, info, internal, long, option, optional, - progDesc, + progDescDoc, strOption, ) +import Options.Applicative.Builder (helpDoc) +import Style (applyFossaStyle, formatStringToDoc, stringToHelpDoc) subcommand :: (ContainerTestOptions -> a) -> Mod CommandFields a subcommand f = command "test" ( info (f <$> cliParser) $ - progDesc "Check for issues from FOSSA and exit non-zero when issues are found" + progDescDoc $ + formatStringToDoc "Check for issues from FOSSA and exit non-zero when issues are found" ) data ContainerTestConfig = ContainerTestConfig @@ -85,9 +87,9 @@ cliParser :: Parser ContainerTestOptions cliParser = ContainerTestOptions <$> commonOpts - <*> optional (option auto (long "timeout" <> help "Duration to wait for build completion (in seconds)")) - <*> flag TestOutputPretty TestOutputJson (long "json" <> help "Output issues as json" <> internal) - <*> optional (strOption (long "format" <> help ("Output the report in the specified format. Currently available formats: (" <> testOutputFormatList <> ")"))) + <*> optional (option auto (applyFossaStyle <> long "timeout" <> stringToHelpDoc "Duration to wait for build completion (in seconds)")) + <*> flag TestOutputPretty TestOutputJson (applyFossaStyle <> long "json" <> stringToHelpDoc "Output issues as JSON" <> internal) + <*> optional (strOption (applyFossaStyle <> long "format" <> helpDoc testFormatHelp)) <*> imageTextArg mergeOpts :: diff --git a/src/App/Fossa/Config/DumpBinaries.hs b/src/App/Fossa/Config/DumpBinaries.hs index dd804547d4..baa943d9ca 100644 --- a/src/App/Fossa/Config/DumpBinaries.hs +++ b/src/App/Fossa/Config/DumpBinaries.hs @@ -11,11 +11,12 @@ import Control.Effect.Lift (Has, Lift) import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding) import Effect.ReadFS (ReadFS) import GHC.Generics (Generic) -import Options.Applicative (InfoMod, progDesc) +import Options.Applicative (InfoMod, progDescDoc) import Path (Abs, Dir, Path) +import Style (formatStringToDoc) dumpInfo :: InfoMod a -dumpInfo = progDesc "Output all embedded binaries to specified path" +dumpInfo = progDescDoc $ formatStringToDoc "Output all embedded binaries to specified path" mkSubCommand :: (DumpBinsConfig -> EffStack ()) -> SubCommand DumpBinsOpts DumpBinsConfig mkSubCommand = SubCommand "dump-binaries" dumpInfo cliParser noLoadConfig mergeOpts diff --git a/src/App/Fossa/Config/LicenseScan.hs b/src/App/Fossa/Config/LicenseScan.hs index df9748c8ed..1a536637e7 100644 --- a/src/App/Fossa/Config/LicenseScan.hs +++ b/src/App/Fossa/Config/LicenseScan.hs @@ -10,6 +10,7 @@ import App.Types (BaseDir) import Control.Effect.Diagnostics (Diagnostics) import Control.Effect.Lift (Has, Lift) import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding) +import Effect.Logger (vsep) import Effect.ReadFS (ReadFS) import GHC.Generics (Generic) import Options.Applicative ( @@ -17,15 +18,17 @@ import Options.Applicative ( InfoMod, Parser, command, - hsubparser, info, internal, - progDesc, + progDescDoc, subparser, ) +import Prettyprinter (Doc) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green)) +import Style (coloredBoldItalicized, formatDoc, formatStringToDoc) licenseScanInfo :: InfoMod a -licenseScanInfo = progDesc "Utilities for native license-scanning" +licenseScanInfo = progDescDoc $ formatStringToDoc "Utilities for native license-scanning" mkSubCommand :: (LicenseScanConfig -> EffStack ()) -> SubCommand LicenseScanCommand LicenseScanConfig mkSubCommand = SubCommand "license-scan" licenseScanInfo cliParser noLoadConfig mergeOpts @@ -62,17 +65,24 @@ mergeOpts _ _ (FossaDeps path) = VendoredDepsOutput <$> collectBaseDir path cliParser :: Parser LicenseScanCommand cliParser = public <|> private where - public = hsubparser fossaDepsCommand + public = subparser fossaDepsCommand private = subparser $ internal <> directScanCommand fossaDepsCommand = command "fossa-deps" ( info (FossaDeps <$> baseDirArg) $ - progDesc "Like `fossa analyze --output`, but only for native scanning of vendored-dependencies." + progDescDoc fossaDepsDesc ) directScanCommand = command "direct" ( info (DirectScan <$> baseDirArg) $ - progDesc "Run a license scan directly on the provided path." + progDescDoc $ + formatStringToDoc "Run a license scan directly on the provided path" ) + fossaDepsDesc :: Maybe (Doc AnsiStyle) + fossaDepsDesc = + Just . formatDoc $ + vsep + [ "Like " <> coloredBoldItalicized Green "fossa analyze --output" <> " but only for native scanning of vendored-dependencies" + ] diff --git a/src/App/Fossa/Config/LinkUserBinaries.hs b/src/App/Fossa/Config/LinkUserBinaries.hs index b05012248c..6e471e48a0 100644 --- a/src/App/Fossa/Config/LinkUserBinaries.hs +++ b/src/App/Fossa/Config/LinkUserBinaries.hs @@ -38,21 +38,24 @@ import Options.Applicative ( InfoMod, Parser, argument, - help, + helpDoc, long, metavar, optional, - progDesc, + progDescDoc, str, strOption, value, ) +import Prettyprinter (Doc, vsep) +import Prettyprinter.Render.Terminal (AnsiStyle) +import Style (applyFossaStyle, boldItalicized, formatDoc, formatStringToDoc, stringToHelpDoc) cmdName :: String cmdName = "experimental-link-user-defined-dependency-binary" linkInfo :: InfoMod a -linkInfo = progDesc "Link one or more binary fingerprints as a user-defined dependency" +linkInfo = progDescDoc $ formatStringToDoc "Link one or more binary fingerprints as a user-defined dependency" mkSubCommand :: (LinkUserBinsConfig -> EffStack ()) -> SubCommand LinkUserBinsOpts LinkUserBinsConfig mkSubCommand = SubCommand cmdName linkInfo cliParser loadConfig mergeOpts @@ -118,10 +121,17 @@ cliParser = assertUserDefinedBinariesMeta :: Parser UserDefinedAssertionMeta assertUserDefinedBinariesMeta = UserDefinedAssertionMeta - <$> (strOption (long "name" <> help "The name to display for the dependency")) - <*> (strOption (long "version" <> help "The version to display for the dependency")) - <*> (strOption (long "license" <> help "The license identifier to use for the dependency")) - <*> optional (strOption (long "description" <> help "The description to use for the dependency")) - <*> optional (strOption (long "homepage" <> help "The URL to the homepage for the dependency")) + <$> (strOption (applyFossaStyle <> long "name" <> stringToHelpDoc "The name to display for the dependency")) + <*> (strOption (applyFossaStyle <> long "version" <> stringToHelpDoc "The version to display for the dependency")) + <*> (strOption (applyFossaStyle <> long "license" <> stringToHelpDoc "The license identifier to use for the dependency")) + <*> optional (strOption (applyFossaStyle <> long "description" <> stringToHelpDoc "The description to use for the dependency")) + <*> optional (strOption (applyFossaStyle <> long "homepage" <> stringToHelpDoc "The URL to the homepage for the dependency")) assertUserDefinedBinariesDir :: Parser String - assertUserDefinedBinariesDir = argument str (metavar "DIR" <> help "The directory containing one or more binaries to assert to the provided values (default: current directory)" <> value ".") + assertUserDefinedBinariesDir = argument str (applyFossaStyle <> metavar "DIR" <> helpDoc dirHelp <> value ".") + dirHelp :: Maybe (Doc AnsiStyle) + dirHelp = + Just . formatDoc $ + vsep + [ "The directory containing one or more binaries to assert to the provided values" + , boldItalicized "Default: " <> "Current directory" + ] diff --git a/src/App/Fossa/Config/ListTargets.hs b/src/App/Fossa/Config/ListTargets.hs index 7a06c75f09..0902c67e12 100644 --- a/src/App/Fossa/Config/ListTargets.hs +++ b/src/App/Fossa/Config/ListTargets.hs @@ -32,10 +32,13 @@ import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding) import Data.Maybe (fromMaybe) import Data.String.Conversion (toText) import Data.Text (strip, toLower) -import Effect.Logger (Has, Logger, Severity (SevDebug, SevInfo)) +import Effect.Logger (Has, Logger, Severity (SevDebug, SevInfo), vsep) import Effect.ReadFS (ReadFS) import GHC.Generics (Generic) -import Options.Applicative (InfoMod, Parser, ReadM, eitherReader, help, long, option, optional, progDesc) +import Options.Applicative (InfoMod, Parser, ReadM, eitherReader, helpDoc, long, option, optional, progDescDoc) +import Prettyprinter (Doc) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green)) +import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, formatStringToDoc) data ListTargetOutputFormat = Legacy @@ -68,7 +71,7 @@ loadConfig :: loadConfig = resolveLocalConfigFile . optConfig . commons listTargetsInfo :: InfoMod a -listTargetsInfo = progDesc "List available analysis-targets in a directory (projects and sub-projects)" +listTargetsInfo = progDescDoc $ formatStringToDoc "List available analysis-targets in a directory (projects and sub-projects)" parser :: Parser ListTargetsCliOpts parser = @@ -78,10 +81,19 @@ parser = <*> optional ( option parseListTargetOutput - ( long "format" - <> help "output format to use: legacy, ndjson, text (default: legacy)" + ( applyFossaStyle + <> long "format" + <> helpDoc listTargetsFormatHelp ) ) + where + listTargetsFormatHelp :: Maybe (Doc AnsiStyle) + listTargetsFormatHelp = + Just . formatDoc $ + vsep + [ boldItalicized "Formats: " <> coloredBoldItalicized Green "legacy" <> boldItalicized "|" <> coloredBoldItalicized Green "ndjson" <> boldItalicized "|" <> coloredBoldItalicized Green "text" + , boldItalicized "Default: " <> coloredBoldItalicized Green "legacy" + ] mergeOpts :: ( Has Diagnostics sig m diff --git a/src/App/Fossa/Config/Report.hs b/src/App/Fossa/Config/Report.hs index 8108f95366..eab4c1f420 100644 --- a/src/App/Fossa/Config/Report.hs +++ b/src/App/Fossa/Config/Report.hs @@ -31,7 +31,7 @@ import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding) import Data.List (intercalate) import Data.String.Conversion (ToText, toText) import Effect.Exec (Exec) -import Effect.Logger (Logger, Severity (..)) +import Effect.Logger (Logger, Severity (..), vsep) import Effect.ReadFS (ReadFS) import Fossa.API.Types (ApiOpts) import GHC.Generics (Generic) @@ -40,7 +40,6 @@ import Options.Applicative ( Parser, argument, auto, - help, long, maybeReader, metavar, @@ -50,8 +49,10 @@ import Options.Applicative ( strOption, switch, ) -import Prettyprinter (Doc, comma, hardline, pretty, punctuate, softline, viaShow) -import Prettyprinter.Render.Terminal (AnsiStyle) +import Options.Applicative.Builder (helpDoc) +import Prettyprinter (Doc, comma, hardline, pretty, punctuate, viaShow) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green, Red)) +import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, stringToHelpDoc, styledDivider) data ReportType = Attribution deriving (Eq, Ord, Enum, Bounded, Generic) @@ -101,31 +102,35 @@ instance Show ReportOutputFormat where show ReportHTML = "html" show ReportCSV = "csv" +allFormats :: [ReportOutputFormat] +allFormats = enumFromTo minBound maxBound + reportOutputFormatList :: String reportOutputFormatList = intercalate ", " $ map show allFormats + +styledReportOutputFormats :: Doc AnsiStyle +styledReportOutputFormats = mconcat $ punctuate styledDivider coloredAllFormats where - allFormats :: [ReportOutputFormat] - allFormats = enumFromTo minBound maxBound + coloredAllFormats :: [Doc AnsiStyle] + coloredAllFormats = map (coloredBoldItalicized Green . viaShow) allFormats instance ToJSON ReportOutputFormat where toEncoding = genericToEncoding defaultOptions +allReports :: [ReportType] +allReports = enumFromTo minBound maxBound + reportInfo :: InfoMod a -reportInfo = progDescDoc (Just desc) +reportInfo = progDescDoc (Just $ formatDoc desc) where - allReports :: [ReportType] - allReports = enumFromTo minBound maxBound - desc :: Doc AnsiStyle desc = - "Access various reports from FOSSA and print to stdout." - <> softline - <> "Currently available reports: (" - <> mconcat (punctuate comma (map viaShow allReports)) - <> ")" + "Access various reports from FOSSA and print to stdout" <> hardline - <> "Examples: " + <> "Currently available reports: " + <> mconcat (punctuate comma (map viaShow allReports)) <> hardline + <> "Example: " <> "fossa report --format html attribution" mkSubCommand :: (ReportConfig -> EffStack ()) -> SubCommand ReportCliOptions ReportConfig @@ -135,14 +140,30 @@ parser :: Parser ReportCliOptions parser = ReportCliOptions <$> commonOpts - <*> switch (long "json" <> help "Output the report in JSON format. Equivalent to '--format json', and overrides --format. Deprecated: prefer --format") - <*> optional (strOption (long "format" <> help ("Output the report in the specified format. Currently available formats: (" <> reportOutputFormatList <> ")"))) - <*> optional (option auto (long "timeout" <> help "Duration to wait for build completion (in seconds)")) + <*> switch (applyFossaStyle <> long "json" <> helpDoc jsonHelp) + <*> optional (strOption (applyFossaStyle <> long "format" <> helpDoc formatHelp)) + <*> optional (option auto (applyFossaStyle <> long "timeout" <> stringToHelpDoc "Duration to wait for build completion (in seconds)")) <*> reportTypeArg <*> baseDirArg + where + jsonHelp :: Maybe (Doc AnsiStyle) + jsonHelp = + Just . formatDoc $ + vsep + [ "Output the report in JSON format. Equivalent to " <> coloredBoldItalicized Green "--format json" <> " and overrides " <> coloredBoldItalicized Green "--format" + , coloredBoldItalicized Red "Deprecated: " <> "prefer " <> coloredBoldItalicized Green "--format" + ] + + formatHelp :: Maybe (Doc AnsiStyle) + formatHelp = + Just . formatDoc $ + vsep + [ "Output the report in the specified format" + , boldItalicized "Formats: " <> styledReportOutputFormats + ] reportTypeArg :: Parser ReportType -reportTypeArg = argument (maybeReader parseType) (metavar "REPORT" <> help "The report type to fetch from the server.") +reportTypeArg = argument (maybeReader parseType) (applyFossaStyle <> metavar "REPORT" <> stringToHelpDoc "The report type to fetch from the server") where parseType :: String -> Maybe ReportType parseType = \case diff --git a/src/App/Fossa/Config/Snippets.hs b/src/App/Fossa/Config/Snippets.hs index d1e641756e..c6fb0cc476 100644 --- a/src/App/Fossa/Config/Snippets.hs +++ b/src/App/Fossa/Config/Snippets.hs @@ -13,7 +13,7 @@ module App.Fossa.Config.Snippets ( labelForTransform, ) where -import App.Fossa.Config.Common (baseDirArg, collectBaseDir, fossaApiKeyCmdText) +import App.Fossa.Config.Common (baseDirArg, collectBaseDir, endpointHelp, fossaApiKeyCmdText, fossaApiKeyHelp) import App.Fossa.Subcommand (EffStack, GetCommonOpts, GetSeverity (..), SubCommand (..)) import App.OptionExtensions (uriOption) import App.Types (BaseDir) @@ -27,9 +27,10 @@ import Data.Text (Text) import Effect.Logger (Severity (..)) import Effect.ReadFS (ReadFS) import GHC.Generics (Generic) -import Options.Applicative (InfoMod, Parser, command, eitherReader, help, hsubparser, info, long, many, metavar, option, optional, progDesc, short, strOption, switch, (<|>)) +import Options.Applicative (InfoMod, Parser, command, eitherReader, helpDoc, info, long, many, metavar, option, optional, progDescDoc, short, strOption, subparser, switch, (<|>)) import Path (Abs, Dir, Path) import Path.IO qualified as Path +import Style (applyFossaStyle, formatStringToDoc, stringToHelpDoc) import Text.URI (URI) data SnippetsCommand @@ -56,13 +57,13 @@ data SnippetsCommand [SnippetTransform] snippetsInfo :: InfoMod a -snippetsInfo = progDesc "FOSSA snippet scanning" +snippetsInfo = progDescDoc $ formatStringToDoc "FOSSA snippet scanning" snippetsAnalyzeInfo :: InfoMod a -snippetsAnalyzeInfo = progDesc "Analyze a local project for snippet matches" +snippetsAnalyzeInfo = progDescDoc $ formatStringToDoc "Analyze a local project for snippet matches" snippetsCommitInfo :: InfoMod a -snippetsCommitInfo = progDesc "Commit matches discovered during analyze into a fossa-deps file" +snippetsCommitInfo = progDescDoc $ formatStringToDoc "Commit matches discovered during analyze into a fossa-deps file" instance GetSeverity SnippetsCommand where getSeverity :: SnippetsCommand -> Severity @@ -79,31 +80,31 @@ mkSubCommand = SubCommand "snippets" snippetsInfo cliParser noLoadConfig mergeOp cliParser :: Parser SnippetsCommand cliParser = analyze <|> commit where - analyze = hsubparser . command "analyze" $ info analyzeOpts snippetsAnalyzeInfo + analyze = subparser . command "analyze" $ info analyzeOpts snippetsAnalyzeInfo analyzeOpts = CommandAnalyze <$> baseDirArg - <*> switch (long "debug" <> help "Enable debug logging") - <*> optional (uriOption (long "endpoint" <> short 'e' <> metavar "URL" <> help "The FOSSA API server base URL (default: https://app.fossa.com)")) - <*> optional (strOption (long fossaApiKeyCmdText <> help "the FOSSA API server authentication key (default: FOSSA_API_KEY from env)")) - <*> strOption (long "output" <> short 'o' <> help "The directory to which matches are output") - <*> switch (long "overwrite-output" <> help "If specified, overwrites the output directory if it exists") - <*> many (option (eitherReader parseTarget) (long "target" <> help ("Analyze this combination of targets") <> metavar "TARGET")) - <*> many (option (eitherReader parseKind) (long "kind" <> help ("Analyze this combination of kinds") <> metavar "KIND")) - <*> many (option (eitherReader parseTransform) (long "transform" <> help ("Analyze this combination of transforms") <> metavar "TRANSFORM")) - commit = hsubparser . command "commit" $ info commitOpts snippetsCommitInfo + <*> switch (applyFossaStyle <> long "debug" <> stringToHelpDoc "Enable debug logging") + <*> optional (uriOption (applyFossaStyle <> long "endpoint" <> short 'e' <> metavar "URL" <> helpDoc endpointHelp)) + <*> optional (strOption (applyFossaStyle <> long fossaApiKeyCmdText <> helpDoc fossaApiKeyHelp)) + <*> strOption (applyFossaStyle <> long "output" <> short 'o' <> stringToHelpDoc "The directory to which matches are output") + <*> switch (applyFossaStyle <> long "overwrite-output" <> stringToHelpDoc "If specified, overwrites the output directory if it exists") + <*> many (option (eitherReader parseTarget) (applyFossaStyle <> long "target" <> stringToHelpDoc "Analyze this combination of targets" <> metavar "TARGET")) + <*> many (option (eitherReader parseKind) (applyFossaStyle <> long "kind" <> stringToHelpDoc "Analyze this combination of kinds" <> metavar "KIND")) + <*> many (option (eitherReader parseTransform) (applyFossaStyle <> long "transform" <> stringToHelpDoc "Analyze this combination of transforms" <> metavar "TRANSFORM")) + commit = subparser . command "commit" $ info commitOpts snippetsCommitInfo commitOpts = CommandCommit <$> baseDirArg - <*> switch (long "debug" <> help "Enable debug logging") - <*> optional (uriOption (long "endpoint" <> short 'e' <> metavar "URL" <> help "The FOSSA API server base URL (default: https://app.fossa.com)")) - <*> optional (strOption (long fossaApiKeyCmdText <> help "the FOSSA API server authentication key (default: FOSSA_API_KEY from env)")) - <*> strOption (long "analyze-output" <> help "The directory to which 'analyze' matches were saved") - <*> switch (long "overwrite-fossa-deps" <> help "If specified, overwrites the 'fossa-deps' file if it exists") - <*> optional (option (eitherReader parseCommitOutputFormat) (long "format" <> help ("The output format for the generated `fossa-deps` file") <> metavar "FORMAT")) - <*> many (option (eitherReader parseTarget) (long "target" <> help ("Commit this combination of targets") <> metavar "TARGET")) - <*> many (option (eitherReader parseKind) (long "kind" <> help ("Commit this combination of kinds") <> metavar "KIND")) - <*> many (option (eitherReader parseTransform) (long "transform" <> help ("Commit this combination of transforms") <> metavar "TRANSFORM")) + <*> switch (applyFossaStyle <> long "debug" <> stringToHelpDoc "Enable debug logging") + <*> optional (uriOption (applyFossaStyle <> long "endpoint" <> short 'e' <> metavar "URL" <> helpDoc endpointHelp)) + <*> optional (strOption (applyFossaStyle <> long fossaApiKeyCmdText <> helpDoc fossaApiKeyHelp)) + <*> strOption (applyFossaStyle <> long "analyze-output" <> stringToHelpDoc "The directory to which 'analyze' matches were saved") + <*> switch (applyFossaStyle <> long "overwrite-fossa-deps" <> stringToHelpDoc "If specified, overwrites the 'fossa-deps' file if it exists") + <*> optional (option (eitherReader parseCommitOutputFormat) (applyFossaStyle <> long "format" <> stringToHelpDoc "The output format for the generated `fossa-deps` file" <> metavar "FORMAT")) + <*> many (option (eitherReader parseTarget) (applyFossaStyle <> long "target" <> stringToHelpDoc "Commit this combination of targets" <> metavar "TARGET")) + <*> many (option (eitherReader parseKind) (applyFossaStyle <> long "kind" <> stringToHelpDoc "Commit this combination of kinds" <> metavar "KIND")) + <*> many (option (eitherReader parseTransform) (applyFossaStyle <> long "transform" <> stringToHelpDoc "Commit this combination of transforms" <> metavar "TRANSFORM")) mergeOpts :: ( Has Diagnostics sig m diff --git a/src/App/Fossa/Config/Test.hs b/src/App/Fossa/Config/Test.hs index c69397b80d..016e9f291f 100644 --- a/src/App/Fossa/Config/Test.hs +++ b/src/App/Fossa/Config/Test.hs @@ -13,6 +13,7 @@ module App.Fossa.Config.Test ( testOutputFormatList, defaultOutputFmt, parseFossaTestOutputFormat, + testFormatHelp, ) where import App.Fossa.Config.Common ( @@ -53,14 +54,17 @@ import Options.Applicative ( Parser, auto, flag, - help, + helpDoc, internal, long, option, optional, - progDesc, + progDescDoc, strOption, ) +import Prettyprinter (Doc, punctuate, viaShow) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green)) +import Style (applyFossaStyle, boldItalicized, coloredBoldItalicized, formatDoc, formatStringToDoc, stringToHelpDoc, styledDivider) data TestOutputFormat = TestOutputPretty @@ -76,9 +80,23 @@ defaultOutputFmt = TestOutputPretty testOutputFormatList :: String testOutputFormatList = intercalate ", " $ map show allFormats + +allFormats :: [TestOutputFormat] +allFormats = enumFromTo minBound maxBound + +styledOutputFormats :: Doc AnsiStyle +styledOutputFormats = mconcat $ punctuate styledDivider coloredAllFormats where - allFormats :: [TestOutputFormat] - allFormats = enumFromTo minBound maxBound + coloredAllFormats :: [Doc AnsiStyle] + coloredAllFormats = map (coloredBoldItalicized Green . viaShow) allFormats + +testFormatHelp :: Maybe (Doc AnsiStyle) +testFormatHelp = + Just . formatDoc $ + vsep + [ "Output the report in the specified format" + , boldItalicized "Formats: " <> styledOutputFormats + ] newtype InvalidReportFormat = InvalidReportFormat String instance ToDiagnostic InvalidReportFormat where @@ -137,7 +155,7 @@ instance ToJSON TestConfig where toEncoding = genericToEncoding defaultOptions testInfo :: InfoMod a -testInfo = progDesc "Check for issues from FOSSA and exit non-zero when issues are found" +testInfo = progDescDoc $ formatStringToDoc "Check for issues from FOSSA and exit non-zero when issues are found" mkSubCommand :: (TestConfig -> EffStack ()) -> SubCommand TestCliOpts TestConfig mkSubCommand = SubCommand "test" testInfo parser loadConfig mergeOpts @@ -146,11 +164,19 @@ parser :: Parser TestCliOpts parser = TestCliOpts <$> commonOpts - <*> optional (option auto (long "timeout" <> help "Duration to wait for build completion in seconds (Defaults to 1 hour)")) - <*> flag defaultOutputFmt TestOutputJson (long "json" <> help "Output issues as json" <> internal) - <*> optional (strOption (long "format" <> help ("Output the report in the specified format. Currently available formats: (" <> testOutputFormatList <> ")"))) + <*> optional (option auto (applyFossaStyle <> long "timeout" <> helpDoc timeoutHelp)) + <*> flag defaultOutputFmt TestOutputJson (applyFossaStyle <> long "json" <> stringToHelpDoc "Output issues as JSON" <> internal) + <*> optional (strOption (applyFossaStyle <> long "format" <> helpDoc testFormatHelp)) <*> baseDirArg - <*> optional (strOption (long "diff" <> help "Checks for new issues of the revision, that does not exist in provided diff revision.")) + <*> optional (strOption (applyFossaStyle <> long "diff" <> stringToHelpDoc "Checks for new issues of the revision that does not exist in provided diff revision")) + where + timeoutHelp :: Maybe (Doc AnsiStyle) + timeoutHelp = + Just . formatDoc $ + vsep + [ "Duration to wait for build completion in seconds" + , boldItalicized "Default: " <> "3600 (1 hour)" + ] loadConfig :: ( Has (Lift IO) sig m diff --git a/src/App/Fossa/Init.hs b/src/App/Fossa/Init.hs index 96ecb60c46..750fd26759 100644 --- a/src/App/Fossa/Init.hs +++ b/src/App/Fossa/Init.hs @@ -21,7 +21,7 @@ import Data.ByteString qualified as BS import Data.FileEmbed.Extra (embedFileIfExists) import Effect.Logger (Logger, Severity (SevInfo), logInfo, pretty, withDefaultLogger) import Effect.ReadFS (ReadFS, getCurrentDir, runReadFSIO) -import Options.Applicative (CommandFields, Mod, Parser, info, progDesc) +import Options.Applicative (CommandFields, Mod, Parser, info, progDescDoc) import Options.Applicative.Builder (command) import Path ( Abs, @@ -33,9 +33,10 @@ import Path ( toFilePath, (), ) +import Style (formatStringToDoc) initCommand :: Mod CommandFields (IO ()) -initCommand = command "init" (info run $ progDesc "Creates .fossa.yml.example and fossa-deps.yml.example file") +initCommand = command "init" (info run $ progDescDoc $ formatStringToDoc "Creates .fossa.yml.example and fossa-deps.yml.example file") where run :: Parser (IO ()) run = pure $ withTelemetry . runStack . withDefaultLogger SevInfo . logWithExit_ . runReadFSIO $ runInit diff --git a/src/App/Fossa/Main.hs b/src/App/Fossa/Main.hs index 1990353f6c..7f5dceb999 100644 --- a/src/App/Fossa/Main.hs +++ b/src/App/Fossa/Main.hs @@ -24,21 +24,20 @@ import Options.Applicative ( Mod, Parser, ParserPrefs, + columns, command, customExecParser, footer, fullDesc, header, - help, + helpIndent, helpShowGlobals, - helper, - hsubparser, info, infoOption, internal, long, prefs, - progDesc, + progDescDoc, short, showHelpOnEmpty, showHelpOnError, @@ -47,17 +46,30 @@ import Options.Applicative ( (<**>), (<|>), ) +import Options.Applicative.Extra (helperWith) +import Style (applyFossaStyle, formatStringToDoc, stringToHelpDoc) appMain :: IO () appMain = do initRTSThreads - join $ customExecParser mainPrefs $ info (subcommands <**> helper <**> versionOpt) progData + join $ customExecParser mainPrefs $ info (subcommands <**> helperOpt <**> versionOpt) progData versionOpt :: Parser (a -> a) versionOpt = infoOption (toString fullVersionDescription) $ mconcat - [long "version", short 'V', help "show version information and exit"] + [applyFossaStyle, long "version", short 'V', stringToHelpDoc "Show version information and exit"] + +helperOpt :: Parser (a -> a) +helperOpt = + helperWith + ( mconcat + [ applyFossaStyle + , long "help" + , short 'h' + , stringToHelpDoc "Show this help text" + ] + ) progData :: InfoMod (IO ()) progData = @@ -77,7 +89,7 @@ subcommands = public <|> private , decodeSubCommand LicenseScan.licenseScanSubCommand ] public = - hsubparser $ + subparser $ mconcat [ decodeSubCommand Analyze.analyzeSubCommand , decodeSubCommand Test.testSubCommand @@ -90,7 +102,7 @@ subcommands = public <|> private ] experimentalLicenseScanCommand :: Mod CommandFields (IO ()) -experimentalLicenseScanCommand = command "experimental-license-scan" (info runInit $ progDesc "The 'experimental-license-scan' command has been deprecated and renamed to 'license-scan'") +experimentalLicenseScanCommand = command "experimental-license-scan" (info runInit $ progDescDoc $ formatStringToDoc "The 'experimental-license-scan' command has been deprecated and renamed to 'license-scan'") where runInit :: Parser (IO ()) runInit = pure $ putStrLn "The 'experimental-license-scan' has been deprecated and renamed to 'license-scan'. Please use the 'license-scan' command instead." @@ -105,5 +117,7 @@ mainPrefs = [ showHelpOnEmpty , showHelpOnError , subparserInline + , columns 150 , helpShowGlobals + , helpIndent 1 -- allows the help message to appear on new line ] diff --git a/src/App/Fossa/Subcommand.hs b/src/App/Fossa/Subcommand.hs index aa70bce51b..65280ed5c3 100644 --- a/src/App/Fossa/Subcommand.hs +++ b/src/App/Fossa/Subcommand.hs @@ -6,6 +6,7 @@ module App.Fossa.Subcommand ( GetSeverity (..), GetCommonOpts (..), SubCommand (..), + updateCommandName, ) where import App.Fossa.Config.Common (CommonOpts, collectTelemetrySink) @@ -50,6 +51,9 @@ class GetCommonOpts a where getCommonOpts :: a -> Maybe CommonOpts getCommonOpts = const Nothing +updateCommandName :: SubCommand cli cfg -> String -> SubCommand cli cfg +updateCommandName subCmd newName = subCmd{commandName = newName} + runSubCommand :: forall cli cfg. (GetCommonOpts cli, GetSeverity cli, Show cfg, ToJSON cfg) => SubCommand cli cfg -> Parser (IO ()) runSubCommand SubCommand{..} = uncurry (runEffs) . mergeAndRun <$> parser where diff --git a/src/Style.hs b/src/Style.hs new file mode 100644 index 0000000000..14d670b177 --- /dev/null +++ b/src/Style.hs @@ -0,0 +1,35 @@ +module Style (coloredText, formatStringToDoc, formatDoc, applyFossaStyle, boldItalicized, coloredBoldItalicized, coloredBoldItalicizedString, stringToHelpDoc, styledDivider) where + +import Data.String.Conversion (toString) +import Effect.Logger (newlineTrailing, vsep) +import Options.Applicative (helpDoc, style) +import Options.Applicative.Builder (Mod) +import Prettyprinter (Doc, annotate, defaultLayoutOptions, indent, layoutPretty, pretty) +import Prettyprinter.Render.Terminal (AnsiStyle, Color (Green), bold, color, italicized, renderStrict) + +formatStringToDoc :: String -> Maybe (Doc AnsiStyle) +formatStringToDoc s = Just $ indent 2 $ vsep [newlineTrailing $ pretty s] + +formatDoc :: Doc AnsiStyle -> Doc AnsiStyle +formatDoc doc = indent 2 $ newlineTrailing doc + +stringToHelpDoc :: String -> Mod f a +stringToHelpDoc = helpDoc . formatStringToDoc + +coloredText :: Color -> Doc AnsiStyle -> String +coloredText clr str = toString . renderStrict . layoutPretty defaultLayoutOptions $ annotate (color clr) str + +coloredBoldItalicizedString :: Color -> Doc AnsiStyle -> String +coloredBoldItalicizedString clr str = toString . renderStrict . layoutPretty defaultLayoutOptions $ annotate (color clr <> bold <> italicized) str + +applyFossaStyle :: Mod f a +applyFossaStyle = style $ annotate (color Green <> bold <> italicized) + +boldItalicized :: Doc AnsiStyle -> Doc AnsiStyle +boldItalicized = annotate (bold <> italicized) + +coloredBoldItalicized :: Color -> Doc AnsiStyle -> Doc AnsiStyle +coloredBoldItalicized clr = annotate (color clr <> bold <> italicized) + +styledDivider :: Doc AnsiStyle +styledDivider = boldItalicized "|"