8000 Added new features and docs for Dex2 standard by adorfman · Pull Request #2 · symkat/dex · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Added new features and docs for Dex2 standard #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,118 @@ prod : Manage the production cluster.
run-playbook : Run the ansible playbook on the development machine.
edit-vault : Edit the vault file.
```
### Home Directory DexFile

You can keep a DexFile in your home directory to store global commands you might want to use outside a project directory. To use this DexFile just set `~~` as the first parameter in your command list.

### Config File Version 2

`dex` now has a new configuration format. The existing format is still supported and will function the same, but using this new format adds some new options and features that allow you to run more dynamic commands.

```YAML
version: 2
vars:
root_var: 'I can be used in every block'
some_list:
- 'this'
- 'that'
work_dir:
from-command: pwd | tr -d '\n'

blocks:
- name: var-example
desc: An Example block command with global and block variables.
vars:
some_string: 'for this block only'
env_var:
from-env: SECOND_CMD
default: 0
commands:
- exec: echo 'Global var work_dir: [% work_dir %], block variable [% some_string %] '
- exec: echo 'SECOND_CMD is set'
condition: [%env_var%] -eq 1
- name: loop-example
desc: An Example block command that looks over a list var.
commands:
- exec: echo 'repeating command with variable [% var %]
for-vars: some_list
```

The root `vars` attribute defines variables that can be used in any block by enclosing the name of the variable
within `[%` and `%]`. These variables can be a string, number a list containing a combination of either.

```YAML
vars:
string_var: 'I can be used in every block'
number:var: 23423
list_var:
- 'foo'
- 'bar'
- 34
```

You can also configure variables to be initialized from the output of an external command or by referencing an environment variable.

```YAML
vars:
perl5_version:
from-command: "perl -MConfig -e 'print $Config{version}'"
default: 'command failed'
perl5_lib:
from-env: PERL5LIB
default: 'NO PERL5LIB SET'

