Hello Guys,
I’ve been using Mage for more than a month now, for some serious work and I can say it works brilliantly.
In the past I was using a lot of Make and the only thing I'm missing from Make in Mage is the ability to pass arguments to targets.
For example, in Makefile I can do something like this:
ARG=
target:
action $(ARG)
and then call it from CLI like:
make target ARG=foo
I know I can use environmental variables for it and it works, but in my opinion it's a bit counter-intuitive when you want to build a nice tool using Mage which will allow user to work in natural fashion by first typing a command name and then feeding it with some arguments/params.
Clearly I'm not the only one who is missing this feature (according to these posts):
https://github.com/magefile/mage/issues/24
https://github.com/magefile/mage/issues/24#issuecomment-424758830
Over the weekend I came up with my own solution to this problem. At first I was trying simply to obtain args using os.Args at target "scope" but it didn't work, as all of passed args were removed, and also immediately I was given errors about mismatched target names (because args passed to targer were interpreted as targets names obviously).
After some reverse engineering I found out that at Mage code-level all args are delivered by the os nicely, but they get removed during Mage initial processing.
So in my solution I modified the template.go file in order to prevent arguments removal and also to prevent error about "unknown targets" so that passed args are no longer interpreted as targets names.
Each processed argument gets marked with prefixes based on the name of target which was preceding it. Because targets args are coming after targets names, we can know which args belong to which target.
Also, my code is dealing correctly with target aliases; aliases are resolved to full original target name and stored as such in a prefix instead. Those names have to later match with caller function detection (more about this later).
The following flags format are supported:
// -- : stop processing any further arguments
// -f --f -flag --flag : boolean flags only
// -f=value --f=value -flag=value --flag=value : boolean, integer, string flags
// f=value flag=value : boolean, integer, string flags (read above below note)
Those formats are matched using below RegEx:
(^\w+\=\S*)|(^-{1,2}\w+\=\S*)|(^-{1,2}\w+$)
(it can be validated here: https://regex101.com/)
Given that we have three targets:
new
build
run
General format:
mage target1 [FLAGS] target2 [FLAGS] ...
examples:
mage target1 --boolean-flag --long-flag=abc -r=30 -v target2 --some-other-value="5a23d6eb3bdbcd6" -vv var=123
mage new
mage new --name=banana --input='/some/path/to/file.ext' --enabled -r
mage new --name=banana --input='/some/path/to/file.ext' --enabled -r build stage=dev -n=3 run
There is also support provided for --
which is causing Mage to stop processing any further CLI arguments or running any further targets:
in the following example, only new
and build
target will be called, and build
will only get stage
argument, but not n
argument for example.
(Note: please notice --
after stage=dev
)
mage new --name=banana --input='/some/path/to/file.ext' --enabled -r build stage=dev -- -n=3 run
Some examples:
-f
-1
-flag
--f
--1
--flag
-a="/path/to/file" --nextFlag
-1="/path/to/file"
-flag="/path/to/file"
--a="/path/to/file"
--1="/path/to/file"
--flag="/path/to/file"
-f=
-1=
-f=3 --nextFlag
-1=abc
-flag=cheese
--f=
--f=300millions
--1=awesomeness
--flag="/path/to/file"
var=
var=some
other_var=something
These cases are ignored by this RegEx:
-
--
target
another_target
---b
---banana
Processing args at example "New" target
In the below snippet the crucial part of the way it works
is this function call:
mg.GetArgsForTarget()
This function is checking callstack and finds out the name of the caller function,
then it's checking all CLI args available at os.Args and finds those which contains caller function
name in it (e.g.: main.new:
). Then it returns the list of all those found args as strings array
which can then be processed by flags package as follows:
mage new --name=banana --input='/some/path/to/banana.jpg' --enabled -r=30
type NewOptions struct {
Name string
Input string
Enabled bool
R int
}
func New() {
var defaultOptions NewOptions
options := defaultOptions
//----------------------------
// Here we call our args filtering
// function, which will give us
// only arguments applying to "New" target
//----------------------------
args := mg.GetArgsForTarget()
//----------------------------
// process args using flags package, and do whatever you want
fs := flag.NewFlagSet("mytool new", flag.ExitOnError)
fs.StringVar(&options.Name, "name", "models", "resource name")
fs.StringVar(&options.Name, "n", "models", "resource name")
fs.StringVar(&options.Input, "input", "", "resource path")
fs.StringVar(&options.Input, "i", "", "resource path")
fs.BoolVar(&options.Enabled, "enabled", false, "something is enabled")
fs.BoolVar(&options.Enabled, "e", false, "something is enabled")
fs.IntVar(&options.R, "r", 0, "radius")
fs.Parse(args)
//----------------------------
// Deal with any parsed args
// as object nicely here
//----------------------------
fmt.Println("Created New Project!")
fmt.Println("Name: ", options.Name)
fmt.Println("Input: ", options.Input)
ps.
I was testing this code a lot, and it's working for me.
Let me know what you think about this change and I'll write tests for it :)