github.com/symfony-cli/symfony-cli@v0.0.0-20240514161054-ece2df437dfa/local/platformsh/generator/commands.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"sort"
    10  	"strings"
    11  	"text/template"
    12  
    13  	"github.com/mitchellh/go-homedir"
    14  	"github.com/pkg/errors"
    15  	"github.com/symfony-cli/console"
    16  	"github.com/symfony-cli/symfony-cli/local/platformsh"
    17  )
    18  
    19  type application struct {
    20  	Namespaces []namespace
    21  	Commands   []command
    22  }
    23  
    24  type namespace struct {
    25  	ID       string
    26  	Commands []string
    27  }
    28  
    29  type command struct {
    30  	Name        string
    31  	Usage       []string
    32  	Description string
    33  	Help        string
    34  	Definition  definition
    35  	Hidden      bool
    36  	Aliases     []string
    37  }
    38  
    39  type definition struct {
    40  	Arguments map[string]argument
    41  	Options   map[string]option
    42  }
    43  
    44  type argument struct {
    45  }
    46  
    47  type option struct {
    48  	Shortcut string
    49  	Default  interface{}
    50  }
    51  
    52  var commandsTemplate = template.Must(template.New("output").Parse(`// Code generated by platformsh/generator/main.go
    53  // DO NOT EDIT
    54  
    55  /*
    56   * Copyright (c) 2021-present Fabien Potencier <fabien@symfony.com>
    57   *
    58   * This file is part of Symfony CLI project
    59   *
    60   * This program is free software: you can redistribute it and/or modify
    61   * it under the terms of the GNU Affero General Public License as
    62   * published by the Free Software Foundation, either version 3 of the
    63   * License, or (at your option) any later version.
    64   *
    65   * This program is distributed in the hope that it will be useful,
    66   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    67   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    68   * GNU Affero General Public License for more details.
    69   *
    70   * You should have received a copy of the GNU Affero General Public License
    71   * along with this program. If not, see <http://www.gnu.org/licenses/>.
    72   */
    73  
    74  package platformsh
    75  
    76  import (
    77  	"github.com/symfony-cli/console"
    78  )
    79  
    80  var Commands = []*console.Command{
    81  {{ .Definition -}}
    82  }
    83  `))
    84  
    85  func generateCommands() {
    86  	home, err := homedir.Dir()
    87  	if err != nil {
    88  		panic(err)
    89  	}
    90  	// as platform.sh and upsun have the same commands, we can use either one
    91  	cloudPath, err := platformsh.Install(home, platformsh.PlatformshBrand)
    92  	if err != nil {
    93  		panic(err.Error())
    94  	}
    95  	definitionAsString, err := parseCommands(cloudPath)
    96  	if err != nil {
    97  		panic(err.Error())
    98  	}
    99  	data := map[string]interface{}{
   100  		"Definition": definitionAsString,
   101  	}
   102  	var buf bytes.Buffer
   103  	if err := commandsTemplate.Execute(&buf, data); err != nil {
   104  		panic(err)
   105  	}
   106  	f, err := os.Create("local/platformsh/commands.go")
   107  	if err != nil {
   108  		panic(err)
   109  	}
   110  	f.Write(buf.Bytes())
   111  
   112  }
   113  
   114  func parseCommands(cloudPath string) (string, error) {
   115  	var buf bytes.Buffer
   116  	var bufErr bytes.Buffer
   117  	cmd := exec.Command(cloudPath, "list", "--format=json", "--all")
   118  	cmd.Stdout = &buf
   119  	cmd.Stderr = &bufErr
   120  	if err := cmd.Run(); err != nil {
   121  		return "", errors.Errorf("unable to list commands: %s\n%s\n%s", err, bufErr.String(), buf.String())
   122  	}
   123  
   124  	// Fix PHP types
   125  	cleanOutput := bytes.ReplaceAll(buf.Bytes(), []byte(`"arguments":[]`), []byte(`"arguments":{}`))
   126  
   127  	var definition application
   128  	if err := json.Unmarshal(cleanOutput, &definition); err != nil {
   129  		return "", err
   130  	}
   131  
   132  	excludedCommands := map[string]bool{
   133  		"list":              true,
   134  		"help":              true,
   135  		"self:stats":        true,
   136  		"decode":            true,
   137  		"environment:drush": true,
   138  		"project:init":      true,
   139  	}
   140  
   141  	excludedOptions := console.AnsiFlag.Names()
   142  	excludedOptions = append(excludedOptions, console.NoAnsiFlag.Names()...)
   143  	excludedOptions = append(excludedOptions, console.NoInteractionFlag.Names()...)
   144  	excludedOptions = append(excludedOptions, console.QuietFlag.Names()...)
   145  	excludedOptions = append(excludedOptions, console.LogLevelFlag.Names()...)
   146  	excludedOptions = append(excludedOptions, console.HelpFlag.Names()...)
   147  	excludedOptions = append(excludedOptions, console.VersionFlag.Names()...)
   148  
   149  	definitionAsString := ""
   150  	for _, command := range definition.Commands {
   151  		if strings.Contains(command.Description, "deprecated") || strings.Contains(command.Description, "DEPRECATED") {
   152  			continue
   153  		}
   154  		if _, ok := excludedCommands[command.Name]; ok {
   155  			continue
   156  		}
   157  		if strings.HasPrefix(command.Name, "local:") {
   158  			continue
   159  		}
   160  		if strings.HasPrefix(command.Name, "self:") {
   161  			command.Hidden = true
   162  		}
   163  		namespace := "cloud"
   164  	loop:
   165  		for _, n := range definition.Namespaces {
   166  			for _, name := range n.Commands {
   167  				if name == command.Name {
   168  					if n.ID != "_global" {
   169  						namespace += ":" + n.ID
   170  					}
   171  					break loop
   172  				}
   173  			}
   174  		}
   175  		name := strings.TrimPrefix("cloud:"+command.Name, namespace+":")
   176  		aliases := []string{}
   177  		if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") {
   178  			aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", command.Name))
   179  		}
   180  
   181  		cmdAliases, err := getCommandAliases(command.Name, cloudPath)
   182  		if err != nil {
   183  			return "", err
   184  		}
   185  		aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", command.Name))
   186  		for _, alias := range cmdAliases {
   187  			aliases = append(aliases, fmt.Sprintf("{Name: \"cloud:%s\"}", alias))
   188  			aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", alias))
   189  			if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") {
   190  				aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", alias))
   191  			}
   192  		}
   193  		if command.Name == "environment:push" {
   194  			aliases = append(aliases, "{Name: \"deploy\"}")
   195  			aliases = append(aliases, "{Name: \"cloud:deploy\"}")
   196  			aliases = append(aliases, "{Name: \"upsun:deploy\", Hidden: true}")
   197  		}
   198  		aliasesAsString := ""
   199  		if len(aliases) > 0 {
   200  			aliasesAsString += "\n\t\tAliases:  []*console.Alias{\n"
   201  			for _, alias := range aliases {
   202  				aliasesAsString += "\t\t\t" + alias + ",\n"
   203  			}
   204  			aliasesAsString += "\t\t},"
   205  		}
   206  		hide := ""
   207  		if command.Hidden {
   208  			hide = "\n\t\tHidden:   console.Hide,"
   209  		}
   210  
   211  		optionNames := make([]string, 0, len(command.Definition.Options))
   212  
   213  	optionsLoop:
   214  		for name := range command.Definition.Options {
   215  			if name == "yes" || name == "no" || name == "version" {
   216  				continue
   217  			}
   218  			for _, excludedOption := range excludedOptions {
   219  				if excludedOption == name {
   220  					continue optionsLoop
   221  				}
   222  			}
   223  
   224  			optionNames = append(optionNames, name)
   225  		}
   226  		sort.Strings(optionNames)
   227  		flags := []string{}
   228  		for _, name := range optionNames {
   229  			option := command.Definition.Options[name]
   230  			optionAliasesAsString := ""
   231  			if option.Shortcut != "" {
   232  				optionAliasesAsString += " Aliases: []string{\""
   233  				optionAliasesAsString += strings.Join(strings.Split(strings.ReplaceAll(option.Shortcut, "-", ""), "|"), "\", \"")
   234  				optionAliasesAsString += "\"},"
   235  			}
   236  			flagType := "String"
   237  			defaultValue := ""
   238  			if value, ok := option.Default.(bool); ok {
   239  				flagType = "Bool"
   240  				if value {
   241  					defaultValue = "true"
   242  				}
   243  			} else if value, ok := option.Default.(string); ok {
   244  				defaultValue = fmt.Sprintf("%#v", value)
   245  			}
   246  			defaultValueAsString := ""
   247  			if defaultValue != "" {
   248  				defaultValueAsString = fmt.Sprintf(" DefaultValue: %s,", defaultValue)
   249  			}
   250  			flags = append(flags, fmt.Sprintf(`&console.%sFlag{Name: "%s",%s%s}`, flagType, name, optionAliasesAsString, defaultValueAsString))
   251  		}
   252  		flagsAsString := ""
   253  		if len(flags) > 0 {
   254  			flagsAsString += "\n\t\tFlags:    []console.Flag{\n"
   255  			for _, flag := range flags {
   256  				flagsAsString += "\t\t\t" + flag + ",\n"
   257  			}
   258  			flagsAsString += "\t\t},"
   259  		}
   260  
   261  		command.Description = strings.ReplaceAll(command.Description, "Platform.sh", "Platform.sh/Upsun")
   262  		definitionAsString += fmt.Sprintf(`	{
   263  		Category: "%s",
   264  		Name:     "%s",%s
   265  		Usage:    %#v,%s%s
   266  	},
   267  `, namespace, name, aliasesAsString, command.Description, hide, flagsAsString)
   268  	}
   269  
   270  	return definitionAsString, nil
   271  }
   272  
   273  func getCommandAliases(name, cloudPath string) ([]string, error) {
   274  	var buf bytes.Buffer
   275  	var bufErr bytes.Buffer
   276  	c := exec.Command(cloudPath, name, "--help", "--format=json")
   277  	c.Stdout = &buf
   278  	c.Stderr = &bufErr
   279  	if err := c.Run(); err != nil {
   280  		// Can currently happen for commands implemented in Go upstream (like app:config-validate)
   281  		// FIXME: to be removed once upstream implements --help --format=json for all commands
   282  		return []string{}, nil
   283  		//return nil, errors.Errorf("unable to get definition for command %s: %s\n%s\n%s", name, err, bufErr.String(), buf.String())
   284  	}
   285  	var cmd command
   286  	if err := json.Unmarshal(buf.Bytes(), &cmd); err != nil {
   287  		return nil, err
   288  	}
   289  	return cmd.Aliases, nil
   290  }