Привет, Хабр!

Сегодня мы рассмотрим, как с помощью замечательной библиотеки Cobra превратить обычный Go-код в инструмент CLI. Cobra позволяет создавать интерфейсы командной строки.

Начнем с самого начала — установки и настройки проекта.

Установка

Первое, что нужно сделать — это установить саму библиотеку Cobra:

go get github.com/spf13/cobra/cobra

Далее нужно инициализировать новый проект. Есть командаcobra init, которая создаст все файлы и структуру директорий для проекта. Переходим в каталог, где будет располагаться проект, и выполняем:

cobra init --pkg-name ваше_имя_проекта

Команда подготовит базовую структуру CLI-приложения, создав каталог cmd и в нем начальный файл root.go, который будет являться точкой входа в приложение.

Теперь все готово для начала работы!

Создаем основную структуру CLI

В корне CLI-проекта на Go, основной файл, который служит точкой входа, это root.go. Этот файл находится в директории cmd/ и содержит определение корневой команды приложения. Пример содержимого root.go:

package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
    Use:   "[your-cli-app-name]",
    Short: "A brief description of your CLI application",
    Long: `A longer description that explains your CLI application in detail, 
    including available commands and their usage.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Welcome to [your-cli-app-name]! Use --help for usage.")
    },
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func main() {
    Execute()
}

Код определяет базовую структуру и команду, которая выполняется по умолчанию, когда пользователи запускают ваше приложение без каких-либо подкоманд.

  • Use: как команда вызывается из командной строки.

  • Short: краткое описание команды, показываемое в справке.

  • Long: более детальное описание, которое также отображается в справке.

  • Run: функция, которая выполняется при вызове команды.

Функция Execute() вызывается в main.go и отвечает за выполнение корневой команды.

Чтобы расширить функциональность CLI-приложения, можно добавить новые команды с помощью команды cobra add <commandName>. Это создаст новый файл в директории cmd/, содержащий шаблон кода для новой команды. Например, добавление команды add создаст файл add.go с следующим содержимым:

package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
)

var addCmd = &cobra.Command{
    Use:   "add",
    Short: "Add numbers",
    Long:  `Adds two numbers and prints the result.`,
    Args:  cobra.ExactArgs(2),
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Adding numbers...")
        // логика добавления номеров
    },
}

func init() {
    rootCmd.AddCommand(addCmd)
}

Код определяет новую команду add, которая требует два аргумента и выполняет функцию, определенную в Run.

Для управления конфигурациями CLI-приложения можно юзать Viper. В root.go можно инициализировать Viper в функции initConfig:

func initConfig() {
    viper.AddConfigPath("path/to/your/config")
    viper.SetConfigName("config")
    if err := viper.ReadInConfig(); err != nil {
        fmt.Println("Can't read config:", err)
        os.Exit(1)
    }
}

Код загружает конфигурационный файл и делает его доступным в приложении.

Флаги и подкоманды

Флаги в Cobra бывают двух видов: локальные и постоянные. Локальные флаги применяются только к той команде, к которой они привязаны, в то время как постоянные флаги также наследуются всеми подкомандами этой команды.

Пример добавления локального флага:

import (
    "fmt"
    "github.com/spf13/cobra"
)

var cmd = &cobra.Command{
    Use: "example",
    Run: func(cmd *cobra.Command, args []string) {
        // логика
    },
}

func init() {
    cmd.Flags().StringP("flagname", "f", "default", "Description of the flag")
}

Флаг flagname доступен только для команды example.

Пример добавления постоянного флага:

func init() {
    cmd.PersistentFlags().StringP("persistflag", "p", "default", "Description of persistent flag")
}

Постоянный флаг persistflag будет доступен как для команды, к которой он привязан, так и для всех её подкоманд.

Подкоманды позволяют строить многоуровневые CLI-структуры, где каждая команда может иметь свои собственные подкоманды.

Пример:

var rootCmd = &cobra.Command{Use: "app"}
var subCmd = &cobra.Command{
    Use:   "sub",
    Short: "This is a subcommand",
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("Subcommand executed")
    },
}

func init() {
    rootCmd.AddCommand(subCmd)
}

В этом случае sub является подкомандой для app. Можно добавить сколько угодно подкоманд, создавая сложные иерархии команд.

Пример использования Cobra

Создадим утилиту для управления задачами, наподобие упрощенной версии todo приложения. Не будем реализовывать логику работы, чтобы не усложнять чтение, рассмотрим только то, что касается Cobraю

Сначала создаем основу приложения. Определим корневую команду и базовую структуру:

package main

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
    "github.com/spf13/viper"
)

var rootCmd = &cobra.Command{
    Use:   "tasker",
    Short: "Tasker is a CLI for managing your tasks",
    Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application.`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func main() {
    Execute()
}

Добавляем подкоманды для создания, просмотра и удаления задач:

func init() {
    rootCmd.AddCommand(addCmd)
    rootCmd.AddCommand(listCmd)
    rootCmd.AddCommand(deleteCmd)
}

var addCmd = &cobra.Command{
    Use:   "add [task]",
    Short: "Add a new task",
    Run: func(cmd *cobra.Command, args []string) {
        // логика добавления задачи
        fmt.Println("Added task:", args[0])
    },
}

var listCmd = &cobra.Command{
    Use:   "list",
    Short: "List all tasks",
    Run: func(cmd *cobra.Command, args []string) {
        // логика отображения всех задач
        fmt.Println("Listing all tasks")
    },
}

var deleteCmd = &cobra.Command{
    Use:   "delete [id]",
    Short: "Delete a task by its ID",
    Run: func(cmd *cobra.Command, args []string) {
        // логика удаления задачи
        fmt.Println("Deleted task with ID:", args[0])
    },
}

Добавим флаги для уточнения действий, например, для фильтрации списка задач:

func init() {
    listCmd.Flags().BoolP("completed", "c", false, "Show only completed tasks")
}

Используем Viper:

func initConfig() {
    viper.AddConfigPath(".")
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")

    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
    }
}

func init() {
    cobra.OnInitialize(initConfig)
}

Подробнее о Cobra можно узнать здесь.

Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.