import ast import datetime import subprocess version = open("../debian/changelog").readlines()[0].split()[1].strip("()") today = datetime.datetime.now().strftime("%d/%m/%Y") def get_current_commit(): p = subprocess.Popen( "git rev-parse --verify HEAD", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) stdout, stderr = p.communicate() current_commit = stdout.strip().decode("utf-8") return current_commit current_commit = get_current_commit() print( f"""--- title: Options template: docs taxonomy: category: docs routes: default: '/dev/forms' --- Doc auto-generated by [this script](https://github.com/YunoHost/yunohost/blob/{current_commit}/doc/generate_options_doc.py) on {today} (YunoHost version {version}) ## Glossary You may encounter some named types which are used for simplicity. - `Translation`: a translated property - used for properties: `ask`, `help` and `Pattern.error` - a `dict` with locales as keys and translations as values: ```toml ask.en = "The text in english" ask.fr = "Le texte en français" ``` It is not currently possible for translators to translate those string in weblate. - a single `str` for a single english default string ```toml help = "The text in english" ``` - `JSExpression`: a `str` JS expression to be evaluated to `true` or `false`: - used for properties: `visible` and `enabled` - operators availables: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()` - [examples available in the advanced section](#advanced-use-cases) - `Binding`: bind a value to a file/property/variable/getter/setter/validator - save the value in `settings.yaml` when not defined - nothing at all with `"null"` - a custom getter/setter/validator with `"null"` + a function starting with `get__`, `set__`, `validate__` in `scripts/config` - a variable/property in a file with `:__FINALPATH__/my_file.php` - a whole file with `__FINALPATH__/my_file.php` - [examples available in the advanced section](#bind) - `Pattern`: a `dict` with a regex to match the value against and an error message ```toml pattern.regexp = '^[A-F]\d\d$' pattern.error = "Provide a room like F12: one uppercase and 2 numbers" # or with translated error pattern.error.en = "Provide a room like F12: one uppercase and 2 numbers" pattern.error.fr = "Entrez un numéro de salle comme F12: une lettre majuscule et deux chiffres." ``` - IMPORTANT: your `pattern.regexp` should be between simple quote, not double. """ ) fname = "../src/utils/form.py" content = open(fname).read() # NB: This magic is because we want to be able to run this script outside of a YunoHost context, # in which we cant really 'import' the file because it will trigger a bunch of moulinette/yunohost imports... tree = ast.parse(content) OptionClasses = [ c for c in tree.body if isinstance(c, ast.ClassDef) and c.name.endswith("Option") ] OptionDocString = {} for c in OptionClasses: if not isinstance(c.body[0], ast.Expr): continue option_type = None if c.name in {"BaseOption", "BaseInputOption"}: option_type = c.name elif c.body[1].target.id == "type": option_type = c.body[1].value.attr docstring = ast.get_docstring(c) if docstring: if "##### Properties" not in docstring: docstring += """ ##### Properties - [common properties](#common-option-properties) """ OptionDocString[option_type] = docstring for option_type, doc in OptionDocString.items(): print("") if option_type == "BaseOption": print("## Common Option properties") elif option_type == "BaseInputOption": print("## Common Inputs properties") elif option_type == "display_text": print("----------------") print("## Readonly Options") print(f"### Option `{option_type}`") elif option_type == "string": print("----------------") print("## Input Options") print(f"### Option `{option_type}`") else: print(f"### Option `{option_type}`") print("") print(doc) print("") print( """ ---------------- ## Advanced use cases ### `visible` & `enabled` expression evaluation Sometimes we may want to conditionaly display a message or prompt for a value, for this we have the `visible` prop. And we may want to allow a user to trigger an action only if some condition are met, for this we have the `enabled` prop. Expressions are evaluated against a context containing previous values of the current section's options. This quite limited current design exists because on the web-admin or on the CLI we cannot guarantee that a value will be present in the form if the user queried only a single panel/section/option. In the case of an action, the user will be shown or asked for each of the options of the section in which the button is present. The expression has to be written in javascript (this has been designed for the web-admin first and is converted to python on the fly on the cli). Available operators are: `==`, `!=`, `>`, `>=`, `<`, `<=`, `!`, `&&`, `||`, `+`, `-`, `*`, `/`, `%` and `match()`. ##### Examples ```toml # simple "my_option_id" is thruthy/falsy visible = "my_option_id" visible = "!my_option_id" # misc visible = "my_value >= 10" visible = "-(my_value + 1) < 0" visible = "!!my_value || my_other_value" ``` For a more complete set of examples, [check the tests at the end of the file](https://github.com/YunoHost/yunohost/blob/dev/src/tests/test_questions.py). ##### match() For more complex evaluation we can use regex matching. ```toml [my_string] default = "Lorem ipsum dolor et si qua met!" [my_boolean] type = "boolean" visible = "my_string && match(my_string, '^Lorem [ia]psumE?')" ``` Match the content of a file. ```toml [my_file] type = "file" accept = ".txt" bind = "/etc/random/lorem.txt" [my_boolean] type = "boolean" visible = "my_file && match(my_file, '^Lorem [ia]psumE?')" ``` with a file with content like: ```txt Lorem ipsum dolor et si qua met! ``` ### `bind` Config panels only `bind` allows us to alter the generic behavior of option's values which is: get from and set in the app `settings.yml`. We can: - alter the source the value comes from with getters or binds to file. - alter the destination with setters or binds to file. - parse/validate the value before destination with validators ---------------- IMPORTANT: with the exception of `bind = "null"` options, options ids should almost **always** correspond to an app setting initialized / reused during install/upgrade. Not doing so may result in inconsistencies between the config panel mechanism and the use of ynh_add_config ---------------- ##### bind to file You can bind a `text` or directly a `file` to a specific file by using `bind = "FILEPATH`. ```toml [panel.section.config_file] type = "file" bind = "__FINALPATH__/config.ini" ``` ```toml [panel.section.config_content] type = "text" bind = "__FINALPATH__/config.ini" default = "key: 'value'" ``` ##### bind a value inside a file Settings usually correspond to key/values in actual app configurations. Hence, a more useful mode is to have `bind = ":FILENAME"` with a colon `:` before. In that case, YunoHost will automagically find a line with `KEY=VALUE` in `FILENAME` (with the adequate separator between `KEY` and `VALUE`). YunoHost will then use this value for the read/get operation. During write/set operations, YunoHost will overwrite the value in **both** FILENAME and in the app's settings.yml Configuration file format supported: `yaml`, `toml`, `json`, `ini`, `env`, `php`, `python`. The feature probably works with others formats, but should be tested carefully. Note that this feature only works with relatively simple cases such as `KEY: VALUE`, but won't properly work with complex data structures like multilin array/lists or dictionnaries. It also doesn't work with XML format, custom config function call, php define(), … ```toml [panel.section.config_value] # Do not use `file` for this since we only want to insert/save a value type = "string" bind = ":__FINALPATH__/config.ini" default = "" ``` By default, `bind = ":FILENAME"` will use the option id as `KEY` but the option id may sometime not be the exact same `KEY` name in the configuration file. For example, [In pepettes app](https://github.com/YunoHost-Apps/pepettes_ynh/blob/5cc2d3ffd6529cc7356ff93af92dbb6785c3ab9a/conf/settings.py##L11), the python variable is `name` and not `project_name`. In that case, the key name can be specified before the colon `:`. ```toml [panel.section.project_name] bind = "name:__FINALPATH__/config.ini" ``` ##### Getters Define an option's custom getter in a bash script `script/config`. It has to be named after an option's id prepended by `get__`. To display a custom alert message for example. We setup a base option in `config_panel.toml`. ```toml [panel.section.alert] type = "alert" # bind to "null" to inform there's something in `scripts/config` bind = "null" # `ask` & `style` will be injected by a custom getter ``` Then add a custom getter that output yaml, every properties defined here will override any property previously declared. ```bash get__alert() { if [ "$whatever" ]; then cat << EOF style: success ask: Your VPN is running :) EOF else cat << EOF style: danger ask: Your VPN is down EOF fi } ``` Or to inject a custom value: ```toml [panel.section.my_hidden_value] type = "number" bind = "null" # option will act as an hidden variable that can be used in context evaluation # (ie: `visible` or `enabled`) readonly = true visible = false # `default` injected by a custom getter ``` ```bash get__my_hidden_value() { if [ "$whatever" ]; then # if only a value is needed echo "10" else # or if we need to override some other props # (use `default` or `value` to inject the value) cat << EOF ask: Here's a number visible: true default: 0 EOF fi } ``` ##### Setters Define an option's custom setter in a bash script `script/config`. It has to be named after an option's id prepended by `set__`. ```toml [panel.section.my_value] type = "string" bind = "null" ask = "gimme complex string" ``` ```bash set__my_value() { if [ -n "$my_value" ]; then # split the string into multiple elements or idk fi # To save the value or modified value as a setting: ynh_app_setting_set --app=$app --key=my_value --value="$my_value" } ``` ##### Validators Define an option's custom validator in a bash script `script/config`. It has to be named after an option's id prepended by `validate__`. Validators allows us to return custom error messages depending on the value. ```toml [panel.section.my_value] type = "string" bind = "null" ask = "Gimme a long string" default = "too short" ``` ```bash validate__my_value() { if [[ "${#my_value}" -lt 12 ]]; then echo 'Too short!'; fi } ``` ##### Actions Define an option's action in a bash script `script/config`. It has to be named after a `button`'s id prepended by `run__`. ```toml [panel.section.my_action] type = "button" # no need to set `bind` to "null" it is its hard default ask = "Run action" ``` ```bash run__my_action() { ynh_print_info "Running 'my_action'..." } ``` A more advanced example could look like: ```toml [panel.my_action_section] name = "Action section" [panel.my_action_section.my_repo] type = "url" bind = "null" # value will not be saved as a setting ask = "gimme a repo link" [panel.my_action_section.my_repo_name] type = "string" bind = "null" # value will not be saved as a setting ask = "gimme a custom folder name" [panel.my_action_section.my_action] type = "button" ask = "Clone the repo" # enabled the button only if the above values is defined enabled = "my_repo && my_repo_name" ``` ```bash run__my_action() { ynh_print_info "Cloning '$my_repo'..." cd /tmp git clone "$my_repo" "$my_repo_name" } ``` """ )