#!/bin/bash # Create a dedicated config file from a template # # usage: ynh_config_add --template="template" --destination="destination" # | arg: -t, --template= - Template config file to use # | arg: -d, --destination= - Destination of the config file # | arg: -j, --jinja - Use jinja template instead of legacy __MY_VAR__ # # examples: # ynh_config_add --template=".env" --destination="$install_dir/.env" use the template file "../conf/.env" # ynh_config_add --jinja --template="config.j2" --destination="$install_dir/config" use the template file "../conf/config.j2" # ynh_config_add --template="/etc/nginx/sites-available/default" --destination="etc/nginx/sites-available/mydomain.conf" # ## ## How it works in "legacy" mode ## # The template can be by default the name of a file in the conf directory # of a YunoHost Package, a relative path or an absolute path. # # The helper will use the template `template` to generate a config file # `destination` by replacing the following keywords with global variables # that should be defined before calling this helper : # ``` # __USER__ by $app # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # ``` # And any dynamic variables that should be defined before calling this helper like: # ``` # __DOMAIN__ by $domain # __PATH__ by $path # __APP__ by $app # __VAR_1__ by $var_1 # __VAR_2__ by $var_2 # ``` # ## ## When --jinja is enabled ## # For a full documentation of the template you can refer to: https://jinja.palletsprojects.com/en/3.1.x/templates/ # In Yunohost context there are no really some specificity except that all variable passed are of type string. # So here are some example of recommended usage: # # If you need a conditional block # # {% if should_my_block_be_shown == 'true' %} # ... # {% endif %} # # or # # {% if should_my_block_be_shown == '1' %} # ... # {% endif %} # # If you need to iterate with loop: # # {% for yolo in var_with_multiline_value.splitlines() %} # ... # {% endfor %} # # or # # {% for jail in my_var_with_coma.split(',') %} # ... # {% endfor %} # # The helper will verify the checksum and backup the destination file # if it's different before applying the new template. # # And it will calculate and store the destination file checksum # into the app settings when configuration is done. # # Requires YunoHost version 4.1.0 or higher. ynh_config_add() { # ============ Argument parsing ============= local -A args_array=([t]=template= [d]=destination= [j]=jinja) local template local destination local jinja ynh_handle_getopts_args "$@" jinja="${jinja:-0}" # =========================================== local template_path if [ -f "$YNH_APP_BASEDIR/conf/$template" ]; then template_path="$YNH_APP_BASEDIR/conf/$template" elif [ -f "$template" ]; then template_path=$template else ynh_die "The provided template $template doesn't exist" fi ynh_backup_if_checksum_is_different "$destination" # Make sure to set the permissions before we copy the file # This is to cover a case where an attacker could have # created a file beforehand to have control over it # (cp won't overwrite ownership / modes by default...) touch $destination chmod 640 $destination _ynh_apply_default_permissions $destination if [[ "$jinja" == 1 ]] then # This is ran in a subshell such that the "export" does not "contaminate" the main process ( export $(compgen -v) j2 "$template_path" -f env -o $destination ) else cp -f "$template_path" "$destination" ynh_replace_vars --file="$destination" fi ynh_store_file_checksum "$destination" } # Replace variables in a file # # [internal] # # usage: ynh_replace_vars --file="file" # | arg: -f, --file= - File where to replace variables # # The helper will replace the following keywords with global variables # that should be defined before calling this helper : # __PATH__ by $path # __PATH__/ by $path/ if $path != /, or just / otherwise (instead of //) # __USER__ by $app # __YNH_NODE_LOAD_PATH__ by $ynh_node_load_PATH # # And any dynamic variables that should be defined before calling this helper like: # __DOMAIN__ by $domain # __APP__ by $app # __VAR_1__ by $var_1 # __VAR_2__ by $var_2 # # Requires YunoHost version 4.1.0 or higher. ynh_replace_vars() { # ============ Argument parsing ============= local -A args_array=([f]=file=) local file ynh_handle_getopts_args "$@" # =========================================== # Replace specific YunoHost variables if test -n "${path:-}"; then # path_slash_less is path, or a blank value if path is only '/' local path_slash_less=${path%/} ynh_replace --match="__PATH__/" --replace="$path_slash_less/" --file="$file" ynh_replace --match="__PATH__" --replace="$path" --file="$file" fi if test -n "${app:-}"; then ynh_replace --match="__USER__" --replace="$app" --file="$file" fi if test -n "${ynh_node_load_PATH:-}"; then ynh_replace --match="__YNH_NODE_LOAD_PATH__" --replace="$ynh_node_load_PATH" --file="$file" fi # Replace others variables # List other unique (__ __) variables in $file local uniques_vars=($(grep -oP '__[A-Z0-9]+?[A-Z0-9_]*?[A-Z0-9]*?__' $file | sort --unique | sed "s@__\([^.]*\)__@\L\1@g")) set +o xtrace # set +x # Do the replacement local delimit=@ for one_var in "${uniques_vars[@]}"; do # Validate that one_var is indeed defined # -v checks if the variable is defined, for example: # -v FOO tests if $FOO is defined # -v $FOO tests if ${!FOO} is defined # More info: https://stackoverflow.com/questions/3601515/how-to-check-if-a-variable-is-set-in-bash/17538964#comment96392525_17538964 [[ -v "${one_var:-}" ]] || ynh_die "Variable \$$one_var wasn't initialized when trying to replace __${one_var^^}__ in $file" # Escape delimiter in match/replace string match_string="__${one_var^^}__" match_string=${match_string//${delimit}/"\\${delimit}"} replace_string="${!one_var}" replace_string=${replace_string//\\/\\\\} replace_string=${replace_string//${delimit}/"\\${delimit}"} # Actually replace (sed is used instead of ynh_replace_string to avoid triggering an epic amount of debug logs) sed --in-place "s${delimit}${match_string}${delimit}${replace_string}${delimit}g" "$file" done set -o xtrace # set -x } # Get a value from heterogeneous file (yaml, json, php, python...) # # usage: ynh_read_var_in_file --file=PATH --key=KEY # | arg: -f, --file= - the path to the file # | arg: -k, --key= - the key to get # | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file) # # This helpers match several var affectation use case in several languages # We don't use jq or equivalent to keep comments and blank space in files # This helpers work line by line, it is not able to work correctly # if you have several identical keys in your files # # Example of line this helpers can managed correctly # .yml # title: YunoHost documentation # email: 'yunohost@yunohost.org' # .json # "theme": "colib'ris", # "port": 8102 # "some_boolean": false, # "user": null # .ini # some_boolean = On # action = "Clear" # port = 20 # .php # $user= # user => 20 # .py # USER = 8102 # user = 'https://donate.local' # CUSTOM['user'] = 'YunoHost' # # Requires YunoHost version 4.3 or higher. ynh_read_var_in_file() { # ============ Argument parsing ============= local -A args_array=([f]=file= [k]=key= [a]=after=) local file local key local after ynh_handle_getopts_args "$@" after="${after:-}" # =========================================== [[ -f $file ]] || ynh_die "File $file does not exists" set +o xtrace # set +x # Get the line number after which we search for the variable local line_number=1 if [[ -n "$after" ]]; then line_number=$(grep -m1 -n $after $file | cut -d: -f1) if [[ -z "$line_number" ]]; then set -o xtrace # set -x return 1 fi fi local filename="$(basename -- "$file")" local ext="${filename##*.}" local endline=',;' local assign="=>|:|=" local comments="#" local string="\"'" if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then endline='#' fi if [[ "$ext" =~ ^ini|env$ ]]; then comments="[;#]" fi if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then comments="//" fi local list='\[\s*['$string']?\w+['$string']?\]' local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' var_part+="[$string]?${key}[$string]?" var_part+='\s*\]?\s*' var_part+="($assign)" var_part+='\s*' # Extract the part after assignation sign local expression_with_comment="$((tail +$line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x echo YNH_NULL return 0 fi # Remove comments if needed local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" local first_char="${expression:0:1}" if [[ "$first_char" == '"' ]]; then echo "$expression" | grep -m1 -o -P '"\K([^"](\\")?)*[^\\](?=")' | head -n1 | sed 's/\\"/"/g' elif [[ "$first_char" == "'" ]]; then echo "$expression" | grep -m1 -o -P "'\K([^'](\\\\')?)*[^\\\\](?=')" | head -n1 | sed "s/\\\\'/'/g" else echo "$expression" fi set -o xtrace # set -x } # Set a value into heterogeneous file (yaml, json, php, python...) # # usage: ynh_write_var_in_file --file=PATH --key=KEY --value=VALUE # | arg: -f, --file= - the path to the file # | arg: -k, --key= - the key to set # | arg: -v, --value= - the value to set # | arg: -a, --after= - the line just before the key (in case of multiple lines with the name of the key in the file) # # Requires YunoHost version 4.3 or higher. ynh_write_var_in_file() { # ============ Argument parsing ============= local -A args_array=([f]=file= [k]=key= [v]=value= [a]=after=) local file local key local value local after ynh_handle_getopts_args "$@" after="${after:-}" # =========================================== [[ -f $file ]] || ynh_die "File $file does not exists" set +o xtrace # set +x # Get the line number after which we search for the variable local after_line_number=1 if [[ -n "$after" ]]; then after_line_number=$(grep -m1 -n $after $file | cut -d: -f1) if [[ -z "$after_line_number" ]]; then set -o xtrace # set -x return 1 fi fi local filename="$(basename -- "$file")" local ext="${filename##*.}" local endline=',;' local assign="=>|:|=" local comments="#" local string="\"'" if [[ "$ext" =~ ^ini|env|toml|yml|yaml$ ]]; then endline='#' fi if [[ "$ext" =~ ^ini|env$ ]]; then comments="[;#]" fi if [[ "php" == "$ext" ]] || [[ "$ext" == "js" ]]; then comments="//" fi local list='\[\s*['$string']?\w+['$string']?\]' local var_part='^\s*((const|var|let)\s+)?\$?(\w+('$list')*(->|\.|\[))*\s*' var_part+="[$string]?${key}[$string]?" var_part+='\s*\]?\s*' var_part+="($assign)" var_part+='\s*' # Extract the part after assignation sign local expression_with_comment="$((tail +$after_line_number ${file} | grep -i -o -P $var_part'\K.*$' || echo YNH_NULL) | head -n1)" if [[ "$expression_with_comment" == "YNH_NULL" ]]; then set -o xtrace # set -x return 1 fi local value_line_number="$(tail +$after_line_number ${file} | grep -m1 -n -i -P $var_part'\K.*$' | cut -d: -f1)" value_line_number=$((after_line_number + value_line_number)) local range="${after_line_number},${value_line_number} " # Remove comments if needed local expression="$(echo "$expression_with_comment" | sed "s@${comments}[^$string]*\$@@g" | sed "s@\s*[$endline]*\s*]*\$@@")" endline=${expression_with_comment#"$expression"} endline="$(echo "$endline" | sed 's/\\/\\\\/g')" value="$(echo "$value" | sed 's/\\/\\\\/g')" value=${value//&/"\&"} local first_char="${expression:0:1}" delimiter=$'\001' if [[ "$first_char" == '"' ]]; then # \ and sed is quite complex you need 2 \\ to get one in a sed # So we need \\\\ to go through 2 sed value="$(echo "$value" | sed 's/"/\\\\"/g')" sed -ri "${range}s$delimiter"'(^'"${var_part}"'")([^"]|\\")*("[\s;,]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}"'"'"${endline}${delimiter}i" ${file} elif [[ "$first_char" == "'" ]]; then # \ and sed is quite complex you need 2 \\ to get one in a sed # However double quotes implies to double \\ to # So we need \\\\\\\\ to go through 2 sed and 1 double quotes str value="$(echo "$value" | sed "s/'/\\\\\\\\'/g")" sed -ri "${range}s$delimiter(^${var_part}')([^']|\\')*('"'[\s,;]*)(\s*'$comments'.*)?$'$delimiter'\1'"${value}'${endline}${delimiter}i" ${file} else if [[ "$value" == *"'"* ]] || [[ "$value" == *'"'* ]] || [[ "$ext" =~ ^php|py|json|js$ ]]; then value='\"'"$(echo "$value" | sed 's/"/\\\\"/g')"'\"' fi if [[ "$ext" =~ ^yaml|yml$ ]]; then value=" $value" fi sed -ri "${range}s$delimiter(^${var_part}).*\$$delimiter\1${value}${endline}${delimiter}i" ${file} fi set -o xtrace # set -x }