diff --git a/Templates/CandidateInfoSheet/CandidateInfoSheet.yml b/Templates/CandidateInfoSheet/CandidateInfoSheet.yml new file mode 100644 index 0000000..2d72aa8 --- /dev/null +++ b/Templates/CandidateInfoSheet/CandidateInfoSheet.yml @@ -0,0 +1,14 @@ +title: "Charles N Wyble Candidate Details" +titlepage: true +titlepage-logo: "D:/tsys/@ReachableCEO/ReachableCEO.png" +toc: true +toc-own-page: true +date: \today +header-left: "\\hspace{1cm}" +header-center: "\\leftmark" +header-right: "Page \\thepage" +footer-left: "Charles N Wyble" +footer-center: "Tenacity. Velocity. Focus." +footer-right: "[Source code for this file](https://github.com/ReachableCEO/ReachableCEOResume/blob/main/ancillary-support-files/CharlesNWybleCandidateInfo.md)" +urlcolor: blue +page-background: "D:/tsys/@ReachableCEO/ExternalVendorCode/pandoc-latex-template/examples/page-background/backgrounds/background1.pdf" \ No newline at end of file diff --git a/Templates/CandidateInfoSheet/CharlesNWybleCandidateInfo.md b/Templates/CandidateInfoSheet/CharlesNWybleCandidateInfo.md new file mode 100644 index 0000000..47e9c58 --- /dev/null +++ b/Templates/CandidateInfoSheet/CharlesNWybleCandidateInfo.md @@ -0,0 +1,110 @@ +# Charles N Wyble + +## Introduction + +Hello, + +I apologize for the form letter response. + +I receive a high volume of recruiter emails every day and I've found this letter to be the most efficient way to +handle the high volume of emails and reduce back and forth emails/texts/calls. + +If you have any questions/comments/concerns not covered by this document, please let me know via e-mail and I'm happy to address them! + +If you ask me something answered in this document, I will not respond and will not move forward with the opportunity, so please read it in detail! + +## Re: share my ID over email + +I WILL NOT share my (full or redacted) photo ID over email or any other electronic written +communication. If that is "required" then I have no interest in moving forward with this opportunity. + +I am happy to get on a teams/zoom/google meet etc call and show my ID. + +## Re: professional references + +I am happy to provide professional references once an interview with the end client/customer/hiring manager/team has been scheduled. I will NOT provide references up front. If that is "required" then I have no interest in moving forward with this opportunity. + +## Re: relocation + +if the role is not based in **Austin TX** or **Raleigh NC** I will need to re-locate + +| Question | Answer | +|-------------------------------------------|--------| +| Am I open to relocation? | Yes | +| Am I willing to re-locate at own expense? | No | +| Am I open to up to 100% travel | Yes | + +Please be aware that: + +- I will **only re-locate at the employer expense**. +- I will need **two weeks of time** to re-locate. +- The net amount of the re-location benefit **MUST be at least $5,000 USD** to fully compensate me for the time/effort to re-locate. +- The full re-location benefit **must be provided prior to the confirmed start date**. +- I **will NOT** accept a reimbursement based re-location package. +- I am happy to come onsite (at client expense (paid up front)) for training/orientation etc. + +\pagebreak + +## Rate Schedule (compensation expectations) + +For **fully remote** roles only: + +I am open to (at the absolute minimum): **\$50.00 per hour(w2)/\$75.00 per hour (1099/corp to corp)/ $100,000.00 annually (w2)**. + +I have a strong preference for roles that are : **\$65.00 per hour(w2)/\$85.00 per hour (1099/corp to corp)/ $130,000.00 annually (w2)**. + +For **on-site roles**: + +I am open to (at the absolute minimum): **\$75.00 per hour(w2)/\$95.00 per hour (1099/corp to corp)/ $150,000.00 annually (w2)**. + +In regards to compensation structure, I am open to: + +- w2 +- corp to corp (I have my own LLC) +- 1099 + +If you have a rate for any of the compensation options above, send them all. I will pick which one works best for my situation and the opportunity. + +If it's a different rate with/without benefits, send both. + +If the above is in alignment with this opportunity, please feel free to send me an RTR with the best rate you can offer. + +\pagebreak + +## Details needed for submission + +### My resume + +[Download Charles resume(pdf)](https://resume.reachableceo.com/job-board/CharlesNWyble-Resume.pdf) + +I am happy to discuss and make edits to the resume content specific to the opportunity if you feel +they are needed. + +### Candidate details + +Here are my complete candidate details for submission to the role. + +| Question | Answer | +|---------------------------------------|--------------------------------------------------------------------------| +| Full name | Charles Wyble | +| E-mail address | | +| Phone number | 818-280-7059 | +| Preferred form of contact | E-mail will get the fastest response | +| Work authorization | US Citizen | +| Are you employed presently? | No | +| Current location | Austin, Texas | +| Current timezone | CST | +| Timezones I can work in | PST/CST/EST | +| Availability to interview | Immediate | +| Availability to start | Immediate for remote/local, two weeks for relocation | +| Open to in-office/hybrid/remote | Yes | +| Any trips planned in next six months? | No | +| Highest Education | High School (no college/university) | +| Graduated Year | 2002 | +| Name of school | Osborne Christian School | +| Location of school | Los Angeles CA | +| Linkedin Profile | [Linkedin Profile](https://www.linkedin.com/in/charles-wyble-412007337/) | +| Github Profile | [Github Profile](https://www.github.com/ReachableCEO/) | +| Last project | Contract, ended October 2024 | +| DOB | 09/14 | +| Total IT/career experience | 22 years | diff --git a/Templates/ContactInfo/Contact-Info-Client-Submit.md b/Templates/ContactInfo/Contact-Info-Client-Submit.md new file mode 100644 index 0000000..6234b5e --- /dev/null +++ b/Templates/ContactInfo/Contact-Info-Client-Submit.md @@ -0,0 +1,4 @@ +Charles N Wyble +===== + +Senior (**Staff level**) **System Engineer/SRE/Architect** with extensive Linux/Windows/Networking/Cyber security background and experience diff --git a/Templates/ContactInfo/Contact-Info.md b/Templates/ContactInfo/Contact-Info.md new file mode 100644 index 0000000..048294e --- /dev/null +++ b/Templates/ContactInfo/Contact-Info.md @@ -0,0 +1,6 @@ +Charles N Wyble +===== + +Senior (**Staff level**) **System Engineer/SRE/Architect** with extensive Linux/Windows/Networking/Cyber security background and experience + +[ [Github Profile](https://github.com/reachableceo) ] . [ [Linkedin Profile](https://www.linkedin.com/in/charles-wyble-412007337) ] . [ reachableceo@reachableceo.com ] . [ 818 280 7059 ] . [ Austin TX / Raleigh NC / Remote ] diff --git a/Templates/SkillsAndProjects/Projects.md b/Templates/SkillsAndProjects/Projects.md new file mode 100644 index 0000000..2831084 --- /dev/null +++ b/Templates/SkillsAndProjects/Projects.md @@ -0,0 +1,11 @@ +- Developed and implemented a process to switch thousands of desktops providing digital signage functionality from Fedora to Debian in a completely automated fashion using a custom initrd. +- Developed and implemented an internal private cloud orchestration and provisioning system for a hardware development engineering team that handled the entire provisioning lifecycle for physical and virtual systems. +- Developed and implemented standardized language and procedures and incident investigation automation for a large technical support organization with high turnover. +- Developed and implemented an automated order status and payment handling interactive voice response application using Angel.ccm with a backend web service returning Voice XML. This allows call center personnel to focus on revenue generating opportunities instead of administrative matters. +- Provided technician support to a team of electrical engineers building the power system for the radar of FrankenSAM in Ukraine. Handled high / low voltage wiring and plumbing and documentation of those systems. +- Provided root cause analysis , mitigation and remediation of security breaches by advanced persistent threat actors at high value targets. +- Project managed a successful brand new data center build from bare dirt to serving content in 86 days. Oversaw 8 billion dollars of capital deployment. +- Led and consulted tier 1 payment compliance industry (PCI) implementations for some of the worlds largest brands (including at a payment processor). +- Rolled out centralized Active Directory authentication, deployed Dell OpenManage, and upgraded network equipment. +- Deployed password vault, Active Directory PKI, and implemented a ground-up network redesign. +- Designed VmWare NSX network. diff --git a/Templates/SkillsAndProjects/Skills.csv b/Templates/SkillsAndProjects/Skills.csv new file mode 100644 index 0000000..721f234 --- /dev/null +++ b/Templates/SkillsAndProjects/Skills.csv @@ -0,0 +1,20 @@ +Linux|22 years|RHEL,Debian,Ubuntu,kickstart,PXE, LDAP,SSSD,RPM/Deb package creation, quotas,extended permissions, clustering,NFS,Samba +Unix|5 years|HPUX/Solaris +Windows|22 years|Server (2008 2016),Windows client automated deployment (7,8,10,11),Active Directory,Group Policy,WSUS,Certificate Services,AD DNS,AD DHCP,complex multiple forest and domain setups +Free/Libre/Open Source software|22 years|Apache,Postfix,Qmail,Dovecot,Courier IMAP,Nginx,Matamo,Discourse,Wordpress, Mautic,Dolibarr,Revive Ad Server,Firefly,Cloudron,Coolify,Gitea, HomeAssistant, Jenkins,Rundeck,N8N, LetsEncrypt,ACME,cfssl +Databases|22 years| MySQL,PostgreSQL, Dbeaver,PHPMyAdmin,PostGIS +Cyber Security|22 years|PCI Compliance (tier 1 implementations),OpenVAS<,Lynis,security hardening,audits,breach response and mitigation, patch and vulnerability management. AppArmor, SeLinux, Centrify, Tripwire, Integrit, OSSEC +Virtualization|22 years|VmWare,Parallels,HyperV,KVM,Xen +Networking|22 years|Linux Virtual Server(LVS),HAProxy,Ubiquiti Unifi,Opnsense,Pfsense,DNS,DHCP,IPAM,PXE,IPS,IDS,GRE,IPSEC.Wireguard,OpenVPN,Nebula,Tailscale,RADIUS. Mostly layer2 data center/campus/access some WAN,firewall,layer3 +Monitoring|22 years|Uptime Kuma,Librenms,Zabbix,Zenoss,Nagios,Elasticsearch,Logstash,Kibana(ELK) +Storage|22 years|Netapp,EMC,EqualLogic,3par,MSA,TrueNAS/ZFS,iscsi,S3,Azure Storage +Cloud|5 years|AWS,Azure,Kubernetes,Helm,Docker +Containerization|15 years|LXC,Docker,OpenVZ +Configuration management/InfrastructureAsCode(IAC)|22 years|FetchApply,Terraform/OpenTOfU,Ansible,AWX,Hashicorp Packer/Vault +Ticket / incident / project management| 22 years| Jira,ServiceNow,Redmine,RT +Git|15 years|Branching,merging,multiple teams,external vendors,submodules +SRE|5 years| Grafana,Prometheus,Signoz,Wazuh +LLM|2 years|OpenWebUI,QA/validation,RAG,data cleaning/prep +Programming|5 years|J2ME,PHP,Ruby,TCL/TK,Java,C,C++ +Automation|22 years|Bash,YAML,TOML,PowerShell,Perl +Embedded development|5 years|Raspberry pi,arduino,seeduino,Lego Mindstorms \ No newline at end of file diff --git a/Templates/WorkHistory/WorkHistory.csv b/Templates/WorkHistory/WorkHistory.csv new file mode 100644 index 0000000..8e5c361 --- /dev/null +++ b/Templates/WorkHistory/WorkHistory.csv @@ -0,0 +1,11 @@ +CDK Global,Senior System Engineer,July 2024 - October 2024 +Apple Computer,Senior System Administrator,March 2024 - July 2024 +SHEIN,Staff Site Reliability Engineer,December 2022 - August 2023 +3M,Senior Site Reliability Engineer,March 2020 - November 2022 +TippingPoint,Staff System Architect,March 2012 - June 2019 +HostGator.com,Automation and Escalation Engineer,March 2011 - May 2012 +RippleTV,System Engineer,October 2008 - January 2010 +Walt Disney Internet Group,Site Reliability Engineer,August 2006 - September 2007 +Electronic Clearing House,Senior System Administrator,April 2005 - July 2006 +GSI Commerce,System Administrator,March 2002 - February 2005 +ReachableCEO Enterprises,Freelancer,January 2001 - December 2024 \ No newline at end of file diff --git a/build/MarkdownResume-TemplateInfo-EndClientVersion.yml b/build/MarkdownResume-TemplateInfo-EndClientVersion.yml new file mode 100644 index 0000000..d9bcaeb --- /dev/null +++ b/build/MarkdownResume-TemplateInfo-EndClientVersion.yml @@ -0,0 +1,7 @@ +title: "{{Candidate Name}} Resume" +header-left: "\\hspace{1cm}" +header-center: "\\leftmark" +header-right: "Page \\thepage" +footer-left: "{{Candidate Name}}" +urlcolor: blue +page-background: "D:/tsys/@ReachableCEO/ExternalVendorCode/pandoc-latex-template/examples/page-background/backgrounds/background5.pdf" \ No newline at end of file diff --git a/build/MarkdownResume-TemplateInfo-JobBoard.yml b/build/MarkdownResume-TemplateInfo-JobBoard.yml new file mode 100644 index 0000000..d2f5b73 --- /dev/null +++ b/build/MarkdownResume-TemplateInfo-JobBoard.yml @@ -0,0 +1,12 @@ +title: "Charles N Wyble Resume" +titlepage: true +titlepage-logo: "D:/tsys/@ReachableCEO/ReachableCEO.png" +date: \today +header-left: "\\hspace{1cm}" +header-center: "\\leftmark" +header-right: "Page \\thepage" +footer-left: "Charles N Wyble" +footer-center: "Tenacity. Velocity. Focus." +footer-right: "[Source code for this resume](https://git.knownelement.com/reachableceo/ReachableCEOResume) " +urlcolor: blue +page-background: "D:/tsys/@ReachableCEO/ExternalVendorCode/pandoc-latex-template/examples/page-background/backgrounds/background5.pdf" \ No newline at end of file diff --git a/build/build-client-submit-stage1.sh b/build/build-client-submit-stage1.sh new file mode 100644 index 0000000..9ca2203 --- /dev/null +++ b/build/build-client-submit-stage1.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +MarkdownOutputFile="../MarkdownOutput/client-submit/CharlesNWybleResume.md" +rm $MarkdownOutputFile + +# Combine markdown files into single intermediate markdown file + +#Pull in my contact info +cat "../boilerplate/Contact-Info-Client-Submit.md" >> $MarkdownOutputFile +echo " " >> $MarkdownOutputFile + +echo "## Highlights from my 22 year IT career" >> $MarkdownOutputFile + +cat ../SkillsAndProjects/Projects.md >> "$MarkdownOutputFile" + +echo "\pagebreak" >> $MarkdownOutputFile + +echo " " >> $MarkdownOutputFile +echo "## Employment History" >> $MarkdownOutputFile +echo " " >> $MarkdownOutputFile + +#And here we do some magic... +#Pull in my : + +# employer +# title +# start/end dates of employment +# long form position summary data from each position + +IFS=$'\n\t' +for position in \ +$(cat ../WorkHistory/WorkHistory.csv); do + +COMPANY="$(echo $position|awk -F ',' '{print $1}')" +TITLE="$(echo $position|awk -F ',' '{print $2}')" +DATEOFEMPLOY="$(echo $position|awk -F ',' '{print $3}')" + +echo "**$COMPANY | $TITLE | $DATEOFEMPLOY**" >> $MarkdownOutputFile +echo " " >> "$MarkdownOutputFile" + +cat ../EmployerItems/$COMPANY.md >> "$MarkdownOutputFile" +echo " " >> "$MarkdownOutputFile" +done +unset IFS + +#Pull in my skills and generate a beautiful table. + +echo "\pagebreak" >> $MarkdownOutputFile +echo " " >> "$MarkdownOutputFile" +echo "## Skills" >> "$MarkdownOutputFile" +echo " " >> "$MarkdownOutputFile" + +#Table heading + +echo "|Skill|Experience|Skill Details|" >> $MarkdownOutputFile +echo "|---|---|---|" >> $MarkdownOutputFile +#Table rows +IFS=$'\n\t' +for skill in \ +$(cat ../SkillsAndProjects/Skills.csv); do +SKILL_NAME="$(echo $skill|awk -F '|' '{print $1}')" +SKILL_YEARS="$(echo $skill|awk -F '|' '{print $2}')" +SKILL_DETAIL="$(echo $skill|awk -F '|' '{print $3}')" +echo "|**$SKILL_NAME**|$SKILL_YEARS|$SKILL_DETAIL|" >> $MarkdownOutputFile +done +unset IFS \ No newline at end of file diff --git a/build/build-client-submit-stage2.sh b/build/build-client-submit-stage2.sh new file mode 100644 index 0000000..d48e32c --- /dev/null +++ b/build/build-client-submit-stage2.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +echo "Generating PDF output for client submission version..." + +PDFOutputFile="D:/tsys/@Reachableceo/resume.reachableceo.com/client-submit/CharlesNWyble-Resume.pdf" +MSWordOutputFile="D:/tsys/@Reachableceo/resume.reachableceo.com/client-submit/CharlesNWyble-Resume.doc" +MarkdownInputFile="../MarkdownOutput/client-submit/CharlesNWybleResume.md " +PandocMetadataFile="./CharlesNWyble-ClientSubmit.yml" + +pandoc \ +"$MarkdownInputFile" \ +--template eisvogel \ +--metadata-file="$PandocMetadataFile" \ +--from markdown \ +--to=pdf \ +--output $PDFOutputFile + +echo "Generating MSWord output for client submission version..." + +pandoc \ +"$MarkdownInputFile" \ +--metadata-file="$PandocMetadataFile" \ +--from markdown \ +--to=docx \ +--reference-doc=resume-docx-reference.docx \ +--output $MSWordOutputFile \ No newline at end of file diff --git a/build/build-jobBoard-stage1.sh b/build/build-jobBoard-stage1.sh new file mode 100644 index 0000000..140a3e5 --- /dev/null +++ b/build/build-jobBoard-stage1.sh @@ -0,0 +1,69 @@ +#!/bin/bash + + +echo "Cleaning up from previous runs..." + +MarkdownOutputFile="../MarkdownOutput/job-board/CharlesNWybleResume.md" +rm $MarkdownOutputFile + +echo "Combining markdown files into single input file for pandoc..." + +#Pull in my contact info +cat "../boilerplate/Contact-Info.md" >> $MarkdownOutputFile +echo " " >> $MarkdownOutputFile + +echo "## Highlights from my 22 year IT career" >> $MarkdownOutputFile + +cat ../SkillsAndProjects/Projects.md >> "$MarkdownOutputFile" + +echo "\pagebreak" >> $MarkdownOutputFile + +echo " " >> $MarkdownOutputFile +echo "## Employment History" >> $MarkdownOutputFile +echo " " >> $MarkdownOutputFile + +#And here we do some magic... +#Pull in my : + +# employer +# title +# start/end dates of employment +# long form position summary data from each position + +IFS=$'\n\t' +for position in \ +$(cat ../WorkHistory/WorkHistory.csv); do + +COMPANY="$(echo $position|awk -F ',' '{print $1}')" +TITLE="$(echo $position|awk -F ',' '{print $2}')" +DATEOFEMPLOY="$(echo $position|awk -F ',' '{print $3}')" + +echo "**$COMPANY | $TITLE | $DATEOFEMPLOY**" >> $MarkdownOutputFile +echo " " >> "$MarkdownOutputFile" + +cat ../EmployerItems/$COMPANY.md >> "$MarkdownOutputFile" +echo " " >> "$MarkdownOutputFile" +done +unset IFS + +#Pull in my skills and generate a beautiful table. + +echo "\pagebreak" >> $MarkdownOutputFile +echo " " >> "$MarkdownOutputFile" +echo "## Skills" >> "$MarkdownOutputFile" +echo " " >> "$MarkdownOutputFile" + +#Table heading + +echo "|Skill|Experience|Skill Details|" >> $MarkdownOutputFile +echo "|---|---|---|" >> $MarkdownOutputFile +#Table rows +IFS=$'\n\t' +for skill in \ +$(cat ../SkillsAndProjects/Skills.csv); do +SKILL_NAME="$(echo $skill|awk -F '|' '{print $1}')" +SKILL_YEARS="$(echo $skill|awk -F '|' '{print $2}')" +SKILL_DETAIL="$(echo $skill|awk -F '|' '{print $3}')" +echo "|**$SKILL_NAME**|$SKILL_YEARS|$SKILL_DETAIL|" >> $MarkdownOutputFile +done +unset IFS \ No newline at end of file diff --git a/build/build-jobBoard-stage2.sh b/build/build-jobBoard-stage2.sh new file mode 100644 index 0000000..a122be1 --- /dev/null +++ b/build/build-jobBoard-stage2.sh @@ -0,0 +1,27 @@ +#!/bin/bash + + +echo "Generating PDF output for job board version..." + +PDFOutputFile="D:/tsys/@Reachableceo/resume.reachableceo.com/job-board/CharlesNWyble-Resume.pdf" +MSWordOutputFile="D:/tsys/@Reachableceo/resume.reachableceo.com/job-board/CharlesNWyble-Resume.doc" +MarkdownInputFile="../MarkdownOutput/job-board/CharlesNWybleResume.md " +PandocMetadataFile="./CharlesNWyble-JobBoard.yml" + +pandoc \ +"$MarkdownInputFile" \ +--template eisvogel \ +--metadata-file="$PandocMetadataFile" \ +--from markdown \ +--to=pdf \ +--output $PDFOutputFile + +echo "Generating MSWord output for client submission version..." + +pandoc \ +"$MarkdownInputFile" \ +--metadata-file="$PandocMetadataFile" \ +--from markdown \ +--to=docx \ +--reference-doc=resume-docx-reference.docx \ +--output $MSWordOutputFile \ No newline at end of file diff --git a/build/mo b/build/mo new file mode 100644 index 0000000..b53d48a --- /dev/null +++ b/build/mo @@ -0,0 +1,1997 @@ +#!/usr/bin/env bash +# +#/ Mo is a mustache template rendering software written in bash. It inserts +#/ environment variables into templates. +#/ +#/ Simply put, mo will change {{VARIABLE}} into the value of that +#/ environment variable. You can use {{#VARIABLE}}content{{/VARIABLE}} to +#/ conditionally display content or iterate over the values of an array. +#/ +#/ Learn more about mustache templates at https://mustache.github.io/ +#/ +#/ Simple usage: +#/ +#/ mo [OPTIONS] filenames... +#/ +#/ Options: +#/ +#/ --allow-function-arguments +#/ Permit functions to be called with additional arguments. Otherwise, +#/ the only way to get access to the arguments is to use the +#/ MO_FUNCTION_ARGS environment variable. +#/ -d, --debug +#/ Enable debug logging to stderr. +#/ -u, --fail-not-set +#/ Fail upon expansion of an unset variable. Will silently ignore by +#/ default. Alternately, set MO_FAIL_ON_UNSET to a non-empty value. +#/ -x, --fail-on-function +#/ Fail when a function returns a non-zero status code instead of +#/ silently ignoring it. Alternately, set MO_FAIL_ON_FUNCTION to a +#/ non-empty value. +#/ -f, --fail-on-file +#/ Fail when a file (from command-line or partial) does not exist. +#/ Alternately, set MO_FAIL_ON_FILE to a non-empty value. +#/ -e, --false +#/ Treat the string "false" as empty for conditionals. Alternately, +#/ set MO_FALSE_IS_EMPTY to a non-empty value. +#/ -h, --help +#/ This message. +#/ -s=FILE, --source=FILE +#/ Load FILE into the environment before processing templates. +#/ Can be used multiple times. The file must be a valid shell script +#/ and should only contain variable assignments. +#/ -o=DELIM, --open=DELIM +#/ Set the opening delimiter. Default is "{{". +#/ -c=DELIM, --close=DELIM +#/ Set the closing delimiter. Default is "}}". +#/ -- Indicate the end of options. All arguments after this will be +#/ treated as filenames only. Use when filenames may start with +#/ hyphens. +#/ +#/ Mo uses the following environment variables: +#/ +#/ MO_ALLOW_FUNCTION_ARGUMENTS - When set to a non-empty value, this allows +#/ functions referenced in templates to receive additional options and +#/ arguments. +#/ MO_CLOSE_DELIMITER - The string used when closing a tag. Defaults to "}}". +#/ Used internally. +#/ MO_CLOSE_DELIMITER_DEFAULT - The default value of MO_CLOSE_DELIMITER. Used +#/ when resetting the close delimiter, such as when parsing a partial. +#/ MO_CURRENT - Variable name to use for ".". +#/ MO_DEBUG - When set to a non-empty value, additional debug information is +#/ written to stderr. +#/ MO_FUNCTION_ARGS - Arguments passed to the function. +#/ MO_FAIL_ON_FILE - If a filename from the command-line is missing or a +#/ partial does not exist, abort with an error. +#/ MO_FAIL_ON_FUNCTION - If a function returns a non-zero status code, abort +#/ with an error. +#/ MO_FAIL_ON_UNSET - When set to a non-empty value, expansion of an unset env +#/ variable will be aborted with an error. +#/ MO_FALSE_IS_EMPTY - When set to a non-empty value, the string "false" will +#/ be treated as an empty value for the purposes of conditionals. +#/ MO_OPEN_DELIMITER - The string used when opening a tag. Defaults to "{{". +#/ Used internally. +#/ MO_OPEN_DELIMITER_DEFAULT - The default value of MO_OPEN_DELIMITER. Used +#/ when resetting the open delimiter, such as when parsing a partial. +#/ MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a +#/ help message. +#/ MO_PARSED - Content that has made it through the template engine. +#/ MO_STANDALONE_CONTENT - The unparsed content that preceeded the current tag. +#/ When a standalone tag is encountered, this is checked to see if it only +#/ contains whitespace. If this and the whitespace condition after a tag is +#/ met, then this will be reset to $'\n'. +#/ MO_UNPARSED - Template content yet to make it through the parser. +#/ +#/ Mo is under a MIT style licence with an additional non-advertising clause. +#/ See LICENSE.md for the full text. +#/ +#/ This is open source! Please feel free to contribute. +#/ +#/ https://github.com/tests-always-included/mo + +#: Disable these warnings for the entire file +#: +#: VAR_NAME was modified in a subshell. That change might be lost. +# shellcheck disable=SC2031 +#: +#: Modification of VAR_NAME is local (to subshell caused by (..) group). +# shellcheck disable=SC2030 + +# Public: Template parser function. Writes templates to stdout. +# +# $0 - Name of the mo file, used for getting the help message. +# $@ - Filenames to parse. +# +# Returns nothing. +mo() ( + local moSource moFiles moDoubleHyphens moParsed moContent + + #: This function executes in a subshell; IFS is reset at the end. + IFS=$' \n\t' + + #: Enable a strict mode. This is also reset at the end. + set -eEu -o pipefail + moFiles=() + moDoubleHyphens=false + MO_OPEN_DELIMITER_DEFAULT="{{" + MO_CLOSE_DELIMITER_DEFAULT="}}" + MO_FUNCTION_CACHE_HIT=() + MO_FUNCTION_CACHE_MISS=() + + if [[ $# -gt 0 ]]; then + for arg in "$@"; do + if $moDoubleHyphens; then + #: After we encounter two hyphens together, all the rest + #: of the arguments are files. + moFiles=(${moFiles[@]+"${moFiles[@]}"} "$arg") + else + case "$arg" in + -h|--h|--he|--hel|--help|-\?) + mo::usage "$0" + exit 0 + ;; + + --allow-function-arguments) + MO_ALLOW_FUNCTION_ARGUMENTS=true + ;; + + -u | --fail-not-set) + MO_FAIL_ON_UNSET=true + ;; + + -x | --fail-on-function) + MO_FAIL_ON_FUNCTION=true + ;; + + -p | --fail-on-file) + MO_FAIL_ON_FILE=true + ;; + + -e | --false) + MO_FALSE_IS_EMPTY=true + ;; + + -s=* | --source=*) + if [[ "$arg" == --source=* ]]; then + moSource="${arg#--source=}" + else + moSource="${arg#-s=}" + fi + + if [[ -e "$moSource" ]]; then + # shellcheck disable=SC1090 + . "$moSource" + else + echo "No such file: $moSource" >&2 + exit 1 + fi + ;; + + -o=* | --open=*) + if [[ "$arg" == --open=* ]]; then + MO_OPEN_DELIMITER_DEFAULT="${arg#--open=}" + else + MO_OPEN_DELIMITER_DEFAULT="${arg#-o=}" + fi + ;; + + -c=* | --close=*) + if [[ "$arg" == --close=* ]]; then + MO_CLOSE_DELIMITER_DEFAULT="${arg#--close=}" + else + MO_CLOSE_DELIMITER_DEFAULT="${arg#-c=}" + fi + ;; + + -d | --debug) + MO_DEBUG=true + ;; + + --) + #: Set a flag indicating we've encountered double hyphens + moDoubleHyphens=true + ;; + + -*) + mo::error "Unknown option: $arg (See --help for options)" + ;; + + *) + #: Every arg that is not a flag or a option should be a file + moFiles=(${moFiles[@]+"${moFiles[@]}"} "$arg") + ;; + esac + fi + done + fi + + mo::debug "Debug enabled" + MO_OPEN_DELIMITER="$MO_OPEN_DELIMITER_DEFAULT" + MO_CLOSE_DELIMITER="$MO_CLOSE_DELIMITER_DEFAULT" + mo::content moContent ${moFiles[@]+"${moFiles[@]}"} || return 1 + mo::parse moParsed "$moContent" + echo -n "$moParsed" +) + + +# Internal: Show a debug message +# +# $1 - The debug message to show +# +# Returns nothing. +mo::debug() { + if [[ -n "${MO_DEBUG:-}" ]]; then + echo "DEBUG ${FUNCNAME[1]:-?} - $1" >&2 + fi +} + + +# Internal: Show a debug message and internal state information +# +# No arguments +# +# Returns nothing. +mo::debugShowState() { + if [[ -z "${MO_DEBUG:-}" ]]; then + return + fi + + local moState moTemp moIndex moDots + + mo::escape moTemp "$MO_OPEN_DELIMITER" + moState="open: $moTemp" + mo::escape moTemp "$MO_CLOSE_DELIMITER" + moState="$moState close: $moTemp" + mo::escape moTemp "$MO_STANDALONE_CONTENT" + moState="$moState standalone: $moTemp" + mo::escape moTemp "$MO_CURRENT" + moState="$moState current: $moTemp" + moIndex=$((${#MO_PARSED} - 20)) + moDots=... + + if [[ "$moIndex" -lt 0 ]]; then + moIndex=0 + moDots= + fi + + mo::escape moTemp "${MO_PARSED:$moIndex}" + moState="$moState parsed: $moDots$moTemp" + + moDots=... + + if [[ "${#MO_UNPARSED}" -le 20 ]]; then + moDots= + fi + + mo::escape moTemp "${MO_UNPARSED:0:20}$moDots" + moState="$moState unparsed: $moTemp" + + echo "DEBUG ${FUNCNAME[1]:-?} - $moState" >&2 +} + +# Internal: Show an error message and exit +# +# $1 - The error message to show +# $2 - Error code +# +# Returns nothing. Exits the program. +mo::error() { + echo "ERROR: $1" >&2 + exit "${2:-1}" +} + + +# Internal: Show an error message with a snippet of context and exit +# +# $1 - The error message to show +# $2 - The starting point +# $3 - Error code +# +# Returns nothing. Exits the program. +mo::errorNear() { + local moEscaped + + mo::escape moEscaped "${2:0:40}" + echo "ERROR: $1" >&2 + echo "ERROR STARTS NEAR: $moEscaped" + exit "${3:-1}" +} + + +# Internal: Displays the usage for mo. Pulls this from the file that +# contained the `mo` function. Can only work when the right filename +# comes is the one argument, and that only happens when `mo` is called +# with `$0` set to this file. +# +# $1 - Filename that has the help message +# +# Returns nothing. +mo::usage() { + while read -r line; do + if [[ "${line:0:2}" == "#/" ]]; then + echo "${line:3}" + fi + done < "$MO_ORIGINAL_COMMAND" + echo "" + echo "MO_VERSION=$MO_VERSION" +} + + +# Internal: Fetches the content to parse into MO_UNPARSED. Can be a list of +# partials for files or the content from stdin. +# +# $1 - Destination variable name +# $2-@ - File names (optional), read from stdin otherwise +# +# Returns nothing. +mo::content() { + local moTarget moContent moFilename + + moTarget=$1 + shift + moContent="" + + if [[ "${#@}" -gt 0 ]]; then + for moFilename in "$@"; do + mo::debug "Using template to load content from file: $moFilename" + #: This is so relative paths work from inside template files + moContent="$moContent$MO_OPEN_DELIMITER>$moFilename$MO_CLOSE_DELIMITER" + done + else + mo::debug "Will read content from stdin" + mo::contentFile moContent || return 1 + fi + + local "$moTarget" && mo::indirect "$moTarget" "$moContent" +} + + +# Internal: Read a file into MO_UNPARSED. +# +# $1 - Destination variable name. +# $2 - Filename to load - if empty, defaults to /dev/stdin +# +# Returns nothing. +mo::contentFile() { + local moFile moResult moContent + + #: The subshell removes any trailing newlines. We forcibly add + #: a dot to the content to preserve all newlines. Reading from + #: stdin with a `read` loop does not work as expected, so `cat` + #: needs to stay. + moFile=${2:-/dev/stdin} + + if [[ -e "$moFile" ]]; then + mo::debug "Loading content: $moFile" + moContent=$( + set +Ee + cat -- "$moFile" + moResult=$? + echo -n '.' + exit "$moResult" + ) || return 1 + moContent=${moContent%.} #: Remove last dot + elif [[ -n "${MO_FAIL_ON_FILE-}" ]]; then + mo::error "No such file: $moFile" + else + mo::debug "File does not exist: $moFile" + moContent="" + fi + + local "$1" && mo::indirect "$1" "$moContent" +} + + +# Internal: Send a variable up to the parent of the caller of this function. +# +# $1 - Variable name +# $2 - Value +# +# Examples +# +# callFunc () { +# local "$1" && mo::indirect "$1" "the value" +# } +# callFunc dest +# echo "$dest" # writes "the value" +# +# Returns nothing. +mo::indirect() { + unset -v "$1" + printf -v "$1" '%s' "$2" +} + + +# Internal: Send an array as a variable up to caller of a function +# +# $1 - Variable name +# $2-@ - Array elements +# +# Examples +# +# callFunc () { +# local myArray=(one two three) +# local "$1" && mo::indirectArray "$1" "${myArray[@]}" +# } +# callFunc dest +# echo "${dest[@]}" # writes "one two three" +# +# Returns nothing. +mo::indirectArray() { + unset -v "$1" + + #: IFS must be set to a string containing space or unset in order for + #: the array slicing to work regardless of the current IFS setting on + #: bash 3. This is detailed further at + #: https://github.com/fidian/gg-core/pull/7 + eval "$(printf "IFS= %s=(\"\${@:2}\") IFS=%q" "$1" "$IFS")" +} + + +# Internal: Trim leading characters from MO_UNPARSED +# +# Returns nothing. +mo::trimUnparsed() { + local moI moC + + moI=0 + moC=${MO_UNPARSED:0:1} + + while [[ "$moC" == " " || "$moC" == $'\r' || "$moC" == $'\n' || "$moC" == $'\t' ]]; do + moI=$((moI + 1)) + moC=${MO_UNPARSED:$moI:1} + done + + if [[ "$moI" != 0 ]]; then + MO_UNPARSED=${MO_UNPARSED:$moI} + fi +} + + +# Internal: Remove whitespace and content after whitespace +# +# $1 - Name of the destination variable +# $2 - The string to chomp +# +# Returns nothing. +mo::chomp() { + local moTemp moR moN moT + + moR=$'\r' + moN=$'\n' + moT=$'\t' + moTemp=${2%% *} + moTemp=${moTemp%%"$moR"*} + moTemp=${moTemp%%"$moN"*} + moTemp=${moTemp%%"$moT"*} + + local "$1" && mo::indirect "$1" "$moTemp" +} + + +# Public: Parses text, interpolates mustache tags. Utilizes the current value +# of MO_OPEN_DELIMITER, MO_CLOSE_DELIMITER, and MO_STANDALONE_CONTENT. Those +# three variables shouldn't be changed by user-defined functions. +# +# $1 - Destination variable name - where to store the finished content +# $2 - Content to parse +# $3 - Preserve standalone status/content - truthy if not empty. When set to a +# value, that becomes the standalone content value +# +# Returns nothing. +mo::parse() { + local moOldParsed moOldStandaloneContent moOldUnparsed moResult + + #: The standalone content is a trick to make the standalone tag detection + #: possible. When it's set to content with a newline and if the tag supports + #: it, the standalone content check happens. This check ensures only + #: whitespace is after the last newline up to the tag, and only whitespace + #: is after the tag up to the next newline. If that is the case, remove + #: whitespace and the trailing newline. By setting this to $'\n', we're + #: saying we are at the beginning of content. + mo::debug "Starting parse of ${#2} bytes" + moOldParsed=${MO_PARSED:-} + moOldUnparsed=${MO_UNPARSED:-} + MO_PARSED="" + MO_UNPARSED="$2" + + if [[ -z "${3:-}" ]]; then + moOldStandaloneContent=${MO_STANDALONE_CONTENT:-} + MO_STANDALONE_CONTENT=$'\n' + else + MO_STANDALONE_CONTENT=$3 + fi + + MO_CURRENT=${MO_CURRENT:-} + mo::parseInternal + moResult="$MO_PARSED$MO_UNPARSED" + MO_PARSED=$moOldParsed + MO_UNPARSED=$moOldUnparsed + + if [[ -z "${3:-}" ]]; then + MO_STANDALONE_CONTENT=$moOldStandaloneContent + fi + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Parse MO_UNPARSED, writing content to MO_PARSED. Interpolates +# mustache tags. +# +# No arguments +# +# Returns nothing. +mo::parseInternal() { + local moChunk + + mo::debug "Starting parse" + + while [[ -n "$MO_UNPARSED" ]]; do + mo::debugShowState + moChunk=${MO_UNPARSED%%"$MO_OPEN_DELIMITER"*} + MO_PARSED="$MO_PARSED$moChunk" + MO_STANDALONE_CONTENT="$MO_STANDALONE_CONTENT$moChunk" + MO_UNPARSED=${MO_UNPARSED:${#moChunk}} + + if [[ -n "$MO_UNPARSED" ]]; then + MO_UNPARSED=${MO_UNPARSED:${#MO_OPEN_DELIMITER}} + mo::trimUnparsed + + case "$MO_UNPARSED" in + '#'*) + #: Loop, if/then, or pass content through function + mo::parseBlock false + ;; + + '^'*) + #: Display section if named thing does not exist + mo::parseBlock true + ;; + + '>'*) + #: Load partial - get name of file relative to cwd + mo::parsePartial + ;; + + '/'*) + #: Closing tag + mo::errorNear "Unbalanced close tag" "$MO_UNPARSED" + ;; + + '!'*) + #: Comment - ignore the tag content entirely + mo::parseComment + ;; + + '='*) + #: Change delimiters + #: Any two non-whitespace sequences separated by whitespace. + mo::parseDelimiter + ;; + + '&'*) + #: Unescaped - mo doesn't escape/unescape + MO_UNPARSED=${MO_UNPARSED#&} + mo::trimUnparsed + mo::parseValue + ;; + + *) + #: Normal environment variable, string, subexpression, + #: current value, key, or function call + mo::parseValue + ;; + esac + fi + done +} + + +# Internal: Handle parsing a block +# +# $1 - Invert condition ("true" or "false") +# +# Returns nothing +mo::parseBlock() { + local moInvertBlock moTokens moTokensString + + moInvertBlock=$1 + MO_UNPARSED=${MO_UNPARSED:1} + mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" + MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} + mo::tokensToString moTokensString "${moTokens[@]:1}" + mo::debug "Parsing block: $moTokensString" + + if mo::standaloneCheck; then + mo::standaloneProcess + fi + + if [[ "${moTokens[1]}" == "NAME" ]] && mo::isFunction "${moTokens[2]}"; then + mo::parseBlockFunction "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" + elif [[ "${moTokens[1]}" == "NAME" ]] && mo::isArray "${moTokens[2]}"; then + mo::parseBlockArray "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" + else + mo::parseBlockValue "$moInvertBlock" "$moTokensString" "${moTokens[@]:1}" + fi +} + + +# Internal: Handle parsing a block whose first argument is a function +# +# $1 - Invert condition ("true" or "false") +# $2-@ - The parsed tokens from inside the block tags +# +# Returns nothing +mo::parseBlockFunction() { + local moTarget moInvertBlock moTokens moTemp moUnparsed moTokensString + + moInvertBlock=$1 + moTokensString=$2 + shift 2 + moTokens=(${@+"$@"}) + mo::debug "Parsing block function: $moTokensString" + mo::getContentUntilClose moTemp "$moTokensString" + #: Pass unparsed content to the function. + #: Keep the updated delimiters if they changed. + + if [[ "$moInvertBlock" != "true" ]]; then + mo::evaluateFunction moResult "$moTemp" "${moTokens[@]:1}" + MO_PARSED="$MO_PARSED$moResult" + fi + + mo::debug "Done parsing block function: $moTokensString" +} + + +# Internal: Handle parsing a block whose first argument is an array +# +# $1 - Invert condition ("true" or "false") +# $2-@ - The parsed tokens from inside the block tags +# +# Returns nothing +mo::parseBlockArray() { + local moInvertBlock moTokens moResult moArrayName moArrayIndexes moArrayIndex moTemp moUnparsed moOpenDelimiterBefore moCloseDelimiterBefore moOpenDelimiterAfter moCloseDelimiterAfter moParsed moTokensString moCurrent + + moInvertBlock=$1 + moTokensString=$2 + shift 2 + moTokens=(${@+"$@"}) + mo::debug "Parsing block array: $moTokensString" + moOpenDelimiterBefore=$MO_OPEN_DELIMITER + moCloseDelimiterBefore=$MO_CLOSE_DELIMITER + mo::getContentUntilClose moTemp "$moTokensString" + moOpenDelimiterAfter=$MO_OPEN_DELIMITER + moCloseDelimiterAfter=$MO_CLOSE_DELIMITER + moArrayName=${moTokens[1]} + eval "moArrayIndexes=(\"\${!${moArrayName}[@]}\")" + + if [[ "${#moArrayIndexes[@]}" -lt 1 ]]; then + #: No elements + if [[ "$moInvertBlock" == "true" ]]; then + #: Restore the delimiter before parsing + MO_OPEN_DELIMITER=$moOpenDelimiterBefore + MO_CLOSE_DELIMITER=$moCloseDelimiterBefore + moCurrent=$MO_CURRENT + MO_CURRENT=$moArrayName + mo::parse moParsed "$moTemp" "blockArrayInvert$MO_STANDALONE_CONTENT" + MO_CURRENT=$moCurrent + MO_PARSED="$MO_PARSED$moParsed" + fi + else + if [[ "$moInvertBlock" != "true" ]]; then + #: Process for each element in the array + moUnparsed=$MO_UNPARSED + + for moArrayIndex in "${moArrayIndexes[@]}"; do + #: Restore the delimiter before parsing + MO_OPEN_DELIMITER=$moOpenDelimiterBefore + MO_CLOSE_DELIMITER=$moCloseDelimiterBefore + moCurrent=$MO_CURRENT + MO_CURRENT=$moArrayName.$moArrayIndex + mo::debug "Iterate over array using element: $MO_CURRENT" + mo::parse moParsed "$moTemp" "blockArray$MO_STANDALONE_CONTENT" + MO_CURRENT=$moCurrent + MO_PARSED="$MO_PARSED$moParsed" + done + + MO_UNPARSED=$moUnparsed + fi + fi + + MO_OPEN_DELIMITER=$moOpenDelimiterAfter + MO_CLOSE_DELIMITER=$moCloseDelimiterAfter + mo::debug "Done parsing block array: $moTokensString" +} + + +# Internal: Handle parsing a block whose first argument is a value +# +# $1 - Invert condition ("true" or "false") +# $2-@ - The parsed tokens from inside the block tags +# +# Returns nothing +mo::parseBlockValue() { + local moInvertBlock moTokens moResult moUnparsed moOpenDelimiterBefore moOpenDelimiterAfter moCloseDelimiterBefore moCloseDelimiterAfter moParsed moTemp moTokensString moCurrent + + moInvertBlock=$1 + moTokensString=$2 + shift 2 + moTokens=(${@+"$@"}) + mo::debug "Parsing block value: $moTokensString" + moOpenDelimiterBefore=$MO_OPEN_DELIMITER + moCloseDelimiterBefore=$MO_CLOSE_DELIMITER + mo::getContentUntilClose moTemp "$moTokensString" + moOpenDelimiterAfter=$MO_OPEN_DELIMITER + moCloseDelimiterAfter=$MO_CLOSE_DELIMITER + + #: Variable, value, or list of mixed things + mo::evaluateListOfSingles moResult "${moTokens[@]}" + + if mo::isTruthy "$moResult" "$moInvertBlock"; then + mo::debug "Block is truthy: $moResult" + #: Restore the delimiter before parsing + MO_OPEN_DELIMITER=$moOpenDelimiterBefore + MO_CLOSE_DELIMITER=$moCloseDelimiterBefore + moCurrent=$MO_CURRENT + MO_CURRENT=${moTokens[1]} + mo::parse moParsed "$moTemp" "blockValue$MO_STANDALONE_CONTENT" + MO_PARSED="$MO_PARSED$moParsed" + MO_CURRENT=$moCurrent + fi + + MO_OPEN_DELIMITER=$moOpenDelimiterAfter + MO_CLOSE_DELIMITER=$moCloseDelimiterAfter + mo::debug "Done parsing block value: $moTokensString" +} + + +# Internal: Handle parsing a partial +# +# No arguments. +# +# Indentation will be applied to the entire partial's contents before parsing. +# This indentation is based on the whitespace that ends the previously parsed +# content. +# +# Returns nothing +mo::parsePartial() { + local moFilename moResult moIndentation moN moR moTemp moT + + MO_UNPARSED=${MO_UNPARSED:1} + mo::trimUnparsed + mo::chomp moFilename "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}" + MO_UNPARSED="${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"}" + moIndentation="" + + if mo::standaloneCheck; then + moN=$'\n' + moR=$'\r' + moT=$'\t' + moIndentation="$moN${MO_PARSED//"$moR"/"$moN"}" + moIndentation=${moIndentation##*"$moN"} + moTemp=${moIndentation// } + moTemp=${moTemp//"$moT"} + + if [[ -n "$moTemp" ]]; then + moIndentation= + fi + + mo::debug "Adding indentation to partial: '$moIndentation'" + mo::standaloneProcess + fi + + mo::debug "Parsing partial: $moFilename" + + #: Execute in subshell to preserve current cwd and environment + moResult=$( + #: It would be nice to remove `dirname` and use a function instead, + #: but that is difficult when only given filenames. + cd "$(dirname -- "$moFilename")" || exit 1 + echo "$( + local moPartialContent moPartialParsed + + if ! mo::contentFile moPartialContent "${moFilename##*/}"; then + exit 1 + fi + + #: Reset delimiters before parsing + mo::indentLines moPartialContent "$moIndentation" "$moPartialContent" + MO_OPEN_DELIMITER="$MO_OPEN_DELIMITER_DEFAULT" + MO_CLOSE_DELIMITER="$MO_CLOSE_DELIMITER_DEFAULT" + mo::parse moPartialParsed "$moPartialContent" + + #: Fix bash handling of subshells and keep trailing whitespace. + echo -n "$moPartialParsed." + )" || exit 1 + ) || exit 1 + + if [[ -z "$moResult" ]]; then + mo::debug "Error detected when trying to read the file" + exit 1 + fi + + MO_PARSED="$MO_PARSED${moResult%.}" +} + + +# Internal: Handle parsing a comment +# +# No arguments. +# +# Returns nothing +mo::parseComment() { + local moContent moContent + + MO_UNPARSED=${MO_UNPARSED#*"$MO_CLOSE_DELIMITER"} + mo::debug "Parsing comment" + + if mo::standaloneCheck; then + mo::standaloneProcess + fi +} + + +# Internal: Handle parsing the change of delimiters +# +# No arguments. +# +# Returns nothing +mo::parseDelimiter() { + local moContent moOpen moClose + + MO_UNPARSED=${MO_UNPARSED:1} + mo::trimUnparsed + mo::chomp moOpen "$MO_UNPARSED" + MO_UNPARSED=${MO_UNPARSED:${#moOpen}} + mo::trimUnparsed + mo::chomp moClose "${MO_UNPARSED%%="$MO_CLOSE_DELIMITER"*}" + MO_UNPARSED=${MO_UNPARSED#*="$MO_CLOSE_DELIMITER"} + mo::debug "Parsing delimiters: $moOpen $moClose" + + if mo::standaloneCheck; then + mo::standaloneProcess + fi + + MO_OPEN_DELIMITER="$moOpen" + MO_CLOSE_DELIMITER="$moClose" +} + + +# Internal: Handle parsing value or function call +# +# No arguments. +# +# Returns nothing +mo::parseValue() { + local moUnparsedOriginal moTokens + + moUnparsedOriginal=$MO_UNPARSED + mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" + mo::evaluate moResult "${moTokens[@]:1}" + MO_PARSED="$MO_PARSED$moResult" + + if [[ "${MO_UNPARSED:0:${#MO_CLOSE_DELIMITER}}" != "$MO_CLOSE_DELIMITER" ]]; then + mo::errorNear "Did not find closing tag" "$moUnparsedOriginal" + fi + + if mo::standaloneCheck; then + mo::standaloneProcess + fi + + MO_UNPARSED=${MO_UNPARSED:${#MO_CLOSE_DELIMITER}} +} + + +# Internal: Determine if the given name is a defined function. +# +# $1 - Function name to check +# +# Be extremely careful. Even if strict mode is enabled, it is not honored +# in newer versions of Bash. Any errors that crop up here will not be +# caught automatically. +# +# Examples +# +# moo () { +# echo "This is a function" +# } +# if mo::isFunction moo; then +# echo "moo is a defined function" +# fi +# +# Returns 0 if the name is a function, 1 otherwise. +mo::isFunction() { + local moFunctionName + + for moFunctionName in "${MO_FUNCTION_CACHE_HIT[@]}"; do + if [[ "$moFunctionName" == "$1" ]]; then + return 0 + fi + done + + for moFunctionName in "${MO_FUNCTION_CACHE_MISS[@]}"; do + if [[ "$moFunctionName" == "$1" ]]; then + return 1 + fi + done + + if declare -F "$1" &> /dev/null; then + MO_FUNCTION_CACHE_HIT=( ${MO_FUNCTION_CACHE_HIT[@]+"${MO_FUNCTION_CACHE_HIT[@]}"} "$1" ) + + return 0 + fi + + MO_FUNCTION_CACHE_MISS=( ${MO_FUNCTION_CACHE_MISS[@]+"${MO_FUNCTION_CACHE_MISS[@]}"} "$1" ) + + return 1 +} + + +# Internal: Determine if a given environment variable exists and if it is +# an array. +# +# $1 - Name of environment variable +# +# Be extremely careful. Even if strict mode is enabled, it is not honored +# in newer versions of Bash. Any errors that crop up here will not be +# caught automatically. +# +# Examples +# +# var=(abc) +# if moIsArray var; then +# echo "This is an array" +# echo "Make sure you don't accidentally use \$var" +# fi +# +# Returns 0 if the name is not empty, 1 otherwise. +mo::isArray() { + #: Namespace this variable so we don't conflict with what we're testing. + local moTestResult + + moTestResult=$(declare -p "$1" 2>/dev/null) || return 1 + [[ "${moTestResult:0:10}" == "declare -a" ]] && return 0 + [[ "${moTestResult:0:10}" == "declare -A" ]] && return 0 + + return 1 +} + + +# Internal: Determine if an array index exists. +# +# $1 - Variable name to check +# $2 - The index to check +# +# Has to check if the variable is an array and if the index is valid for that +# type of array. +# +# Returns true (0) if everything was ok, 1 if there's any condition that fails. +mo::isArrayIndexValid() { + local moDeclare moTest + + moDeclare=$(declare -p "$1") + moTest="" + + if [[ "${moDeclare:0:10}" == "declare -a" ]]; then + #: Numerically indexed array - must check if the index looks like a + #: number because using a string to index a numerically indexed array + #: will appear like it worked. + if [[ "$2" == "0" ]] || [[ "$2" =~ ^[1-9][0-9]*$ ]]; then + #: Index looks like a number + eval "moTest=\"\${$1[$2]+ok}\"" + fi + elif [[ "${moDeclare:0:10}" == "declare -A" ]]; then + #: Associative array + eval "moTest=\"\${$1[$2]+ok}\"" + fi + + if [[ -n "$moTest" ]]; then + return 0; + fi + + return 1 +} + + +# Internal: Determine if a variable is assigned, even if it is assigned an empty +# value. +# +# $1 - Variable name to check. +# +# Can not use logic like this in case invalid variable names are passed. +# [[ "${!1-a}" == "${!1-b}" ]] +# +# Using logic like this gives false positives. +# [[ -v "$a" ]] +# +# Declaring a variable is not the same as assigning the variable. +# export x +# declare -p x # Output: declare -x x +# export y="" +# declare -p y # Output: declare -x y="" +# unset z +# declare -p z # Error code 1 and output: bash: declare: z: not found +# +# Returns true (0) if the variable is set, 1 if the variable is unset. +mo::isVarSet() { + if declare -p "$1" &> /dev/null && [[ -v "$1" ]]; then + return 0 + fi + + return 1 +} + + +# Internal: Determine if a value is considered truthy. +# +# $1 - The value to test +# $2 - Invert the value, either "true" or "false" +# +# Returns true (0) if truthy, 1 otherwise. +mo::isTruthy() { + local moTruthy + + moTruthy=true + + if [[ -z "${1-}" ]]; then + moTruthy=false + elif [[ -n "${MO_FALSE_IS_EMPTY-}" ]] && [[ "${1-}" == "false" ]]; then + moTruthy=false + fi + + #: XOR the results + #: moTruthy inverse desiredResult + #: true false true + #: true true false + #: false false false + #: false true true + if [[ "$moTruthy" == "$2" ]]; then + mo::debug "Value is falsy, test result: $moTruthy inverse: $2" + return 1 + fi + + mo::debug "Value is truthy, test result: $moTruthy inverse: $2" + return 0 +} + + +# Internal: Convert token list to values +# +# $1 - Destination variable name +# $2-@ - Tokens to convert +# +# Sample call: +# +# mo::evaluate dest NAME username VALUE abc123 PAREN 2 +# +# Returns nothing. +mo::evaluate() { + local moTarget moStack moValue moType moIndex moCombined moResult + + moTarget=$1 + shift + + #: Phase 1 - remove all command tokens (PAREN, BRACE) + moStack=() + + while [[ $# -gt 0 ]]; do + case "$1" in + PAREN|BRACE) + moType=$1 + moValue=$2 + mo::debug "Combining $moValue tokens" + moIndex=$((${#moStack[@]} - (2 * moValue))) + mo::evaluateListOfSingles moCombined "${moStack[@]:$moIndex}" + + if [[ "$moType" == "PAREN" ]]; then + moStack=("${moStack[@]:0:$moIndex}" NAME "$moCombined") + else + moStack=("${moStack[@]:0:$moIndex}" VALUE "$moCombined") + fi + ;; + + *) + moStack=(${moStack[@]+"${moStack[@]}"} "$1" "$2") + ;; + esac + + shift 2 + done + + #: Phase 2 - check if this is a function or if we should just concatenate values + if [[ "${moStack[0]:-}" == "NAME" ]] && mo::isFunction "${moStack[1]}"; then + #: Special case - if the first argument is a function, then the rest are + #: passed to the function. + mo::debug "Evaluating function: ${moStack[1]}" + mo::evaluateFunction moResult "" "${moStack[@]:1}" + else + #: Concatenate + mo::debug "Concatenating ${#moStack[@]} stack items" + mo::evaluateListOfSingles moResult ${moStack[@]+"${moStack[@]}"} + fi + + local "$moTarget" && mo::indirect "$moTarget" "$moResult" +} + + +# Internal: Convert an argument list to individual values. +# +# $1 - Destination variable name +# $2-@ - A list of argument types and argument name/value. +# +# This assumes each value is separate from the rest. In contrast, mo::evaluate +# will pass all arguments to a function if the first value is a function. +# +# Sample call: +# +# mo::evaluateListOfSingles dest NAME username VALUE abc123 +# +# Returns nothing. +mo::evaluateListOfSingles() { + local moResult moTarget moTemp + + moTarget=$1 + shift + moResult="" + + while [[ $# -gt 1 ]]; do + mo::evaluateSingle moTemp "$1" "$2" + moResult="$moResult$moTemp" + shift 2 + done + + mo::debug "Evaluated list of singles: $moResult" + + local "$moTarget" && mo::indirect "$moTarget" "$moResult" +} + + +# Internal: Evaluate a single argument +# +# $1 - Name of variable for result +# $2 - Type of argument, either NAME or VALUE +# $3 - Argument +# +# Returns nothing +mo::evaluateSingle() { + local moResult moType moArg + + moType=$2 + moArg=$3 + mo::debug "Evaluating $moType: $moArg ($MO_CURRENT)" + + if [[ "$moType" == "VALUE" ]]; then + moResult=$moArg + elif [[ "$moArg" == "." ]]; then + mo::evaluateVariable moResult "" + elif [[ "$moArg" == "@key" ]]; then + mo::evaluateKey moResult + elif mo::isFunction "$moArg"; then + mo::evaluateFunction moResult "" "$moArg" + else + mo::evaluateVariable moResult "$moArg" + fi + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Return the value for @key based on current's name +# +# $1 - Name of variable for result +# +# Returns nothing +mo::evaluateKey() { + local moResult + + if [[ "$MO_CURRENT" == *.* ]]; then + moResult="${MO_CURRENT#*.}" + else + moResult="${MO_CURRENT}" + fi + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Handle a variable name +# +# $1 - Destination variable name +# $2 - Variable name +# +# Returns nothing. +mo::evaluateVariable() { + local moResult moArg moNameParts + + moArg=$2 + moResult="" + mo::findVariableName moNameParts "$moArg" + mo::debug "Evaluate variable ($moArg, $MO_CURRENT): ${moNameParts[*]}" + + if [[ -z "${moNameParts[1]}" ]]; then + if mo::isArray "${moNameParts[0]}"; then + eval mo::join moResult "," "\${${moNameParts[0]}[@]}" + else + if mo::isVarSet "${moNameParts[0]}"; then + moResult=${moNameParts[0]} + moResult="${!moResult}" + elif [[ -n "${MO_FAIL_ON_UNSET-}" ]]; then + mo::error "Environment variable not set: ${moNameParts[0]}" + fi + fi + else + if mo::isArray "${moNameParts[0]}"; then + eval "set +u;moResult=\"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\"" + else + mo::error "Unable to index a scalar as an array: $moArg" + fi + fi + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Find the name of a variable to use +# +# $1 - Destination variable name, receives an array +# $2 - Variable name from the template +# +# The array contains the following values +# [0] - Variable name +# [1] - Array index, or empty string +# +# Example variables +# a="a" +# b="b" +# c=("c.0" "c.1") +# d=([b]="d.b" [d]="d.d") +# +# Given these inputs (function input, current value), produce these outputs +# a c => a +# a c.0 => a +# b d => d.b +# b d.d => d.b +# a d => d.a +# a d.d => d.a +# c.0 d => c.0 +# d.b d => d.b +# '' c => c +# '' c.0 => c.0 +# Returns nothing. +mo::findVariableName() { + local moVar moNameParts moResultBase moResultIndex moCurrent + + moVar=$2 + moResultBase=$moVar + moResultIndex="" + + if [[ -z "$moVar" ]]; then + moResultBase=${MO_CURRENT%%.*} + + if [[ "$MO_CURRENT" == *.* ]]; then + moResultIndex=${MO_CURRENT#*.} + fi + elif [[ "$moVar" == *.* ]]; then + mo::debug "Find variable name; name has dot: $moVar" + moResultBase=${moVar%%.*} + moResultIndex=${moVar#*.} + elif [[ -n "$MO_CURRENT" ]]; then + moCurrent=${MO_CURRENT%%.*} + mo::debug "Find variable name; look in array: $moCurrent" + + if mo::isArrayIndexValid "$moCurrent" "$moVar"; then + moResultBase=$moCurrent + moResultIndex=$moVar + fi + fi + + local "$1" && mo::indirectArray "$1" "$moResultBase" "$moResultIndex" +} + + +# Internal: Join / implode an array +# +# $1 - Variable name to receive the joined content +# $2 - Joiner +# $3-@ - Elements to join +# +# Returns nothing. +mo::join() { + local joiner part result target + + target=$1 + joiner=$2 + result=$3 + shift 3 + + for part in "$@"; do + result="$result$joiner$part" + done + + local "$target" && mo::indirect "$target" "$result" +} + + +# Internal: Call a function. +# +# $1 - Variable for output +# $2 - Content to pass +# $3 - Function to call +# $4-@ - Additional arguments as list of type, value/name +# +# Returns nothing. +mo::evaluateFunction() { + local moArgs moContent moFunctionResult moTarget moFunction moTemp moFunctionCall + + moTarget=$1 + moContent=$2 + moFunction=$3 + shift 3 + moArgs=() + + while [[ $# -gt 1 ]]; do + mo::evaluateSingle moTemp "$1" "$2" + moArgs=(${moArgs[@]+"${moArgs[@]}"} "$moTemp") + shift 2 + done + + mo::escape moFunctionCall "$moFunction" + + if [[ -n "${MO_ALLOW_FUNCTION_ARGUMENTS-}" ]]; then + mo::debug "Function arguments are allowed" + + if [[ ${#moArgs[@]} -gt 0 ]]; then + for moTemp in "${moArgs[@]}"; do + mo::escape moTemp "$moTemp" + moFunctionCall="$moFunctionCall $moTemp" + done + fi + fi + + mo::debug "Calling function: $moFunctionCall" + + #: Call the function in a subshell for safety. Employ the trick to preserve + #: whitespace at the end of the output. + moContent=$( + export MO_FUNCTION_ARGS=(${moArgs[@]+"${moArgs[@]}"}) + echo -n "$moContent" | eval "$moFunctionCall ; moFunctionResult=\$? ; echo -n '.' ; exit \"\$moFunctionResult\"" + ) || { + moFunctionResult=$? + if [[ -n "${MO_FAIL_ON_FUNCTION-}" && "$moFunctionResult" != 0 ]]; then + mo::error "Function failed with status code $moFunctionResult: $moFunctionCall" "$moFunctionResult" + fi + } + + local "$moTarget" && mo::indirect "$moTarget" "${moContent%.}" +} + + +# Internal: Check if a tag appears to have only whitespace before it and after +# it on a line. There must be a new line before and there must be a newline +# after or the end of a string +# +# No arguments. +# +# Returns 0 if this is a standalone tag, 1 otherwise. +mo::standaloneCheck() { + local moContent moN moR moT + + moN=$'\n' + moR=$'\r' + moT=$'\t' + + #: Check the content before + moContent=${MO_STANDALONE_CONTENT//"$moR"/"$moN"} + + #: By default, signal to the next check that this one failed + MO_STANDALONE_CONTENT="" + + if [[ "$moContent" != *"$moN"* ]]; then + mo::debug "Not a standalone tag - no newline before" + + return 1 + fi + + moContent=${moContent##*"$moN"} + moContent=${moContent//"$moT"/} + moContent=${moContent// /} + + if [[ -n "$moContent" ]]; then + mo::debug "Not a standalone tag - non-whitespace detected before tag" + + return 1 + fi + + #: Check the content after + moContent=${MO_UNPARSED//"$moR"/"$moN"} + moContent=${moContent%%"$moN"*} + moContent=${moContent//"$moT"/} + moContent=${moContent// /} + + if [[ -n "$moContent" ]]; then + mo::debug "Not a standalone tag - non-whitespace detected after tag" + + return 1 + fi + + #: Signal to the next check that this tag removed content + MO_STANDALONE_CONTENT=$'\n' + + return 0 +} + + +# Internal: Process content before and after a tag. Remove prior whitespace up +# to the previous newline. Remove following whitespace up to and including the +# next newline. +# +# No arguments. +# +# Returns nothing. +mo::standaloneProcess() { + local moI moTemp + + mo::debug "Standalone tag - processing content before and after tag" + moI=$((${#MO_PARSED} - 1)) + mo::debug "zero done ${#MO_PARSED}" + mo::escape moTemp "$MO_PARSED" + mo::debug "$moTemp" + + while [[ "${MO_PARSED:$moI:1}" == " " || "${MO_PARSED:$moI:1}" == $'\t' ]]; do + moI=$((moI - 1)) + done + + if [[ $((moI + 1)) != "${#MO_PARSED}" ]]; then + MO_PARSED="${MO_PARSED:0:${moI}+1}" + fi + + moI=0 + + while [[ "${MO_UNPARSED:${moI}:1}" == " " || "${MO_UNPARSED:${moI}:1}" == $'\t' ]]; do + moI=$((moI + 1)) + done + + if [[ "${MO_UNPARSED:${moI}:1}" == $'\r' ]]; then + moI=$((moI + 1)) + fi + + if [[ "${MO_UNPARSED:${moI}:1}" == $'\n' ]]; then + moI=$((moI + 1)) + fi + + if [[ "$moI" != 0 ]]; then + MO_UNPARSED=${MO_UNPARSED:${moI}} + fi +} + + +# Internal: Apply indentation before any line that has content in MO_UNPARSED. +# +# $1 - Destination variable name. +# $2 - The indentation string. +# $3 - The content that needs the indentation string prepended on each line. +# +# Returns nothing. +mo::indentLines() { + local moContent moIndentation moResult moN moR moChunk + + moIndentation=$2 + moContent=$3 + + if [[ -z "$moIndentation" ]]; then + mo::debug "Not applying indentation, empty indentation" + + local "$1" && mo::indirect "$1" "$moContent" + return + fi + + if [[ -z "$moContent" ]]; then + mo::debug "Not applying indentation, empty contents" + + local "$1" && mo::indirect "$1" "$moContent" + return + fi + + moResult= + moN=$'\n' + moR=$'\r' + + mo::debug "Applying indentation: '${moIndentation}'" + + while [[ -n "$moContent" ]]; do + moChunk=${moContent%%"$moN"*} + moChunk=${moChunk%%"$moR"*} + moContent=${moContent:${#moChunk}} + + if [[ -n "$moChunk" ]]; then + moResult="$moResult$moIndentation$moChunk" + fi + + moResult="$moResult${moContent:0:1}" + moContent=${moContent:1} + done + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Escape a value +# +# $1 - Destination variable name +# $2 - Value to escape +# +# Returns nothing +mo::escape() { + local moResult + + moResult=$2 + moResult=$(declare -p moResult) + moResult=${moResult#*=} + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Internal: Get the content up to the end of the block by minimally parsing and +# balancing blocks. Returns the content before the end tag to the caller and +# removes the content + the end tag from MO_UNPARSED. This can change the +# delimiters, adjusting MO_OPEN_DELIMITER and MO_CLOSE_DELIMITER. +# +# $1 - Destination variable name +# $2 - Token string to match for a closing tag +# +# Returns nothing. +mo::getContentUntilClose() { + local moChunk moResult moTemp moTokensString moTokens moTarget moTagStack moResultTemp + + moTarget=$1 + moTagStack=("$2") + mo::debug "Get content until close tag: ${moTagStack[0]}" + moResult="" + + while [[ -n "$MO_UNPARSED" ]] && [[ "${#moTagStack[@]}" -gt 0 ]]; do + moChunk=${MO_UNPARSED%%"$MO_OPEN_DELIMITER"*} + moResult="$moResult$moChunk" + MO_UNPARSED=${MO_UNPARSED:${#moChunk}} + + if [[ -n "$MO_UNPARSED" ]]; then + moResultTemp="$MO_OPEN_DELIMITER" + MO_UNPARSED=${MO_UNPARSED:${#MO_OPEN_DELIMITER}} + mo::getContentTrim moTemp + moResultTemp="$moResultTemp$moTemp" + mo::debug "First character within tag: ${MO_UNPARSED:0:1}" + + case "$MO_UNPARSED" in + '#'*) + #: Increase block + moResultTemp="$moResultTemp${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + mo::getContentTrim moTemp + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + moResultTemp="$moResultTemp${moTemp[0]}" + moTagStack=("${moTemp[1]}" "${moTagStack[@]}") + ;; + + '^'*) + #: Increase block + moResultTemp="$moResultTemp${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + mo::getContentTrim moTemp + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + moResultTemp="$moResultTemp${moTemp[0]}" + moTagStack=("${moTemp[1]}" "${moTagStack[@]}") + ;; + + '>'*) + #: Partial - ignore + moResultTemp="$moResultTemp${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + mo::getContentTrim moTemp + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + moResultTemp="$moResultTemp${moTemp[0]}" + ;; + + '/'*) + #: Decrease block + moResultTemp="$moResultTemp${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + mo::getContentTrim moTemp + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + + if [[ "${moTagStack[0]}" == "${moTemp[1]}" ]]; then + moResultTemp="$moResultTemp${moTemp[0]}" + moTagStack=("${moTagStack[@]:1}") + + if [[ "${#moTagStack[@]}" -eq 0 ]]; then + #: Erase all portions of the close tag + moResultTemp="" + fi + else + mo::errorNear "Unbalanced closing tag, expected: ${moTagStack[0]}" "${moTemp[0]}${MO_UNPARSED}" + fi + ;; + + '!'*) + #: Comment - ignore + mo::getContentComment moTemp + moResultTemp="$moResultTemp$moTemp" + ;; + + '='*) + #: Change delimiters + mo::getContentDelimiter moTemp + moResultTemp="$moResultTemp$moTemp" + ;; + + '&'*) + #: Unescaped - bypass one then ignore + moResultTemp="$moResultTemp${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + mo::getContentTrim moTemp + moResultTemp="$moResultTemp$moTemp" + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + moResultTemp="$moResultTemp${moTemp[0]}" + ;; + + *) + #: Normal variable - ignore + mo::getContentWithinTag moTemp "$MO_CLOSE_DELIMITER" + moResultTemp="$moResultTemp${moTemp[0]}" + ;; + esac + + moResult="$moResult$moResultTemp" + fi + done + + MO_STANDALONE_CONTENT="$MO_STANDALONE_CONTENT$moResult" + + if mo::standaloneCheck; then + moResultTemp=$MO_PARSED + MO_PARSED=$moResult + mo::standaloneProcess + moResult=$MO_PARSED + MO_PARSED=$moResultTemp + fi + + local "$moTarget" && mo::indirect "$moTarget" "$moResult" +} + + +# Internal: Convert a list of tokens to a string +# +# $1 - Destination variable for the string +# $2-$@ - Token list +# +# Returns nothing. +mo::tokensToString() { + local moTarget moString moTokens + + moTarget=$1 + shift 1 + moTokens=("$@") + moString=$(declare -p moTokens) + moString=${moString#*=} + + local "$moTarget" && mo::indirect "$moTarget" "$moString" +} + + +# Internal: Trims content from MO_UNPARSED, returns trimmed content. +# +# $1 - Destination variable +# +# Returns nothing. +mo::getContentTrim() { + local moChar moResult + + moChar=${MO_UNPARSED:0:1} + moResult="" + + while [[ "$moChar" == " " ]] || [[ "$moChar" == $'\r' ]] || [[ "$moChar" == $'\t' ]] || [[ "$moChar" == $'\n' ]]; do + moResult="$moResult$moChar" + MO_UNPARSED=${MO_UNPARSED:1} + moChar=${MO_UNPARSED:0:1} + done + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Get the content up to and including a close tag +# +# $1 - Destination variable +# +# Returns nothing. +mo::getContentComment() { + local moResult + + mo::debug "Getting content for comment" + moResult=${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*} + MO_UNPARSED=${MO_UNPARSED:${#moResult}} + + if [[ "$MO_UNPARSED" == "$MO_CLOSE_DELIMITER"* ]]; then + moResult="$moResult$MO_CLOSE_DELIMITER" + MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} + fi + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Get the content up to and including a close tag. First two non-whitespace +# tokens become the new open and close tag. +# +# $1 - Destination variable +# +# Returns nothing. +mo::getContentDelimiter() { + local moResult moTemp moOpen moClose + + mo::debug "Getting content for delimiter" + moResult="" + mo::getContentTrim moTemp + moResult="$moResult$moTemp" + mo::chomp moOpen "$MO_UNPARSED" + MO_UNPARSED="${MO_UNPARSED:${#moOpen}}" + moResult="$moResult$moOpen" + mo::getContentTrim moTemp + moResult="$moResult$moTemp" + mo::chomp moClose "${MO_UNPARSED%%="$MO_CLOSE_DELIMITER"*}" + MO_UNPARSED="${MO_UNPARSED:${#moClose}}" + moResult="$moResult$moClose" + mo::getContentTrim moTemp + moResult="$moResult$moTemp" + MO_OPEN_DELIMITER="$moOpen" + MO_CLOSE_DELIMITER="$moClose" + + local "$1" && mo::indirect "$1" "$moResult" +} + + +# Get the content up to and including a close tag. First two non-whitespace +# tokens become the new open and close tag. +# +# $1 - Destination variable, an array +# $2 - Terminator string +# +# The array contents: +# [0] The raw content within the tag +# [1] The parsed tokens as a single string +# +# Returns nothing. +mo::getContentWithinTag() { + local moUnparsed moTokens + + moUnparsed=${MO_UNPARSED} + mo::tokenizeTagContents moTokens "$MO_CLOSE_DELIMITER" + MO_UNPARSED=${MO_UNPARSED#"$MO_CLOSE_DELIMITER"} + mo::tokensToString moTokensString "${moTokens[@]:1}" + moParsed=${moUnparsed:0:$((${#moUnparsed} - ${#MO_UNPARSED}))} + + local "$1" && mo::indirectArray "$1" "$moParsed" "$moTokensString" +} + + +# Internal: Parse MO_UNPARSED and retrieve the content within the tag +# delimiters. Converts everything into an array of string values. +# +# $1 - Destination variable for the array of contents. +# $2 - Stop processing when this content is found. +# +# The list of tokens are in RPN form. The first item in the resulting array is +# the number of actual tokens (after combining command tokens) in the list. +# +# Given: a 'bc' "de\"\n" (f {g 'h'}) +# Result: ([0]=4 [1]=NAME [2]=a [3]=VALUE [4]=bc [5]=VALUE [6]=$'de\"\n' +# [7]=NAME [8]=f [9]=NAME [10]=g [11]=VALUE [12]=h +# [13]=BRACE [14]=2 [15]=PAREN [16]=2 +# +# Returns nothing +mo::tokenizeTagContents() { + local moResult moTerminator moTemp moUnparsedOriginal moTokenCount + + moTerminator=$2 + moResult=() + moUnparsedOriginal=$MO_UNPARSED + moTokenCount=0 + mo::debug "Tokenizing tag contents until terminator: $moTerminator" + + while true; do + mo::trimUnparsed + + case "$MO_UNPARSED" in + "") + mo::errorNear "Did not find matching terminator: $moTerminator" "$moUnparsedOriginal" + ;; + + "$moTerminator"*) + mo::debug "Found terminator" + local "$1" && mo::indirectArray "$1" "$moTokenCount" ${moResult[@]+"${moResult[@]}"} + return + ;; + + '('*) + #: Do not tokenize the open paren - treat this as RPL + MO_UNPARSED=${MO_UNPARSED:1} + mo::tokenizeTagContents moTemp ')' + moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]:1}" PAREN "${moTemp[0]}") + MO_UNPARSED=${MO_UNPARSED:1} + ;; + + '{'*) + #: Do not tokenize the open brace - treat this as RPL + MO_UNPARSED=${MO_UNPARSED:1} + mo::tokenizeTagContents moTemp '}' + moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]:1}" BRACE "${moTemp[0]}") + MO_UNPARSED=${MO_UNPARSED:1} + ;; + + ')'* | '}'*) + mo::errorNear "Unbalanced closing parenthesis or brace" "$MO_UNPARSED" + ;; + + "'"*) + mo::tokenizeTagContentsSingleQuote moTemp + moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}") + ;; + + '"'*) + mo::tokenizeTagContentsDoubleQuote moTemp + moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}") + ;; + + *) + mo::tokenizeTagContentsName moTemp + moResult=(${moResult[@]+"${moResult[@]}"} "${moTemp[@]}") + ;; + esac + + mo::debug "Got chunk: ${moTemp[0]} ${moTemp[1]}" + moTokenCount=$((moTokenCount + 1)) + done +} + + +# Internal: Get the contents of a variable name. +# +# $1 - Destination variable name for the token list (array of strings) +# +# Returns nothing +mo::tokenizeTagContentsName() { + local moTemp + + mo::chomp moTemp "${MO_UNPARSED%%"$MO_CLOSE_DELIMITER"*}" + moTemp=${moTemp%%(*} + moTemp=${moTemp%%)*} + moTemp=${moTemp%%\{*} + moTemp=${moTemp%%\}*} + MO_UNPARSED=${MO_UNPARSED:${#moTemp}} + mo::trimUnparsed + mo::debug "Parsed default token: $moTemp" + + local "$1" && mo::indirectArray "$1" "NAME" "$moTemp" +} + + +# Internal: Get the contents of a tag in double quotes. Parses the backslash +# sequences. +# +# $1 - Destination variable name for the token list (array of strings) +# +# Returns nothing. +mo::tokenizeTagContentsDoubleQuote() { + local moResult moUnparsedOriginal + + moUnparsedOriginal=$MO_UNPARSED + MO_UNPARSED=${MO_UNPARSED:1} + moResult= + mo::debug "Getting double quoted tag contents" + + while true; do + if [[ -z "$MO_UNPARSED" ]]; then + mo::errorNear "Unbalanced double quote" "$moUnparsedOriginal" + fi + + case "$MO_UNPARSED" in + '"'*) + MO_UNPARSED=${MO_UNPARSED:1} + local "$1" && mo::indirectArray "$1" "VALUE" "$moResult" + return + ;; + + \\b*) + moResult="$moResult"$'\b' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\e*) + #: Note, \e is ESC, but in Bash $'\E' is ESC. + moResult="$moResult"$'\E' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\f*) + moResult="$moResult"$'\f' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\n*) + moResult="$moResult"$'\n' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\r*) + moResult="$moResult"$'\r' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\t*) + moResult="$moResult"$'\t' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\v*) + moResult="$moResult"$'\v' + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + \\*) + moResult="$moResult${MO_UNPARSED:1:1}" + MO_UNPARSED=${MO_UNPARSED:2} + ;; + + *) + moResult="$moResult${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + ;; + esac + done +} + + +# Internal: Get the contents of a tag in single quotes. Only gets the raw +# value. +# +# $1 - Destination variable name for the token list (array of strings) +# +# Returns nothing. +mo::tokenizeTagContentsSingleQuote() { + local moResult moUnparsedOriginal + + moUnparsedOriginal=$MO_UNPARSED + MO_UNPARSED=${MO_UNPARSED:1} + moResult= + mo::debug "Getting single quoted tag contents" + + while true; do + if [[ -z "$MO_UNPARSED" ]]; then + mo::errorNear "Unbalanced single quote" "$moUnparsedOriginal" + fi + + case "$MO_UNPARSED" in + "'"*) + MO_UNPARSED=${MO_UNPARSED:1} + local "$1" && mo::indirectArray "$1" VALUE "$moResult" + return + ;; + + *) + moResult="$moResult${MO_UNPARSED:0:1}" + MO_UNPARSED=${MO_UNPARSED:1} + ;; + esac + done +} + + +# Save the original command's path for usage later +MO_ORIGINAL_COMMAND="$(cd "${BASH_SOURCE[0]%/*}" || exit 1; pwd)/${BASH_SOURCE[0]##*/}" +MO_VERSION="3.0.7" + +# If sourced, load all functions. +# If executed, perform the actions as expected. +if [[ "$0" == "${BASH_SOURCE[0]}" ]] || [[ -z "${BASH_SOURCE[0]}" ]]; then + mo "$@" +fi diff --git a/build/resume-docx-reference.docx b/build/resume-docx-reference.docx new file mode 100644 index 0000000..f631233 Binary files /dev/null and b/build/resume-docx-reference.docx differ