Skip to content

Commit

Permalink
feat: add manpages (#456)
Browse files Browse the repository at this point in the history
* implement man generate cmd

* make docs

* refactor and fix docs

* add installation help text

* update changelog
  • Loading branch information
glimberea authored Sep 6, 2024
1 parent a85ed7b commit 67b67fd
Show file tree
Hide file tree
Showing 51 changed files with 9,935 additions and 138 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## [v6.7.8] (?)

### Added
- Added support for `manpages` generation via `ionosctl man` command

### Changed
- Added authentication warning to `image upload` help text

Expand Down
148 changes: 148 additions & 0 deletions commands/man.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package commands

import (
"compress/gzip"
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/ionos-cloud/ionosctl/v6/internal/constants"
"github.com/ionos-cloud/ionosctl/v6/internal/core"
"github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter"
"github.com/ionos-cloud/ionosctl/v6/pkg/confirm"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"github.com/spf13/viper"
)

func Man() *core.Command {
manCmd := &core.Command{
Command: &cobra.Command{
Use: "man",
Aliases: []string{"manpages"},
Short: "Generate manpages for ionosctl",
Long: `WARNING: This command is only supported on Linux.
The 'man' command allows you to generate manpages for ionosctl in a given directory. By default, the manpages will be compressed using gzip, but you can skip this step by using the '--skip-compression' flag.
In order to install the manpages, there are a few steps you need to follow:
- Decide where you would like to install the manpages. You can check which directories are available to you by running 'manpath'. If you want to install the manpages to a directory that is not listed, you can add a new entry to '~/.manpath' (see 'man 5 manpath' for how to do it). The directory must contain subdirectories for each section (e.g. 'man1', 'man5', etc.).
- Copy the manpages to the installation directory, in the 'man1' section.
- Run 'sudo mandb' to update the 'man' internal database.
After following these steps, you should be able to use 'man ionosctl' to access the manpages.`,
TraverseChildren: true,
Example: `ionosctl man --target-dir /tmp/ionosctl-man`,
PreRunE: func(cmd *cobra.Command, args []string) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("manpages generation is only supported on Linux")
}

targetDir, _ := cmd.Flags().GetString(constants.FlagTargetDir)
if !filepath.IsAbs(targetDir) {
return fmt.Errorf("target-dir must be an absolute path")
}

return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
targetDir, _ := cmd.Flags().GetString(constants.FlagTargetDir)
skipCompression, _ := cmd.Flags().GetBool(constants.FlagSkipCompression)

_, _ = fmt.Fprintf(cmd.OutOrStdout(), jsontabwriter.GenerateVerboseOutput("Checking if target directory for generation already exists"))
if err := handleExistingManpagesTargetDir(cmd, targetDir); err != nil {
return err
}

if err := os.MkdirAll(targetDir, 0700); err != nil {
return fmt.Errorf("error creating target directory %s: %w", targetDir, err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), jsontabwriter.GenerateVerboseOutput("Generating manpages"))
if err := doc.GenManTree(cmd.Root(), nil, targetDir); err != nil {
return fmt.Errorf("error generating manpages: %v", err)
}

if skipCompression {
_, _ = fmt.Fprintf(cmd.OutOrStdout(), jsontabwriter.GenerateLogOutput("Manpages successfully generated."))
return nil
}

if err := compressManpages(targetDir); err != nil {
return fmt.Errorf("error compressing manpages: %v", err)
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), jsontabwriter.GenerateLogOutput("Manpages successfully generated and compressed."))
return nil
},
},
}

manCmd.Command.Flags().String(constants.FlagTargetDir, "/tmp/ionosctl-man", "Target directory where manpages will be generated. Must be an absolute path")
manCmd.Command.Flags().Bool(constants.FlagSkipCompression, false, "Skip compressing manpages with gzip, just generate them")

return manCmd
}

func handleExistingManpagesTargetDir(c *cobra.Command, targetDir string) error {
if _, err := os.Stat(targetDir); os.IsNotExist(err) {
return nil
}

if !confirm.FAsk(
c.InOrStdin(),
fmt.Sprintf("Target directory %s already exists. Do you want to replace it", targetDir),
viper.GetBool(constants.ArgForce),
) {
return fmt.Errorf(confirm.UserDenied)
}

if err := os.RemoveAll(targetDir); err != nil {
return fmt.Errorf("error deleting target directory %s: %w", targetDir, err)
}

return nil
}