```

The `from-command` attribute will execute the set command and, assuming the command exits with a value of 0, assign its' STDOUT to the value of the variable. If the command returns multiple lines the variable will become a list containing
each line. If the command exits with a non-zero value then the variable will be assigned the `default` attribute value
or remain undefined if no 'default' attribute is provided.

`from-env` will check for a matching environment variable and if found will assign that value to the variable. When the environment variable is not defined the 'default' attribute value is used.

`blocks` is similar to the root list in the Standard Format. It defines a list of named blocks of commands and nestable sub blocks of commands to run.

```YAML
blocks:
- name: block-example
desc: An Example block.
vars:
local_var: 'for this block only'
commands:
- diag: '[%local_var%] execute update'
- exec: /bin/uptime
```

Within each block you can define `vars` with the same options the root `vars` attribute, but these variables will only be available for commands in that block.

The `commands` attribute replaces the `shell` attribute and lets you define three kinds of commands.

* `diag` - This command is an alias for echo and will print the string template to the terminal.

* `dir` - Sets the working directory for commands executed after this.

* `exec` - A command to execute.

The following configuration attributes are also available for each command.

* `condition` - Takes a condition in the same format as the *test* command. If the condition returns false the command
will be skipped.

* `for-vars` - Can be a list or the name of variable that contains a list. The command will be executed for each element of the list. The value and index for each element in the list will be available as the `var` and `index` variables.

```YAML
blocks:
- name: for-vars-example
desc: An Example block.
vars:
local_list:
- 1
- 2
- 3
commands:
- diag: 'value [%var%] at index [%index%]'
for-vars: local_list
```

## License

Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ go 1.21.0
toolchain go1.23.5

require github.com/goccy/go-yaml v1.15.16

require github.com/stretchr/testify v1.10.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goccy/go-yaml v1.15.16 h1:PMTVcGI9uNPIn7KLs0H7KC1rE+51yPl5YNh4i8rGuRA=
github.com/goccy/go-yaml v1.15.16/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
205 changes: 85 additions & 120 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,137 +1,102 @@
package main

import (
"fmt"
"os"
"os/exec"
"strings"
"errors"
"github.com/goccy/go-yaml"
"fmt"
"io"
"os"
"path/filepath"

v1 "dex/v1"
v2 "dex/v2"
)

// Paths to search for dex files.
func config_files () ( []string ) {
return []string{ "dex.yaml", "dex.yml", ".dex.yaml", ".dex.yml" }
}

/* Struct to turn the YAML file into */
type DexFile []struct {
Name string `yaml:"name"`
Desc string `yaml:"desc"`
Commands []string `yaml:"shell"`
Children DexFile `yaml:"children"`
}
var configFileLocations = []string{"dex.yaml", "dex.yml", ".dex.yaml", ".dex.yml"}

/* Main function:
1. Load the config file, throw an error and exit if there is no config file.
2. Turn the YAML structure from the config file into a DexFile struct.
3. If there was no commands to run, display the menu of commands the DexFile knows about.
4. If there was a command to run, find it and run it. If it's invalid, say so and display the menu.
/*
1. Try to locate a dex file, throw an error and exit if there is no config file.
2. Load the content of the dex file
3. Attempt to parse the dex file as v1 and then v2 YAML.
*/
func main() {

/* Find the name of the dex file we're using. */
filename, err := find_config_file(); if err != nil {
fmt.Fprintln(os.Stderr, err )
os.Exit(1)
}

/* Load the contents of the dex file */
data, err := os.ReadFile(filename); if err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("Could not read data from config file (%v): %w", filename, err) )
os.Exit(1)
}

/* Get the YAML structure from the contents of the dex file */
var dex_file DexFile
if err := yaml.Unmarshal([]byte(data), &dex_file); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("Could not parse YAML from file (%v): %w", filename, err))
os.Exit(1)
}

/* No commands asked for: show menu and exit */
if ( len(os.Args) == 1 ) {
display_menu( dex_file, 0, false )
os.Exit(0)
}

/* No commands were found from the arguments the user passed: show error, menu and exit */
commands, err := resolve_cmd_to_codeblock( dex_file, os.Args[1:] ); if err != nil {
fmt. 6D4E Fprintf(os.Stderr, "Error: No commands were found at %v\n\nSee the menu:\n", os.Args[1:] )
display_menu( dex_file, 0, true )
os.Exit(1)
}

/* Found commands: run them */
run_commands( commands )

/* Find the name of the dex file we're using. */
if filename, err := findConfigFile(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
/* Load the raw yaml data */
} else if dexData, err := loadDexFile(filename); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
/* Attempt parsing as v1 */
} else if dexFile, err := v1.ParseConfig(dexData); err == nil {
v1.Run(dexFile, os.Args)
/* Attempt parsing as v2 */
} else if dexFile, err := v2.ParseConfig(dexData); err == nil {
v2.Run(dexFile, os.Args)
/* failure */
} else {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

/* Search through the config_files array and return the first
dex file that exists.
*/
func find_config_file() ( filename string, err error ) {
config_files := config_files()

for _, filename := range config_files {
if _, err := os.Stat(filename); err == nil {
return filename, nil
}
}
func loadDexFile(filename string) ([]byte, error) {

return "", errors.New(fmt.Sprintf("No dex file was found. Searched %v", config_files))
if fileContent, err := os.Open(filename); err != nil {
return []byte{}, fmt.Errorf("yamlFile.Get err #%s", err)
} else if dexData, err := io.ReadAll(fileContent); err != nil {
return []byte{}, fmt.Errorf("yamlFile.Get err #%v ", err)
} else {
return dexData, err
}
}

/* Display the menu by recursively processing each element of the DexFile and
showing the name and description for the command. Children are indented with
4 spaces.
/*
Search through the config_files array and return the first
dex file that exists.
*/
func display_menu ( dex_file DexFile, indent int, to_stderr bool ) {
for _, elem := range dex_file {
if ( to_stderr == true ) {
fmt.Fprintf( os.Stderr, "%s%-24v: %v\n", strings.Repeat(" ", indent * 4 ), elem.Name, elem.Desc )
} else {
fmt.Printf( "%s%-24v: %v\n", strings.Repeat(" ", indent * 4 ), elem.Name, elem.Desc )
}

if ( len(elem.Children) >= 1 ) {
display_menu( elem.Children, indent + 1, to_stderr )
}
}
func findConfigFile() (string, error) {

/* If the first block parameter is "~~", this parameter
is removed and we check for dex files in the users
home directory instead of the current working directory
*/
useHome := false
if len(os.Args) > 1 && os.Args[1] == "~~" {
os.Args = os.Args[1:]
useHome = true
}

homeDir, err := os.UserHomeDir()
if useHome && err != nil {
fmt.Fprintf(os.Stderr, "error finding home directory: %v", err)
}

/* DEX_FILE environment variable takes priority. If ~~ was set
then we check for the DEX_FILE path relative to the users
home directory.
*/
if dexFileEnv := os.Getenv("DEX_FILE"); len(dexFileEnv) > 0 {
if useHome {
dexFileEnv = filepath.Join(homeDir, dexFileEnv)
}

if _, err := os.Stat(dexFileEnv); err == nil {
return dexFileEnv, nil
}
}

for _, filename := range configFileLocations {

if useHome {
filename = filepath.Join(homeDir, filename)
}

if _, err := os.Stat(filename); err == nil {
return filename, nil
}
}

return "", fmt.Errorf("no dex file was found. Searched %v", configFileLocations)
}

/* Find the list of commands to run for a given command path.

For example, cmd = [ 'foo', 'bar', 'blee' ] would check if 'foo' is a valid command,
then call itself with the child DexFile of foo, and cmd = ['bar', 'blee']. Then bar's
child DexFile would be called with [ 'blee' ] and return the list of commands.
*/
func resolve_cmd_to_codeblock ( dex_file DexFile, cmds []string ) ( []string, error ) {
for _, elem := range dex_file {
if ( elem.Name == cmds[0] ) {
if ( len(cmds) >= 2 ) {
return resolve_cmd_to_codeblock( elem.Children, cmds[1:] )
} else {
return elem.Commands, nil
}
}
}
return []string{}, errors.New("Could not find command.")
}

/* Given a list of commands, run them.
Uses bash so that quoting, shell expansion, etc works.
Writes the stdout/stderr as one would expect.
*/
func run_commands( commands []string ) {
for _, command := range commands {
cmd := exec.Command( "/bin/bash", "-c", command )
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err := cmd.Run(); if err != nil {
fmt.Fprintln(os.Stderr, "Failed to run command: ", err )
}
}
}

Loading
Loading
0