Initial commit

This commit is contained in:
Steven Goldsmith
2022-11-11 20:07:33 -05:00
parent 5ec9f5e6d6
commit ebd4b3fd9e
30 changed files with 7133 additions and 2 deletions

View File

@@ -1,2 +1,62 @@
# configng
Next generation bash based configuration API
# configrd
This is a refactoring of [armbian-config](https://github.com/armbian/config) using [Bash Utility](https://labbots.github.io/bash-utility)
embedded in this project. This allows for functional programming in Bash and also modernizes
the monolithic nature of armbian-config. Error handling and validation are also included.
The idea is to provide an API in Bash that can be called from a TUI, GUI or CLI. Please
follow the coding standards which follow Bash Utility functions.
Why Bash? Well, because it's going to be in every distribution. Striped down distributions
may not include Python, C/C++, etc. build/runtime environments
## Quick start
* `sudo apt install git`
* `cd ~/`
* `git clone https://github.com/armbian/configng.git`
* `cd ~/configrd/test`
* `sudo ./cpu_test.sh`
If all goes well you should see all the functions in cpu.sh called and output diaplayed.
## Coding standards
[Shell Style Guide](https://google.github.io/styleguide/shellguide.html) has some good ideas,
but fundementally look at the code in Bash Utility:
```
# @description Strip characters from the beginning of a string.
#
# @example
# echo "$(string::lstrip "Hello World!" "He")"
# #Output
# llo World!
#
# @arg $1 string The input string.
# @arg $2 string The characters you want to strip.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout Returns the modified string.
string::lstrip() {
[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
printf '%s\n' "${1##$2}"
}
```
Functions should follow filename::func_name style. Then you can tell just from the name which
file the function is located in. Return codes should also follow a similar pattern:
* 0 Successful
* 1 Not found
* 2 Function missing arguments
* 3-255 all other errors
Validate values:
```
# Validate minimum frequency is <= maximum frequency
[ "$min_freq" -gt "$max_freq" ] && printf "%s: Minimum frequency must be <= maximum frequency\n" "${FUNCNAME[0]}" && return 5
```
Return values should use stdout:
```
# Return value
printf '%s\n' "$(cat $file)"
```
Only use sudo when needed and never run as root!

View File

@@ -0,0 +1,22 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
# for shfmt
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash
switch_case_indent = true
space_redirects = true
[*.md]
trim_trailing_whitespace = false

View File

@@ -0,0 +1,3 @@
/tmp
gh-pages
hugo-docs

View File

@@ -0,0 +1,8 @@
{
"plugins": [
"remark-preset-lint-markdown-style-guide",
["remark-lint-list-item-spacing", false],
["remark-lint-maximum-line-length", false],
["remark-lint-no-duplicate-headings", false]
]
}

View File

@@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <admin@labbots.com>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>
[homepage]: https://www.contributor-covenant.org

View File

@@ -0,0 +1,129 @@
# Contributing to Bash-Utility
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
The following is a set of guidelines for contributing to this project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
## Table of Contents
- [Code Contributions](#code-contributions)
- [Code Guidelines](#code-guidelines)
- [Styleguide](#styleguide)
- [Bashdoc guideline](#bashdoc-guideline)
- [Documentation](#documentation)
- [Commit Guidelines](#commit-guidelines)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Contact](#contact)
## Code Contributions
Great, the more, the merrier.
Sane code contributions are always welcome, whether to the code or documentation.
Before making a pull request, make sure to follow below guidelines:
### Code Guidelines
#### Styleguide
- Variable names must be meaningful and self-documenting.
- Long variable names must be structured by underscores to improve legibility.
- Global variables and constants must be ALL CAPS with underscores. (eg., INPUT_FILE)
- local variables used within functions must be all lower case with underscores ( only if required ). (eg., input_data)
- Variable names can be alphanumeric with underscores. No special characters in variable names.
- Variables name must not start with number.
- Variables within function must be declared. So the scope of variable is restricted to the function.
- Avoid accessing global variables within functions.
- Function names must be all lower case with underscores to seperate words (snake_case).
- Function name must start with section name followed by 2 colons and then the function name (eg., `array::contains()`)
- Try using bash builtins and string substitution as much as possible.
- Use printf everywhere instead of echo.
- Before adding a new logic, be sure to check the existing code.
- Make sure to add the function in appropriate section based on its operation.
- Use [shfmt](https://github.com/mvdan/sh) to format the script. Use below command:
```shell
shfmt upload.sh
```
The repo already provides the .editorconfig file, which shfmt reads, so no need for extra flags.
You can also install shfmt for various editors, refer their repo for information.
Note: This is strictly necessary to maintain consistency, do not skip.
- Script should pass all [shellcheck](https://www.shellcheck.net/) warnings, if not, then disable the warning and give a valid reason.
#### Bashdoc guideline
The documentation is generated based on the function documentation within the script file. So ensure to follow the style so the documentation is
properly generated by the generator.
Follow the below bashdoc template to add function introductory comment.
```bash
# @description Multiline description goes here and
# there
#
# @example
# sample::function a b c
# echo 123
#
# @arg $1 string Some arg.
# @arg $2 any Rest of arguments.
#
# @noargs
#
# @exitcode 0 If successfull.
# @exitcode >0 On failure
# @exitcode 5 On some error.
#
# @stdout Path to something.
#
# @see sample::other_function(()
sample::function() {
}
```
- Each function must have a description detailing what the function does and a sample usage example to show how the function can be used.
- specify whether the function accepts args or no args by specifying @arg or @noargs tag in the comment.
- Make sure to document the exitcode emitted by the function.
- If the function is similar to other function add a reference to function using @see tag.
### Documentation
- Refrain from making unnecessary newlines or whitespace.
- Use pure markdown as much as possible, html is accepted but shouldn't be a priority.
- The markdown must pass RemarkLint checks.
- The function documentation and Table of Content is autogenerated using `generate_readme.sh`. So DO NOT edit them manually. Use the following command to update ToC and Bashdoc.
```bash
./bin/generate_readme.sh -f README.md -s src/
```
### Commit Guidelines
It is recommended to use small commits over one large commit. Small, focused commits make the review process easier and are more likely to be accepted.
It is also important to summarise the changes made with brief commit messages. If the commit fixes a specific issue, it is also good to note that in the commit message.
The commit message should start with a single line that briefly describes the changes. That should be followed by a blank line and then a more detailed explanation.
Before committing check for unnecessary whitespace with `git diff --check`.
### Pull Request Guidelines
The following guidelines will increase the likelihood that your pull request will get accepted:
- Follow the commit and code guidelines.
- Keep the patches on topic and focused.
- Try to avoid unnecessary formatting and clean-up where reasonable.
A pull request should contain the following:
- At least one commit (all of which should follow the Commit Guidelines).
- Title that summarises the issue/feature.
- Description that briefly summarises the changes.
After submitting a pull request, you should get a response within 7 days. If you do not, don't hesitate to ping the thread.
## Contact
For further inquiries, you can contact the developer by opening an issue on the repository.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 labbots
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# shellcheck disable=SC1091
source src/array.sh
source src/string.sh
source src/variable.sh
source src/file.sh
source src/misc.sh
source src/date.sh
source src/interaction.sh
source src/check.sh
source src/format.sh
source src/collection.sh
source src/json.sh
source src/terminal.sh
source src/validation.sh
source src/debug.sh
source src/os.sh

View File

@@ -0,0 +1,275 @@
#!/usr/bin/awk -f
# Varibles
# style = readme or doc
# toc = true or false
BEGIN {
if (! style) {
style = "doc"
}
if (! toc) {
toc = 0
}
styles["empty", "from"] = ".*"
styles["empty", "to"] = ""
styles["h1", "from"] = ".*"
styles["h1", "to"] = "# &"
styles["h2", "from"] = ".*"
styles["h2", "to"] = "## &"
styles["h3", "from"] = ".*"
styles["h3", "to"] = "### &"
styles["h4", "from"] = ".*"
styles["h4", "to"] = "#### &"
styles["h5", "from"] = ".*"
styles["h5", "to"] = "##### &"
styles["code", "from"] = ".*"
styles["code", "to"] = "```&"
styles["/code", "to"] = "```"
styles["argN", "from"] = "^(\\$[0-9]) (\\S+)"
styles["argN", "to"] = "**\\1** (\\2):"
styles["arg@", "from"] = "^\\$@ (\\S+)"
styles["arg@", "to"] = "**...** (\\1):"
styles["li", "from"] = ".*"
styles["li", "to"] = "- &"
styles["i", "from"] = ".*"
styles["i", "to"] = "*&*"
styles["anchor", "from"] = ".*"
styles["anchor", "to"] = "[&](#&)"
styles["exitcode", "from"] = "([>!]?[0-9]{1,3}) (.*)"
styles["exitcode", "to"] = "**\\1**: \\2"
styles["h_rule", "to"] = "---"
styles["comment", "from"] = ".*"
styles["comment", "to"] = "<!-- & -->"
output_format["readme", "h1"] = "h2"
output_format["readme", "h2"] = "h3"
output_format["readme", "h3"] = "h4"
output_format["readme", "h4"] = "h5"
output_format["bashdoc", "h1"] = "h1"
output_format["bashdoc", "h2"] = "h2"
output_format["bashdoc", "h3"] = "h3"
output_format["bashdoc", "h4"] = "h4"
output_format["webdoc", "h1"] = "empty"
output_format["webdoc", "h2"] = "h3"
output_format["webdoc", "h3"] = "h4"
output_format["webdoc", "h4"] = "h5"
}
function render(type, text) {
if((style,type) in output_format){
type = output_format[style,type]
}
return gensub( \
styles[type, "from"],
styles[type, "to"],
"g",
text \
)
}
function render_list(item, anchor) {
return "- [" item "](#" anchor ")"
}
function generate_anchor(text) {
# https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb#L44-L45
text = tolower(text)
gsub(/[^[:alnum:]_ -]/, "", text)
gsub(/ /, "-", text)
return text
}
function reset() {
has_example = 0
has_args = 0
has_exitcode = 0
has_stdout = 0
content_desc = ""
content_example = ""
content_args = ""
content_exitcode = ""
content_seealso = ""
content_stdout = ""
}
/^[[:space:]]*# @internal/ {
is_internal = 1
}
/^[[:space:]]*# @file/ {
sub(/^[[:space:]]*# @file /, "")
filedoc = render("h1", $0) "\n"
if(style == "webdoc"){
filedoc = filedoc render("comment", "file=" $0) "\n"
}
}
/^[[:space:]]*# @brief/ {
sub(/^[[:space:]]*# @brief /, "")
if(style == "webdoc"){
filedoc = filedoc render("comment", "brief=" $0) "\n"
}
filedoc = filedoc "\n" $0
}
/^[[:space:]]*# @description/ {
in_description = 1
in_example = 0
reset()
docblock = ""
}
in_description {
if (/^[^[[:space:]]*#]|^[[:space:]]*# @[^d]|^[[:space:]]*[^#]/) {
if (!match(content_desc, /\n$/)) {
content_desc = content_desc "\n"
}
in_description = 0
} else {
sub(/^[[:space:]]*# @description /, "")
sub(/^[[:space:]]*# /, "")
sub(/^[[:space:]]*#$/, "")
content_desc = content_desc "\n" $0
}
}
in_example {
if (! /^[[:space:]]*#[ ]{3}/) {
in_example = 0
content_example = content_example "\n" render("/code") "\n"
} else {
sub(/^[[:space:]]*#[ ]{3}/, "")
content_example = content_example "\n" $0
}
}
/^[[:space:]]*# @example/ {
in_example = 1
content_example = content_example "\n" render("h3", "Example")
content_example = content_example "\n\n" render("code", "bash")
}
/^[[:space:]]*# @arg/ {
if (!has_args) {
has_args = 1
content_args = content_args "\n" render("h3", "Arguments") "\n\n"
}
sub(/^[[:space:]]*# @arg /, "")
$0 = render("argN", $0)
$0 = render("arg@", $0)
content_args = content_args render("li", $0) "\n"
}
/^[[:space:]]*# @noargs/ {
content_args = content_args "\n" render("i", "Function has no arguments.") "\n"
}
/^[[:space:]]*# @exitcode/ {
if (!has_exitcode) {
has_exitcode = 1
content_exitcode = content_exitcode "\n" render("h3", "Exit codes") "\n\n"
}
sub(/^[[:space:]]*# @exitcode /, "")
$0 = render("exitcode", $0)
content_exitcode = content_exitcode render("li", $0) "\n"
}
/^[[:space:]]*# @see/ {
sub(/[[:space:]]*# @see /, "")
anchor = generate_anchor($0)
$0 = render_list($0, anchor)
content_seealso = content_seealso "\n" render("h3", "See also") "\n\n" $0 "\n"
}
/^[[:space:]]*# @stdout/ {
has_stdout = 1
sub(/^[[:space:]]*# @stdout /, "")
content_stdout = content_stdout "\n" render("h3", "Output on stdout")
content_stdout = content_stdout "\n\n" render("li", $0) "\n"
}
{
docblock = content_desc content_args content_exitcode content_stdout content_example content_seealso
if(style == "webdoc"){
docblock = docblock "\n" render("h_rule") "\n"
}
}
/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)([ \t]*)(\(([ \t]*)\))?[ \t]*\{/ && docblock != "" && !in_example {
if (is_internal) {
is_internal = 0
} else {
func_name = gensub(\
/^[ \t]*(function([ \t])+)?([a-zA-Z0-9_:-]+)[ \t]*\(.*/, \
"\\3()", \
"g" \
)
doc = doc "\n" render("h2", func_name) "\n" docblock
if (toc) {
url = generate_anchor(func_name)
content_idx = content_idx "\n" "- [" func_name "](#" url ")"
}
}
docblock = ""
reset()
}
END {
if (filedoc != "") {
print filedoc
}
if (toc) {
print ""
print render("h2", "Table of Contents")
print content_idx
print ""
print render("h_rule")
}
print doc
}

View File

@@ -0,0 +1,400 @@
#!/usr/bin/env bash
#https://github.com/bkrem/make-toc.sh/blob/master/make-toc.sh
#https://gitlab.com/pedrolab/doctoc.sh/-/blob/master/doctoc.sh
_usage() {
printf "
Script to autogenerate markdown based on bash source code.\n
The script generates table of contents and bashdoc and update the given markdown file.\n
Usage:\n %s [options.. ]\n
Options:\n
-f | --file <filename.md> - Relative or absolute path to the README.md file.
-s | --sh-dir <folderpath> - path to the bash script source folder to generate shdocs.\n
-l | --toc-level <number> - Minimum level of header to print in Table of Contents.\n
-d | --toc-depth <number> - Maximum depth of tree to print in Table of Contents.\n
-w | --webdoc - Flag to indicate generation of webdoc.\n
-p | --dest-dir <folderpath> - Path in which wedoc files must be generated.\n
-h | --help - Display usage instructions.\n" "${0##*/}"
exit 0
}
_setup_arguments() {
unset MINLEVEL MAXLEVEL SCRIPT_FILE SOURCE_MARKDOWN SOURCE_SCRIPT_DIR SCRIPT_DIR WEBDOC WEBDOC_DEST_DIR
MINLEVEL=1
MAXLEVEL=3
SCRIPT_FILE="${0##*/}"
declare source="${BASH_SOURCE[0]}"
while [ -h "$source" ]; do # resolve $source until the file is no longer a symlink
SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)"
source="$(readlink "$source")"
[[ $source != /* ]] && source="$SCRIPT_DIR/$source" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
done
SCRIPT_DIR="$(cd -P "$(dirname "$source")" > /dev/null 2>&1 && pwd)"
SOURCE_MARKDOWN="${SCRIPT_DIR}/../README.md"
SOURCE_SCRIPT_DIR="${SCRIPT_DIR}/../src"
WEBDOC_DEST_DIR="${SCRIPT_DIR}/../hugo-docs/content/functions"
SHORTOPTS="whp:f:m:d:s:-:"
while getopts "${SHORTOPTS}" OPTION; do
case "${OPTION}" in
-)
_check_longoptions() { { [[ -z "$1" ]] && printf '%s: --%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1; } || :; }
case "${OPTARG}" in
help)
_usage
;;
file)
_check_longoptions "${!OPTIND}"
SOURCE_MARKDOWN="${!OPTIND}" && OPTIND=$((OPTIND + 1))
;;
toc-level)
_check_longoptions "${!OPTIND}"
MINLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1))
;;
toc-depth)
_check_longoptions "${!OPTIND}"
MAXLEVEL="${!OPTIND}" && OPTIND=$((OPTIND + 1))
;;
sh-dir)
_check_longoptions "${!OPTIND}"
SOURCE_SCRIPT_DIR="${!OPTIND}" && OPTIND=$((OPTIND + 1))
;;
webdoc)
WEBDOC=true
;;
dest-dir)
WEBDOC_DEST_DIR="${OPTARG}"
;;
'')
_usage
;;
*)
printf '%s: --%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1
;;
esac
;;
h)
_usage
;;
f)
SOURCE_MARKDOWN="${OPTARG}"
;;
m)
MINLEVEL="${OPTARG}"
;;
d)
MAXLEVEL="${OPTARG}"
;;
s)
SOURCE_SCRIPT_DIR="${OPTARG}"
;;
w)
WEBDOC=true
;;
p)
WEBDOC_DEST_DIR="${OPTARG}"
;;
:)
printf '%s: -%s: option requires an argument\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1
;;
?)
printf '%s: -%s: Unknown option\nTry '"%s -h/--help"' for more information.\n' "${0##*/}" "${OPTARG}" "${0##*/}" && exit 1
;;
esac
done
shift "$((OPTIND - 1))"
if [[ -w "${SOURCE_MARKDOWN}" ]]; then
declare src_file src_extension
src_file="${SOURCE_MARKDOWN##*/}"
src_extension="${src_file##*.}"
if [[ "${src_extension,,}" != "md" ]]; then
printf "Provided file %s is not a markdown.\n" "${src_file}" && exit 1
fi
else
printf "Provided file %s does not exist or no enough permission to access it.\n" "${SOURCE_MARKDOWN}" && exit 1
fi
if [[ ! -d "${SOURCE_SCRIPT_DIR}" ]]; then
printf "Provided directory for bash script files %s does not exist.\n" "${SOURCE_SCRIPT_DIR}" && exit 1
fi
declare re='^[0-9]+$'
if ! [[ "${MINLEVEL}" =~ $re ]] || ! [[ "${MAXLEVEL}" =~ $re ]]; then
echo "error: Not a number" >&2
exit 1
fi
if [[ "${MINLEVEL}" -gt "${MAXLEVEL}" ]]; then
printf "Minimum level for TOC cannot be greater than the depth of TOC to be printed.\n" && exit 1
fi
[ -d "${WEBDOC_DEST_DIR}" ] || mkdir -p "${WEBDOC_DEST_DIR}"
}
_setup_tempfile() {
declare temp_file
type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; }
trap 'rm -f "${temp_file}"' EXIT
printf "%s" "${temp_file}"
}
_generate_shdoc() {
declare file
file="$(realpath "${1}")"
if [[ -s "${file}" ]]; then
awk -v style="readme" -v toc=0 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "$2"
#awk -v style="doc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "../docs/${file##*/}.md"
fi
}
_insert_shdoc_to_file() {
declare shdoc_tmp_file source_markdown start_shdoc info_shdoc end_shdoc
shdoc_tmp_file="$1"
source_markdown="$2"
start_shdoc="<!-- START ${SCRIPT_FILE} generated SHDOC please keep comment here to allow auto update -->"
info_shdoc="<!-- DO NOT EDIT THIS SECTION, INSTEAD RE-RUN ${SCRIPT_FILE} TO UPDATE -->"
end_shdoc="<!-- END ${SCRIPT_FILE} generated SHDOC please keep comment here to allow auto update -->"
sed -i "1s/^/${info_shdoc}\n/" "${shdoc_tmp_file}"
if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${source_markdown}" &> /dev/null; then
# src https://stackoverflow.com/questions/2699666/replace-delimited-block-of-text-in-file-with-the-contents-of-another-file
sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${source_markdown}"
echo -e "Updated bashdoc content to ${source_markdown} successfully\n"
else
{
printf "%s\n" "${start_shdoc}"
cat "${shdoc_tmp_file}"
printf "%s\n" "${end_shdoc}"
} >> "${source_markdown}"
echo -e "Created bashdoc content to ${source_markdown} successfully\n"
fi
}
_process_sh_files() {
declare shdoc_tmp_file source_script_dir source_markdown
source_markdown="${1}"
source_script_dir="${2}"
shdoc_tmp_file=$(_setup_tempfile)
find "${source_script_dir}" -name '*.sh' -print0 | sort -z |
while IFS= read -r -d '' line; do
_generate_shdoc "${line}" "${shdoc_tmp_file}"
done
_insert_shdoc_to_file "${shdoc_tmp_file}" "${source_markdown}"
rm "${shdoc_tmp_file}"
}
_generate_toc() {
declare line level title anchor output counter temp_output invalid_chars
invalid_chars="'[]/?!:\`.,()*\";{}+=<>~$|#@&–—"
while IFS='' read -r line || [[ -n "${line}" ]]; do
level="$(echo "${line}" | sed -E 's/(#+).*/\1/; s/#/ /g; s/^ //')"
title="$(echo "${line}" | sed -E 's/^#+ //')"
[[ "${title}" = "Table of Contents" ]] && continue
# tr does not do OK the lowercase for non ascii chars, add sed to pipeline -> src https://stackoverflow.com/questions/13381746/tr-upper-lower-with-cyrillic-text
anchor="$(echo "${title}" | tr '[:upper:] ' '[:lower:]-' | sed 's/[[:upper:]]*/\L&/' | tr -d "${invalid_chars}")"
# check new line introduced is not duplicated, if is duplicated, introduce a number at the end
temp_output=$output"$level- [$title](#$anchor)\n"
counter=1
while true; do
nlines="$(echo -e "${temp_output}" | wc -l)"
duplines="$(echo -e "${temp_output}" | sort | uniq | wc -l)"
if [ "${nlines}" = "${duplines}" ]; then
break
fi
temp_output=$output"$level- [$title](#$anchor-$counter)\n"
counter=$((counter + 1))
done
output="$temp_output"
# grep: filter header candidates to be included in toc
# sed: remove the ignored headers (case: minlevel greater than one) to avoid unnecessary spacing later in level variable assignment
done <<< "$(grep -E "^#{${MINLEVEL},${MAXLEVEL}} " "${1}" | tr -d '\r' | sed "s/^#\{$((MINLEVEL - 1))\}//g")"
# when in toc we have two `--` quit one
echo "$output"
}
_insert_toc_to_file() {
declare source_markdown toc_text start_toc info_toc end_toc toc_block utext_ampersand utext_slash
source_markdown="${1}"
toc_text="${2}"
start_toc="<!-- START ${SCRIPT_FILE} generated TOC please keep comment here to allow auto update -->"
info_toc="<!-- DO NOT EDIT THIS SECTION, INSTEAD RE-RUN ${SCRIPT_FILE} TO UPDATE -->"
end_toc="<!-- END ${SCRIPT_FILE} generated TOC please keep comment here to allow auto update -->"
toc_block="$start_toc\n$info_toc\n## Table of Contents\n\n$toc_text\n$end_toc"
# temporary replace of '/' (confused with separator of substitutions) and '&' (confused with match regex symbol) to run the special sed command
utext_ampersand="id8234923000230gzz"
utext_slash="id9992384923423gzz"
toc_block="${toc_block//\&/${utext_ampersand}}"
toc_block="${toc_block//\//${utext_slash}}"
# search multiline toc block -> https://stackoverflow.com/questions/2686147/how-to-find-patterns-across-multiple-lines-using-grep/2686705
# grep color for debugging -> https://superuser.com/questions/914856/grep-display-all-output-but-highlight-search-matches
if grep --color=always -Pzl "(?s)${start_toc}.*\n.*${end_toc}" "${source_markdown}" &> /dev/null; then
# src https://askubuntu.com/questions/533221/how-do-i-replace-multiple-lines-with-single-word-in-fileinplace-replace
sed -i ":a;N;\$!ba;s/$start_toc.*$end_toc/$toc_block/g" "${source_markdown}"
echo -e "Updated TOC content in ${source_markdown} succesfully\n"
else
sed -i 1i"$toc_block" "${source_markdown}"
echo -e "Created TOC in ${source_markdown} succesfully\n"
fi
# undo symbol replacements
sed -i "s,${utext_ampersand},\&,g" "${source_markdown}"
sed -i "s,${utext_slash},\/,g" "${source_markdown}"
}
_process_toc() {
declare toc_temp_file source_markdown level toc_text
source_markdown="${1}"
toc_temp_file=$(_setup_tempfile)
sed '/```/,/```/d' "${source_markdown}" > "${toc_temp_file}"
level=$MINLEVEL
while [[ $(grep -Ec "^#{$level} " "${toc_temp_file}") -le 1 ]]; do
level=$((level + 1))
done
MINLEVEL=${level}
toc_text=$(_generate_toc "${toc_temp_file}")
rm "${toc_temp_file}"
_insert_toc_to_file "${source_markdown}" "${toc_text}"
}
_generate_webdoc() {
declare dest_dir filename file_basename dest_file_path shdoc_tmp_file is_new_file
declare title description start_shdoc end_shdoc file_modified_date file_modified_date_epoc
declare webdoc_lastmod_date webdoc_lastmod_epoc
file="$(realpath "${1}")"
dest_dir="${2}"
filename="${file##*/}"
file_basename="${filename%.*}"
dest_file_path="${dest_dir}/${file_basename}.md"
file_modified_date="$(date -r "${file}" +"%FT%T%:z")"
file_modified_date_epoc="$(date -r "${file}" +"%s")"
start_shdoc="<!-- START ${SCRIPT_FILE} generated SHDOC please keep comment here to allow auto update -->"
end_shdoc="<!-- END ${SCRIPT_FILE} generated SHDOC please keep comment here to allow auto update -->"
if [[ ! -f "${dest_file_path}" ]]; then
cat << EOF > "${dest_file_path}"
---
title : <!-- file -->
description : <!-- brief -->
date : ${file_modified_date}
lastmod : ${file_modified_date}
---
${start_shdoc}
${end_shdoc}
EOF
is_new_file=true
else
is_new_file=false
webdoc_lastmod_date="$(sed -ne 's/-->//; s/^.*lastmod : //p' "${dest_file_path}")"
webdoc_lastmod_epoc="$(date -d "${webdoc_lastmod_date}" +"%s")"
fi
if [[ "${is_new_file}" = true || "${file_modified_date_epoc}" -gt "${webdoc_lastmod_epoc}" ]]; then
shdoc_tmp_file=$(_setup_tempfile)
if [[ -s "${file}" ]]; then
awk -v style="webdoc" -v toc=1 -f "${SCRIPT_DIR}"/bashdoc.awk < "${file}" >> "${shdoc_tmp_file}"
fi
if grep --color=always -Pzl "(?s)${start_shdoc}.*\n.*${end_shdoc}" "${dest_file_path}" &> /dev/null; then
sed -i -ne "/${start_shdoc}/ {p; r ${shdoc_tmp_file}" -e ":a; n; /${end_shdoc}/ {p; b}; ba}; p" "${dest_file_path}"
fi
# Extract title and description from webdoc
title="$(sed -ne 's/-->//; s/^.*<!-- file=//p' "${dest_file_path}")"
description="$(sed -ne 's/-->//; s/^.*<!-- brief=//p' "${dest_file_path}")"
sed -i '/^.*<!-- file=/d' "${dest_file_path}"
sed -i '/^.*<!-- brief=/d' "${dest_file_path}"
# Replace Frontmatter content with values from the document
if [[ "${is_new_file}" = true ]]; then
sed -i -e "s/<!-- file -->/${title}/g" "${dest_file_path}"
sed -i -e "s/<!-- brief -->/${description}/g" "${dest_file_path}"
else
sed -i -e "s/title : .*/title : ${title}/g" "${dest_file_path}"
sed -i -e "s/description : .*/description : ${description}/g" "${dest_file_path}"
fi
# Update the last modified timestamp in front matter
sed -i -e "s/lastmod : .*/lastmod : ${file_modified_date}/g" "${dest_file_path}"
echo -e "Updated bashdoc content to ${dest_file_path} successfully."
rm "${shdoc_tmp_file}"
fi
}
_process_webdoc_files() {
declare source_script_dir dest_dir
source_script_dir="${1}"
dest_dir="${2}"
find "${source_script_dir}" -name '*.sh' -print0 | sort -z |
while IFS= read -r -d '' line; do
_generate_webdoc "${line}" "${dest_dir}"
done
}
_count_library_functions() {
declare source_script_dir
source_script_dir="${1}"
find "${source_script_dir}" -name '*.sh' -print0 | sort -z |
{
declare -i function_count=0 count=0
while IFS= read -r -d '' line; do
count=0
count=$(grep -c '^[[:alpha:]]*::[[:alnum:]_]*()' "${line}")
function_count=$((function_count + count))
done
printf "Total library functions: %s \n" "${function_count}"
}
}
main() {
# export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
# set -x
trap 'exit "$?"' INT TERM && trap 'exit "$?"' EXIT
set -o errexit -o noclobber -o pipefail
_setup_arguments "${@}"
_process_sh_files "${SOURCE_MARKDOWN}" "${SOURCE_SCRIPT_DIR}"
_process_toc "${SOURCE_MARKDOWN}"
if [[ -n ${WEBDOC} ]]; then
_process_webdoc_files "${SOURCE_SCRIPT_DIR}" "${WEBDOC_DEST_DIR}"
fi
_count_library_functions "${SOURCE_SCRIPT_DIR}"
}
main "${@}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,284 @@
#!/usr/bin/env bash
# @file Array
# @brief Functions for array operations and manipulations.
# @description Check if item exists in the given array.
#
# @example
# array=("a" "b" "c")
# array::contains "c" ${array[@]}
# #Output
# 0
#
# @arg $1 mixed Item to search (needle).
# @arg $2 array array to be searched (haystack).
#
# @exitcode 0 If successful.
# @exitcode 1 If no match found in the array.
# @exitcode 2 Function missing arguments.
array::contains() {
[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare query="${1:-}"
shift
for element in "${@}"; do
[[ "${element}" == "${query}" ]] && return 0
done
return 1
}
# @description Remove duplicate items from the array.
#
# @example
# array=("a" "b" "a" "c")
# printf "%s" "$(array::dedupe ${array[@]})"
# #Output
# a
# b
# c
#
# @arg $1 array Array to be deduped.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout Deduplicated array.
array::dedupe() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -A arr_tmp
declare -a arr_unique
for i in "$@"; do
{ [[ -z ${i} || ${arr_tmp[${i}]} ]]; } && continue
arr_unique+=("${i}") && arr_tmp[${i}]=x
done
printf '%s\n' "${arr_unique[@]}"
}
# @description Check if a given array is empty.
#
# @example
# array=("a" "b" "c" "d")
# array::is_empty "${array[@]}"
#
# @arg $1 array Array to be checked.
#
# @exitcode 0 If the given array is empty.
# @exitcode 2 If the given array is not empty.
array::is_empty() {
declare -a array
local array=("$@")
if [ ${#array[@]} -eq 0 ]; then
return 0
else
return 1
fi
}
# @description Join array elements with a string.
#
# @example
# array=("a" "b" "c" "d")
# printf "%s" "$(array::join "," "${array[@]}")"
# #Output
# a,b,c,d
# printf "%s" "$(array::join "" "${array[@]}")"
# #Output
# abcd
#
# @arg $1 string String to join the array elements (glue).
# @arg $2 array array to be joined with glue string.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout String containing a string representation of all the array elements in the same order,with the glue string between each element.
array::join() {
[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare delimiter="${1}"
shift
printf "%s" "${1}"
shift
printf "%s" "${@/#/${delimiter}}"
}
# @description Return an array with elements in reverse order.
#
# @example
# array=(1 2 3 4 5)
# printf "%s" "$(array::reverse "${array[@]}")"
# #Output
# 5 4 3 2 1
#
# @arg $1 array The input array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout The reversed array.
array::reverse() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare min=0
declare -a array
array=("$@")
declare max=$((${#array[@]} - 1))
while [[ $min -lt $max ]]; do
# Swap current first and last elements
x="${array[$min]}"
array[$min]="${array[$max]}"
array[$max]="$x"
# Move closer
((min++, max--))
done
printf '%s\n' "${array[@]}"
}
# @description Returns a random item from the array.
#
# @example
# array=("a" "b" "c" "d")
# printf "%s\n" "$(array::random_element "${array[@]}")"
# #Output
# c
#
# @arg $1 array The input array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout Random item out of the array.
array::random_element() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -a array
local array=("$@")
printf '%s\n' "${array[RANDOM % $#]}"
}
# @description Sort an array from lowest to highest.
#
# @example
# sarr=("a c" "a" "d" 2 1 "4 5")
# array::array_sort "${sarr[@]}"
# #Output
# 1
# 2
# 4 5
# a
# a c
# d
#
# @arg $1 array The input array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout sorted array.
array::sort() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -a array=("$@")
declare -a sorted
declare noglobtate
noglobtate="$(shopt -po noglob)"
set -o noglob
declare IFS=$'\n'
sorted=($(sort <<< "${array[*]}"))
unset IFS
eval "${noglobtate}"
printf "%s\n" "${sorted[@]}"
}
# @description Sort an array in reverse order (highest to lowest).
#
# @example
# sarr=("a c" "a" "d" 2 1 "4 5")
# array::array_sort "${sarr[@]}"
# #Output
# d
# a c
# a
# 4 5
# 2
# 1
#
# @arg $1 array The input array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout reverse sorted array.
array::rsort() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -a array=("$@")
declare -a sorted
declare noglobtate
noglobtate="$(shopt -po noglob)"
set -o noglob
declare IFS=$'\n'
sorted=($(sort -r<<< "${array[*]}"))
unset IFS
eval "${noglobtate}"
printf "%s\n" "${sorted[@]}"
}
# @description Bubble sort an integer array from lowest to highest.
# This sort does not work on string array.
# @example
# iarr=(4 5 1 3)
# array::bsort "${iarr[@]}"
# #Output
# 1
# 3
# 4
# 5
#
# @arg $1 array The input array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout bubble sorted array.
array::bsort() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare tmp
declare arr=("$@")
for ((i = 0; i <= $((${#arr[@]} - 2)); ++i)); do
for ((j = ((i + 1)); j <= ((${#arr[@]} - 1)); ++j)); do
if [[ ${arr[i]} -gt ${arr[j]} ]]; then
# echo $i $j ${arr[i]} ${arr[j]}
tmp=${arr[i]}
arr[i]=${arr[j]}
arr[j]=$tmp
fi
done
done
printf "%s\n" "${arr[@]}"
}
# @description Merge two arrays.
# Pass the variable name of the array instead of value of the variable.
# @example
# a=("a" "c")
# b=("d" "c")
# array::merge "a[@]" "b[@]"
# #Output
# a
# c
# d
# c
#
# @arg $1 string variable name of first array.
# @arg $2 string variable name of second array.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout Merged array.
array::merge() {
[[ $# -ne 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -a arr1=("${!1}")
declare -a arr2=("${!2}")
declare out=("${arr1[@]}" "${arr2[@]}")
printf "%s\n" "${out[@]}"
}

View File

@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# @file Check
# @brief Helper functions.
# @description Check if the command exists in the system.
#
# @example
# check::command_exists "tput"
#
# @arg $1 string Command name to be searched.
#
# @exitcode 0 If the command exists.
# @exitcode 1 If the command does not exist.
# @exitcode 2 Function missing arguments.
check::command_exists() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
hash "${1}" 2> /dev/null
}
# @description Check if the script is executed with sudo privilege.
#
# @example
# check::is_sudo
#
# @noargs
#
# @exitcode 0 If the script is executed with root privilege.
# @exitcode 1 If the script is not executed with root privilege
check::is_sudo() {
if [[ $(id -u) -ne 0 ]]; then
return 1
fi
}

View File

@@ -0,0 +1,287 @@
#!/usr/bin/env bash
# @file Collection
# @brief (Experimental) Functions to iterates over a list of elements, yielding each in turn to an iteratee function.
# @description Iterates over elements of collection and invokes iteratee for each element.
# Input to the function can be a pipe output, here-string or file.
# @example
# test_func(){
# printf "print value: %s\n" "$1"
# return 0
# }
# arr1=("a b" "c d" "a" "d")
# printf "%s\n" "${arr1[@]}" | collection::each "test_func"
# collection::each "test_func" < <(printf "%s\n" "${arr1[@]}") #alternative approach
# #output
# print value: a b
# print value: c d
# print value: a
# print value: d
#
# @example
# # If other function from this library is already used to process the array.
# # Then following method could be used to pass the array to the function.
# out=("$(array::dedupe "${arr1[@]}")")
# collection::each "test_func" <<< "${out[@]}"
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
# @exitcode other exitcode returned by iteratee.
#
# @stdout Output of iteratee function.
collection::each() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'${it}'"
fi
declare -i ret="$?"
if [[ $ret -ne 0 ]]; then
return $ret
fi
done
}
# @description Checks if iteratee function returns truthy for all elements of collection. Iteration is stopped once predicate returns false.
# Input to the function can be a pipe output, here-string or file.
# @example
# arri=("1" "2" "3" "4")
# printf "%s\n" "${arri[@]}" | collection::every "variable::is_numeric"
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 1 If iteratee function fails.
# @exitcode 2 Function missing arguments.
collection::every() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'${it}'"
fi
declare -i ret="$?"
if [[ $ret -ne 0 ]]; then
return 1
fi
done
}
# @description Iterates over elements of array, returning all elements where iteratee returns true.
# Input to the function can be a pipe output, here-string or file.
# @example
# arri=("1" "2" "3" "a")
# printf "%s\n" "${arri[@]}" | collection::filter "variable::is_numeric"
# #output
# 1
# 2
# 3
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout array values matching the iteratee function.
collection::filter() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'${it}'"
fi
declare -i ret="$?"
if [[ $ret = 0 ]]; then
printf "%s\n" "${it}"
fi
done
}
# @description Iterates over elements of collection, returning the first element where iteratee returns true.
# Input to the function can be a pipe output, here-string or file.
# @example
# arr=("1" "2" "3" "a")
# check_a(){
# [[ "$1" = "a" ]]
# }
# printf "%s\n" "${arr[@]}" | collection::find "check_a"
# #Output
# a
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 1 If no match found.
# @exitcode 2 Function missing arguments.
#
# @stdout first array value matching the iteratee function.
collection::find() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'${it}'"
fi
declare -i ret="$?"
if [[ $ret = 0 ]]; then
printf "%s" "${it}"
return 0
fi
done
return 1
}
# @description Invokes the iteratee with each element passed as argument to the iteratee.
# Input to the function can be a pipe output, here-string or file.
# @example
# opt=("-a" "-l")
# printf "%s\n" "${opt[@]}" | collection::invoke "ls"
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
# @exitcode other exitcode returned by iteratee.
#
# @stdout Output from the iteratee function.
collection::invoke() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare -a args=()
declare func="${1}"
while read -r it; do
args=("${args[@]}" "$it")
done
eval "${func}" "${args[@]}"
}
# @description Creates an array of values by running each element in array through iteratee.
# Input to the function can be a pipe output, here-string or file.
# @example
# arri=("1" "2" "3")
# add_one(){
# i=${1}
# i=$(( i + 1 ))
# printf "%s\n" "$i"
# }
# printf "%s\n" "${arri[@]}" | collection::map "add_one"
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
# @exitcode other exitcode returned by iteratee.
#
# @stdout Output result of iteratee on value.
collection::map() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
declare out
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
out="$("${func}")"
else
out="$("${func}" "$it")"
fi
declare -i ret=$?
if [[ $ret -ne 0 ]]; then
return $ret
fi
printf "%s\n" "${out}"
done
}
# @description The opposite of filter function; this method returns the elements of collection that iteratee does not return true.
# Input to the function can be a pipe output, here-string or file.
# @example
# arri=("1" "2" "3" "a")
# printf "%s\n" "${arri[@]}" | collection::reject "variable::is_numeric"
# #Ouput
# a
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout array values not matching the iteratee function.
# @see collection::filter
collection::reject() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'$it'"
fi
declare -i ret=$?
if [[ $ret -ne 0 ]]; then
echo "$it"
fi
done
}
# @description Checks if iteratee returns true for any element of the array.
# Input to the function can be a pipe output, here-string or file.
# @example
# arr=("a" "b" "3" "a")
# printf "%s\n" "${arr[@]}" | collection::reject "variable::is_numeric"
#
# @arg $1 string Iteratee function.
#
# @exitcode 0 If match successful.
# @exitcode 1 If no match found.
# @exitcode 2 Function missing arguments.
collection::some() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare func="${1}"
declare IFS=$'\n'
while read -r it; do
if [[ "${func}" == *"$"* ]]; then
eval "${func}"
else
eval "${func}" "'$it'"
fi
declare -i ret=$?
if [[ $ret -eq 0 ]]; then
return 0
fi
done
return 1
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# @file Debug
# @brief Functions to facilitate debugging scripts.
# @description Prints the content of array as key value pair for easier debugging.
# Pass the variable name of the array instead of value of the variable.
# @example
# array=(foo bar baz)
# printf "Array\n"
# printarr "array"
# declare -A assoc_array
# assoc_array=([foo]=bar [baz]=foobar)
# printf "Assoc Array\n"
# printarr "assoc_array"
# #Output
# Array
# 0 = foo
# 1 = bar
# 2 = baz
# Assoc Array
# baz = foobar
# foo = bar
#
# @arg $1 string variable name of the array.
#
# @stdout Formatted key value of array.
debug::print_array() {
declare -n __arr="$1"
for k in "${!__arr[@]}"; do printf "%s = %s\n" "$k" "${__arr[$k]}"; done
}
# @description Function to print ansi escape sequence as is.
# This function helps debug ansi escape sequence in text by displaying the escape codes.
#
# @example
# txt="$(tput bold)$(tput setaf 9)This is bold red text$(tput sgr0).$(tput setaf 10)This is green text$(tput sgr0)"
# debug::print_ansi "$txt"
# #Output
# \e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text\e(B\e[m
#
# @arg $1 string input with ansi escape sequence.
#
# @stdout Ansi escape sequence printed in output as is.
debug::print_ansi() {
#echo $(tr -dc '[:print:]'<<<$1)
printf "%s\n" "${1//$'\e'/\\e}"
}

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env bash
# @file File
# @brief Functions for handling files.
# @description Create temporary file.
# Function creates temporary file with random name. The temporary file will be deleted when script finishes.
#
# @example
# echo "$(file::make_temp_file)"
# #Output
# tmp.vgftzy
#
# @noargs
#
# @exitcode 0 If successful.
# @exitcode 1 If failed to create temp file.
#
# @stdout file name of temporary file created.
file::make_temp_file() {
declare temp_file
type -p mktemp &> /dev/null && { temp_file="$(mktemp -u)" || temp_file="${PWD}/$((RANDOM * 2)).LOG"; }
trap 'rm -f "${temp_file}"' EXIT
printf "%s" "${temp_file}"
}
# @description Create temporary directory.
# Function creates temporary directory with random name. The temporary directory will be deleted when script finishes.
#
# @example
# echo "$(utility::make_temp_dir)"
# #Output
# tmp.rtfsxy
#
# @arg $1 string Temporary directory prefix
# @arg $2 string Flag to auto remove directory on exit trap (true)
#
# @exitcode 0 If successful.
# @exitcode 1 If failed to create temp directory.
# @exitcode 2 Missing arguments.
#
# @stdout directory name of temporary directory created.
file::make_temp_dir() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare temp_dir prefix="${1}" trap_rm="${2}"
temp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t "${prefix}")
if [[ -n "${trap_rm}" ]]; then
trap 'rm -rf "${temp_dir}"' EXIT
fi
printf "%s" "${temp_dir}"
}
# @description Get only the filename from string path.
#
# @example
# echo "$(file::name "/path/to/test.md")"
# #Output
# test.md
#
# @arg $1 string path.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout name of the file with extension.
file::name() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
printf "%s" "${1##*/}"
}
# @description Get the basename of file from file name.
#
# @example
# echo "$(file::basename "/path/to/test.md")"
# #Output
# test
#
# @arg $1 string path.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout basename of the file.
file::basename() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare file basename
file="${1##*/}"
basename="${file%.*}"
printf "%s" "${basename}"
}
# @description Get the extension of file from file name.
#
# @example
# echo "$(file::extension "/path/to/test.md")"
# #Output
# md
#
# @arg $1 string path.
#
# @exitcode 0 If successful.
# @exitcode 1 If no extension is found in the filename.
# @exitcode 2 Function missing arguments.
#
# @stdout extension of the file.
file::extension() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare file extension
file="${1##*/}"
extension="${file##*.}"
[[ "${file}" = "${extension}" ]] && return 1
printf "%s" "${extension}"
}
# @description Get directory name from file path.
#
# @example
# echo "$(file::dirname "/path/to/test.md")"
# #Output
# /path/to
#
# @arg $1 string path.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout directory path.
file::dirname() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare tmp=${1:-.}
[[ ${tmp} != *[!/]* ]] && { printf '/\n' && return; }
tmp="${tmp%%"${tmp##*[!/]}"}"
[[ ${tmp} != */* ]] && { printf '.\n' && return; }
tmp=${tmp%/*} && tmp="${tmp%%"${tmp##*[!/]}"}"
printf '%s' "${tmp:-/}"
}
# @description Get absolute path of file or directory.
#
# @example
# file::full_path "../path/to/file.md"
# #Output
# /home/labbots/docs/path/to/file.md
#
# @arg $1 string relative or absolute path to file/direcotry.
#
# @exitcode 0 If successful.
# @exitcode 1 If file/directory does not exist.
# @exitcode 2 Function missing arguments.
#
# @stdout Absolute path to file/directory.
file::full_path() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare input="${1}"
if [[ -f ${input} ]]; then
printf "%s/%s\n" "$(cd "$(file::dirname "${input}")" && pwd)" "${input##*/}"
elif [[ -d ${input} ]]; then
printf "%s\n" "$(cd "${input}" && pwd)"
else
return 1
fi
}
# @description Get mime type of provided input.
#
# @example
# file::mime_type "../src/file.sh"
# #Output
# application/x-shellscript
#
# @arg $1 string relative or absolute path to file/directory.
#
# @exitcode 0 If successful.
# @exitcode 1 If file/directory does not exist.
# @exitcode 2 Function missing arguments.
# @exitcode 3 If file or mimetype command not found in system.
#
# @stdout mime type of file/directory.
file::mime_type() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare mime_type
if [[ -f "${1}" ]] || [[ -d "${1}" ]]; then
if type -p mimetype &> /dev/null; then
mime_type=$(mimetype --output-format %m "${1}")
elif type -p file &> /dev/null; then
mime_type=$(file --brief --mime-type "${1}")
else
return 3
fi
else
return 1
fi
printf "%s" "${mime_type}"
}
# @description Search if a given pattern is found in file.
#
# @example
# file::contains_text "./file.sh" "^[ @[:alpha:]]*"
# file::contains_text "./file.sh" "@file"
# #Output
# 0
#
# @arg $1 string relative or absolute path to file/directory.
# @arg $2 string search key or regular expression.
#
# @exitcode 0 If given search parameter is found in file.
# @exitcode 1 If search paramter not found in file.
# @exitcode 2 Function missing arguments.
file::contains_text() {
[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1
declare -r file="$1"
declare -r text="$2"
grep -q "$text" "$file"
}

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env bash
# @file Format
# @brief Functions to format provided input.
# @internal
# @description Initialisation script when the code is sourced.
#
# @noargs
__init(){
_check_terminal_window_size
}
# @internal
# @description Checks the terminal window size, if necessary, updates the values of LINES and COLUMNS.
#
# @noargs
_check_terminal_window_size() {
shopt -s checkwinsize && (: && :)
trap 'shopt -s checkwinsize; (:;:)' SIGWINCH
}
# @description Format seconds to human readable format.
#
# @example
# echo "$(format::human_readable_seconds "356786")"
# #Output
# 4 days 3 hours 6 minute(s) and 26 seconds
#
# @arg $1 int number of seconds.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout formatted time string.
format::human_readable_seconds() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare T="${1}"
declare DAY="$((T / 60 / 60 / 24))" HR="$((T / 60 / 60 % 24))" MIN="$((T / 60 % 60))" SEC="$((T % 60))"
[[ ${DAY} -gt 0 ]] && printf '%d days ' "${DAY}"
[[ ${HR} -gt 0 ]] && printf '%d hours ' "${HR}"
[[ ${MIN} -gt 0 ]] && printf '%d minute(s) ' "${MIN}"
[[ ${DAY} -gt 0 || ${HR} -gt 0 || ${MIN} -gt 0 ]] && printf 'and '
printf '%d seconds\n' "${SEC}"
}
# @description Format bytes to human readable format.
#
# @example
# echo "$(format::bytes_to_human "2250")"
# #Output
# 2.19 KB
#
# @arg $1 int size in bytes.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout formatted file size string.
format::bytes_to_human() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 2
declare b=${1:-0} d='' s=0 S=(Bytes {K,M,G,T,P,E,Y,Z}B)
while ((b > 1024)); do
d="$(printf ".%02d" $((b % 1024 * 100 / 1024)))"
b=$((b / 1024)) && ((s++))
done
printf "%s\n" "${b}${d} ${S[${s}]}"
}
# @description Remove Ansi escape sequences from given text.
#
# @example
# format::strip_ansi "\e[1m\e[91mThis is bold red text\e(B\e[m.\e[92mThis is green text.\e(B\e[m"
# #Output
# This is bold red text.This is green text.
#
# @arg $1 string Input text to be ansi stripped.
#
# @exitcode 0 If successful.
#
# @stdout Ansi stripped text.
format::strip_ansi() {
declare tmp esc tpa re
tmp="${1}"
esc=$(printf "\x1b")
tpa=$(printf "\x28")
re="(.*)${esc}[\[${tpa}][0-9]*;*[mKB](.*)"
while [[ "${tmp}" =~ $re ]]; do
tmp="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
done
printf "%s" "${tmp}"
}
# @description Prints the given text to centre of terminal.
#
# @example
# format::text_center "This text is in centre of the terminal." "-"
#
# @arg $1 string Text to be printed.
# @arg $2 string Filler symbol to be added to prefix and suffix of the text (optional). Defaults to space as filler.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout formatted text.
format::text_center() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1
declare input="${1}" symbol="${2:- }" filler out no_ansi_out
no_ansi_out=$(format::strip_ansi "$input")
declare -i str_len=${#no_ansi_out}
declare -i filler_len="$(((COLUMNS - str_len) / 2))"
[[ -n "${symbol}" ]] && symbol="${symbol:0:1}"
for ((i = 0; i < filler_len; i++)); do
filler+="${symbol}"
done
out="${filler}${input}${filler}"
[[ $(((COLUMNS - str_len) % 2)) -ne 0 ]] && out+="${symbol}"
printf "%s" "${out}"
}
# @description Format String to print beautiful report.
#
# @example
# format::report "Initialising mission state" "Success"
# #Output
# Initialising mission state ....................................................................[ Success ]
#
# @arg $1 string Text to be printed on the left.
# @arg $2 string Text to be printed within the square brackets.
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout formatted text.
format::report() {
[[ $# -lt 2 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1
declare symbol="." to_print y hl hlout out
declare input1="${1} " input2="${2}"
input2="[ $input2 ]"
to_print="$((COLUMNS * 60 / 100))"
y=$(( to_print - ( ${#input1} + ${#input2} ) ))
hl="$(printf '%*s' $y '')"
hlout=${hl// /${symbol}}
out="${input1}${hlout}${input2}"
printf "%s\n" "${out}"
}
# @description Trim given text to width of the terminal window.
#
# @example
# format::trim_text_to_term "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." "This is part of second sentence."
# #Output
# Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod..This is part of second sentence.
#
# @arg $1 string Text of first sentence.
# @arg $2 string Text of second sentence (optional).
#
# @exitcode 0 If successful.
# @exitcode 2 Function missing arguments.
#
# @stdout trimmed text.
format::trim_text_to_term() {
[[ $# = 0 ]] && printf "%s: Missing arguments\n" "${FUNCNAME[0]}" && return 1
declare to_print out input1="$1" input2="$2"
if [[ $# = 1 ]]; then
to_print="$((COLUMNS * 93 / 100))"
{ [[ ${#input1} -gt ${to_print} ]] && out="${input1:0:to_print}.."; } || { out="$input1"; }
else
to_print="$((COLUMNS * 40 / 100))"
{ [[ ${#input1} -gt ${to_print} ]] && out+=" ${input1:0:to_print}.."; } || { out+=" $input1"; }
to_print="$((COLUMNS * 53 / 100))"
{ [[ ${#input2} -gt ${to_print} ]] && out+="${input2:0:to_print}.. "; } || { out+="$input2 "; }
fi
printf "%s" "$out"
}
__init

Some files were not shown because too many files have changed in this diff Show More