├── app.yaml ├── app_engine_handlers.py ├── minifier.py ├── run_tests.sh ├── template.html └── tests ├── minified_t10.sh ├── minified_t2.sh ├── minified_t3.sh ├── minified_t5.sh ├── minified_t7.sh ├── minified_t9.sh ├── minified_t_arithmetic_expansion.sh ├── minified_t_case.sh ├── minified_t_dot.sh ├── minified_t_escaped.sh ├── minified_t_functions.sh ├── minified_t_heredocs_and_herestrings.sh ├── minified_t_if_statement.sh ├── minified_t_line_continuation.sh ├── minified_t_parameters_expansion.sh ├── minified_t_pipe.sh ├── minified_t_process_substitution.sh ├── minified_t_while_until_loops.sh ├── t10.sh ├── t2.sh ├── t3.sh ├── t5.sh ├── t7.sh ├── t9.sh ├── t_arithmetic_expansion.sh ├── t_case.sh ├── t_dot.sh ├── t_escaped.sh ├── t_functions.sh ├── t_heredocs_and_herestrings.sh ├── t_if_statement.sh ├── t_line_continuation.sh ├── t_parameters_expansion.sh ├── t_pipe.sh ├── t_process_substitution.sh └── t_while_until_loops.sh /app.yaml: -------------------------------------------------------------------------------- 1 | #application: bash-minifier 2 | #version: 1 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: true 6 | 7 | handlers: 8 | - url: /.* 9 | script: app_engine_handlers.application 10 | -------------------------------------------------------------------------------- /app_engine_handlers.py: -------------------------------------------------------------------------------- 1 | import webapp2 2 | import cgi 3 | import urllib 4 | import minifier 5 | 6 | class MainPage(webapp2.RequestHandler): 7 | 8 | def get_template(self): 9 | template = "" 10 | with open("template.html", "r") as tfile: 11 | template = tfile.read() 12 | return template 13 | 14 | def get(self): 15 | url = self.request.get("url", None) 16 | self.response.headers["Content-Type"] = "text/html" 17 | if url: 18 | try: 19 | stream = urllib.urlopen(url) 20 | if stream.code != 200: 21 | raise ValueError 22 | user_source = stream.read() 23 | return self.returnMinifiedSource(user_source) 24 | except: 25 | self.response.set_status(400) 26 | return self.response.write("

400