func compressManpages(genDir string) error {
files, err := os.ReadDir(genDir)
if err != nil {
return fmt.Errorf("error opening manpages target directory %s: %v", genDir, err)
}

for _, file := range files {
uncompressedFilePath := fmt.Sprintf("%s/%s", genDir, file.Name())
fileContent, err := os.ReadFile(uncompressedFilePath)
if err != nil {
return fmt.Errorf("error reading uncompressed manpage file %s: %v", file.Name(), err)
}

compressedFilePath := fmt.Sprintf("%s.gz", uncompressedFilePath)
if err = gzipManFile(fileContent, compressedFilePath); err != nil {
return fmt.Errorf("error compressing manpage file: %v", err)
}

if err = os.Remove(uncompressedFilePath); err != nil {
return fmt.Errorf("error removing uncompressed manpage file %s: %v", file.Name(), err)
}
}

return nil
}

func gzipManFile(fileContent []byte, newFileName string) error {
gzipFile, err := os.OpenFile(newFileName, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return fmt.Errorf("error creating gzipped manpage file %s: %v", newFileName, err)
}
defer gzipFile.Close()

gzipWriter := gzip.NewWriter(gzipFile)
_, err = gzipWriter.Write(fileContent)
if err != nil {
return fmt.Errorf("error writing to gzipped manpage file %s: %v", newFileName, err)
}
defer gzipWriter.Close()

return nil
}
37 changes: 20 additions & 17 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@ import (
"os"
"strings"

"github.com/ionos-cloud/ionosctl/v6/commands/cfg"
"github.com/ionos-cloud/ionosctl/v6/commands/dns"
vm_autoscaling "github.com/ionos-cloud/ionosctl/v6/commands/vm-autoscaling"
"github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter"
"github.com/ionos-cloud/ionosctl/v6/internal/version"

certificates "github.com/ionos-cloud/ionosctl/v6/commands/certmanager"
"github.com/ionos-cloud/ionosctl/v6/commands/cfg"
cloudapiv6 "github.com/ionos-cloud/ionosctl/v6/commands/cloudapi-v6"
container_registry "github.com/ionos-cloud/ionosctl/v6/commands/container-registry"
"github.com/ionos-cloud/ionosctl/v6/commands/dataplatform"
"github.com/ionos-cloud/ionosctl/v6/commands/dbaas"
"github.com/ionos-cloud/ionosctl/v6/commands/dns"
logging_service "github.com/ionos-cloud/ionosctl/v6/commands/logging-service"
"github.com/ionos-cloud/ionosctl/v6/commands/token"
vm_autoscaling "github.com/ionos-cloud/ionosctl/v6/commands/vm-autoscaling"
"github.com/ionos-cloud/ionosctl/v6/internal/config"
"github.com/ionos-cloud/ionosctl/v6/internal/constants"
"github.com/ionos-cloud/ionosctl/v6/internal/core"
"github.com/ionos-cloud/ionosctl/v6/internal/printer/jsontabwriter"
"github.com/ionos-cloud/ionosctl/v6/internal/version"
"github.com/mitchellh/go-homedir"
"github.com/spf13/cobra"
"github.com/spf13/viper"
Expand Down Expand Up @@ -160,6 +159,8 @@ func initConfig() {
func addCommands() {
rootCmd.AddCommand(Shell())
rootCmd.AddCommand(VersionCmd())
rootCmd.AddCommand(Man())

// cfg
rootCmd.AddCommand(cfg.ConfigCmd())
// Config namespace commands are also available via the root command, but are hidden
Expand Down Expand Up @@ -287,14 +288,16 @@ EXAMPLES:
Use "{{.CommandPath}} [command] --help" for more information about a command.{{print "\n"}}{{end}}`
)

var helpTemplate = strings.Join([]string{
seeAlso,
additionalHelpTopics,
globalFlags,
localFlags,
aliases,
examples,
usage,
availableCommands,
moreInfo,
}, "")
var helpTemplate = strings.Join(
[]string{
seeAlso,
additionalHelpTopics,
globalFlags,
localFlags,
aliases,
examples,
usage,
availableCommands,
moreInfo,
}, "",
)
53 changes: 53 additions & 0 deletions docs/subcommands/CLI Setup/man.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
description: "Generate manpages for ionosctl"
---

# Man

## Usage

```text
ionosctl man [flags]
```

## Aliases

For `man` command:

```text
[manpages]
```

## Description

WARNING: This command is only supported on Linux.

The 'man' command allows you to generate manpages for ionosctl in a given directory. By default, the manpages will be compressed using gzip, but you can skip this step by using the '--skip-compression' flag.
In order to install the manpages, there are a few steps you need to follow:
- Decide where you would like to install the manpages. You can check which directories are available to you by running 'manpath'. If you want to install the manpages to a directory that is not listed, you can add a new entry to '~/.manpath' (see 'man 5 manpath' for how to do it). The directory must contain subdirectories for each section (e.g. 'man1', 'man5', etc.).
- Copy the manpages to the installation directory, in the 'man1' section.
- Run 'sudo mandb' to update the 'man' internal database.

After following these steps, you should be able to use 'man ionosctl' to access the manpages.

## Options

```text
-u, --api-url string Override default host url (default "https://api.ionos.com")
-c, --config string Configuration file used for authentication (default "$XDG_CONFIG_HOME/ionosctl/config.json")
-f, --force Force command to execute without user input
-h, --help Print usage
--no-headers Don't print table headers when table output is used
-o, --output string Desired output format [text|json|api-json] (default "text")
-q, --quiet Quiet output
--skip-compression Skip compressing manpages with gzip, just generate them
--target-dir string Target directory where manpages will be generated. Must be an absolute path (default "/tmp/ionosctl-man")
-v, --verbose Print step-by-step process when running command
```

## Examples

```text
ionosctl man --target-dir /tmp/ionosctl-man
```

30 changes: 15 additions & 15 deletions docs/summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* [location](subcommands%2FCLI%20Setup%2Flocation.md)
* [login](subcommands%2FCLI%20Setup%2Flogin.md)
* [logout](subcommands%2FCLI%20Setup%2Flogout.md)
* [man](subcommands%2FCLI%20Setup%2Fman.md)
* [version](subcommands%2FCLI%20Setup%2Fversion.md)
* [whoami](subcommands%2FCLI%20Setup%2Fwhoami.md)
* Certificate Manager
Expand Down Expand Up @@ -128,21 +129,6 @@
* [list](subcommands%2FCompute%20Engine%2Flocation%2Fcpu%2Flist.md)
* [get](subcommands%2FCompute%20Engine%2Flocation%2Fget.md)
* [list](subcommands%2FCompute%20Engine%2Flocation%2Flist.md)
* logging
* service
* logs
* [add](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Flogs%2Fadd.md)
* [get](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Flogs%2Fget.md)
* [list](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Flogs%2Flist.md)
* [remove](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Flogs%2Fremove.md)
* [update](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Flogs%2Fupdate.md)
* pipeline
* [create](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Fcreate.md)
* [delete](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Fdelete.md)
* [get](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Fget.md)
* [key](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Fkey.md)
* [list](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Flist.md)
* [update](subcommands%2FCompute%20Engine%2Flogging%2Fservice%2Fpipeline%2Fupdate.md)
* nic
* [create](subcommands%2FCompute%20Engine%2Fnic%2Fcreate.md)
* [delete](subcommands%2FCompute%20Engine%2Fnic%2Fdelete.md)
Expand Down Expand Up @@ -346,6 +332,20 @@
* [list](subcommands%2FDatabase-as-a-Service%2Fpostgres%2Fversion%2Flist.md)
* Interactive Shell
* [shell](subcommands%2FInteractive%20Shell%2Fshell.md)
* Logging Service
* logs
* [add](subcommands%2FLogging-Service%2Flogs%2Fadd.md)
* [get](subcommands%2FLogging-Service%2Flogs%2Fget.md)
* [list](subcommands%2FLogging-Service%2Flogs%2Flist.md)
* [remove](subcommands%2FLogging-Service%2Flogs%2Fremove.md)
* [update](subcommands%2FLogging-Service%2Flogs%2Fupdate.md)
* pipeline
* [create](subcommands%2FLogging-Service%2Fpipeline%2Fcreate.md)
* [delete](subcommands%2FLogging-Service%2Fpipeline%2Fdelete.md)
* [get](subcommands%2FLogging-Service%2Fpipeline%2Fget.md)
* [key](subcommands%2FLogging-Service%2Fpipeline%2Fkey.md)
* [list](subcommands%2FLogging-Service%2Fpipeline%2Flist.md)
* [update](subcommands%2FLogging-Service%2Fpipeline%2Fupdate.md)
* Managed Backup
* [create](subcommands%2FManaged-Backup%2Fcreate.md)
* [delete](subcommands%2FManaged-Backup%2Fdelete.md)
Expand Down
8 changes: 6 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/kardianos/ftps v1.0.3
github.com/mitchellh/go-homedir v1.1.0
github.com/mmatczuk/anyflag v0.0.0-20230209112147-9567d4cab866
github.com/spf13/cobra v1.8.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.16.0
github.com/stretchr/testify v1.8.4
Expand All @@ -43,7 +43,11 @@ require (
github.com/ionos-cloud/sdk-go-dbaas-mariadb v1.0.0
)

require github.com/pkg/term v1.2.0-beta.2 // indirect
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/pkg/term v1.2.0-beta.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
)

// `ionosctl shell` requirements
require (
Expand Down
8 changes: 5 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down Expand Up @@ -400,6 +401,7 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
Expand All @@ -418,8 +420,8 @@ github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
Expand Down
6 changes: 6 additions & 0 deletions internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ const (
FlagSkipVerify = "skip-verify"
)

// Manpages
const (
FlagTargetDir = "target-dir"
FlagSkipCompression = "skip-compression"
)

// Resource info
const (
DatacenterId = "Datacenter ID: %v"
Expand Down
Loading

0 comments on commit 67b67fd

Please sign in to comment.