То, что я в данный момент глубоко погружаюсь в отбросы межпланетных файловых систем, протоколов Дхармы и межсетевого сжигания токенов, не может служить оправданием для некоторых старых добрых улучшений в навыках программирования и передовых методах.

Осваивая заржавевшие навыки программирования на Go, я случайно натолкнулся на интересную проблему, которая, как мне кажется, является действенным решением, - разработка с использованием Blueprint Driven Cli.

Зачем разрабатывать реализацию на основе командной строки?

На протяжении более десяти лет, когда я начинал писать код в большом проекте, я старался разработать приложение командной строки (CLI).

Использование первого подхода с использованием интерфейса командной строки независимо от того, является ли окончательный проект, например, службой Sync (REST) ​​или Async, помогает сосредоточить мышление и позволяет моделировать проблемное пространство без острых ощущений и излишеств, связанных с развертыванием и сложными цепочками вызовов.

Кроме того, это дает возможность идентифицировать программные модули, распределять и консолидировать работу по внедрению в команде, а в качестве БОЛЬШОГО бонуса создает тестовую оснастку, которая будет использоваться в будущем.

Если вы еще не пробовали этот подход, я рекомендую вам это сделать. Это наиболее похоже на чистый холст, который можно получить при кодировании.

Зачем создавать интерфейс командной строки, управляемый спецификацией?

В отличие от других моих проектов, BLKCTL (BLACKCHAIN ​​Control) является относительно большим и еще не «хорошо изученным» проектом. Под капотом будет довольно сложный код, по объему сопоставимый с хорошо известным Kubectl - Kubernetes Cli.

Когда вы работаете со службами на основе REST, в частности, вы учитесь ценить преимущества подходов к разработке на основе Blueprint (в частности, с OpenApi). К сожалению, в фреймворках Cli отсутствует «разработка на основе чертежей», с помощью которой можно было бы легко структурировать обсуждения проекта, генерировать заглушки кода и распространять среди команды разработчиков для сборки.

Разработка Cli на основе Blueprint может значительно повысить продуктивность, а также помочь лучше планировать и общаться внутри проектной группы.

На платформах CLI

В Python прекрасные фреймворки Click, а в Go - выдающиеся фреймворки spf13 / cobra были моими любимыми фреймворками Cli. Интересно, что основные участники (Армин Ронахер и Стив Франсиа) обоих фреймворков являются лидерами в соответствующих сообществах программистов.

На Java я только пробовал Spring Shell, но, судя по недавним исследованиям, проект Picocli выглядит особенно хорошо продуманным.

Для этого проекта мы будем использовать Cobra Cli Framework.

Фреймворк Cobra CLI де-факто является стандартом для фреймворков CLI в Go Lang. Он использовался во многих известных проектах, включая Docker, Kubectl и Hugo.

Помимо предоставления необходимых функций для сред CLI, таких как команды, подкоманды (вложенные команды), передача аргументов и флаги, он предлагает ряд замечательных функций зрелой среды. Такие как

  • Генерация документов
  • Автопомощь
  • Определение дополнительных, обязательных и комбинационных параметров
  • Создавать сценарии автозаполнения оболочки

Структура CLI

Обратите внимание, что следующий структурный анализ взят из Create kubectl Like CLI With GO And Cobra, авторства Mayank Bairagi. Это одно из лучших руководств по началу работы с фреймворком Cobra.

Давайте сначала взглянем на синтаксис некоторых популярных приложений командной строки.

kubectl create deployment webapp ---port=80
aws s3 mb s3://mybucket  --region=us-east-1
docker network create --subnet 203.0.113.0/24 iptastic

Структуру можно абстрагировать следующим образом

Cobra построена на структуре команд, аргументов и флагов. Команды представляют действия, аргументы - вещи, а флаги - модификаторы этих действий. Как заявляет Mayank, «Resource» необходимо заменить в Cobra на подкоманду. Т.е. Команда, которая является дочерней по отношению к другой команде.

Blueprint, который написан на YAML, копирует эту специфическую структуру Cli и, конечно, может быть расширен новыми командами по мере их добавления, как мы это делали бы с REST Service Blueprints.

План

В дрянном :-) чертеже на основе YAML ниже мы видим группу команд верхнего уровня с двумя командами. Вторая команда в данном случае является дочерней по отношению к первой (обратите внимание, что определение все еще является скелетным и может быть значительно улучшено)

