From 5f17453af9e6b97888411d0875d565328759512d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1el=20Mu=C3=B1iz?= Date: Sat, 8 Jun 2024 05:11:51 +0200 Subject: [PATCH] Documentation has been written --- Alfons.moon | 10 +++--- README.md | 25 ++++++++++++++ alfons.lua | 77 +++++++++++++++++------------------------- docs/api.md | 15 ++++++-- docs/autocompletion.md | 30 ++++++++++++++++ docs/documenting.md | 28 +++++++++++++++ docs/provide.md | 74 +++++++++++++++++++++++++++++++++++++--- 7 files changed, 200 insertions(+), 59 deletions(-) create mode 100644 docs/autocompletion.md create mode 100644 docs/documenting.md diff --git a/Alfons.moon b/Alfons.moon index ed7537a..3fc14f5 100644 --- a/Alfons.moon +++ b/Alfons.moon @@ -1,9 +1,9 @@ tasks: --- @task make Install a local version of a version - --- @argument make [v] Current version + --- @option make [v] Current version make: => sh "rockbuild -m --delete #{@v}" if @v --- @task release Create and upload a release of Alfons using %{magenta}`rockbuild`%{reset}. - --- @argument release [v] Current version + --- @option release [v] Current version release: => sh "rockbuild -m -t #{@v} u" if @v --- @task compile Compile all MoonScript files compile: => @@ -17,8 +17,8 @@ tasks: continue if (file\match "tasks") delete file --- @task pack Pack an Alfons build using amalg.lua - --- @argument pack [output o] Output file (Default: %{green}"alfons.lua"%{reset}) - --- @argument pack [entry s] Entry file (Default: %{green}"bin/alfons.lua"%{reset}) + --- @option pack [output o] Output file (Default: %{green}"alfons.lua"%{reset}) + --- @option pack [entry s] Entry file (Default: %{green}"bin/alfons.lua"%{reset}) pack: => show "Packing using amalg.lua" @o or= @output or "alfons.lua" @@ -33,7 +33,7 @@ tasks: tasks.pack! tasks.clean! --- @task test Run an Alfons test - --- @argument test [n] Name of the test to run + --- @option test [n] Name of the test to run test: => sh "moon test/#{@n or ''}.moon" -- dummy tasks hello: => print "hello!" diff --git a/README.md b/README.md index a9568f3..16601a8 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,16 @@ Alfons is a task runner to help you manage your project. It's inspired by the worst use cases of Make (this means using `make` instead of shell scripts), it will read an Alfonsfile, extract the exported functions and run the tasks in order. I would tell you that there is no real reason to use this thing, but it's becoming surprisingly useful, so actually try it out. +> [!TIP] +> Check out the 5.2 update! +> Shell autocompletions, help messages, Taskfile documentations and extra environment functions have been added. + ## Table of contents - [Alfons 5](#alfons-5) - [Table of contents](#table-of-contents) - [Changelog](#changelog) + - [5.2](#52) - [5](#5) - [4.4](#44) - [4.3](#43) @@ -37,6 +42,26 @@ Alfons is a task runner to help you manage your project. It's inspired by the wo ## Changelog +### 5.2 +- **5.2** (08.06.2024) Implemented Taskfile documentation, help messages and autocompletion + +- **Taskfile documentation.** You can now document your Taskfiles for automatic help messages and shell completion. + - Check [the documentation](docs/documenting.md) for more info. +- **Help messages.** You can now display a help message with `--help`, or even get help for a specific task with `--help [task]`. + - This help message can be automatically generated from the detected tasks in the Taskfile. + - It works best when you document your Taskfile, so you can add descriptions and options. +- **Shell autocompletion.** Shell autocompletion is now available in Zsh and Bash flavors. + - The Zsh flavor is by far the most complete, since Zsh's completion system is slightly more capable. + - Bash is only able to list tasks, and sometimes options or flags for those tasks. Use Zsh. + - Check [the documentation](docs/autocompletion.md) for more info and install instructions. +- **Additions to the environment.** + - `lines`: Split a string into lines + - `split`: Split any string by any pattern + - `sanitize`: Neutralizes pattern magic characters in strings + - `keys`: Get the keys of a table as an array + - `slice`: Creates a slice of an array + - `map`, `reduce`, `filter` do to arrays what you would expect + - `contains` has been rewritten ### 5 - **5.0.2** (23.01.2023) Rolled back test mode diff --git a/alfons.lua b/alfons.lua index c4910d9..8246e0c 100644 --- a/alfons.lua +++ b/alfons.lua @@ -711,7 +711,6 @@ do end local style style = require("ansikit.style").style -local inspect = require("inspect") local printerr printerr = function(t) return io.stderr:write(t .. "\n") @@ -735,30 +734,30 @@ parseDirective = function(directive) local description = table.concat((slice(parts, 2)), ' ') return "task", name, { description = description, - arguments = { } + options = { } } elseif "flag" == _exp_0 then local parts = split(rest, '%s+') local name, flag = parts[1], parts[2] return (name == '*' and "flag" or "task-flag"), name, flag - elseif "argument" == _exp_0 then + elseif "option" == _exp_0 then local parts = split(rest, '%s+') local task = parts[1] - local argument_names, argument_values, description = { }, { }, "" - local in_argument_names, in_argument_values, in_description = false, false, false + local option_names, option_values, description = { }, { }, "" + local in_option_names, in_option_values, in_description = false, false, false for _index_0 = 2, #parts do local part = parts[_index_0] local stripped_part = part:match("([^%[%]%<%>]+)") - if not in_argument_names and part:match("^%[") then - in_argument_names = true + if not in_option_names and part:match("^%[") then + in_option_names = true end - if not in_argument_values and part:match("^%<") then - in_argument_values = true + if not in_option_values and part:match("^%<") then + in_option_values = true end - if in_argument_names then - table.insert(argument_names, stripped_part) + if in_option_names then + table.insert(option_names, stripped_part) end - if in_argument_values then + if in_option_values then local part_object = { value = stripped_part } @@ -768,22 +767,22 @@ parseDirective = function(directive) optional = true } end - table.insert(argument_values, part_object) + table.insert(option_values, part_object) end if in_description then description = description .. (part .. " ") end - if in_argument_names and part:match("%]$") then - in_argument_names = false + if in_option_names and part:match("%]$") then + in_option_names = false end - if in_argument_values and part:match("%>$") then - in_argument_values = false + if in_option_values and part:match("%>$") then + in_option_values = false in_description = true end end - return "argument", task, { - argument_names = argument_names, - argument_values = argument_values, + return "option", task, { + option_names = option_names, + option_values = option_values, description = description } end @@ -821,13 +820,13 @@ parseComments = function(content, marker) value } end - elseif "argument" == _exp_0 then + elseif "option" == _exp_0 then if not (state.tasks[key]) then state.tasks[key] = { } end - state.tasks[key].arguments[value.argument_names[1]] = { - names = value.argument_names, - values = value.argument_values, + state.tasks[key].options[value.option_names[1]] = { + names = value.option_names, + values = value.option_values, description = value.description } end @@ -865,21 +864,6 @@ do end) inotify = ok and inotify or nil end -local contains -contains = function(t, v) - return #(function() - local _accum_0 = { } - local _len_0 = 1 - for _index_0 = 1, #t do - local vv = t[_index_0] - if vv == v then - _accum_0[_len_0] = vv - _len_0 = _len_0 + 1 - end - end - return _accum_0 - end)() ~= 0 -end local prints prints = function(...) return printerr(unpack((function(...) @@ -1406,6 +1390,7 @@ slice = function(arr, start, _end) end return _accum_0 end +local contains contains = function(arr, value) for _index_0 = 1, #arr do local v = arr[_index_0] @@ -1933,7 +1918,7 @@ if LIST_OPTIONS then return _accum_0 end)()), ' ')) end - for option_name, option in pairs(task.arguments) do + for option_name, option in pairs(task.options) do local _list_0 = option.names for _index_0 = 1, #_list_0 do local name = _list_0[_index_0] @@ -1957,7 +1942,7 @@ if GET_OPTION_TYPE then local parseComments parseComments = require("alfons.parser").parseComments local task_name, raw_option = GET_OPTION_TYPE:match("([^;]+)::([^;]+)") - local option = raw_option:gsub('%-', '') + local needed_option = raw_option:gsub('%-', '') if 'string' ~= type(GET_OPTION_TYPE) then errors(2, "Error: --get-option-type must be used with a task name and option in the format `task;option`.") end @@ -1967,12 +1952,12 @@ if GET_OPTION_TYPE then if not task or (task.flags and contains(task.flags, "hide")) then errors(2, "Error: Task '" .. tostring(task_name) .. "' does not exist.") end - for argument_name, argument in pairs(task.arguments) do - if contains(argument.names, option) then - local _list_0 = argument.values + for option_name, option in pairs(task.options) do + if contains(option.names, needed_option) then + local _list_0 = option.values for _index_0 = 1, #_list_0 do local value = _list_0[_index_0] - print(ZSH_OPTION_TYPES[value] or "") + print(ZSH_OPTION_TYPES[value.value] or "") end end end @@ -2072,7 +2057,7 @@ if HELP then }, ((function() local _accum_0 = { } local _len_0 = 1 - for option_name, option in pairs(task.arguments) do + for option_name, option in pairs(task.options) do _accum_0[_len_0] = Option((formatOptions(option)), option.description) _len_0 = _len_0 + 1 end diff --git a/docs/api.md b/docs/api.md index 15a5a2d..a0173b7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -14,6 +14,8 @@ Starting with 4.2, Alfons provides an API to embed Alfons functionality in your - [alfons.provide](#alfonsprovide) - [alfons.setfenv](#alfonssetfenv) - [alfons.version](#alfonsversion) + - [alfons.help](#alfonshelp) + - [alfons.parser](#alfonsparser) - [alfons.init](#alfonsinit) - [initEnv](#initenv) - [runString](#runstring) @@ -53,8 +55,7 @@ The rock also installs optional dependencies for Alfons like `http` or `linotify ## alfons.file -Contains just two functions, `readLua`, which takes a filename and returns its contents; and `readMoon`, which does the same, but compiling the file contents as MoonScript. - +Contains a generic function `readFile` that reads a file content and loads it, an alias `readLua`, and then `readMoon` which compiles MoonScript content from a file before returning the Lua value, and `readTeal` for a Teal equivalent. ## alfons.getopt Contains a single function, `getopt`, which takes a list of arguments, and returns a table of parsed options. @@ -81,6 +82,14 @@ Returns a function (not a table). The function is simply a reimplementation of s Returns a table with a field `VERSION` which contains the current Alfons version. +## alfons.help + +Contains functions to generate help messages, including a column formatter. + +## alfons.parser + +Contains the comment parser for documenting Taskfiles. + ## alfons.init Provides the main functions required to run a Taskfile. @@ -197,4 +206,4 @@ env.finalize! #### Finish -And just like that, we have implemented Alfons! It is a pretty simple tool, despite the looks of it, really. \ No newline at end of file +And just like that, we have implemented Alfons! It is a pretty simple tool, despite the looks of it, really. diff --git a/docs/autocompletion.md b/docs/autocompletion.md new file mode 100644 index 0000000..8fccca1 --- /dev/null +++ b/docs/autocompletion.md @@ -0,0 +1,30 @@ +# Autocompletion + +Alfons 5.2 introduced dynamic shell autocompletion. That means that on most shells (but especially Zsh), you will get completions for tasks, task options and maybe even task values. + +## Bash + +The Bash completion script is very poor. This is because Bash's autocompletion is very poor. It will list the available tasks, and if it can detect a task, also suggest the options for that task. + +### Installing + +``` +# cp bin/completion.bash /etc/bash_completion.d/alfons +``` + +## Zsh + +Zsh will autocomplete tasks, task options, and for certain task options, also suggest values. + +If a task option has a value of ``, ``, `` or ``, it will use Zsh's internal resolvers to complete suggestions. + +### Installing + +Move the completion file to anywhere in your `$FPATH`. + +```sh +# Oh my Zsh! +$ cp bin/completion.zsh $HOME/.oh-my-zsh/completions/_alfons +# Hopefully cross platform +$ sudo cp bin/completion.zsh /usr/share/zsh/functions/Completion/_alfons +``` diff --git a/docs/documenting.md b/docs/documenting.md new file mode 100644 index 0000000..e752894 --- /dev/null +++ b/docs/documenting.md @@ -0,0 +1,28 @@ +# Documenting + +In Alfons 5.2, a new feature was implemented to document your own Taskfiles. Previously, there was no way to generate a help message, or to have shell completion. Thanks to the docstrings, and a little bit of magic, this is now possible. + +## Docstrings + +Docstrings are comments in the Taskfile that begin with `---`. These are followed by a tag (`@task`, `@argument`, `@flag`) that determines how the rest of the comment will be parsed. + +The docstrings are untied from the actual code since it needs to be language-agnostic, and there are many ways within a single language to define tasks. As such, the tasks displayed in the new help message are a mix between documented tasks, and undocumented tasks read from the Taskfile. + +## Declaring a task + +`--- @task name Description of the task.` + +A task is declared using the docstring above. + +## Declaring an option for a task + +`--- @option task [long s] Description of the option.` + +The docstring above has several parts. First, the name of the task that you are referencing. Then, between square brackets, you have to put all the forms of the option, the long preferrably first. Between the angle brackets, you can put each of the expected values. Everything that comes afterward is the description of the option. + +## Flagging + +`--- @flag * hide` +`--- @flag task hide` + +As of right now, the only flag available is `hide`, which makes either all tasks (`*`) or a task invisible to autocompletion and the help message. diff --git a/docs/provide.md b/docs/provide.md index 1ca39ab..258f843 100644 --- a/docs/provide.md +++ b/docs/provide.md @@ -9,9 +9,19 @@ To see the documentation for `build` and `watch`, check out their respective mar - [Functions](#functions) - [Table of Contents](#table-of-contents) - [Tables](#tables) - - [contains](#contains) - [npairs](#npairs) - [env](#env) + - [keys](#keys) + - [Arrays](#arrays) + - [map](#map) + - [reduce](#reduce) + - [filter](#filter) + - [slice](#slice) + - [contains](#contains) + - [Strings](#strings) + - [lines](#lines) + - [split](#split) + - [sanitize](#sanitize) - [IO](#io) - [prints](#prints) - [printError](#printerror) @@ -44,10 +54,6 @@ To see the documentation for `build` and `watch`, check out their respective mar ## Tables -### contains - -Checks whether a table `t` contains a value `v`. - ### npairs Exactly like [ipairs](https://www.lua.org/manual/5.4/manual.html#pdf-ipairs), but it does not stop after a `nil` value. @@ -62,6 +68,64 @@ $ TEST=5 alfons You can access `TEST` by using `env.TEST`. +### keys + +`keys (table:{*:*}) -> [*]` + +Returns all the keys in a table. + +## Arrays + +### map + +`map (arr:[*], predicate:(value:*, key:*) -> *) -> [*]` + +Maps over an array. + +### reduce + +`reduce (arr:[*], predicate:(accumulator:*, value:*) -> *, initial: *?) -> *` + +Reduces an array to a single value using an accumulator. Equivalent to `foldl`. + +### filter + +`filter (arr:[*], predicate:(value:*, key: *) -> boolean) -> [*]` + +Creates a new array with only the values that pass the predicate. + +### slice + +`slice (arr:[*], start:number?, end:number?) -> [*]` + +Creates a slice of an array that starts at `start` and ends at `end`. + +### contains + +`contains (arr:[*], value:*) -> boolean` + +Checks if an array contains a certain value. + +## Strings + +### lines + +`lines (string) -> [string]` + +Splits a string into lines. + +### split + +`split (str:string, re:string, plain:boolean, matches:number) -> [string]` + +Splits a string `str` into parts by a pattern `re`, which is interpreted as a Lua pattern except if the `plain` flag is set to true. Additionally, a maximum number of matches can be set with the `matches` argument. + +### sanitize + +`sanitize (string) -> string` + +Makes sure that a string is safe to use in patterns without magic characters. + ## IO ### prints