diff --git a/examples/README.md b/examples/README.md index b5aeed992..4b2bf5560 100644 --- a/examples/README.md +++ b/examples/README.md @@ -58,9 +58,10 @@ Each of these examples demonstrates one aspect or feature of bashly. ## Bashly library features -- [config-ini](config-ini#readme) - using the config (INI) functions -- [colors](colors#readme) - using the color print feature +- [config](config#readme) - using the config functions +- [ini](ini#readme) - using the INI handling functions - [yaml](yaml#readme) - using the YAML reading functions +- [colors](colors#readme) - using the color print feature - [completions](completions#readme) - adding bash completion functionality - [validations](validations#readme) - adding argument validation functions - [hooks](hooks#readme) - adding before/after hooks diff --git a/examples/ini/.gitignore b/examples/ini/.gitignore new file mode 100644 index 000000000..bd44730a3 --- /dev/null +++ b/examples/ini/.gitignore @@ -0,0 +1 @@ +configly \ No newline at end of file diff --git a/examples/ini/README.md b/examples/ini/README.md new file mode 100644 index 000000000..3b4eac8a4 --- /dev/null +++ b/examples/ini/README.md @@ -0,0 +1,261 @@ +# Config Example + +Demonstrates how to add functions for reading and writing INI files. + +This example was generated with: + +```bash +$ bashly init +# ... now edit src/bashly.yml to match the example ... +$ bashly add config +$ bashly generate +# ... now edit all files in the src folder ... +$ bashly generate +``` + +Running the `bashly add config` command simply added the +[src/lib/config.sh](src/lib/config.sh) file, which includes functions for +reading and writing values from an INI file. + +See the files in the [src](src) folder for usage examples. + + + +----- + +## `bashly.yml` + +```yaml +name: configly +help: Sample application that uses the config functions +version: 0.1.0 + +commands: +- name: list + alias: l + help: Show the entire config file + +- name: get + alias: g + help: Read a value from the config file + + args: + - name: key + required: true + help: Config key + + examples: + - configly get hello + - configly get user.name + +- name: set + alias: s + help: Save a value in the config file + + args: + - name: key + required: true + help: Config key + - name: value + required: true + help: Config value + + examples: + - configly set hello world + - configly set user.email me@example.com + +- name: del + alias: d + help: Remove a value from the config file + + args: + - name: key + required: true + help: Config key + + examples: + - configly del hello + - configly del user.name +``` + +## `config.ini` + +```ini +; comments are allowed, sections are optional +hello = world +bashly = works + +[options] +name = value for options.name +path = value for options.path + +[user] +name = value for user.name +email = value for user.email + +``` + +## `src/get_command.sh` + +```bash +# Using the standard library (lib/config.sh) to show a value from the config +config_load config.ini + +key="${args[key]}" +value=${config[$key]} + +if [[ "$value" ]]; then + echo "$key = $value" +else + echo "No such key: $key" +fi + +``` + +## `src/list_command.sh` + +```bash +# Using the standard library (lib/config.sh) to show the entire config file +config_load config.ini +config_show + +## Or to iterate through keys manually +# for key in $(config_keys); do +# echo "$key = ${config[$key]}" +# done +``` + +## `src/set_command.sh` + +```bash +# Using the standard library (lib/config.sh) to store a value to the config +config_load config.ini + +key="${args[key]}" +value="${args[value]}" + +config["$key"]="$value" +config_save saved.ini +cat saved.ini + +``` + + +## Generated script output + +### `$ ./configly -h` + +```shell +configly - Sample application that uses the config functions + +Usage: + configly COMMAND + configly [COMMAND] --help | -h + configly --version | -v + +Commands: + list Show the entire config file + get Read a value from the config file + set Save a value in the config file + del Remove a value from the config file + +Options: + --help, -h + Show this help + + --version, -v + Show version number + + + +``` + +### `$ ./configly set hello WORLD` + +```shell +bashly = works +hello = WORLD + +[options] +name = value for options.name +path = value for options.path + +[user] +email = value for user.email +name = value for user.name + + +``` + +### `$ ./configly set user.name Megatron` + +```shell +bashly = works +hello = world + +[options] +name = value for options.name +path = value for options.path + +[user] +email = value for user.email +name = Megatron + + +``` + +### `$ ./configly get hello` + +```shell +hello = world + + +``` + +### `$ ./configly get user.name` + +```shell +user.name = value for user.name + + +``` + +### `$ ./configly get invalid_key` + +```shell +No such key: invalid_key + + +``` + +### `$ ./configly del user.email` + +```shell +bashly = works +hello = world + +[options] +name = value for options.name +path = value for options.path + +[user] +name = value for user.name + + +``` + +### `$ ./configly list` + +```shell +bashly = works +hello = world +options.name = value for options.name +options.path = value for options.path +user.email = value for user.email +user.name = value for user.name + + +``` + + + diff --git a/examples/ini/config.ini b/examples/ini/config.ini new file mode 100644 index 000000000..6a09ffdfa --- /dev/null +++ b/examples/ini/config.ini @@ -0,0 +1,11 @@ +; comments are allowed, sections are optional +hello = world +bashly = works + +[options] +name = value for options.name +path = value for options.path + +[user] +name = value for user.name +email = value for user.email diff --git a/examples/ini/saved.ini b/examples/ini/saved.ini new file mode 100644 index 000000000..9faeccdbd --- /dev/null +++ b/examples/ini/saved.ini @@ -0,0 +1,9 @@ +bashly = works +hello = world + +[options] +name = value for options.name +path = value for options.path + +[user] +name = value for user.name diff --git a/examples/ini/src/bashly.yml b/examples/ini/src/bashly.yml new file mode 100644 index 000000000..f053172be --- /dev/null +++ b/examples/ini/src/bashly.yml @@ -0,0 +1,50 @@ +name: configly +help: Sample application that uses the config functions +version: 0.1.0 + +commands: +- name: list + alias: l + help: Show the entire config file + +- name: get + alias: g + help: Read a value from the config file + + args: + - name: key + required: true + help: Config key + + examples: + - configly get hello + - configly get user.name + +- name: set + alias: s + help: Save a value in the config file + + args: + - name: key + required: true + help: Config key + - name: value + required: true + help: Config value + + examples: + - configly set hello world + - configly set user.email me@example.com + +- name: del + alias: d + help: Remove a value from the config file + + args: + - name: key + required: true + help: Config key + + examples: + - configly del hello + - configly del user.name diff --git a/examples/ini/src/del_command.sh b/examples/ini/src/del_command.sh new file mode 100644 index 000000000..7a9955bcf --- /dev/null +++ b/examples/ini/src/del_command.sh @@ -0,0 +1,9 @@ +# Using the standard library (lib/ini.sh) to delete a value from the config +ini_load config.ini + +key="${args[key]}" +unset "ini[$key]" + +ini_save saved.ini +cat saved.ini + diff --git a/examples/ini/src/get_command.sh b/examples/ini/src/get_command.sh new file mode 100644 index 000000000..259d3f77f --- /dev/null +++ b/examples/ini/src/get_command.sh @@ -0,0 +1,11 @@ +# Using the standard library (lib/ini.sh) to show a value from the config +ini_load config.ini + +key="${args[key]}" +value=${ini[$key]} + +if [[ "$value" ]]; then + echo "$key = $value" +else + echo "No such key: $key" +fi diff --git a/examples/ini/src/lib/ini.sh b/examples/ini/src/lib/ini.sh new file mode 100644 index 000000000..3416c4f82 --- /dev/null +++ b/examples/ini/src/lib/ini.sh @@ -0,0 +1,110 @@ +## INI functions [@bashly-upgrade ini] +## This file is a part of Bashly standard library +## +## Usage: +## +## - In your script, call `ini_load path/to/config.ini`. +## - A global associative array named `ini` will become available to you, +## - When updating any of the associative array's values, call +## `ini_save path/to/config.ini` to save the data. +## - If a global variable named INI_FILE exists, you can omit the filename +## in `ini_load` and `ini_save`. +## - INI sections are optional. +## +## Get a value: +## +## ${ini[section1.key1]} # for keys under a [section] +## ${ini[key1]} # for keys not under a [section] +## ${ini[key1]:-default} # get a default value if the INI key is unset +## +## Update/create a value: +## +## ini[section1.key1]="value" +## ini_save path/to/config.ini +## +## Delete a value +## +## unset ini[section1.key1] +## ini_save path/to/config.ini + +## Load an INI file and populate the associative array `ini`. +# shellcheck disable=SC2120 +ini_load() { + declare -gA ini + + local ini_file="${1:-$INI_FILE}" + + local section="" + local key="" + local value="" + local section_regex="^\[(.+)\]" + local key_regex="^([^ =]+) *= *(.*) *$" + local comment_regex="^;" + + [[ -f "$ini_file" ]] || touch "$ini_file" + + while IFS= read -r line; do + if [[ $line =~ $comment_regex ]]; then + continue + elif [[ $line =~ $section_regex ]]; then + section="${BASH_REMATCH[1]}." + elif [[ $line =~ $key_regex ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + ini["${section}${key}"]="$value" + fi + done <"$ini_file" +} + +## Save the associative array `ini` back to a file +# shellcheck disable=SC2120 +ini_save() { + declare -gA ini + + local ini_file="${1:-$INI_FILE}" + + local current_section="" + local has_free_keys=false + + rm -f "$ini_file" + + for key in $(ini_keys); do + [[ $key == *.* ]] && continue + has_free_keys=true + value="${ini[$key]}" + echo "$key = $value" >>"$ini_file" + done + + [[ "${has_free_keys}" == "true" ]] && echo >>"$ini_file" + + for key in $(ini_keys); do + [[ $key == *.* ]] || continue + value="${ini[$key]}" + IFS="." read -r section_name key_name <<<"$key" + + if [[ "$current_section" != "$section_name" ]]; then + [[ $current_section ]] && echo >>"$ini_file" + echo "[$section_name]" >>"$ini_file" + current_section="$section_name" + fi + + echo "$key_name = $value" >>"$ini_file" + done +} + +## Show all loaded key-value pairs +ini_show() { + declare -gA ini + + for key in $(ini_keys); do + echo "$key = ${ini[$key]}" + done +} + +## Get a newline delimited, sorted list of keys +ini_keys() { + declare -gA ini + + local keys=("${!ini[@]}") + for a in "${keys[@]}"; do echo "$a"; done | sort +} diff --git a/examples/ini/src/list_command.sh b/examples/ini/src/list_command.sh new file mode 100644 index 000000000..bad608540 --- /dev/null +++ b/examples/ini/src/list_command.sh @@ -0,0 +1,8 @@ +# Using the standard library (lib/ini.sh) to show the entire config file +ini_load config.ini +ini_show + +## Or iterate through keys manually +# for key in $(ini_keys); do +# echo "$key = ${ini[$key]}" +# done \ No newline at end of file diff --git a/examples/ini/src/set_command.sh b/examples/ini/src/set_command.sh new file mode 100644 index 000000000..305f82729 --- /dev/null +++ b/examples/ini/src/set_command.sh @@ -0,0 +1,9 @@ +# Using the standard library (lib/ini.sh) to store a value to the config +ini_load config.ini + +key="${args[key]}" +value="${args[value]}" + +ini["$key"]="$value" +ini_save saved.ini +cat saved.ini diff --git a/examples/ini/test.sh b/examples/ini/test.sh new file mode 100644 index 000000000..c19634279 --- /dev/null +++ b/examples/ini/test.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -x + +bashly add ini --force +bashly generate + +### Try Me ### + +./configly -h +./configly set hello WORLD +./configly set user.name Megatron +./configly get hello +./configly get user.name +./configly get invalid_key +./configly del user.email +./configly list diff --git a/lib/bashly/libraries/ini/ini.sh b/lib/bashly/libraries/ini/ini.sh new file mode 100644 index 000000000..3416c4f82 --- /dev/null +++ b/lib/bashly/libraries/ini/ini.sh @@ -0,0 +1,110 @@ +## INI functions [@bashly-upgrade ini] +## This file is a part of Bashly standard library +## +## Usage: +## +## - In your script, call `ini_load path/to/config.ini`. +## - A global associative array named `ini` will become available to you, +## - When updating any of the associative array's values, call +## `ini_save path/to/config.ini` to save the data. +## - If a global variable named INI_FILE exists, you can omit the filename +## in `ini_load` and `ini_save`. +## - INI sections are optional. +## +## Get a value: +## +## ${ini[section1.key1]} # for keys under a [section] +## ${ini[key1]} # for keys not under a [section] +## ${ini[key1]:-default} # get a default value if the INI key is unset +## +## Update/create a value: +## +## ini[section1.key1]="value" +## ini_save path/to/config.ini +## +## Delete a value +## +## unset ini[section1.key1] +## ini_save path/to/config.ini + +## Load an INI file and populate the associative array `ini`. +# shellcheck disable=SC2120 +ini_load() { + declare -gA ini + + local ini_file="${1:-$INI_FILE}" + + local section="" + local key="" + local value="" + local section_regex="^\[(.+)\]" + local key_regex="^([^ =]+) *= *(.*) *$" + local comment_regex="^;" + + [[ -f "$ini_file" ]] || touch "$ini_file" + + while IFS= read -r line; do + if [[ $line =~ $comment_regex ]]; then + continue + elif [[ $line =~ $section_regex ]]; then + section="${BASH_REMATCH[1]}." + elif [[ $line =~ $key_regex ]]; then + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + ini["${section}${key}"]="$value" + fi + done <"$ini_file" +} + +## Save the associative array `ini` back to a file +# shellcheck disable=SC2120 +ini_save() { + declare -gA ini + + local ini_file="${1:-$INI_FILE}" + + local current_section="" + local has_free_keys=false + + rm -f "$ini_file" + + for key in $(ini_keys); do + [[ $key == *.* ]] && continue + has_free_keys=true + value="${ini[$key]}" + echo "$key = $value" >>"$ini_file" + done + + [[ "${has_free_keys}" == "true" ]] && echo >>"$ini_file" + + for key in $(ini_keys); do + [[ $key == *.* ]] || continue + value="${ini[$key]}" + IFS="." read -r section_name key_name <<<"$key" + + if [[ "$current_section" != "$section_name" ]]; then + [[ $current_section ]] && echo >>"$ini_file" + echo "[$section_name]" >>"$ini_file" + current_section="$section_name" + fi + + echo "$key_name = $value" >>"$ini_file" + done +} + +## Show all loaded key-value pairs +ini_show() { + declare -gA ini + + for key in $(ini_keys); do + echo "$key = ${ini[$key]}" + done +} + +## Get a newline delimited, sorted list of keys +ini_keys() { + declare -gA ini + + local keys=("${!ini[@]}") + for a in "${keys[@]}"; do echo "$a"; done | sort +} diff --git a/lib/bashly/libraries/libraries.yml b/lib/bashly/libraries/libraries.yml index e0f49606a..9426fea8f 100644 --- a/lib/bashly/libraries/libraries.yml +++ b/lib/bashly/libraries/libraries.yml @@ -20,7 +20,7 @@ completions_yaml: handler: Bashly::Libraries::CompletionsYAML config: - help: Add standard functions for handling INI files to the lib directory. + help: Add standard functions for handling key=value configuration files to the lib directory. files: - source: "config/config.sh" target: "%{user_lib_dir}/config.%{user_ext}" @@ -39,6 +39,12 @@ hooks: - source: "hooks/after.sh" target: "%{user_source_dir}/after.%{user_ext}" +ini: + help: Add standard functions for reading/writing INI files to the lib directory. + files: + - source: "ini/ini.sh" + target: "%{user_lib_dir}/ini.%{user_ext}" + lib: help: |- Create the lib directory for any additional user scripts. diff --git a/spec/approvals/cli/add/list b/spec/approvals/cli/add/list index 11b899a1c..b1bc8aab1 100644 --- a/spec/approvals/cli/add/list +++ b/spec/approvals/cli/add/list @@ -12,7 +12,8 @@ completions_yaml [PATH] Generate a completions YAML configuration for Completely. config - Add standard functions for handling INI files to the lib directory. + Add standard functions for handling key=value configuration files to the lib + directory. help Add a help command, in addition to the standard --help flag. @@ -21,6 +22,9 @@ hooks Add placeholders for initialize/before/after hooks which are executed on script initialization, and before/after any command. +ini + Add standard functions for reading/writing INI files to the lib directory. + lib Create the lib directory for any additional user scripts. All *.sh scripts in this directory will be included in the final bash script. diff --git a/spec/approvals/examples/ini b/spec/approvals/examples/ini new file mode 100644 index 000000000..8c688262e --- /dev/null +++ b/spec/approvals/examples/ini @@ -0,0 +1,76 @@ ++ bashly add ini --force +created src/lib/ini.sh ++ bashly generate +creating user files in src +skipped src/list_command.sh (exists) +skipped src/get_command.sh (exists) +skipped src/set_command.sh (exists) +skipped src/del_command.sh (exists) +created ./configly +run ./configly --help to test your bash script ++ ./configly -h +configly - Sample application that uses the config functions + +Usage: + configly COMMAND + configly [COMMAND] --help | -h + configly --version | -v + +Commands: + list Show the entire config file + get Read a value from the config file + set Save a value in the config file + del Remove a value from the config file + +Options: + --help, -h + Show this help + + --version, -v + Show version number + ++ ./configly set hello WORLD +bashly = works +hello = WORLD + +[options] +name = value for options.name +path = value for options.path + +[user] +email = value for user.email +name = value for user.name ++ ./configly set user.name Megatron +bashly = works +hello = world + +[options] +name = value for options.name +path = value for options.path + +[user] +email = value for user.email +name = Megatron ++ ./configly get hello +hello = world ++ ./configly get user.name +user.name = value for user.name ++ ./configly get invalid_key +No such key: invalid_key ++ ./configly del user.email +bashly = works +hello = world + +[options] +name = value for options.name +path = value for options.path + +[user] +name = value for user.name ++ ./configly list +bashly = works +hello = world +options.name = value for options.name +options.path = value for options.path +user.email = value for user.email +user.name = value for user.name diff --git a/spec/bashly/library_source_spec.rb b/spec/bashly/library_source_spec.rb index 5c4ea7976..2c62ca917 100644 --- a/spec/bashly/library_source_spec.rb +++ b/spec/bashly/library_source_spec.rb @@ -101,7 +101,7 @@ it 'returns all libraries as keys' do expect(subject.libraries.keys).to match_array %i[ colors completions completions_script completions_yaml config - help hooks lib settings strings test validations yaml + help hooks ini lib settings strings test validations yaml ] end