cmd/root.go
author convert-repo
Fri, 05 May 2017 21:55:09 +0000
changeset 69 7217174c217a
parent 66 43d43c8b53aa
child 80 7f3e2577ed85
permissions -rw-r--r--
update tags

// Copyright © 2017 Mikael Berthe <mikael@lilotux.net>
//
// Licensed under the MIT license.
// Please see the LICENSE file is this directory.

package cmd

import (
	"fmt"
	"io/ioutil"
	"os"

	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"

	"github.com/McKael/madon"
	"github.com/McKael/madonctl/printer"
)

// AppName is the CLI application name
const AppName = "madonctl"

// AppWebsite is the application website URL
const AppWebsite = "https://github.com/McKael/madonctl"

// defaultConfigFile is the path to the default configuration file
const defaultConfigFile = "$HOME/.config/" + AppName + "/" + AppName + ".yaml"

// Madon API client
var gClient *madon.Client

// Options
var cfgFile string
var safeMode bool
var instanceURL, appID, appSecret string
var login, password, token string
var verbose bool
var outputFormat string
var outputTemplate, outputTemplateFile string
var colorMode string

// Shell completion functions
const shellComplFunc = `
__madonctl_visibility() {
	COMPREPLY=( direct private unlisted public )
}
__madonctl_output() {
	COMPREPLY=( plain json yaml template )
}
__madonctl_color() {
	COMPREPLY=( auto on off )
}
`

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
	Use:               AppName,
	Short:             "A CLI utility for Mastodon API",
	PersistentPreRunE: checkOutputFormat,
	Long: `madonctl is a CLI tool for the Mastodon REST API.

You can use a configuration file to store common options.
For example, create ` + defaultConfigFile + ` with the following
contents:

	---
	instance: "INSTANCE"
	login: "USERNAME"
	password: "USERPASSWORD"
	...

The simplest way to generate a configuration file is to use the 'config dump'
command.

(Configuration files in JSON are also accepted.)

If you want shell auto-completion (for bash or zsh), you can generate the
completion scripts with "madonctl completion $SHELL".
For example if you use bash:

	madonctl completion bash > _bash_madonctl
	source _bash_madonctl

Now you should have tab completion for subcommands and flags.

Note: Most examples assume the user's credentials are set in the configuration
file.
`,
	Example: `  madonctl instance
  madonctl toot "Hello, World"
  madonctl toot --visibility direct "@McKael Hello, You"
  madonctl toot --visibility private --spoiler CW "The answer was 42"
  madonctl post --file image.jpg Selfie
  madonctl --instance INSTANCE --login USERNAME --password PASS timeline
  madonctl accounts notifications --list --clear
  madonctl accounts blocked
  madonctl accounts search Gargron
  madonctl search --resolve https://mastodon.social/@Gargron
  madonctl accounts follow --remote Gargron@mastodon.social
  madonctl accounts --account-id 399 statuses
  madonctl status --status-id 416671 show
  madonctl status --status-id 416671 favourite
  madonctl status --status-id 416671 boost
  madonctl accounts show
  madonctl accounts show -o yaml
  madonctl accounts --account-id 1 followers --template '{{.acct}}{{"\n"}}'
  madonctl config whoami
  madonctl timeline :mastodon`,
	BashCompletionFunction: shellComplFunc,
}

// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
	if err := RootCmd.Execute(); err != nil {
		errPrint("Error: %s", err.Error())
		os.Exit(-1)
	}
}

