Make commands available
The previous step taught us how to handle dependencies, and now that we can execute our scripts and tools, we can also make them available as omni commands.
Making scripts and tools into omni commands allow anybody using the repository to simply call those in a standard, known way. It also makes those commands appear in omni help
while in the repository, making it easy to discover available commands.
There are two main ways to make commands accessible with omni: configuration commands and path commands.
Define configuration commands
Declare commands
One simple way to make commands available while in the repository is to define configuration commands. As the name hints, those commands are defined directly in the omni configuration at the root of the git repository.
The same commands we ran on the command line previously can be defined as follows in our configuration file:
up:
- python
- go
- custom:
name: "Compile sayhello go binary"
met?: |
bin="go/bin/gosayhello"
[ -f "$bin" ] && \
[ -x "$bin" ] && \
[ "$bin" -nt go/src/gosayhello.go ]
meet: "go build -o go/bin/gosayhello go/src/gosayhello.go"
unmeet: "rm go/bin/gosayhello"
commands:
pysayhello:
run: python python/pysayhello.py "$@"
gosayhello:
run: go/bin/gosayhello "$@"
This now enables anyone in the repository to run omni pysayhello xaf
and omni gosayhello xaf
to call our commands. Note the "$@"
at the end of the run
parameter, which is bash for "pass all the arguments received along", which will allow to pass the arguments received after omni pysayhello
and omni gosayhello
directly to our commands.
Add commands help
However, running the omni help
command would not help understanding what our commands are doing:
[...]
Configuration < .omni.yaml
gosayhello
pysayhello
We can improve that by giving a command description directly in the configuration, and categorizing our commands too:
up:
- python
- go
- custom:
name: "Compile sayhello go binary"
met?: |
bin="go/bin/gosayhello"
[ -f "$bin" ] && \
[ -x "$bin" ] && \
[ "$bin" -nt go/src/gosayhello.go ]
meet: "go build -o go/bin/gosayhello go/src/gosayhello.go"
unmeet: "rm go/bin/gosayhello"
commands:
pysayhello:
category: Hello world
desc: |
Says hello in Python
This is a simple script to say hello,
and it is written in Python!
run: python python/pysayhello.py "$@"
gosayhello:
category: Hello world
desc: |
Says hello in Go
This is a simple script to say hello,
and it is written in Go!
run: go/bin/gosayhello "$@"
Now let's take another look at the help:
[...]
Hello world < Configuration < .omni.yaml
gosayhello Says hello in Go
pysayhello Says hello in Python
This is better! But what about the individual help messages?
- omni help gosayhello
- omni help pysayhello
omni - omnipotent tool
Says hello in Go
This is a simple script to say hello, and it is written in Go!
Usage: omni gosayhello
Source: .omni.yaml
omni - omnipotent tool
Says hello in Python
This is a simple script to say hello, and it is written in Python!
Usage: omni pysayhello
Source: .omni.yaml
We can see the long help of our command appearing. However, something's missing: the usage syntax does not indicate we have to pass the name of someone as argument to personalize the message, nor that we can pass the --goodbye
option to say good bye instead of hello. We can fix this:
up:
- python
- go
- custom:
name: "Compile sayhello go binary"
met?: |
bin="go/bin/gosayhello"
[ -f "$bin" ] && \
[ -x "$bin" ] && \
[ "$bin" -nt go/src/gosayhello.go ]
meet: "go build -o go/bin/gosayhello go/src/gosayhello.go"
unmeet: "rm go/bin/gosayhello"
commands:
pysayhello:
category: Hello world
desc: |
Says hello in Python
This is a simple script to say hello,
and it is written in Python!
syntax:
arguments:
- name: Personalize the message for that target name
options:
- "--goodbye": Say good bye instead of hello
run: python python/pysayhello.py "$@"
gosayhello:
category: Hello world
desc: |
Says hello in Go
This is a simple script to say hello,
and it is written in Go!
syntax:
arguments:
- name: Personalize the message for that target name
options:
- "--goodbye": Say good bye instead of hello
run: go/bin/gosayhello "$@"
And now let's check again the commands help messages:
- omni help gosayhello
- omni help pysayhello
omni - omnipotent tool
Says hello in Go
This is a simple script to say hello, and it is written in Go!
Usage: omni gosayhello <name> [--goodbye]
name Personalize the message for that target name
--goodbye Say good bye instead of hello
Source: .omni.yaml
omni - omnipotent tool
Says hello in Python
This is a simple script to say hello, and it is written in Python!
Usage: omni pysayhello <name> [--goodbye]
name Personalize the message for that target name
--goodbye Say good bye instead of hello
Source: .omni.yaml
Our commands are now both easily accessible by an omni command while inside the repository, but also provide decent help to whomever wishes to use them.
Define path commands
Declare commands
Configuration commands cannot be part of the omnipath
, which would allow to make our commands usable from anywhere omni is accessible (without the help of omni scope
). If we want to make our commands part of the omnipath
, we can change them to be path commands instead.
We can remove the commands
key in the configuration and add a new path
key which will target directories where our executable files are located. Only executable files will be considered. In our case, this will look as follows:
up:
- python
- go
- custom:
name: "Compile sayhello go binary"
met?: |
bin="go/bin/gosayhello"
[ -f "$bin" ] && \
[ -x "$bin" ] && \
[ "$bin" -nt go/src/gosayhello.go ]
meet: "go build -o go/bin/gosayhello go/src/gosayhello.go"
unmeet: "rm go/bin/gosayhello"
path:
append:
- python
- go/bin
Do not forget to set python/pysayhello.py
as executable (chmod +x
on MacOS and Linux) or it won't be considered by omni
From the repository, we can now run omni pysayhello xaf
and omni gosayhello xaf
as path commands. Note that we didn't have to add anything else than the path to handle parameters, as omni will directly pass all leftover parameters after identifying the executable that needs to be run.
Add commands help
However, we are in the same pickle as before regarding the help messages shown by omni help
:
[...]
Uncategorized
gosayhello
pysayhello
We can fix that using metadata headers in text files, that omni will directly read from the file when trying to provide help for a command. Of course, since we are using a binary file for our Go tool, this method cannot directly work, so we need to create a new executable file, in bash for instance, that will wrap the call to our tool and provide readable metadata headers to omni.
These are the new go/wrapper/gosayhello.sh
and the modified python/pysayhello.py
files:
- Go tool
- Python tool
#!/usr/bin/env bash
#
# category: Hello world
# arg: name: Personalize the message for that name
# opt:--goodbye: Say good bye instead of hello
# help: Says hello in Go
# help:
# help: This is a simple script to say hello,
# help: and it is written in Go!
# This script's directory
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# Run the binary
exec "${DIR}/../bin/gosayhello" "$@"
#!/usr/bin/env python
#
# category: Hello world
# arg: name: Personalize the message for that name
# opt:--goodbye: Say good bye instead of hello
# help: Says hello in Python
# help:
# help: This is a simple script to say hello,
# help: and it is written in Python!
import argparse
parser = argparse.ArgumentParser(description='Say hello in Python')
parser.add_argument('name', help='name to greet')
parser.add_argument('--goodbye', action='store_true')
args = parser.parse_args()
greeting = "Goodbye" if args.goodbye else "Hello"
print("{}, {}!".format(greeting, args.name))
We will also need to update our path
variable to point to the new go/wrapper
directory:
up:
- python
- go
- custom:
name: "Compile sayhello go binary"
met?: |
bin="go/bin/gosayhello"
[ -f "$bin" ] && \
[ -x "$bin" ] && \
[ "$bin" -nt go/src/gosayhello.go ]
meet: "go build -o go/bin/gosayhello go/src/gosayhello.go"
unmeet: "rm go/bin/gosayhello"
path:
append:
- python
- go/wrapper
Do not forget to set go/wrapper/gosayhello.sh
as executable (chmod +x
on MacOS and Linux) or it won't be considered by omni
We can now see the commands metadata reflected in the omni help
message:
[...]
Hello world
gosayhello Says hello in Go
pysayhello Says hello in Python
As well as the individual help messages:
- omni help gosayhello
- omni help pysayhello
omni - omnipotent tool
Says hello in Go
This is a simple script to say hello, and it is written in Go!
Usage: omni gosayhello <name> [--goodbye]
name Personalize the message for that name
--goodbye Say good bye instead of hello
Source: go/wrapper/gosayhello.sh
omni - omnipotent tool
Says hello in Python
This is a simple script to say hello, and it is written in Python!
Usage: omni pysayhello <name> [--goodbye]
name Personalize the message for that name
--goodbye Say good bye instead of hello
Source: python/pysayhello.py
We can also observe that, using path commands, the help message reflects the path to our tools as Source
of the command.
Add commands to global omnipath
Finally, now that our commands are ready, if we want to make them available from anywhere in our system, we could simply edit our user configuration's path
configuration to contain the paths to our executable directories:
# [...]
path:
append:
# [...]
- /path/to/repo/python
- /path/to/repo/go/wrapper
# [...]
Bonus: have commands use omni help
As a last step to fully integrate our commands with omni, we might want omni gosayhello --help
and omni pysayhello --help
to be identical to omni help gosayhello
and omni help pysayhello
respectively. Since omni will directly pass arguments after a command to the command itself, it needs to be handled directly from our commands.
- Go tool (using shell wrapper)
- Go tool (directly in Go)
- Python tool
#!/usr/bin/env bash
#
# category: Hello world
# arg: name: Personalize the message for that name
# opt:--goodbye: Say good bye instead of hello
# help: Says hello in Go
# help:
# help: This is a simple script to say hello,
# help: and it is written in Go!
if [[ " $* " == *" --help "* ]] || [[ " $* " == *" -h "* ]]; then
omni help ${OMNI_SUBCOMMAND:-gosayhello}
exit 0
fi
# This script's directory
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
# Run the binary
exec "${DIR}/../bin/gosayhello" "$@"
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"strings"
)
func main() {
goodbye := flag.Bool("goodbye", false, "say goodbye instead of hello")
help := flag.Bool("help", false, "print usage")
shortHelp := flag.Bool("h", false, "print usage")
flag.Parse()
if *help || *shortHelp {
// Read OMNI_SUBCOMMAND from environment, and split it over spaces
// to get the subcommand name. If it's empty, then just assume
// the command is `gosayhello`
var helpCommand []string
omniSubcommand := os.Getenv("OMNI_SUBCOMMAND")
if omniSubcommand != "" {
helpCommand = strings.Split(omniSubcommand, " ")
} else {
helpCommand = []string{"gosayhello"}
}
// Prepend `help` to the subcommand name
helpCommand = append([]string{"help"}, helpCommand...)
// Then call omni to get the help for the subcommand, while
// passing through stdout and stderr
cmd := exec.Command("omni", helpCommand...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Error executing command: %s\n", err)
os.Exit(1)
}
os.Exit(0)
}
name := flag.Arg(0)
if name == "" {
panic("Who do you wanna greet?")
}
greeting := "Hello"
if *goodbye {
greeting = "Goodbye"
}
fmt.Printf("%s, %s!\n", greeting, name)
}
#!/usr/bin/env python
#
# category: Hello world
# arg: name: Personalize the message for that name
# opt:--goodbye: Say good bye instead of hello
# help: Says hello in Python
# help:
# help: This is a simple script to say hello,
# help: and it is written in Python!
import argparse
import os
parser = argparse.ArgumentParser(description='Say hello in Python', add_help=False)
parser.add_argument('-h', '--help', action='store_true', help='show help')
parser.add_argument('name', help='name to greet', nargs='?', default=None)
parser.add_argument('--goodbye', action='store_true')
args = parser.parse_args()
if args.help:
subcommand = (os.getenv('OMNI_SUBCOMMAND', '') or 'pysayhello').split(' ')
args = ['omni', 'help'] + subcommand
os.execvp(args[0], args)
exit(1)
if args.name is None:
parser.error("Who do you wanna greet?")
greeting = "Goodbye" if args.goodbye else "Hello"
print("{}, {}!".format(greeting, args.name))
For the Go tool, we have the choice to handle things in the wrapper, or directly in the Go source. There is no need to handle it in both places at once, if we consider that our tool will always be called through omni.