---
commands:
  - command: 
      name: create
      description: some descriptions
      parent_comand: root
      flags: 
        - flag:
            name: some file
            shorthand: f
            default: na
            usage: yaml file to use
  - command: 
      name: card
      description: some descriptions
      parent_comand: create
      flags: 
        - flag:
            name: some file
            shorthand: f
            default: na
            usage: yaml file to use

Шаблон

Не намного больше, чем общий код команды котельной плиты.

package cmd
import (
 "fmt"
 "github.com/spf13/cobra"
)
// {{.Command.Name }}Cmd represents the {{.Command.Name }} command
var {{.Command.Name }}Cmd = &cobra.Command{
 Use:   "{{.Command.Name }}",
 Short: "{{.Command.Name }}",
 Long: `{{.Command.Description }}`,
 Run: func(cmd *cobra.Command, args []string) {
  fmt.Println("{{.Command.Name }} called")
 },
}
func init() {
    /** Here we add the command to the parent command
    avoiding some manual editing **/
 {{ .Command.ParentComand }}Cmd.AddCommand({{ .Command.Name }}Cmd)
}The Main App

Используемый конвейер генерации кода на данный момент очень минимален.

  1. Создайте Blueprint на основе YAML на основе приведенной выше структуры.
  2. Создайте подходящий шаблон кода (в данном случае только 1)
  3. Сценарий объединяет Blueprint и Template для создания отдельных файлов для каждой команды.

Основное приложение предсказуемо загружает Blueprint и генерирует код, используя шаблон после итерации по объектам, определенным в спецификации. Хотя есть и другие подходы, которые могут избежать использования определенного приложения (например, gomplete), от них отказываются в пользу приложения, которое также использует определенную структуру для правильности схемы, а также поддерживает другие расширения в будущем.

Кстати, код определения типа был создан с помощью этого онлайн-инструмента, который автоматически генерирует определения типа Go на основе предоставленного YAML: https://zhwt.github.io/yaml-to-go/.

package main
​
import (
    "log"
  "gopkg.in/yaml.v3"
  "html/template"
  "os"
  
)
  
type CommandList struct {
  Commands []struct {
    Command struct {
      Name         string `yaml:"name"`
      Description  string `yaml:"description"`
      ParentComand string `yaml:"parent_comand"`
      Flags        []struct {
        Flag struct {
          Name      string `yaml:"name"`
          Shorthand string `yaml:"shorthand"`
          Default   string `yaml:"default"`
          Usage     string `yaml:"usage"`
        } `yaml:"flag"`
      } `yaml:"flags"`
    } `yaml:"command"`
  } `yaml:"commands"`
}
​
func check(e error) {
    if e != nil {
    log.Fatalf("error: %v", e)
    panic(e)
    }
}
​
func generate(commandList CommandList) {  
​
  for _,command := range commandList.Commands  { 
​
      templateFile := "templates/cmd.tmpl"
      tmpl, err0 := template.ParseFiles(templateFile) 
      check(err0)
      
      f, erra := os.Create( "cmd/" + command.Command.Name + ".go")
      check(erra)
      err1 := tmpl.Execute(f, command)
      check(err1) 
​
      f.Close()    
  }
}
​
func main() {
​
  data, err0 := os.ReadFile("spec.yaml")
  check(err0)
​
  commandList := CommandList{}
​
  err1 := yaml.Unmarshal([]byte(data), &commandList)
  check(err1)
​
  generate(commandList)
​
}

Заключение и следующие шаги

Прототип помог мне понять преимущества подхода на основе Blueprint к разработке Cli, а также оценить усилия, необходимые для преобразования прототипа в полноценный инструмент.

Я пришел к выводу, что мы продолжим создание инструментов, поскольку это определенно поможет нам организовать усилия по разработке вокруг нашего приложения командной строки BLKCTL, намного менее ручного и сделав нас более продуктивными. В дальнейшем это существенно поможет улучшить документацию по приложению. С правильным типом «специфичных для конвейера расширений» мы также можем выборочно заменять команды и вводить управление версиями команд.

Поскольку это было короткое упражнение на пару часов от начала до конца, у меня не было времени исследовать, используются ли аналогичные инструменты для «прямого» создания клиентов. Вы можете получить клиентов из спецификаций Open API, но это кажется слишком специфичным для OpenAPI. Тем не менее направление, которое мы будем исследовать, - это применение спецификаций OpenAPI для создания клиентов.

Чтобы быть в курсе прогресса, подписывайтесь на меня на Medium. Также скоро будет добавлено публичное репозиторий Github.