func init() {
	cobra.OnInitialize(initConfig)

	// Global flags
	RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "",
		"config file (default is "+defaultConfigFile+")")
	RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Verbose mode")
	RootCmd.PersistentFlags().StringVarP(&instanceURL, "instance", "i", "", "Mastodon instance")
	RootCmd.PersistentFlags().StringVarP(&login, "login", "L", "", "Instance user login")
	RootCmd.PersistentFlags().StringVarP(&password, "password", "P", "", "Instance user password")
	RootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "User token")
	RootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "plain",
		"Output format (plain|json|yaml|template)")
	RootCmd.PersistentFlags().StringVar(&outputTemplate, "template", "",
		"Go template (for output=template)")
	RootCmd.PersistentFlags().StringVar(&outputTemplateFile, "template-file", "",
		"Go template file (for output=template)")
	RootCmd.PersistentFlags().StringVar(&colorMode, "color", "",
		"Color mode (auto|on|off; for output=template)")

	// Configuration file bindings
	viper.BindPFlag("output", RootCmd.PersistentFlags().Lookup("output"))
	viper.BindPFlag("verbose", RootCmd.PersistentFlags().Lookup("verbose"))
	// XXX viper.BindPFlag("apiKey", RootCmd.PersistentFlags().Lookup("api-key"))
	viper.BindPFlag("instance", RootCmd.PersistentFlags().Lookup("instance"))
	viper.BindPFlag("login", RootCmd.PersistentFlags().Lookup("login"))
	viper.BindPFlag("password", RootCmd.PersistentFlags().Lookup("password"))
	viper.BindPFlag("token", RootCmd.PersistentFlags().Lookup("token"))
	viper.BindPFlag("color", RootCmd.PersistentFlags().Lookup("color"))

	// Flag completion
	annotationOutput := make(map[string][]string)
	annotationOutput[cobra.BashCompCustom] = []string{"__madonctl_output"}
	annotationColor := make(map[string][]string)
	annotationColor[cobra.BashCompCustom] = []string{"__madonctl_color"}

	RootCmd.PersistentFlags().Lookup("output").Annotations = annotationOutput
	RootCmd.PersistentFlags().Lookup("color").Annotations = annotationColor
}

func checkOutputFormat(cmd *cobra.Command, args []string) error {
	of := viper.GetString("output")
	switch of {
	case "", "plain", "json", "yaml", "template":
		return nil // Accepted
	}
	return errors.Errorf("output format '%s' not supported", of)
}

// initConfig reads in config file and ENV variables if set.
func initConfig() {
	if cfgFile != "" { // enable ability to specify config file via flag
		viper.SetConfigFile(cfgFile)
	}

	viper.SetConfigName(AppName) // name of config file (without extension)
	viper.AddConfigPath("$HOME/.config/" + AppName)
	viper.AddConfigPath("$HOME/." + AppName)

	// Read in environment variables that match, with a prefix
	viper.SetEnvPrefix(AppName)
	viper.AutomaticEnv()

	// If a config file is found, read it in.
	err := viper.ReadInConfig()
	if err != nil {
		if cfgFile != "" {
			errPrint("Error: cannot read configuration file '%s': %v", cfgFile, err)
			os.Exit(-1)
		}
	} else if viper.GetBool("verbose") {
		errPrint("Using config file: %s", viper.ConfigFileUsed())
	}
}

// getOutputFormat return the requested output format, defaulting to "plain".
func getOutputFormat() string {
	of := viper.GetString("output")
	if of == "" {
		of = "plain"
	}
	// Override format if a template is provided
	if of == "plain" && (outputTemplate != "" || outputTemplateFile != "") {
		// If the format is plain and there is a template option,
		// set the format to "template".
		of = "template"
	}
	return of
}

// getPrinter returns a resource printer for the requested output format.
func getPrinter() (printer.ResourcePrinter, error) {
	var opt string
	of := getOutputFormat()

	// Initialize color mode
	switch viper.GetString("color") {
	case "on", "yes", "force":
		printer.ColorMode = 1
	case "off", "no":
		printer.ColorMode = 2
	}

	if of == "template" {
		opt = outputTemplate
		if outputTemplateFile != "" {
			tmpl, err := ioutil.ReadFile(outputTemplateFile)
			if err != nil {
				return nil, err
			}
			opt = string(tmpl)
		}
	}
	return printer.NewPrinter(of, opt)
}

func errPrint(format string, a ...interface{}) (n int, err error) {
	return fmt.Fprintf(os.Stderr, format+"\n", a...)
}