Bad url") 27 | params = dict(user_source="", processed_source="") 28 | self.response.write(self.get_template() % params) 29 | 30 | def post(self): 31 | user_source = self.request.get("user_source", "") 32 | self.returnMinifiedSource(user_source) 33 | 34 | def returnMinifiedSource(self, user_source): 35 | processed_source = "" 36 | if user_source.strip(): 37 | try: 38 | processed_source = minifier.minify(user_source.replace("\r","")) 39 | except Exception, e: 40 | processed_source = str(e) 41 | self.response.headers["Content-Type"] = "text/html" 42 | params = dict(user_source=cgi.escape(user_source), 43 | processed_source=cgi.escape(processed_source)) 44 | self.response.write(self.get_template() % params) 45 | 46 | 47 | 48 | application = webapp2.WSGIApplication([ 49 | ("/", MainPage), 50 | ]) 51 | 52 | 53 | -------------------------------------------------------------------------------- /minifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | 5 | 6 | class BashFileIterator: 7 | class _Delimiter(object): 8 | def __init__(self, character, _type=''): 9 | self.character = character 10 | # type may be 'AP' or 'AS' (Arithmetic Expansion delimited by (()) or [] respectively), 11 | # 'S' (Command Substitution) or 'P' (Parameter Expansion) 12 | # type is set only for parenthesis or curly brace and square brace that opens group 13 | # e.g. in this statement $((1+2)) only the 1st '(' will have type ('AP') 14 | self.type = _type 15 | 16 | def is_group_opening(self): 17 | return bool(self.type or self.character in ("'", '"', '`')) 18 | 19 | def __eq__(self, other): 20 | if isinstance(other, BashFileIterator._Delimiter): 21 | return other.character == self.character 22 | elif isinstance(other, basestring): 23 | return other == self.character 24 | return False 25 | 26 | def __ne__(self, other): 27 | return not self.__eq__(other) 28 | 29 | def __str__(self): 30 | return self.character 31 | 32 | __repr__ = __str__ 33 | 34 | def __init__(self, src): 35 | self.src = src 36 | self.reset() 37 | 38 | def reset(self): 39 | self.pos = 0 40 | self.insideComment = False 41 | self.insideHereDoc = False 42 | 43 | # possible characters in stack: 44 | # (, ) -- means Arithmetic Expansion or Command Substitution 45 | # {, } -- means Parameter Expansion 46 | # [, ] -- means Arithmetic Expansion 47 | # ` -- means Command Substitution 48 | # ' -- means single-quoted string 49 | # " -- means double-quoted string 50 | self._delimiters_stack = [] 51 | self._indices_of_escaped_characters = set() 52 | 53 | def getLastGroupOpeningDelimiter(self): 54 | return next((d for d in reversed(self._delimiters_stack) if d.is_group_opening()), 55 | BashFileIterator._Delimiter('')) 56 | 57 | def pushDelimiter(self, character, _type=''): 58 | d = BashFileIterator._Delimiter(character, _type=_type) 59 | last_opening = self.getLastGroupOpeningDelimiter() 60 | last = self._delimiters_stack[-1] if len(self._delimiters_stack) > 0 else BashFileIterator._Delimiter('') 61 | 62 | if d in ('{', '}'): 63 | if _type != '': # delimiter that opens group 64 | self._delimiters_stack.append(d) 65 | elif d == '}' and last == '{': 66 | self._delimiters_stack.pop() 67 | elif d in ('(', ')'): 68 | if _type != '': # delimiter that opens group 69 | self._delimiters_stack.append(d) 70 | elif last_opening == '(': 71 | if last == '(' and d == ')': 72 | self._delimiters_stack.pop() 73 | else: 74 | self._delimiters_stack.append(d) 75 | elif d in ('[', ']'): 76 | if _type != '': # delimiter that opens group 77 | self._delimiters_stack.append(d) 78 | elif last_opening == '[': 79 | if last == '[' and d == ']': 80 | self._delimiters_stack.pop() 81 | else: 82 | self._delimiters_stack.append(d) 83 | elif d == "'" and last_opening != '"' or d == '"' and last_opening != "'" or d == '`': 84 | if d == last_opening: 85 | self._delimiters_stack.pop() 86 | else: 87 | self._delimiters_stack.append(d) 88 | 89 | def isInsideGroup(self): 90 | return len(self._delimiters_stack) != 0 91 | 92 | def getPreviousCharacters(self, n, should_not_start_with_escaped=True): 93 | """ 94 | 'should_not_start_with_escaped' means return empty string if the first character is escaped 95 | """ 96 | first_character_index = max(0, self.pos - n) 97 | if first_character_index in self._indices_of_escaped_characters: 98 | return '' 99 | else: 100 | return self.src[max(0, self.pos - n):self.pos] 101 | 102 | def getPreviousCharacter(self, should_not_start_with_escaped=True): 103 | return self.getPreviousCharacters(1, should_not_start_with_escaped=should_not_start_with_escaped) 104 | 105 | def getNextCharacters(self, n): 106 | return self.src[self.pos + 1:self.pos + n + 1] 107 | 108 | def getNextCharacter(self): 109 | return self.getNextCharacters(1) 110 | 111 | def getPreviousWord(self): 112 | word = '' 113 | i = 1 114 | while i <= self.pos: 115 | newWord = self.getPreviousCharacters(i) 116 | if not newWord.isalpha(): 117 | break 118 | word = newWord 119 | i += 1 120 | return word 121 | 122 | def getNextWord(self): 123 | word = '' 124 | i = 1 125 | while self.pos + i < len(self.src): 126 | newWord = self.getNextCharacters(i) 127 | if not newWord.isalpha(): 128 | break 129 | word = newWord 130 | i += 1 131 | return word 132 | 133 | def getPartOfLineAfterPos(self, skip=0): 134 | result = '' 135 | i = self.pos + 1 + skip 136 | while i < len(self.src) and self.src[i] != '\n': 137 | result += self.src[i] 138 | i += 1 139 | return result 140 | 141 | def getPartOfLineBeforePos(self, skip=0): 142 | result = '' 143 | i = self.pos - 1 - skip 144 | while i >= 0 and self.src[i] != '\n': 145 | result = self.src[i] + result 146 | i -= 1 147 | return result 148 | 149 | def charactersGenerator(self): 150 | hereDocWord = '' 151 | _yieldNextNCharactersAsIs = 0 152 | 153 | def close_heredoc(): 154 | self.insideHereDoc = False 155 | 156 | callbacks_after_yield = [] 157 | 158 | while self.pos < len(self.src): 159 | ch = self.src[self.pos] 160 | 161 | if _yieldNextNCharactersAsIs > 0: 162 | _yieldNextNCharactersAsIs -= 1 163 | elif ch == "\\" and not self.isEscaped(): 164 | self._indices_of_escaped_characters.add(self.pos + 1) 165 | else: 166 | if ch == "\n" and not self.isInsideSingleQuotedString() and not self.isInsideDoubleQuotedString(): 167 | # handle end of comments and heredocs 168 | if self.insideComment: 169 | self.insideComment = False 170 | elif self.insideHereDoc and self.getPartOfLineBeforePos() == hereDocWord: 171 | callbacks_after_yield.append(close_heredoc) 172 | elif not self.isInsideComment() and not self.isInsideHereDoc(): 173 | if ch in ('"', "'"): 174 | # single quote can't be escaped inside single-quoted string 175 | if not self.isEscaped() or ch == "'" and self.isInsideSingleQuotedString(): 176 | self.pushDelimiter(ch) 177 | elif not self.isInsideSingleQuotedString(): 178 | if not self.isEscaped(): 179 | if ch == "#" and not self.isInsideGroup() and \ 180 | (self.getPreviousCharacter() in ('\n', '\t', ' ', ';') or self.pos == 0): 181 | # handle comments 182 | self.insideComment = True 183 | elif ch == '`': 184 | self.pushDelimiter(ch) 185 | elif ch == '$': 186 | next_char = self.getNextCharacter() 187 | if next_char in ('{', '(', '['): 188 | next_2_chars = self.getNextCharacters(2) 189 | _type = 'AP' if next_2_chars == '((' else {'{': 'P', '(': 'S', '[': 'AS'}[next_char] 190 | self.pushDelimiter(next_char, _type=_type) 191 | _yieldNextNCharactersAsIs = 1 192 | elif ch in ('{', '}', '(', ')', '[', ']'): 193 | self.pushDelimiter(ch) 194 | elif ch == '<' and self.getNextCharacter() == '<' and not self.isInsideGroup(): 195 | _yieldNextNCharactersAsIs = 1 196 | 197 | # we should handle correctly heredocs and herestrings like this one: 198 | # echo <<< one 199 | 200 | if self.getNextCharacters(2) != '<<': 201 | # heredoc 202 | self.insideHereDoc = True 203 | hereDocWord = self.getPartOfLineAfterPos(skip=1) 204 | if hereDocWord[0] == '-': 205 | hereDocWord = hereDocWord[1:] 206 | hereDocWord = hereDocWord.strip().replace('"', '').replace("'", '') 207 | 208 | yield ch 209 | 210 | while len(callbacks_after_yield) > 0: 211 | callbacks_after_yield.pop()() 212 | 213 | self.pos += 1 214 | 215 | assert not self.isInsideGroup(), 'Invalid syntax' 216 | raise StopIteration 217 | 218 | def isEscaped(self): 219 | return self.pos in self._indices_of_escaped_characters 220 | 221 | def isInsideDoubleQuotedString(self): 222 | return self.getLastGroupOpeningDelimiter() == '"' 223 | 224 | def isInsideSingleQuotedString(self): 225 | return self.getLastGroupOpeningDelimiter() == "'" 226 | 227 | def isInsideComment(self): 228 | return self.insideComment 229 | 230 | def isInsideHereDoc(self): 231 | return self.insideHereDoc 232 | 233 | def isInsideParameterExpansion(self): 234 | return self.getLastGroupOpeningDelimiter() == '{' 235 | 236 | def isInsideArithmeticExpansion(self): 237 | return self.getLastGroupOpeningDelimiter().type in ('AP', 'AS') 238 | 239 | def isInsideCommandSubstitution(self): 240 | last_opening_delimiter = self.getLastGroupOpeningDelimiter() 241 | return last_opening_delimiter == '`' or last_opening_delimiter.type == 'S' 242 | 243 | def isInsideAnything(self): 244 | return self.isInsideGroup() or self.insideHereDoc or self.insideComment 245 | 246 | def isInsideGroupWhereWhitespacesCannotBeTruncated(self): 247 | return self.isInsideComment() or self.isInsideDoubleQuotedString() or self.isInsideDoubleQuotedString() or \ 248 | self.isInsideHereDoc() or self.isInsideParameterExpansion() 249 | 250 | 251 | def minify(src): 252 | # first: remove all comments 253 | it = BashFileIterator(src) 254 | src = "" # result 255 | for ch in it.charactersGenerator(): 256 | if not it.isInsideComment(): 257 | src += ch 258 | 259 | # secondly: remove empty strings, strip lines and truncate spaces (replace groups of whitespaces by single space) 260 | it = BashFileIterator(src) 261 | src = "" # result 262 | emptyLine = True # means that no characters has been printed in current line so far 263 | previousSpacePrinted = True 264 | for ch in it.charactersGenerator(): 265 | if it.isInsideSingleQuotedString(): 266 | # first of all check single quoted string because line continuation does not work inside 267 | src += ch 268 | elif ch == "\\" and not it.isEscaped() and it.getNextCharacter() == "\n": 269 | # then check line continuation 270 | # line continuation will occur on the next iteration. just skip this backslash 271 | continue 272 | elif ch == "\n" and it.isEscaped(): 273 | # line continuation occurred 274 | # backslash at the very end of line means line continuation 275 | # so remove previous backslash and skip current newline character ch 276 | continue 277 | elif it.isInsideGroupWhereWhitespacesCannotBeTruncated() or it.isEscaped(): 278 | src += ch 279 | elif ch in (' ', '\t') and not previousSpacePrinted and not emptyLine and \ 280 | not it.getNextCharacter() in (' ', '\t', '\n'): 281 | src += " " 282 | previousSpacePrinted = True 283 | elif ch == "\n" and it.getPreviousCharacter() != "\n" and not emptyLine: 284 | src += ch 285 | previousSpacePrinted = True 286 | emptyLine = True 287 | elif ch not in (' ', '\t', '\n'): 288 | src += ch 289 | previousSpacePrinted = False 290 | emptyLine = False 291 | 292 | # thirdly: get rid of newlines 293 | it = BashFileIterator(src) 294 | src = "" # result 295 | for ch in it.charactersGenerator(): 296 | if it.isInsideAnything() or ch != "\n": 297 | src += ch 298 | else: 299 | prevWord = it.getPreviousWord() 300 | nextWord = it.getNextWord() 301 | if it.getNextCharacter() == '{': # functions declaration, see test t8.sh 302 | if it.getPreviousCharacter() == ')': 303 | continue 304 | else: 305 | src += ' ' 306 | elif prevWord in ("until", "while", "then", "do", "else", "in", "elif", "if") or \ 307 | nextWord in ("in",) or \ 308 | it.getPreviousCharacter() in ("{", "(") or \ 309 | it.getPreviousCharacters(2) in ("&&", "||"): 310 | src += " " 311 | elif nextWord in ("esac",) and it.getPreviousCharacters(2) != ';;': 312 | if it.getPreviousCharacter() == ';': 313 | src += ';' 314 | else: 315 | src += ';;' 316 | elif it.getNextCharacter() != "" and it.getPreviousCharacter() not in (";", '|'): 317 | src += ";" 318 | 319 | # finally: remove spaces around semicolons and pipes and other delimiters 320 | it = BashFileIterator(src) 321 | src = "" # result 322 | other_delimiters = ('|', '&', ';', '<', '>', '(', ')') # characters that may not be surrounded by whitespaces 323 | for ch in it.charactersGenerator(): 324 | if it.isInsideGroupWhereWhitespacesCannotBeTruncated(): 325 | src += ch 326 | elif ch in (' ', '\t') \ 327 | and (it.getPreviousCharacter() in other_delimiters or 328 | it.getNextCharacter() in other_delimiters) \ 329 | and it.getNextCharacters(2) not in ('<(', '>('): # process substitution 330 | # see test t_process_substitution.sh for details 331 | continue 332 | else: 333 | src += ch 334 | 335 | return src 336 | 337 | 338 | if __name__ == "__main__": 339 | # https://www.gnu.org/software/bash/manual/html_node/Reserved-Word-Index.html 340 | # http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html 341 | # http://pubs.opengroup.org/onlinepubs/9699919799/ 342 | 343 | # get bash source from file or from stdin 344 | src = "" 345 | if len(sys.argv) > 1: 346 | with open(sys.argv[1], "r") as ifile: 347 | src = ifile.read() 348 | else: 349 | src = sys.stdin.read() 350 | # use stdout.write instead of print to avoid newline at the end (print with comma at the end does not work) 351 | sys.stdout.write(minify(src)) 352 | 353 | 354 | # important rules: 355 | # 1. A single-quote cannot occur within single-quotes. 356 | # 2. The input characters within the double-quoted string that are also enclosed between "$(" and the matching ')' 357 | # shall not be affected by the double-quotes, but rather shall define that command whose output replaces the "$(...)" 358 | # when the word is expanded. 359 | # 3. Within the double-quoted string of characters from an enclosed "${" to the matching '}', an even number of 360 | # unescaped double-quotes or single-quotes, if any, shall occur. A preceding character shall be used 361 | # to escape a literal '{' or '}' 362 | # 4. 363 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd tests > /dev/null 4 | passed=0 5 | failed=0 6 | for f in t*.sh; do 7 | echo -n "$f " 8 | python ../minifier.py $f > _tmp_$f 9 | if [ `diff _tmp_$f minified_$f | wc -c` -ne 0 ]; then 10 | failed=$((failed+1)) 11 | echo FAILED 12 | else 13 | passed=$((passed+1)) 14 | echo passed 15 | fi 16 | rm _tmp_$f 17 | done 18 | echo PASSED $passed, FAILED $failed 19 | popd > /dev/null -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bash Minifier 4 | 5 | 10 | 11 | 12 | 13 |

