2##! Author: Pieter van der Star (info@pietervanderstar.nl)
3##! Modifications by: (unmodified)
5##! @warning Do not use this script if your input is large and you are impatient.
6##! @copyright This program is free software: you can redistribute it and/or modify
7##! it under the terms of the GNU Affero General Public License as
8##! published by the Free Software Foundation, either version 3 of the
9##! License, or (at your option) any later version.
11##! This program is distributed in the hope that it will be useful,
12##! but WITHOUT ANY WARRANTY; without even the implied warranty of
13##! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14##! GNU Affero General Public License for more details.
16##! You should have received a copy of the GNU Affero General Public License
17##! along with this program. If not, see <https:
20#┏━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┯━━━━━━━━━━━━━━━━━━━━━━━━━━┓
21#┃ 2 nov 2023 │ First header information in place │ Pieter van der Star ┃
22#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
23#┃ 9 nov 2023 │ First release │ Pieter van der Star ┃
24#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
25#┃ 10 nov 2023 │ - Added support for function call │ Pieter van der Star ┃
26#┃ │ detection when called in conditional. │ ┃
27#┃ │ - Added function call with piping using │ ┃
28#┃ │ '<' where the pipe operator and operand│ ┃
29#┃ │ are not recognised as arguments. │ ┃
30#┃ │ - Documented the last undocumented │ ┃
32#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
33#┃ 22 dec 2023 │ - Added more function documentation │ Pieter van der Star ┃
34#┃ │ - Removed unused function │ ┃
35#┃ │ consume_comment. │ ┃
36#┃ │ - Extended operator support in │ ┃
37#┃ │ parse_assignment. │ ┃
38#┃ │ - Minor style improvements. │ ┃
39#┃ │ - Fixed bug where assignments were │ ┃
40#┃ │ sometimes rewritten as 0 or 1 │ ┃
41#┃ │ (depending on the const status). │ ┃
42#┃ │ - Added argument flags and argument │ ┃
44#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
45#┃ 20 feb 2024 │ - Added support for "name() {" fuctions. │ Pieter van der Star ┃
46#┃ │ - Improved documentation. │ ┃
47#┃ │ - Removed debug code │ ┃
48#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
49#┃ 22 feb 2024 │ - Added translation for $# into │ Pieter van der Star ┃
50#┃ │ func_num_args(). │ ┃
51#┃ │ - Fixed bug where $ was appended to ints │ ┃
52#┃ │ in arithmatic in assignement. │ ┃
53#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
54#┃ 24 feb 2024 │ - Fixed a lot of small bugs after testing│ Pieter van der Star ┃
55#┃ │ on larger code base (my own scripts). │ ┃
56#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
57#┃ 25 feb 2024 │ - Revized conditional handling. │ Pieter van der Star ┃
58#┠───────────────┼──────────────────────────────────────────┼──────────────────────────┨
59#┃ 29 feb 2024 │ - Added support for heredoc in │ Pieter van der Star ┃
60#┃ │ conditional handling. │ ┃
61#┃ │ - Added trailing newline to │ ┃
62#┃ │ update_function error message. │ ┃
63#┃ │ - Added for loop support. │ ┃
64#┃ │ - Added support for indirect expansion in│ ┃
66#┃ │ - Added support for hyphen ('-') in │ ┃
68#┗━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━┛
71##! @brief This script transforms bash scripts into php-ish that Doxygen can parse.
73##! @todo Review function documentation.
74##! @todo Add support for [[ regex without if.
75##! @todo Add support for "source <script>".
76##! @todo Add support for line continuation ("\" followed by newline).
77##! @todo Add return type other than int if the function doesn't contain a return statement. (Also handle return void.)
78##! @todo Count the newlines in multiline condition (when using heredocs) for linenumber in error messages.
79##! @bug The line number for variable errors (such as incorrect flag to declare) seems to be one line too early.
80##! @bug When the source document is updated while the filter is running the filter sometimes ends
81##! up in an endless loop showing the error "Unknown item in get_composition_element_size".
83##! This script works by parsing the Bash script file and then outputting matching PHP code on
84##! stdout. The parsing stores each tokens in one big flat array. This array is called
85##! composition. To identify each kind of token the first element is an identifier, which
86##! determines how many elements follow. Thy are stored as listed in the table below:
87##! |Purpose |Identifier|Elements[1]| Purpose of element |
88##! |:-----------------------|:---------|----------:|:----------------------------------------|
89##! |shebang |! | 1| Contents of the shebang |
90##! |Doxygen comment |d | 1| Contents of the comment |
91##! |comment |# | 1| Contents fo the comment |
93##! |variable |v | 1| Indicates readonly variable |
94##! | | | 2| Type of the variable |
95##! | | | 3| Name of the variable |
96##! | | | 4| Assigned value, or empty if not assigned|
97##! |function |f | 1| Name of the function |
98##! | | | 2| Number of parameters, can be 0 if none |
99##! | | | >2 (odd)| Name of the parameter |
100##! | | | >2 (even)| Type of the parameter |
101##! |scope opening |{ | -| |
102##! |scope closing |} | -| |
103##! |command/function call |c | 1| The command |
104##! | | | 2| Number of arguments, can be 0 if none |
105##! | | | >2| Parameter values |
106##! |flow control |j | 1| Type of control (if, while else etc) |
107##! | | | 2| The condition |
108##! |return statement |r | -| |
109##! |variable assignment |= | 1| The operand |
110##! | | | 2| The operator |
111##! | | | 3| The value |
112##! |whitespace |w | 1| The value |
113##! |switch option |o | 1| The value |
114##! |break statement (;;) | | -| |
115##! @b 1 Elements is the index of the element after the identifier.
116##! @note The matching code may not be valid php but is good enough for Doxygen to parse.
117##! @note Not all traits of the bash language map to php. This script tries to map as close as possible, and otherwise discards this information.
120##! The script works in three stages:
125##! Parsing extracts the meaningful data from the script file.\n
126##! Analysis enhances that data.\n
127##! Writing outputs the data.
129##! This code is written with readibility as first priority (after the "it should do its job"
133##! -# Add this file to the projects root directory (where your doxyfile lives).
134##! -# Update some of the doxyfile values.
135##! -# First tell Doxygen to document .sh files as if it is a php file:
136##! <tt>EXTENSION_MAPPING = sh=php</tt>.
137##! -# Then Doxygen needs to know about this filter:
138##! <tt>FILTER_PATTERNS = *.sh=./doxygen-bash-filter.sh</tt>.
139##! -# add .sh to the FILE_PATTERNS list.
140##! -# Optionally add doxygen-bash-filter.sh to the EXCLUDE values.
141##! <tt>EXCLUDE = doxygen-bash-filter.sh</tt>.
142##! @note When @c EXTENSION_MAPPING, @c FILTER_PATTERNS or @c EXCLUDE is already set add the values to the
145##! @defgroup return_values Return values
146##! @brief List of return values used in this script.
149##! Return value for success.
150declare -i -r C_SUCCESS=0;
151##! Return value on parse error.
152declare -i -r C_PARSE_ERROR=1;
153##! Return value on error when analysing parsed composition.
154declare -i -r C_ANALYSIS_ERROR=2;
155##! Return value on error when writing result.
156declare -i -r C_WRITE_ERROR=3;
160 ##############################################################################################
164 ##############################################################################################
166##! @brief This is a function just to test the function declaration option not used in the rest of this document.
168 echo "This is a function and here just as a test";
170##! @brief This is a function just to test the function declaration option not used in the rest of this document.
172 echo "This is a function and here just as a test";
175##! @brief This is a function just to test the function declaration option not used in the rest of this document.
177 echo "This is a function and here just as a test";
180##! @brief Reads the next character from the input stream
181##! @post Sets inherited $char with the next character, unless it is an escaped char in which case it.
182##! @post Sets the char variable to two chars ( '\' and <char>).
183function get_next_char {
185 if [ "$char" == "\\" ]; then
186 read -N 1 -r second_char;
187 char="$char$second_char"
190##! @brief Removes the leading and trailing double quotes " from the given string.
191##! @param $arg1 Name of the string variable to remove the quotes from
192##! @post The string named by $arg1 has its leading and trailing double quotes removed.
194 local -n
string=
"$1";
195 if [[
"$string" =~ \
"(\$[[:alnum:]_]+)\" ]]; then
196 string=${BASH_REMATCH[1]};
200##! @brief Discard whitespaces (except newline) from the input stream.
201##! @param $arg1 Should the characters be appended to token? (1 yes, !=1 no)
202##! @returns Error code according to @ref return_values.
203##! @post Sets the inherited $char to the first non-whitespace character gotten via
204##! @ref get_next_char().
205##! @post Updates inherited $token (depending on $arg1) with all characters until a closing quote is
207##! @note For the purposes of this function "\n
" is not a whitespace character.
208function consume_whitespaces {
209 local -i append_token="$1
";
210 if [ "$append_token
" -eq 1 ]; then
213 while [ "$char
" != "" ]; do
214 if [[ "$char
" =~ [^[:blank:]] ]]; then
217 if [ "$append_token
" -eq 1 ]; then
225##! @brief Append everything to $token untill a command separator is encountered.
226##! @returns Error code according to @ref return_values.
227##! @post Updates inherited $token with all characters until a command separator is encountered.
228##! @post Sets the inherited $char to the separator character (except when "<space>#
", then sets
230##! @note Separators are:
233##! - "<non-newline whitespace>#
"
234function consume_until_cmd_separator {
235 while [ "$char
" != "" ]; do
236 if [ "$char
" == "\
"" ]; then #unescaped
'"'
240 if [[
"$char" ==
"\'" ]]; then #unescaped
'''
244 if [
"$char" == $
'\n' ]; then #newline
247 if [
"$char" == $
';' ]; then #unescaped
';'
250 if [[
"${token: -1}$char" =~ ^[[:blank:]]# ]]; then # whitespace followed by
'#'
259##! @brief Append everything to $token untill a non word character is encountered.
260##! @returns Error code according to @ref return_values.
261##! @post Updates inherited $token with all characters until a non word character is encountered.
262##! @post Sets the inherited $char to the non word character
263##! @note Word characters are:
264##! - Letters A to Z both upper and lowercase
265##! - The underscore character ("_")
267 while [[
"$char" =~ [[:alnum:]_] ]];
do
273##! @brief Append everything to $token untill a closing quote is encountered.
274##! @param $arg1 Should the characters be appended to $token? (1 yes, !=1 no)
275##! @returns Error code according to @ref return_values.
276##! @post Updates inherited $token (depending on $arg1) with all characters until a closing quote is
278##! @post Sets the inherited $char to the closing quote.
280 local -i append_token=
"$1";
282 local -r opening=
"$char";
283 if [
"$append_token" -eq 1 ]; then
286 while [
"$char" !=
"" ];
do
288 if [
"$append_token" -eq 1 ]; then
291 if [[
"$char" ==
"$opening" ]]; then
296 echo
"Warning: missing closing ${opening}-character @line: $linenumber." 1>&2;
300##! @brief Append everything to $token until the matching closing bracket (']') is encountered.
301##! @returns Error code according to @ref return_values.
302##! @post Updates inherited $token with all characters until the matching closing bracket is
304##! @post Sets the inherited $char to the closing bracket.
309 while [[
"$char" !=
"" && $level -gt 0 ]];
do
310 if [
"$char" ==
"\"" ]; then #unescaped
'"'
314 if [[
"$char" ==
"\'" ]]; then #unescaped
'''
320 if [
"$char" ==
"[" ]; then
323 if [
"$char" ==
"]" ]; then
331##! @brief Consumes all characters until a matching closing parenthesis is found.
332##! @returns Error code according to @ref return_values.
333##! @post Updates inherited $token with all characters until the matching closing bracket is encountered.
334##! @post Sets the inherited $char to the closing parenthes.
339 while [[
"$char" !=
"" && $level -gt 0 ]];
do
340 if [
"$char" ==
"\"" ]; then #unescaped
'"'
343 if [
"$char" ==
"\'" ]; then #unescaped
'''
347 if [
"$char" ==
"(" ]; then
350 if [
"$char" ==
")" ]; then
358##! @brief Append everything to $token untill a whitespace is encountered.
359##! @returns Error code according to @ref return_values.
360##! @post Updates inherited $token with all characters until a whitespace is encountered.
361##! @post Sets the inherited $char to the first whitespace.
364 while [
"$char" !=
"" ];
do
365 if [[
"$char" =~ [[:blank:]] ]]; then
374##! @brief Parse the comment and add it to the inherited composition array.
375##! @returns Error code according to @ref return_values.
376##! @post Upon successful paring of the comment the inherited $token is cleared.
377##! @post Sets the inherited $char to the first character after the comment.
379 local index=${#composition[@]};
381 while [[
"$char" !=
"" &&
"$char" != $
'\n' ]];
do
386 if [
"${token:1:2}" ==
"! " ]; then #check
if comment is shebang
387 composition[$index]=
"!"; #indicate we have shebang
389 elif [
"${token:1:2}" ==
"#!" ]; then #check
if comment is a doxygen comment
390 composition[$index]=
"d"; #update to a doxygen comment
393 composition[$index]=
"#"; #indicate we have a
"normal" comment
396 composition[$((index+1))]=
"$token";
398 if [
"$char" != $
'\n' ]; then
405##! @brief Parse a variable and add it to the inherited composition array.
406##! @returns Error code according to @ref return_values.
407##! @pre Expects the inherited $char to be set to the first character of the function to handle.
408##! @post Clears the inherited $token upon completion. Sets the inherited char to the first char after the variable
410 local index=${#composition[@]};
411 composition[$index]=
"v"; #
this is a variable
412 composition[$((index+1))]=0; #1
if readonly, otherwise 0
413 composition[$((index+2))]=
"str"; #type
414 composition[$((index+3))]=
""; #name
415 composition[$((index+4))]=
""; #assignment (
if any)
417 local -i -r S_PROPERTY=1;
418 local -i -r S_NAME=2;
421 while [
"$char" !=
"" ];
do
425 "r") composition[$((index+1))]=1;;
426 "i") composition[$((index+2))]=
"int";;
427 "a") composition[$((index+2))]=
"list";;
428 "A") composition[$((index+2))]=
"list";;
429 "g") ;; ##! @todo add support
for global
430 "n") ;; ##! @todo how to handle
this? Seems only interesting with regards to arguments which then
get a reference instead of value.
436 ##! @todo Change name handling so a name of the shape "$name" (including the quotes is allowed. Quick fix now allows both " and $ everywhere in the name.
437 if [[ "$char" =~ ([[:space:]]|[\;=]) ]]; then
439 elif [[ "$char" =~ ([^[:alnum:]_\"\$]) ]]; then
440 return $C_PARSE_ERROR;
442 composition[$((index+3))]+="$char";
445 consume_whitespaces 0;
446 if [ "$char" == "-" ]; then
450 continue; #skip reading new char, process first
457 if [ "$char" == "=" ]; then
460 consume_until_cmd_separator;
461 composition[$((index+4))]="$token";
471##! @brief Parse a function declaration and add it to the inherited composition array.
472##! @post Updates char to the last char of the function.
473## Start by getting the name, then just consume until the opening bracket, discarding the stuff beteen.
474function parse_function {
475 local index=${#composition[@]};
476 composition[$index]="f"; #this is a function
477 composition[$((index+1))]=""; #name
478 composition[$((index+2))]=0; #placeholder for number of parameters
480 #before we get the name we need to discard the whitespaces in between the function keyword and the name
485 consume_until_non_word_char;
486 composition[$((index+1))]="$token";
488 #then just consume until the opening bracket
489 while [ "$char" != "{" ]; do
497##! @brief Parse a function or command call and add it to the inherited composition array.
498##! @returns Error code according to @ref return_values.
499##! @pre Expects inherited $token to be set to the function name.
500##! @pre Expects inherited $char to be set to the first char after the function name.
501function parse_command {
502 local index=${#composition[@]};
503 composition[$index]="c"; #this is a command/call
504 composition[$((index+1))]="$token";
505 composition[$((index+2))]=0; #number of arguments
509 consume_whitespaces 0;
512 while [ "$char" != "" ]; do
513 if [[ "$char" == "<" ]]; then #input piping operator shouldn't be considered an argument
515 #The thing being piped however can be considered an argument, so
516 ##! @todo conditionally add the thing being piped as argument based on command line parameter.
517 consume_until_cmd_separator;
520 elif [[ "$char" =~ [[:space:]] || "$char" == ";" ]]; then
521 if [ ${#token} -gt 0 ]; then
522 if [[ "$nargs" -eq 0 && "$token" == "()" ]]; then
523 #This isn´t a command but a function definition
524 composition[$index]="f"; #update to a function
525 #index+1 can stay as is, the content is correct, name is the same token
526 #index+1 can stay as is, the content is correct for now 0 parameters
529 #then just consume until the opening bracket
530 while [ "$char" != "{" ]; do
538 composition[$((index+2+nargs))]="$token";
541 if [[ "$char" == $'\n' || "$char" == ";" ]]; then
544 elif [[ "$char" == "\"" || "$char" == "\'" ]]; then
552 if [[
"$char" != $
'\n' ]]; then
555 composition[$((index+2))]=
"$nargs"; #number of arguments
559##! @brief consumes input until the keyword ($arg1) has been found.
560##! @param $arg1 Keyword to search for.
561##! @returns Error code according to @ref return_values.
562##! @retval $C_SUCCESS if keyword found.
563##! @retval $C_PARSE_ERROR if keyword not found before end of file occurred.
564##! @post Sets the inherited $token to everything before the keyword.
565##! Sets the inherited $char to the first whitespace following the keyword.
566##! @todo handle parenthesis, if keyword is within the parenthesis it doesn't count.
569 #shellcheck disable=2078 #While true by design.
572 if [
"$char" ==
"" ]; then
575 if [[
"$char" =~ [[:space:]] ]]; then
576 if [
"$word" ==
"$1" ]; then
587##! @brief Parse a flow control statement and add it to the inherited composition array.
588##! @returns Error code according to @ref return_values.
589##! @pre Expects the inherited $token to contain the control flow keyword.
590##! @post Clears the inherited $token upon successful return.
592 local keyword=
"$token"; #control keyword
593 local index=${#composition[@]};
594 composition[$index]=
"j"; #
this is flow control (j
for jumps)
595 composition[$((index+1))]=
"$keyword"; #control keyword
596 composition[$((index+2))]=
""; #condition
597 local end_keyword=
"";
610 "else"|
"fi"|
"done"|
"esac"|
"continue"|
"break") {
611 composition[$((index+2))]=
""; #condition doesn
't exist
616 echo "unknown flow control token |$char|" 1>&2;
617 return $C_PARSE_ERROR;
621 consume_until_keyword "$end_keyword";
623 composition[$((index+2))]="$token"; #condition
624 consume_whitespaces 0;
630##! @brief Gets the condition of the switch.
631##! @returns Error code (for now always success) according to @ref return_values.
632##! @post Updates the condition field in the composition element.
634 #get the switch condition
636 while [[ ! "$token" =~ [[:blank:]]?(.*)[[:blank:]]+in ]]; do
637 consume_until_cmd_separator;
639 token="${BASH_REMATCH[1]}"; #remove the " in" part
640 strip_quotes_if_variable "token";
641 composition[$((index+2))]="$token"; #condition
646##! @brief Gets the value of the switch case.
647##! @returns Error code according to @ref return_values.
648##! @post Adds the case option to the composition.
649function parse_case_option {
650 local index=${#composition[@]};
651 composition[$index]="o"; #this is a switch option (o for option)
652 if [[ "$token" =~ (.*)\) ]]; then
653 composition[$((index+1))]="${BASH_REMATCH[1]}"; #option value
655 return $C_PARSE_ERROR;
660##! @brief Extract variable name and value from assignment statements.
661##! @returns Error code (for now always success) according to @ref return_values.
662##! @pre Expects the inhertied $token to be filled with the value being assigned.
663##! @post Clears the inherited $token.
665##! Adds the assignment information to the composition.
666function parse_assignment {
667 local index=${#composition[@]};
668 composition[$index]="="; #this is an assignment
670 case "${token: -2}" in
671 "+="|"-="|"*="|"/="|"%="|"<<="|">>="|"&="|"^="|"|=") {
672 composition[$((index+1))]="${token::-2}"; #operand
673 composition[$((index+2))]="${token: -2}"; #operator
676 composition[$((index+1))]="${token::-1}"; #operand
677 composition[$((index+2))]="${token: -1}"; #operator
682 consume_until_cmd_separator;
683 composition[$((index+3))]="$token"; #value
690##! @brief Parses an sh file into a composition.
691##! @returns Error code (for now always success) according to @ref return_values.
693#!! Reads tokens from the input pipe and parses this into a composition containing the flow of
695##! Call like <tt>parse < "inputfile.sh"</tt>.
700 local -i linenumber=0;
702 #shellcheck disable=2078 #While true by design.
704 if [[ "$token$char" =~ ^[[:blank:]]+[^[:blank:]]$ ]]; then
705 composition[${#composition[@]}]="w"; #indicate we have whitespace
706 composition[${#composition[@]}]="$token"; #add the contents
710 #if [ "$char" == "(" ]; then
711 # consume_until_closing_parenthesis;
714 if [ "$char" == "[" ]; then
715 consume_until_closing_square_bracket;
718 if [[ "$char" == "\"" || "$char" == "\'" ]]; then
719 consume_until_closing_quote 1;
723 if [[ "$token$char" =~ ^#.* ]]; then
725 if ! parse_comment; then
726 echo "Comment error @line: $linenumber" 1>&2;
727 return $C_PARSE_ERROR;
729 elif [[ "$token$char" =~ ^declare[[:blank:]]$ ]]; then
730 if ! parse_variable; then
731 echo "Variable error @line: $linenumber" 1>&2;
732 return $C_PARSE_ERROR;
734 elif [[ "$token$char" =~ ^local[[:blank:]]$ ]]; then
735 if ! parse_variable; then
736 echo "Variable error @line: $linenumber" 1>&2;
737 return $C_PARSE_ERROR;
739 elif [[ "$token" == $'\n
' ]]; then
740 composition[${#composition[@]}]="n"; #indicate we have a newline
741 #increment the linecount
742 linenumber=$((linenumber+1))
743 ##! @todo make this a switch for use during debugging?
744 #echo "Parsing now at line: $linenumber" 1>&2;
746 elif [ "$token" == $';
' ]; then
747 #check the next char to detect a break ";;".
748 if [ "$char" == $';
' ]; then
749 composition[${#composition[@]}]=";"; #indicate we have a break statement
753 #just skip over the command separator (if just ;)
755 elif [[ "$token$char" =~ ^function[[:blank:]]$ ]]; then
756 if ! parse_function; then
757 echo "Function error @line: $linenumber" 1>&2;
758 return $C_PARSE_ERROR;
760 elif [[ "$token" =~ ^[\{\}]$ ]]; then
761 composition[${#composition[@]}]="$token"; #indicate we have an opening of a scope
763 elif [[ "$token" =~ ^return[[:blank:]]$ ]]; then
764 composition[${#composition[@]}]="r"; #indicate we have a return statement
765 consume_until_cmd_separator;
766 ##! @todo why do we have an extra space between return and value?
767 composition[${#composition[@]}]="$token";
769 elif [[ "$char" == "=" ]]; then
770 if ! parse_assignment; then
771 echo "Assignment error @line: $linenumber" 1>&2;
772 return $C_PARSE_ERROR;
775 elif [[ "$token" =~ ^[^\(]*\)$ ]]; then
778 #elif [[ "$token$char" =~ ([[:alnum:]_]*)[[:space:]]*\([[:space:]]*\) ]]; then
779 #echo "Func? \"$token\"";
780 #if ! parse_function "${BASH_REMATCH[1]}"; then
781 # echo "Function error" 1>&2;
782 # return $C_PARSE_ERROR;
784 elif [[ "$token$char" =~ ^[[:alnum:]_\.\/-]+[^[:alnum:]_\.\/-]$ || "$char" == "" || $char == ";" ]]; then
786 "while"|"for"|"done"|"if"|"elif"|"else"|"fi"|"case"|"esac"|"continue"|"break") {
787 if ! parse_flow_control; then
788 echo "flow control error @line: $linenumber" 1>&2;
789 return $C_PARSE_ERROR;
795 if ! parse_command; then
796 echo "Command error @line: $linenumber" 1>&2;
797 return $C_PARSE_ERROR;
802 if [ "$char" == "" ]; then
808 if [ ${#token} -ne 0 ]; then
809 echo "token not empty. Still containts: \"$token\"" 1>&2;
811 printf "ASCII values: " 1>&2;
812 while [ "$i" -lt "${#token}" ]; do
813 printf " %d" "'${token:
$i:1}
" 1>&2;
817 return $C_PARSE_ERROR;
822##! @brief Calculate the offset of the next element in components based on the current one.
823##! @param $arg1 Index of the composition element to get the offset for.
824##! @returns The offset for this element.
825function get_composition_element_size {
828 local identifier="${composition[
$i]}
";
829 case "$identifier
" in
830 "n
"|"{
"|"}
"|";
") { #skip next 0 elements (no skip)
833 "!
"|"d
"|"#
"|"r
"|"w
"|"o
") { # skip next 1 elements
845 "c
") { #Skip dynamic for command/function call
846 local -i nargs="${composition[$((i+2))]}
";#number of arguments
847 return "$((nargs+2))
";
851 #no good value to return, but as the result is often used for increment return 1 so we
858 ##############################################################################################
862 ##############################################################################################
864##! @brief Extract function arguments ($n) and updates the function accordingly.
865##! @param $arg1 Index of the fuction in the composition.
866##! @returns Error code according to @ref return_values (for now always success).
868##! Updates the function in the composition.
869function update_function {
870 local -i composition_index;
871 composition_index="$1
";
873 local -i i="$composition_index
";
877 #Search through the functions scope for any variable set to a parameter.
878 #shellcheck disable=SC2078 #Intentional always true condition. Using breaks to terminate loop
880 if [ "$i" -gt "${#composition[@]}
" ]; then
881 printf "ERROR: Encountered end of file before scope has been closed. \n\tNested level: %d.\n
" "$level
" 1>&2;
882 return $C_ANALYSIS_ERROR;
884 get_composition_element_size "$i";
888 if [ "${composition[
$i]}
" == "{
" ]; then #handle deepen scope
892 if [ "${composition[
$i]}
" == "}
" ]; then #handle "undeepen
" scope
894 if [ "$level
" -eq 0 ]; then
900 if [ "${composition[
$i]}
" == "c
" ]; then # "decode
" variable declaration
901 local -i num_parameters="${composition[$((i+2))]}
";
903 while [ "$j
" -lt "$num_parameters
" ]; do
904 argument="${composition[$((i+3+j))]}
"; #argument to called function/program
905 if [[ "$argument
" =~ \"?\$([[:digit:]]*) ]]; then
906 local -i argument_index="${BASH_REMATCH[1]}
";
907 if [ "$num_args
" -lt "$argument_index
" ]; then
908 num_args="$argument_index
";
914 if [ "${composition[
$i]}
" == "v
" ]; then # "decode
" variable declaration
915 local variable="${composition[$((i+4))]}
";
917 if [ "${composition[
$i]}
" == "=
" ]; then #"decode
" variable assignment
918 local variable="${composition[$((i+3))]}
";
920 if [[ "$variable
" =~ \"?\$([[:digit:]]*) ]]; then
921 local -i argument_index="${BASH_REMATCH[1]}
";
922 if [ "$num_args
" -lt "$argument_index
" ]; then
923 num_args="$argument_index
";
927 composition[$((composition_index+2))]="$num_args
";
931##! @brief Analyses the composition and updates information if required.
932##! @returns Error code (for now always success) according to @ref return_values.
933##! @todo Add error checking where applicable.
936 while [ "$i" -lt ${#composition[@]} ]; do
937 if [ "${composition[
$i]}
" == "f
" ]; then
938 update_function "$i";
941 get_composition_element_size "$i";
948 ##############################################################################################
952 ##############################################################################################
954##! @brief Helper for sanitize_condition, parses the condition into variables and operators
955function parse_condition {
959 #shellcheck disable=2078 #While true by design.
962 #echo "char >${
char}<
" 1>&2;
963 if [ "$char
" == "" ]; then
966 if [[ "$token$char
" =~ ^(\[)+[[:space:]] ]]; then
967 #ignore the opening brackets
971 if [[ "$token$char
" =~ ^(\[|\])+[[:space:]] ]]; then
972 #ignore the closing brackets
976 if [[ "$token$char
" =~ ^[[:space:]] ]]; then
980 if [[ "$char
" == "\
"" ||
"$char" ==
"\'" ]]; then
983 if [[
"$token$char" =~ ^(.*)[[:blank:]]+(-[[:alpha:]]+|==|\!=|=~|&&|\|\||\!|\]+;)[[:space:]$] ]]; then
984 local condition=
"${BASH_REMATCH[1]}";
985 local
operator=
"${BASH_REMATCH[2]}";
987 if [
"${#condition}" -gt 0 ]; then
988 conditions[${#conditions[@]}]=
"v"; #indicate variable
989 conditions[${#conditions[@]}]=
"$condition";
991 if [[
"$operator" !=
"];" &&
"$operator" !=
"]];" ]]; then #ignore the closing brackets
992 conditions[${#conditions[@]}]=
"o"; #indicate
operator
993 conditions[${#conditions[@]}]=
"$operator";
996 if [
"$char" ==
"]" ]; then
1004 #As heredoc always ends with a newline we need to remove that.
1006 if [ ${#token} -gt 0 ]; then
1007 conditions[${#conditions[@]}]=
"v"; #indicate variable
1008 conditions[${#conditions[@]}]=
"$token";
1013##! @brief Helper for write_as_php, cleans up the conditionals
1014##! @returns Error code according to @ref return_values.
1015##! @pre Expects the inherited $condition variable to be populated with the condition to sanitize.
1016##! @post Updates the inherited $condition variable with the sanitized version.
1018##! Removes bash brackets around condition.
1019##! The behaviour is too tightly coupled to the writing of php for this to be part of tha analysis
1020##! stage, in practise this function is doing both analysis and writing.
1022 #we start by parsing the complete condition in parts
1023 local -a conditions;
1029 local partial_condition=
"";
1030 while [
"$i" -lt ${#conditions[@]} ];
do
1031 if [
"${conditions[$i]}" ==
"o" ]; then
1032 local
operator=
"${conditions[$((i+1))]}";
1036 if [[
"$partial_condition" =~ (.*)[[:blank:]]+=~[[:blank:]]+(.*)[[:blank:]]* ]]; then
1037 partial_condition=
"preg_match(\"${BASH_REMATCH[2]}\",${BASH_REMATCH[1]})";
1039 condition+=
"$partial_condition $operator "
1040 partial_condition=
""
1043 partial_condition+=
" ${operator} ";
1047 if [
"${conditions[$i]}" ==
"v" ]; then
1048 local variable=
"${conditions[$((i+1))]}";
1049 if [[
"$variable" =~ (!+[[:blank:]]+)([[:alnum:]_]+)(.*) ]]; then
1051 negation=
"${BASH_REMATCH[1]}";
1052 #the variable will be put throught the parser, so temporarily store the composition somewhere
1053 #else, same goes for the index i.
1054 local -a temp_composition;
1056 #shellcheck disable=SC2124 #temp_composition is an array, not a string
1057 temp_composition=${composition[@]};
1061 local -a composition;
1064 parse <<<
"${BASH_REMATCH[2]}"
1065 variable=$(write_as_php 0);
1067 #restore the composition
1068 #shellcheck disable=SC2124 #composition is an array, not a string
1069 composition=${temp_composition[@]};
1072 #the parser has appended a closing semicolon, so remove that.
1073 variable=
"${variable::-1}";
1074 #and prepend the negation if present.
1075 variable=
"$negation$variable";
1079 partial_condition+=
"${variable}";
1085 if [[
"$partial_condition" =~ (.*)[[:blank:]]+=~[[:blank:]]+(.*)[[:blank:]]* ]]; then
1086 partial_condition=
"preg_match(\"${BASH_REMATCH[2]}\",${BASH_REMATCH[1]})";
1090 condition+=
"$partial_condition";
1095##! @brief Sanitize a variable
1096##! @param $arg1 Variable to sanitize
1097##! @returns Error code according to @ref return_values.
1098##! @post Updates inherited $variable to the sanitized value.
1100##! The behaviour is too tightly coupled to the writing of php for this to be part of tha analysis
1101##! stage, in practise this function is doing both analysis and writing.
1104 if [[
"$variable" =~ (.*)\$\{#(.*)\[@\]\}(.*) ]]; then
1105 variable=
"${BASH_REMATCH[1]}count(${BASH_REMATCH[2]})${BASH_REMATCH[3]}";
1108 if [[
"$variable" =~ (.*)\$\{#(.*)\}(.*) ]]; then
1109 variable=
"${BASH_REMATCH[1]}strlen(${BASH_REMATCH[2]})${BASH_REMATCH[3]}";
1112 if [[
"$variable" =~ (.*)\$\(\((.*)\)\)(.*) ]]; then
1113 variable=
"${BASH_REMATCH[1]}(${BASH_REMATCH[2]})${BASH_REMATCH[3]}";
1115 #rewrite indirect @todo what is the equivalent in php? $$??
1116 if [[
"$variable" =~ (.*)\$\{!(.*)@\}(.*) ]]; then
1117 variable=
"array_filter(get_defined_vars(), function(\$value){return str_starts_with(\$value, '${BASH_REMATCH[2]}');});";
1119 if [[
"$variable" =~ ^\$#$ ]]; then ##! @todo
test this
1120 variable=
"func_num_args()";
1126##! @brief Sanitize an assignment (right hand side argument of assignment).
1127##! @param $arg1 Assignment to sanitize
1128##! @returns Error code according to @ref return_values.
1129##! @post Updates inherited $assignment to the sanitized value.
1132 if [[
"$assignment" =~ (.*)\$\(\((.*)\)\)(.*) ]]; then
1133 #remove $(( from start and )) from end
1134 assignment=
"${BASH_REMATCH[1]}${BASH_REMATCH[2]}${BASH_REMATCH[3]}";
1137 while [
"${#assignment}" -gt 0 ];
do
1138 if [[
"$assignment" =~ ^(\s*)([[:alnum:]_]*)(\s*)([\+-/\*%]).* ]]; then
1140 length=$((${#BASH_REMATCH[1]}+${#BASH_REMATCH[2]}+${#BASH_REMATCH[3]}+${#BASH_REMATCH[4]}));
1142 stack[${#stack[@]}]=
"${BASH_REMATCH[2]}";
1143 stack[${#stack[@]}]=
"${BASH_REMATCH[4]}";
1144 assignment=${assignment: $length};
1146 stack[${#stack[@]}]=
"$assignment";
1151 #put variable indications in front of the variables and sanitize the variable.
1152 for variable in
"${stack[@]}";
do
1153 case "${variable::1}" in
1154 "+"|
"-"|
"*"|
"/"|[0-9]) ;;#
do nothing
1159 assignment=
"$assignment$variable";
1163##! @brief clean the switch option value and replace single | or operator with phps dual ||.
1164##! @param $arg1 The option string to sanitize.
1165##! @pre Expects to inherit clean variable named option to polate with the sanitized option.
1166function sanitize_option {
1170 local -i in_string=0;
1171 while [
$i -lt ${#input} ];
do
1172 local
char=
"${input:$i:1}";
1174 if [
"$char" =
"\"" ]; then
1175 if [
"$in_string" -eq 0 ]; then
1183 if [
"$in_string" -eq 1 ]; then
1187 #replace single or operator from bash with the dual from php
1188 if [
"$char" =
"|" ]; then
1196##! @brief Write the php
1197##! @param $arg1 Include the @c <?php and @c ?> tags. If value is 1 then the tags are included,
1198## otherwise they are skipped.
1199##! @returns Error code (for now always success) according to @ref return_values.
1201##! Writes the inherited composition as php to stdout.
1202function write_as_php {
1203 local -i include_php_tags;
1204 local -i linenumber=0;
1205 include_php_tags=
"$1";
1207 if [
"$include_php_tags" -eq 1 ]; then
1210 ##! Variable for iterating over the composition.
1213 while [
"$i" -lt ${#composition[@]} ];
do
1214 case ${composition[
$i]} in
1216 printf
"// #!%s" "${composition[$((i+1))]}";
1219 "d") { #doxygen comment
1220 printf
"//!%s" "${composition[$((i+1))]}";
1224 printf
"// %s" "${composition[$((i+1))]}";
1229 linenumber=$((linenumber+1));
1233 if [
"${composition[$((i+1))]}" -eq 1 ]; then
1236 printf
"$%s" "${composition[$((i+3))]}";#name
1237 #cannot use type, or should we autoprepend with "${composition[$((i+2))]}"?
1238 if [
"${composition[$((i+4))]}" !=
"" ]; then
1239 #clean up the assigned value
1240 local variable; #needed by
sanitize_var (sets its
return value there).
1242 printf
" = %s" "$variable";#value
1249 printf
"%s(" "${composition[$((i+1))]}";#name
1252 while [
"$j" -lt
"${composition[$((i+2))]}" ];
do
1254 printf
"\$arg%d" "$j";#argument<index>
1255 if [
"$j" -lt
"${composition[$((i+2))]}" ]; then
1259 printf
"): int {"; ## @todo add parameters & conditional typehint (
void if no
return??)
1268 "c") { #command /
function call
1269 printf
"%s" "${composition[$((i+1))]}(";#name
1270 local -i nargs=
"${composition[$((i+2))]}";#number of arguments
1273 while [
"$j" -le
"$nargs" ];
do
1274 local variable; #needed by
sanitize_var (sets its
return value there).
1276 printf
"%s" "$variable"; #arguments
1277 if [
"$j" -lt
"$nargs" ]; then
1285 "j") { #command /
function call
1286 local keyword=
"${composition[$((i+1))]}";
1287 local condition=
"${composition[$((i+2))]}";
1288 #sanitize the condition
1293 printf
"if ( %s ) {" "$condition";
1296 printf
"} else if ( %s ) {" "$condition";
1301 "fi"|
"done"|
"esac") {
1305 printf
"while ( %s ) {" "$condition";
1308 printf
"switch ( %s ) {" "$condition";
1310 "continue"|
"break") {
1311 printf
"%s;" "$keyword";
1314 printf
"for (%s) {" "$condition";
1316 *) echo
"Unknown flow control at write: \"$keyword\"" 1>&2;
return $C_WRITE_ERROR;;
1321 printf
"%s;" "${composition[$((i+1))]}"; ##! @todo When parsing, exclude trailing ;
1325 local variable; #needed by
sanitize_var (sets its
return value there).
1330 printf
"$%s %s %s;" "$variable" "${composition[$((i+2))]}" "$assignment";
1334 printf
"%s" "${composition[$((i+1))]}";
1337 "o") { #
switch option
1338 ##! @todo Add check on value being * and insert default: instead.
1339 ##! @todo Add check on value being multiple (using |) and insert "||" instead.
1340 local option; #needed by sanitize_option
1341 sanitize_option
"${composition[$((i+1))]}";
1342 printf
"case %s:" "$option";
1345 ";") { #
break statement (
switch)
1348 *) echo
"something else: |${composition[$i]}|";;
1352 if [
"$include_php_tags" -eq 1 ]; then
1358##! @brief Main function
1359##! @param $arg1 Input file to rewrite to doxygen parsable php.
1360##! @returns Error code according to @ref return_values.
1362##! @todo Add flag to write to file instead of stdout (for uses outside doxgen and debugging).
1363##! @todo Add getopt.
1365 local inputfile=
"$1";
1367 local -a composition;
1368 if !
parse <
"$inputfile"; then
1369 echo
"Parse unsuccessful" 1>&2;
1373 if !
analyse <
"$inputfile"; then
1374 echo
"Analysis unsuccessful" 1>&2;
1378 printf
"" > composition.txt
1379 while [
"$i" -lt ${#composition[@]} ];
do
1380 printf
"%3d: %s" "$i" "${composition[$i]}" >> composition.txt
1384 while [
"$j" -lt
"$size" ];
do
1385 printf
", %s" "${composition[$((i+j+1))]}" >> composition.txt;
1388 printf
"\n" >> composition.txt
1391 if ! write_as_php 1; then
1392 echo
"write of php unsuccessful" 1>&2;
consume_until_closing_parenthesis()
Consumes all characters until a matching closing parenthesis is found.
parse_variable()
Parse a variable and add it to the inherited composition array.
consume_until_space()
Append everything to $token untill a whitespace is encountered.
parse_comment()
Parse the comment and add it to the inherited composition array.
parse()
Parses an sh file into a composition.
parse_assignment()
Extract variable name and value from assignment statements.
parse_condition()
Helper for sanitize_condition, parses the condition into variables and operators.
consume_until_keyword()
consumes input until the keyword ($arg1) has been found.
get_next_char()
Reads the next character from the input stream.
parse_case()
Gets the condition of the switch.
consume_until_cmd_separator()
Append everything to $token untill a command separator is encountered.
sanitize_condition()
Helper for write_as_php, cleans up the conditionals.
consume_until_non_word_char()
Append everything to $token untill a non word character is encountered.
test3()
This is a function just to test the function declaration option not used in the rest of this document...
parse_command()
Parse a function or command call and add it to the inherited composition array.
consume_until_closing_square_bracket()
Append everything to $token until the matching closing bracket (']') is encountered.
consume_whitespaces($arg1)
Discard whitespaces (except newline) from the input stream.
parse_function()
Parse a function declaration and add it to the inherited composition array.
strip_quotes_if_variable($arg1)
Removes the leading and trailing double quotes " from the given string.
get_composition_element_size($arg1)
Calculate the offset of the next element in components based on the current one.
consume_until_closing_quote($arg1)
Append everything to $token untill a closing quote is encountered.
parse_flow_control()
Parse a flow control statement and add it to the inherited composition array.
test2()
This is a function just to test the function declaration option not used in the rest of this document...
sanitize_assignment($arg1)
Sanitize an assignment (right hand side argument of assignment).
parse_case_option()
Gets the value of the switch case.
test()
This is a function just to test the function declaration option not used in the rest of this document...
sanitize_var($arg1)
Sanitize a variable.
analyse()
Analyses the composition and updates information if required.
update_function($arg1)
Extract function arguments ($n) and updates the function accordingly.
const $C_ANALYSIS_ERROR
Return value on error when analysing parsed composition.
const $C_PARSE_ERROR
Return value on parse error.
const $C_WRITE_ERROR
Return value on error when writing result.
$i
Counter for the number of rounds already done.