Bash Scripts Minifier

14 |
15 | 16 |
17 | 18 |
19 | 20 |

21 |
Source code on GitHub
22 |
please report problems to vs.kulaga (at) gmail.com
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/minified_t10.sh: -------------------------------------------------------------------------------- 1 | echo " 1 '2 '3 '4 5 ";echo " ( ( ) ";echo " { { } ";echo " { ' { ' } ' " -------------------------------------------------------------------------------- /tests/minified_t2.sh: -------------------------------------------------------------------------------- 1 | tname=${tname#*.} -------------------------------------------------------------------------------- /tests/minified_t3.sh: -------------------------------------------------------------------------------- 1 | if [ $# -ne 1 ];then printf "Filename is required.\n";:;fi;echo "hi | hello `whoami|tr a-z A-Z`!" -------------------------------------------------------------------------------- /tests/minified_t5.sh: -------------------------------------------------------------------------------- 1 | echo 1#2;echo 1##2;echo ${#}#c;echo 1 \# 2;echo \#1 -------------------------------------------------------------------------------- /tests/minified_t7.sh: -------------------------------------------------------------------------------- 1 | isql<&2;}>./xx$(date +'%T').txt;xx;e()(var0=20;echo 'i am "e" func';);e;echo var0=$var0;function no_braces()if true;then echo 'i am func without braces';fi;no_braces -------------------------------------------------------------------------------- /tests/minified_t_heredocs_and_herestrings.sh: -------------------------------------------------------------------------------- 1 | cat<< EOF 2 | 3 | Working dir "$PWD" `pwd` 4 | 5 | 6 | line 2 7 | 8 | EOF 9 | ls /home/;cat<< EOF 10 | 11 | line 1 12 | 13 | 14 | Working dir "$PWD" `pwd` 15 | 16 | EOF 17 | echo $((1<<5));echo 1;echo 2;tr a-z A-Z<< END_TEXT 18 | one two three 19 | four five six 20 | END_TEXT 21 | tr a-z A-Z<<- END_TEXT 22 | one two three 23 | four five six 24 | END_TEXT 25 | cat<< EOF 26 | Working dir "$PWD" `pwd` 27 | EOF 28 | cat<< 'EOF' 29 | Working dir "$PWD" `pwd` 30 | EOF 31 | tr a-z A-Z<<(echo "# of lines:" `wc -l` 1>&2)|awk '{print $1*2}' -------------------------------------------------------------------------------- /tests/minified_t_while_until_loops.sh: -------------------------------------------------------------------------------- 1 | echo "while";counter=0;while [ "$counter" -lt 10 ];do echo $counter;counter=$((counter+2));done;echo "until";counter=8;until [ "$counter" -lt 0 ];do echo $counter;counter=$((counter-2));done -------------------------------------------------------------------------------- /tests/t10.sh: -------------------------------------------------------------------------------- 1 | echo " 1 '2 '3 '4 5 " 2 | 3 | echo " ( ( ) " 4 | 5 | echo " { { } " 6 | 7 | echo " { ' { ' } ' " 8 | -------------------------------------------------------------------------------- /tests/t2.sh: -------------------------------------------------------------------------------- 1 | tname=${tname#*.} 2 | -------------------------------------------------------------------------------- /tests/t3.sh: -------------------------------------------------------------------------------- 1 | if [ $# -ne 1 ]; then 2 | printf "Filename is required.\n" 3 | : 4 | fi 5 | 6 | echo "hi | hello `whoami | tr a-z A-Z`!" 7 | -------------------------------------------------------------------------------- /tests/t5.sh: -------------------------------------------------------------------------------- 1 | # 1 2 | echo 1#2 #3 3 | echo 1##2 # 4 | echo ${#}#c 5 | 6 | echo 1 \# 2 7 | 8 | echo \#1 9 | 10 | # 2 -------------------------------------------------------------------------------- /tests/t7.sh: -------------------------------------------------------------------------------- 1 | isql <&2 74 | } > ./xx$(date +'%T').txt 75 | 76 | xx 77 | 78 | # function that invokes subshell 79 | e () ( 80 | var0=20 81 | echo 'i am "e" func' 82 | ) 83 | 84 | e 85 | 86 | echo var0=$var0 # should be empty 87 | 88 | # single command function 89 | 90 | function no_braces () if true 91 | then 92 | echo 'i am func without braces'; 93 | fi 94 | 95 | no_braces 96 | -------------------------------------------------------------------------------- /tests/t_heredocs_and_herestrings.sh: -------------------------------------------------------------------------------- 1 | # https://en.wikipedia.org/wiki/Here_document#Unix_shells 2 | 3 | cat << EOF 4 | 5 | Working dir "$PWD" `pwd` 6 | 7 | 8 | line 2 9 | 10 | EOF 11 | ls /home/ 12 | 13 | cat << EOF 14 | 15 | line 1 16 | 17 | 18 | Working dir "$PWD" `pwd` 19 | 20 | EOF 21 | 22 | echo $((1<<5)) 23 | 24 | echo 1 25 | 26 | echo 2 27 | 28 | tr a-z A-Z << END_TEXT 29 | one two three 30 | four five six 31 | END_TEXT 32 | 33 | tr a-z A-Z <<- END_TEXT 34 | one two three 35 | four five six 36 | END_TEXT 37 | 38 | cat << EOF 39 | Working dir "$PWD" `pwd` 40 | EOF 41 | 42 | cat << 'EOF' 43 | Working dir "$PWD" `pwd` 44 | EOF 45 | 46 | # HERESTRINGS 47 | 48 | tr a-z A-Z <<< one 49 | 50 | tr a-z A-Z <<< 'one two three' 51 | 52 | FOO='one two three' 53 | tr a-z A-Z <<< $FOO 54 | 55 | tr a-z A-Z <<< 'one 56 | two three' 57 | 58 | tr a-z A-Z <<< ' 59 | one 60 | two three 61 | ' 62 | 63 | tr a-z A-Z <<< " 64 | one 65 | two three 66 | " 67 | 68 | bc <<< 2^10 -------------------------------------------------------------------------------- /tests/t_if_statement.sh: -------------------------------------------------------------------------------- 1 | if 2 | 3 | [ "a" = "b" ] 4 | 5 | then 6 | echo 1 7 | elif 8 | 9 | [ "a" = "c" ] 10 | 11 | then 12 | echo 2 13 | 14 | elif [ "a" = "d" ];then 15 | echo 3 16 | 17 | else 18 | 19 | echo 4 20 | 21 | fi -------------------------------------------------------------------------------- /tests/t_line_continuation.sh: -------------------------------------------------------------------------------- 1 | # line continuation 2 | # it does not work inside comments and single quoted strings 3 | 4 | # all next 4 echos should look identically after minification 5 | echo "ab" "cd" 6 | 7 | echo "ab" \ 8 | "cd" 9 | 10 | echo "ab"\ 11 | "cd" 12 | 13 | echo "ab" \ 14 | "cd" 15 | 16 | echo "double quoted:" 17 | echo "ab\ 18 | cd" 19 | 20 | echo "single quoted:" 21 | echo 'ab\ 22 | cd' 23 | 24 | echo "parameter expansion \${}:" 25 | a=abcd 26 | echo ${a\ 27 | } 28 | 29 | echo "command substitution \$():" 30 | echo $(echo a\ 31 | b c d) 32 | 33 | echo "command substitution \`\`:" 34 | echo `echo a\ 35 | b c d` 36 | 37 | echo "heredoc:" 38 | cat << HD 39 | HI!\ 40 | I'm here doc 41 | HD 42 | 43 | 44 | echo "arithmetic expansion \$(()):" 45 | echo 15 \* 99 = $((15\ 46 | *99)) 47 | 48 | echo "comment:" 49 | # echo "if you THIS -- next statement is incorrect:";\ 50 | echo "line continuation does not work inside comments" -------------------------------------------------------------------------------- /tests/t_parameters_expansion.sh: -------------------------------------------------------------------------------- 1 | a=abc{def 2 | echo ${a#a*{} 3 | 4 | echo $( echo 11 ) 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/t_pipe.sh: -------------------------------------------------------------------------------- 1 | seq 1 100 | 2 | grep 9 -------------------------------------------------------------------------------- /tests/t_process_substitution.sh: -------------------------------------------------------------------------------- 1 | # for details see 2 | # https://superuser.com/questions/1059781/what-exactly-is-in-bash-and-in-zsh 3 | # https://en.wikipedia.org/wiki/Process_substitution 4 | 5 | cat < <(which wc) 6 | 7 | cat <(which wc) 8 | 9 | seq 1 10 | tee >(echo "# of lines:" `wc -l` 1>&2) | awk '{print $1*2}' 10 | 11 | -------------------------------------------------------------------------------- /tests/t_while_until_loops.sh: -------------------------------------------------------------------------------- 1 | echo "while" 2 | 3 | counter=0 4 | 5 | while 6 | 7 | [ "$counter" -lt 10 ] 8 | 9 | do 10 | 11 | 12 | echo $counter 13 | 14 | 15 | counter=$((counter+2)) 16 | 17 | done 18 | 19 | echo "until" 20 | 21 | 22 | counter=8 23 | 24 | until 25 | 26 | [ "$counter" -lt 0 ] 27 | 28 | do 29 | 30 | 31 | echo $counter 32 | 33 | 34 | counter=$((counter-2)) 35 | 36 | done --------------------------------------------------------------------------------