├── .editorconfig ├── .travis.yml ├── src ├── utils │ ├── index.js │ ├── non-null.js │ ├── filter.js │ ├── is-valid-name.js │ ├── logger-phase.js │ └── tokens.js ├── modes │ ├── posix │ │ ├── tokenizer │ │ │ ├── reducers │ │ │ │ ├── end.js │ │ │ │ ├── expansion-special-parameter.js │ │ │ │ ├── comment.js │ │ │ │ ├── single-quoting.js │ │ │ │ ├── expansion-parameter.js │ │ │ │ ├── expansion-start.js │ │ │ │ ├── index.js │ │ │ │ ├── expansion-parameter-extended.js │ │ │ │ ├── expansion-arithmetic.js │ │ │ │ ├── expansion-command-or-arithmetic.js │ │ │ │ ├── expansion-command-tick.js │ │ │ │ ├── double-quoting.js │ │ │ │ ├── operator.js │ │ │ │ └── start.js │ │ │ └── index.js │ │ ├── enums │ │ │ ├── index.js │ │ │ ├── reserved-words.js │ │ │ ├── io-file-operators.js │ │ │ ├── operators.js │ │ │ └── parameter-operators.js │ │ ├── rules │ │ │ ├── syntaxerror-oncontinue.js │ │ │ ├── io-number.js │ │ │ ├── path-expansion.js │ │ │ ├── operator-tokens.js │ │ │ ├── default-node-type.js │ │ │ ├── for-name-variable.js │ │ │ ├── assignment-word.js │ │ │ ├── new-line-list.js │ │ │ ├── quote-removal.js │ │ │ ├── field-splitting.js │ │ │ ├── function-name.js │ │ │ ├── identify-maybe-simple-commands.js │ │ │ ├── linebreak-in.js │ │ │ ├── tilde-expanding.js │ │ │ ├── index.js │ │ │ ├── alias-substitution.js │ │ │ ├── identify-simplecommand-names.js │ │ │ ├── separator.js │ │ │ ├── arithmetic-expansion.js │ │ │ ├── command-expansion.js │ │ │ ├── reserved-words.js │ │ │ └── parameter-expansion.js │ │ └── index.js │ ├── bash │ │ └── rules │ │ │ └── alias-substitution.js │ └── word-expansion │ │ └── index.js ├── shell-lexer.js └── index.js ├── test ├── fixtures │ ├── single-quoted-word.js │ ├── escaped-quotes.js │ ├── quoted-c-strings.js │ ├── double-quote.js │ ├── single-double-quote.js │ ├── command-argument.js │ ├── quoted-reserved.js │ ├── escaped-reserved-word.js │ ├── new-lines-start.js │ ├── quoted-dollar.js │ ├── assignment-in-suffix.js │ ├── command-subst-single-quotes.js │ ├── single-quotes-in-double.js │ ├── tile-argument.js │ ├── parameter-substitution.js │ ├── assignment-doublequote.js │ ├── multiple-tilde.js │ ├── arhitmetic-substitution-single-quotes.js │ ├── subshell-spaces.js │ ├── tilde-expansion.js │ ├── pipe.js │ ├── and.js │ ├── bang.js │ ├── line-continuation-newline.js │ ├── empty-assignment.js │ ├── parameter-double-quote.js │ ├── two-assignments.js │ ├── redirection-2.js │ ├── parameter-expansion.js │ ├── sp-default_value.js │ ├── comment.js │ ├── parameter-expansion-end.js │ ├── sp-positional-list.js │ ├── sp-last-exit-status.js │ ├── sp-positional-string.js │ ├── sp-shell-process-id.js │ ├── sp-shell-script-name.js │ ├── sp-current-option-flags.js │ ├── spacial-parameter-positional-count.js │ ├── parameter-substitution-assignment.js │ ├── special-argument-last-background-pid.js │ ├── pipe-assignments.js │ ├── until.js │ ├── while.js │ ├── for.js │ ├── case-simple.js │ ├── xp-command-backtick.js │ ├── fixture1.js │ ├── command-substitution-assignment.js │ ├── command-substitution.js │ ├── subshell.js │ ├── arithmetic-substitution.js │ ├── single-quote.js │ ├── different-expansions.js │ ├── numer-redirection.js │ ├── for-new-line.js │ ├── function.js │ ├── case.js │ └── or.js ├── _benchmark.js ├── test-word-expansion-mode.js ├── test-pathname-expansion.js ├── test-comments.js ├── test-regressions.js ├── test-loc-case.js ├── test-while-until.js ├── _utils.js ├── test-case.js ├── test-tilde-expanding.js ├── test-function.js ├── test-loc-line-continuations.js ├── test-tokenization-rules.js ├── test-positional-parameter.js ├── test-quote-removal.js ├── test-loc-function-def.js ├── test-quoting.js ├── test-for.js ├── test-reserved-words.js ├── test-fixtures.js ├── test-alias-substitution.js ├── test-if.js ├── test-special-parameter.js ├── test-loc-while.js ├── test-loc-until.js ├── test-loc-simple-command.js ├── test-loc-for.js └── test-loc.js ├── license ├── .gitignore ├── package.json ├── readme.md └── documents ├── mode.md └── api.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = tab 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | sudo: false 5 | before_script: 6 | - npm run build 7 | script: 8 | - npm test 9 | after_success: 10 | - npm run cover-publish 11 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | exports.loggerPhase = require('./logger-phase'); 2 | exports.tokens = require('./tokens'); 3 | exports.isValidName = require('./is-valid-name'); 4 | exports.replaceRule = require('iterable-transform-replace'); 5 | -------------------------------------------------------------------------------- /src/utils/non-null.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const filter = require('./filter'); 4 | 5 | const nonNull = tk => { 6 | return tk !== null; 7 | }; 8 | 9 | module.exports = filter(nonNull); 10 | filter.predicate = nonNull; 11 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/end.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const eof = require('../../../../utils/tokens').eof; 4 | 5 | module.exports = function end() { 6 | return { 7 | nextReduction: null, 8 | tokensToEmit: [eof()] 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/filter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const filterIterator = require('filter-iterator'); 3 | const reverse = require('reverse-arguments'); 4 | const curry = require('curry'); 5 | 6 | const filter = curry.to(2, reverse(filterIterator)); 7 | 8 | module.exports = filter; 9 | -------------------------------------------------------------------------------- /src/modes/posix/enums/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.IOFileOperators = require('./io-file-operators'); 4 | exports.operators = require('./operators'); 5 | exports.parameterOperators = require('./parameter-operators'); 6 | exports.reservedWords = require('./reserved-words'); 7 | -------------------------------------------------------------------------------- /test/fixtures/single-quoted-word.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "ec'h'o", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | } 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/escaped-quotes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "ec\\'\\\"ho", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "ec'\"ho", 10 | type: "Word" 11 | } 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/quoted-c-strings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\"ec\\t\\nho\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "ec\t\nho", 10 | type: "Word" 11 | } 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/double-quote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo\"ciao\"'mondo'", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echociaomondo", 10 | type: "Word" 11 | } 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /test/fixtures/single-double-quote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo'ciao'\"mondo\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echociaomondo", 10 | type: "Word" 11 | } 12 | } 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/syntaxerror-oncontinue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const map = require('map-iterable'); 3 | 4 | module.exports = function syntaxerrorOnContinue() { 5 | return map(tk => { 6 | if (tk && tk.is('CONTINUE')) { 7 | throw new SyntaxError('Unclosed ' + tk.value); 8 | } 9 | 10 | return tk; 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/is-valid-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Check if a string represents a valid POSIX shell name, as specified in 5 | http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_231 6 | */ 7 | 8 | module.exports = function isValidName(text) { 9 | return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(text); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/logger-phase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const logger = name => () => function * (tokens) { 4 | for (const tk of tokens) { 5 | if (!tk) { 6 | console.log(`In ${name} token null.`); 7 | } 8 | console.log( 9 | name, 10 | '<<<', 11 | tk, 12 | '>>>' 13 | ); 14 | yield tk; 15 | } 16 | }; 17 | 18 | module.exports = logger; 19 | -------------------------------------------------------------------------------- /test/fixtures/command-argument.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo ciao", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "ciao", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/quoted-reserved.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\"if\" true", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "if", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "true", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/escaped-reserved-word.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "i\"f\" true", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "if", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "true", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/new-lines-start.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\n\n\necho world", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "world", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/quoted-dollar.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo \"\\$ciao\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "\\$ciao", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/assignment-in-suffix.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo TEST=1", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "TEST=1", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /src/modes/posix/enums/reserved-words.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | 'if': 'If', 4 | 'then': 'Then', 5 | 'else': 'Else', 6 | 'elif': 'Elif', 7 | 'fi': 'Fi', 8 | 'do': 'Do', 9 | 'done': 'Done', 10 | 'case': 'Case', 11 | 'esac': 'Esac', 12 | 'while': 'While', 13 | 'until': 'Until', 14 | 'for': 'For', 15 | 'in': 'In', 16 | '{': 'Lbrace', 17 | '}': 'Rbrace', 18 | '!': 'Bang' 19 | }; 20 | -------------------------------------------------------------------------------- /test/fixtures/command-subst-single-quotes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo '`echo ciao`'", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "`echo ciao`", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/single-quotes-in-double.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo \"TEST1 'TEST2\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "TEST1 'TEST2", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/tile-argument.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo ~username/subdir", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "/home/username/subdir", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/parameter-substitution.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo '${echo } $ciao'", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "${echo } $ciao", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/_benchmark.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // 3 | const bashParser = require('../lib'); 4 | 5 | const source = ` 6 | foo () { 7 | foo='hello'; 8 | rm -rf .; 9 | dest=bar 10 | eval "dest=foo" 11 | } 12 | foo a b c $d 13 | `; 14 | 15 | console.profile('bashParser'); 16 | 17 | for (let i = 0; i < 1000; i++) { 18 | bashParser(source); 19 | } 20 | 21 | console.profileEnd('bashParser'); 22 | console.log('done'); 23 | -------------------------------------------------------------------------------- /test/fixtures/assignment-doublequote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo=\"ciao mondo\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echo=ciao mondo", 15 | type: "AssignmentWord" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/multiple-tilde.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo ~/subdir/~other/", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "/home/current/subdir/~other/", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/arhitmetic-substitution-single-quotes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo '$((42 * 42))'", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "$((42 * 42))", 15 | type: "Word" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /test/fixtures/subshell-spaces.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "( ls )", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Subshell", 8 | list: { 9 | type: "CompoundList", 10 | commands: [ 11 | { 12 | type: "SimpleCommand", 13 | name: { 14 | text: "ls", 15 | type: "Word" 16 | } 17 | } 18 | ] 19 | } 20 | } 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /src/modes/posix/enums/io-file-operators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ioFileOperators = module.exports = [ 4 | 'LESS', 5 | 'DLESS', 6 | 'DGREAT', 7 | 'LESSAND', 8 | 'GREATAND', 9 | 'GREAT', 10 | 'LESSGREAT', 11 | 'CLOBBER' 12 | ]; 13 | 14 | ioFileOperators.isOperator = function isOperator(tk) { 15 | for (const op of ioFileOperators) { 16 | if (tk.type === op) { 17 | return true; 18 | } 19 | } 20 | return false; 21 | }; 22 | -------------------------------------------------------------------------------- /test/fixtures/tilde-expansion.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "a=~/subdir:~/othersubdir/ciao", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "a=/home/current/subdir:/home/current/othersubdir/ciao", 15 | type: "AssignmentWord" 16 | } 17 | ] 18 | } 19 | ] 20 | } 21 | } -------------------------------------------------------------------------------- /src/modes/posix/enums/operators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const operators = { 3 | '&': 'AND', 4 | '|': 'PIPE', 5 | '(': 'OPEN_PAREN', 6 | ')': 'CLOSE_PAREN', 7 | '>': 'GREAT', 8 | '<': 'LESS', 9 | '&&': 'AND_IF', 10 | '||': 'OR_IF', 11 | ';;': 'DSEMI', 12 | '<<': 'DLESS', 13 | '>>': 'DGREAT', 14 | '<&': 'LESSAND', 15 | '>&': 'GREATAND', 16 | '<>': 'LESSGREAT', 17 | '<<-': 'DLESSDASH', 18 | '>|': 'CLOBBER', 19 | ';': 'SEMICOLON' 20 | }; 21 | module.exports = operators; 22 | -------------------------------------------------------------------------------- /test/fixtures/pipe.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "run | cry", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Pipeline", 8 | commands: [ 9 | { 10 | type: "SimpleCommand", 11 | name: { 12 | text: "run", 13 | type: "Word" 14 | } 15 | }, 16 | { 17 | type: "SimpleCommand", 18 | name: { 19 | text: "cry", 20 | type: "Word" 21 | } 22 | } 23 | ] 24 | } 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /test/fixtures/and.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "run && \n stop", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "LogicalExpression", 8 | op: "and", 9 | left: { 10 | type: "SimpleCommand", 11 | name: { 12 | text: "run", 13 | type: "Word" 14 | } 15 | }, 16 | right: { 17 | type: "SimpleCommand", 18 | name: { 19 | text: "stop", 20 | type: "Word" 21 | } 22 | } 23 | } 24 | ] 25 | } 26 | } -------------------------------------------------------------------------------- /test/fixtures/bang.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "! run | cry", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Pipeline", 8 | commands: [ 9 | { 10 | type: "SimpleCommand", 11 | name: { 12 | text: "run", 13 | type: "Word" 14 | } 15 | }, 16 | { 17 | type: "SimpleCommand", 18 | name: { 19 | text: "cry", 20 | type: "Word" 21 | } 22 | } 23 | ], 24 | bang: true 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /test/fixtures/line-continuation-newline.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo \\\n\n\necho there", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | } 12 | }, 13 | { 14 | type: "SimpleCommand", 15 | name: { 16 | text: "echo", 17 | type: "Word" 18 | }, 19 | suffix: [ 20 | { 21 | text: "there", 22 | type: "Word" 23 | } 24 | ] 25 | } 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-special-parameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | 5 | module.exports = function expansionSpecialParameter(state, source) { 6 | const char = source && source.shift(); 7 | 8 | const xp = last(state.expansion); 9 | 10 | return { 11 | nextReduction: state.previousReducer, 12 | nextState: state.appendChar(char).replaceLastExpansion({ 13 | parameter: char, 14 | type: 'parameter_expansion', 15 | loc: Object.assign({}, xp.loc, {end: state.loc.current}) 16 | }) 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /test/fixtures/empty-assignment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "IFS= read -r var", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "read", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "IFS=", 15 | type: "AssignmentWord" 16 | } 17 | ], 18 | suffix: [ 19 | { 20 | text: "-r", 21 | type: "Word" 22 | }, 23 | { 24 | text: "var", 25 | type: "Word" 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/io-number.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | const map = require('map-iterable'); 4 | const lookahead = require('iterable-lookahead'); 5 | 6 | module.exports = function ioNumber(options, mode) { 7 | return compose(map((tk, idx, iterable) => { 8 | const next = iterable.ahead(1); 9 | 10 | if (tk && tk.is('WORD') && tk.value.match(/^[0-9]+$/) && mode.enums.IOFileOperators.isOperator(next)) { 11 | return tk.changeTokenType('IO_NUMBER', tk.value); 12 | } 13 | 14 | return tk; 15 | }), lookahead); 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/parameter-double-quote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\"foo ${other} baz\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "foo bar baz", 10 | expansion: [ 11 | { 12 | loc: { 13 | start: 5, 14 | end: 12 15 | }, 16 | parameter: "other", 17 | type: "ParameterExpansion", 18 | resolved: true 19 | } 20 | ], 21 | originalText: "\"foo ${other} baz\"", 22 | type: "Word" 23 | } 24 | } 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /test/fixtures/two-assignments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "TEST1=1 TEST2=2 echo world", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "TEST1=1", 15 | type: "AssignmentWord" 16 | }, 17 | { 18 | text: "TEST2=2", 19 | type: "AssignmentWord" 20 | } 21 | ], 22 | suffix: [ 23 | { 24 | text: "world", 25 | type: "Word" 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/path-expansion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const map = require('map-iterable'); 4 | const tokens = require('../../../utils/tokens'); 5 | 6 | module.exports = options => map(token => { 7 | if (token.is('WORD') && typeof options.resolvePath === 'function') { 8 | return tokens.setValue(token, options.resolvePath(token.value)); 9 | } 10 | 11 | if (token.is('ASSIGNMENT_WORD') && typeof options.resolvePath === 'function') { 12 | const parts = token.value.split('='); 13 | return tokens.setValue(token, parts[0] + '=' + options.resolvePath(parts[1])); 14 | } 15 | 16 | return token; 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixtures/redirection-2.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "2>&1 world", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "test-value", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | type: "Redirect", 15 | op: { 16 | text: ">&", 17 | type: "greatand" 18 | }, 19 | file: { 20 | text: "1", 21 | type: "Word" 22 | }, 23 | numberIo: { 24 | text: "2", 25 | type: "io_number" 26 | } 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/comment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const newLine = require('../../../../utils/tokens').newLine; 4 | 5 | module.exports = function comment(state, source, reducers) { 6 | const char = source && source.shift(); 7 | 8 | if (char === undefined) { 9 | return { 10 | nextReduction: reducers.end, 11 | nextState: state 12 | }; 13 | } 14 | 15 | if (char === '\n') { 16 | return { 17 | tokensToEmit: [newLine()], 18 | nextReduction: reducers.start, 19 | nextState: state 20 | }; 21 | } 22 | 23 | return { 24 | nextReduction: comment, 25 | nextState: state 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/parameter-expansion.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo word${other}test", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "word${other}test", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 4, 19 | end: 11 20 | }, 21 | parameter: "other", 22 | type: "ParameterExpansion" 23 | } 24 | ], 25 | type: "Word" 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /test/fixtures/sp-default_value.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "${other:+default_value}", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "${other:+default_value}", 10 | expansion: [ 11 | { 12 | loc: { 13 | start: 0, 14 | end: 22 15 | }, 16 | parameter: "other", 17 | type: "ParameterExpansion", 18 | op: "useAlternativeValue", 19 | word: { 20 | text: "default_value" 21 | } 22 | } 23 | ], 24 | type: "Word" 25 | } 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /test/fixtures/comment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo world #this is a comment\necho ciao", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "world", 15 | type: "Word" 16 | } 17 | ] 18 | }, 19 | { 20 | type: "SimpleCommand", 21 | name: { 22 | text: "echo", 23 | type: "Word" 24 | }, 25 | suffix: [ 26 | { 27 | text: "ciao", 28 | type: "Word" 29 | } 30 | ] 31 | } 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/operator-tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const hasOwnProperty = require('has-own-property'); 3 | const map = require('map-iterable'); 4 | const tokens = require('../../../utils/tokens'); 5 | 6 | const reduceToOperatorTokenVisitor = operators => ({ 7 | OPERATOR(tk) { 8 | if (hasOwnProperty(operators, tk.value)) { 9 | return tokens.changeTokenType( 10 | tk, 11 | operators[tk.value], 12 | tk.value 13 | ); 14 | } 15 | return tk; 16 | } 17 | }); 18 | 19 | module.exports = (options, mode) => map( 20 | tokens.applyTokenizerVisitor(reduceToOperatorTokenVisitor(mode.enums.operators)) 21 | ); 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/parameter-expansion-end.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$11", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$11", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: 1, 22 | type: "ParameterExpansion", 23 | kind: "positional" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-positional-list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$@", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$@", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "@", 22 | type: "ParameterExpansion", 23 | kind: "positional-list" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-last-exit-status.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$?", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$?", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "?", 22 | type: "ParameterExpansion", 23 | kind: "last-exit-status" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-positional-string.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$*", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$*", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "*", 22 | type: "ParameterExpansion", 23 | kind: "positional-string" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-shell-process-id.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$$", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$$", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "$", 22 | type: "ParameterExpansion", 23 | kind: "shell-process-id" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-shell-script-name.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$0", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$0", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "0", 22 | type: "ParameterExpansion", 23 | kind: "shell-script-name" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/sp-current-option-flags.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$-", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$-", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "-", 22 | type: "ParameterExpansion", 23 | kind: "current-option-flags" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/spacial-parameter-positional-count.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$#", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$#", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "#", 22 | type: "ParameterExpansion", 23 | kind: "positional-count" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/parameter-substitution-assignment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=${11}test", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=${11}test", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 13 20 | }, 21 | parameter: 11, 22 | type: "ParameterExpansion", 23 | kind: "positional" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /test/fixtures/special-argument-last-background-pid.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echoword=$!", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "echoword=$!", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 10 20 | }, 21 | parameter: "!", 22 | type: "ParameterExpansion", 23 | kind: "last-background-pid" 24 | } 25 | ], 26 | type: "AssignmentWord" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/default-node-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toPascal = require('to-pascal-case'); 4 | const map = require('map-iterable'); 5 | 6 | module.exports = () => map(token => { 7 | const tk = Object.assign({}, token); 8 | if (tk.type) { 9 | tk.originalType = token.type; 10 | // console.log({defaultNodeType, tk}) 11 | if (token.is('WORD') || token.is('NAME') || token.is('ASSIGNMENT_WORD')) { 12 | tk.type = toPascal(tk.type); 13 | } else { 14 | tk.type = token.type.toLowerCase(); 15 | } 16 | 17 | for (const xp of tk.expansion || []) { 18 | xp.type = toPascal(xp.type); 19 | } 20 | 21 | delete tk._; 22 | } 23 | // Object.freeze(tk); 24 | return tk; 25 | }); 26 | 27 | -------------------------------------------------------------------------------- /src/modes/posix/rules/for-name-variable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | const map = require('map-iterable'); 4 | const lookahead = require('iterable-lookahead'); 5 | const isValidName = require('../../../utils/is-valid-name'); 6 | 7 | module.exports = function forNameVariable() { 8 | return compose(map((tk, idx, iterable) => { 9 | let lastToken = iterable.behind(1) || {is: () => false}; 10 | 11 | // if last token is For and current token form a valid name 12 | // type of token is changed from WORD to NAME 13 | 14 | if (lastToken.is('For') && tk.is('WORD') && isValidName(tk.value)) { 15 | return tk.changeTokenType('NAME', tk.value); 16 | } 17 | 18 | return tk; 19 | }), lookahead); 20 | }; 21 | -------------------------------------------------------------------------------- /test/fixtures/pipe-assignments.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "foo | IFS= read var", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Pipeline", 8 | commands: [ 9 | { 10 | type: "SimpleCommand", 11 | name: { 12 | text: "foo", 13 | type: "Word" 14 | } 15 | }, 16 | { 17 | type: "SimpleCommand", 18 | name: { 19 | text: "read", 20 | type: "Word" 21 | }, 22 | prefix: [ 23 | { 24 | text: "IFS=", 25 | type: "AssignmentWord" 26 | } 27 | ], 28 | suffix: [ 29 | { 30 | text: "var", 31 | type: "Word" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | } -------------------------------------------------------------------------------- /test/test-word-expansion-mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable camelcase */ 3 | 4 | const test = require('ava'); 5 | const bashParser = require('../src'); 6 | const utils = require('./_utils'); 7 | 8 | test('expand on a single word', t => { 9 | const result = bashParser('ls $var > res.txt', { 10 | mode: 'word-expansion' 11 | }); 12 | // utils.logResults(result); 13 | utils.checkResults(t, result, { 14 | type: 'Script', 15 | commands: [{ 16 | type: 'Command', 17 | name: { 18 | type: 'Word', 19 | text: 'ls $var > res.txt', 20 | expansion: [{ 21 | parameter: 'var', 22 | loc: { 23 | start: 3, 24 | end: 6 25 | }, 26 | type: 'ParameterExpansion' 27 | }] 28 | } 29 | }] 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/test-pathname-expansion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('parameter substitution in commands', t => { 9 | const result = bashParser('echo', { 10 | resolvePath() { 11 | return 'ciao'; 12 | } 13 | }); 14 | utils.checkResults(t, result.commands[0].name, { 15 | type: 'Word', 16 | text: 'ciao' 17 | }); 18 | }); 19 | 20 | test('parameter substitution in assignment', t => { 21 | const result = bashParser('a=echo', { 22 | resolvePath() { 23 | return 'ciao'; 24 | } 25 | }); 26 | utils.checkResults(t, result.commands[0].prefix[0], { 27 | type: 'AssignmentWord', 28 | text: 'a=ciao' 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/single-quoting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('../../../../utils/tokens'); 4 | 5 | const tokenOrEmpty = t.tokenOrEmpty; 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function singleQuoting(state, source, reducers) { 9 | const char = source && source.shift(); 10 | 11 | if (char === undefined) { 12 | return { 13 | nextState: state, 14 | nextReduction: null, 15 | tokensToEmit: tokenOrEmpty(state).concat(continueToken('\'')) 16 | }; 17 | } 18 | 19 | if (char === '\'') { 20 | return { 21 | nextReduction: reducers.start, 22 | nextState: state.appendChar(char) 23 | }; 24 | } 25 | 26 | return { 27 | nextReduction: reducers.singleQuoting, 28 | nextState: state.appendChar(char) 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /test/fixtures/until.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "until true; do sleep 1; done", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Until", 8 | clause: { 9 | type: "CompoundList", 10 | commands: [ 11 | { 12 | type: "SimpleCommand", 13 | name: { 14 | text: "true", 15 | type: "Word" 16 | } 17 | } 18 | ] 19 | }, 20 | do: { 21 | type: "CompoundList", 22 | commands: [ 23 | { 24 | type: "SimpleCommand", 25 | name: { 26 | text: "sleep", 27 | type: "Word" 28 | }, 29 | suffix: [ 30 | { 31 | text: "1", 32 | type: "Word" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /test/fixtures/while.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "while true; do sleep 1; done", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "While", 8 | clause: { 9 | type: "CompoundList", 10 | commands: [ 11 | { 12 | type: "SimpleCommand", 13 | name: { 14 | text: "true", 15 | type: "Word" 16 | } 17 | } 18 | ] 19 | }, 20 | do: { 21 | type: "CompoundList", 22 | commands: [ 23 | { 24 | type: "SimpleCommand", 25 | name: { 26 | text: "sleep", 27 | type: "Word" 28 | }, 29 | suffix: [ 30 | { 31 | text: "1", 32 | type: "Word" 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/assignment-word.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const map = require('map-iterable'); 3 | const isValidName = require('../../../utils/is-valid-name'); 4 | 5 | module.exports = function assignmentWord() { 6 | return map((tk, idx, ctx) => { 7 | // apply only on valid positions 8 | // (start of simple commands) 9 | if (tk._.maybeStartOfSimpleCommand) { 10 | ctx.commandPrefixNotAllowed = false; 11 | } 12 | 13 | // check if it is an assignment 14 | if (!ctx.commandPrefixNotAllowed && tk.is('WORD') && tk.value.indexOf('=') > 0 && ( 15 | // left part must be a valid name 16 | isValidName(tk.value.slice(0, tk.value.indexOf('='))) 17 | )) { 18 | return tk.changeTokenType('ASSIGNMENT_WORD', tk.value); 19 | } 20 | 21 | ctx.commandPrefixNotAllowed = true; 22 | return tk; 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /test/test-comments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | test('loc take into account line continuations', t => { 8 | const cmd = 'echo world #this is a comment\necho ciao'; 9 | const result = bashParser(cmd); 10 | 11 | // utils.logResults(result); 12 | 13 | const expected = { 14 | type: 'Script', 15 | commands: [{ 16 | type: 'Command', 17 | name: { 18 | type: 'Word', text: 'echo' 19 | }, 20 | suffix: [{ 21 | type: 'Word', text: 'world' 22 | }] 23 | }, { 24 | type: 'Command', 25 | name: { 26 | type: 'Word', text: 'echo' 27 | }, 28 | suffix: [{ 29 | type: 'Word', text: 'ciao' 30 | }] 31 | }] 32 | }; 33 | 34 | utils.checkResults(t, result, expected); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /test/fixtures/for.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "for x\n do echo $x\n done", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "For", 8 | name: { 9 | text: "x", 10 | type: "Name" 11 | }, 12 | do: { 13 | type: "CompoundList", 14 | commands: [ 15 | { 16 | type: "SimpleCommand", 17 | name: { 18 | text: "echo", 19 | type: "Word" 20 | }, 21 | suffix: [ 22 | { 23 | text: "$x", 24 | expansion: [ 25 | { 26 | loc: { 27 | start: 0, 28 | end: 1 29 | }, 30 | parameter: "x", 31 | type: "ParameterExpansion" 32 | } 33 | ], 34 | type: "Word" 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | } 41 | ] 42 | } 43 | } -------------------------------------------------------------------------------- /test/fixtures/case-simple.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "case foo in * ) echo bar;; esac", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Case", 8 | clause: { 9 | text: "foo", 10 | type: "Word" 11 | }, 12 | cases: [ 13 | { 14 | type: "CaseItem", 15 | pattern: [ 16 | { 17 | text: "*", 18 | type: "Word" 19 | } 20 | ], 21 | body: { 22 | type: "CompoundList", 23 | commands: [ 24 | { 25 | type: "SimpleCommand", 26 | name: { 27 | text: "echo", 28 | type: "Word" 29 | }, 30 | suffix: [ 31 | { 32 | text: "bar", 33 | type: "Word" 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | } 40 | ] 41 | } 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /test/fixtures/xp-command-backtick.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo `ciao`", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word" 11 | }, 12 | suffix: [ 13 | { 14 | text: "`ciao`", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 0, 19 | end: 5 20 | }, 21 | command: "ciao", 22 | type: "CommandExpansion", 23 | commandAST: { 24 | type: "Script", 25 | commands: [ 26 | { 27 | type: "SimpleCommand", 28 | name: { 29 | text: "ciao", 30 | type: "Word" 31 | } 32 | } 33 | ] 34 | } 35 | } 36 | ], 37 | type: "Word" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/new-line-list.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | const map = require('map-iterable'); 4 | const lookahead = require('iterable-lookahead'); 5 | const tokens = require('../../../utils/tokens'); 6 | const filterNonNull = require('../../../utils/non-null'); 7 | 8 | const SkipRepeatedNewLines = { 9 | NEWLINE(tk, iterable) { 10 | const lastToken = iterable.behind(1) || tokens.mkToken('EMPTY'); 11 | 12 | if (lastToken.is('NEWLINE')) { 13 | return null; 14 | } 15 | 16 | return tokens.changeTokenType(tk, 'NEWLINE_LIST', '\n'); 17 | } 18 | }; 19 | 20 | /* resolve a conflict in grammar by tokenize multiple NEWLINEs as a 21 | newline_list token (it was a rule in POSIX grammar) */ 22 | module.exports = () => compose( 23 | filterNonNull, 24 | map( 25 | tokens.applyTokenizerVisitor(SkipRepeatedNewLines) 26 | ), 27 | lookahead 28 | ); 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/fixture1.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "foo='hello ; rm -rf /'\ndest=bar\neval \"dest=foo\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "foo=hello ; rm -rf /", 15 | type: "AssignmentWord" 16 | } 17 | ] 18 | }, 19 | { 20 | type: "SimpleCommand", 21 | name: { 22 | text: "", 23 | type: "Word" 24 | }, 25 | prefix: [ 26 | { 27 | text: "dest=bar", 28 | type: "AssignmentWord" 29 | } 30 | ] 31 | }, 32 | { 33 | type: "SimpleCommand", 34 | name: { 35 | text: "eval", 36 | type: "Word" 37 | }, 38 | suffix: [ 39 | { 40 | text: "dest=foo", 41 | type: "Word" 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/quote-removal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const parse = require('shell-quote-word'); 3 | const unescape = require('unescape-js'); 4 | const map = require('map-iterable'); 5 | const tokens = require('../../../utils/tokens'); 6 | 7 | function unquote(text) { 8 | const unquoted = parse(text); 9 | 10 | if (unquoted.length === 0) { 11 | return text; 12 | } 13 | 14 | if (unquoted[0].comment) { 15 | return ''; 16 | } 17 | return unescape(unquoted[0]); 18 | } 19 | 20 | function unresolvedExpansions(token) { 21 | if (!token.expansion) { 22 | return false; 23 | } 24 | const unresolved = token.expansion.filter(xp => !xp.resolved); 25 | return unresolved.length > 0; 26 | } 27 | 28 | module.exports = () => map(token => { 29 | if (token.is('WORD') || token.is('ASSIGNMENT_WORD')) { 30 | if (!unresolvedExpansions(token)) { 31 | return tokens.setValue(token, unquote(token.value)); 32 | } 33 | } 34 | return token; 35 | }); 36 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-parameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | 5 | module.exports = function expansionParameter(state, source, reducers) { 6 | const char = source && source.shift(); 7 | 8 | const xp = last(state.expansion); 9 | 10 | if (char === undefined) { 11 | return { 12 | nextReduction: reducers.start, 13 | nextState: state.replaceLastExpansion({ 14 | loc: Object.assign({}, xp.loc, {end: state.loc.previous}) 15 | }) 16 | }; 17 | } 18 | 19 | if (char.match(/[0-9a-zA-Z_]/)) { 20 | return { 21 | nextReduction: reducers.expansionParameter, 22 | nextState: state.appendChar(char).replaceLastExpansion({ 23 | parameter: xp.parameter + (char || '') 24 | }) 25 | }; 26 | } 27 | 28 | return state.previousReducer( 29 | state.replaceLastExpansion({loc: Object.assign({}, xp.loc, {end: state.loc.previous})}), 30 | [char].concat(source), 31 | reducers 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/modes/posix/rules/field-splitting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const map = require('map-iterable'); 4 | const merge = require('transform-spread-iterable'); 5 | const compose = require('compose-function'); 6 | const mkFieldSplitToken = require('../../../utils/tokens').mkFieldSplitToken; 7 | 8 | exports.mark = function markFieldSplitting(result, text, options) { 9 | if (typeof options.resolveEnv === 'function' && 10 | text[0] !== '\'' && text[0] !== '"' 11 | ) { 12 | const ifs = options.resolveEnv('IFS'); 13 | 14 | if (ifs !== null) { 15 | return result.replace(new RegExp(`[${ifs}]+`, 'g'), '\0'); 16 | } 17 | } 18 | 19 | return result; 20 | }; 21 | 22 | exports.split = () => compose( 23 | merge, 24 | map(token => { 25 | if (token.is('WORD')) { 26 | const fields = token.value.split('\0'); 27 | if (fields.length > 1) { 28 | let idx = 0; 29 | return fields.map(field => 30 | mkFieldSplitToken(token, field, idx++) 31 | ); 32 | } 33 | } 34 | 35 | return token; 36 | }) 37 | ); 38 | 39 | -------------------------------------------------------------------------------- /src/modes/posix/rules/function-name.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | const map = require('map-iterable'); 4 | const lookahead = require('iterable-lookahead'); 5 | 6 | module.exports = function functionName() { 7 | return compose(map((tk, idx, iterable) => { 8 | // apply only on valid positions 9 | // (start of simple commands) 10 | // if token can form the name of a function, 11 | // type of token is changed from WORD to NAME 12 | 13 | /* console.log( 14 | tk._.maybeStartOfSimpleCommand, 15 | tk.is('WORD'), 16 | iterable.ahead(1) && 17 | iterable.ahead(1).is('OPEN_PAREN'), 18 | iterable.ahead(2) && 19 | iterable.ahead(2).is('CLOSE_PAREN') 20 | );*/ 21 | 22 | if ( 23 | tk._.maybeStartOfSimpleCommand && 24 | tk.is('WORD') && 25 | iterable.ahead(2) && 26 | iterable.ahead(1).is('OPEN_PAREN') && 27 | iterable.ahead(2).is('CLOSE_PAREN') 28 | ) { 29 | tk = tk.changeTokenType('NAME', tk.value); 30 | } 31 | 32 | return tk; 33 | }), lookahead.depth(2)); 34 | }; 35 | -------------------------------------------------------------------------------- /test/test-regressions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('Redirect should be allowed immediately following argument', t => { 9 | const result = bashParser('echo foo>file.txt'); 10 | 11 | utils.checkResults(t, result, { 12 | type: 'Script', 13 | commands: [{ 14 | type: 'Command', 15 | name: {type: 'Word', text: 'echo'}, 16 | suffix: [ 17 | {type: 'Word', text: 'foo'}, 18 | { 19 | type: 'Redirect', 20 | op: {type: 'great', text: '>'}, 21 | file: {type: 'Word', text: 'file.txt'} 22 | } 23 | ] 24 | }] 25 | }); 26 | }); 27 | 28 | test('Equal sign should be allowed in arguments', t => { 29 | const result = bashParser('echo foo=bar'); 30 | utils.checkResults(t, result, { 31 | type: 'Script', 32 | commands: [{ 33 | type: 'Command', 34 | name: {type: 'Word', text: 'echo'}, 35 | suffix: [{type: 'Word', text: 'foo=bar'}] 36 | }] 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isSpecialParameter(char) { 4 | return char.match(/^[0-9\-!@#\?\*\$]$/); 5 | } 6 | 7 | module.exports = function expansionStart(state, source, reducers) { 8 | const char = source && source.shift(); 9 | 10 | if (char === '{') { 11 | return { 12 | nextReduction: reducers.expansionParameterExtended, 13 | nextState: state.appendChar(char) 14 | }; 15 | } 16 | 17 | if (char === '(') { 18 | return { 19 | nextReduction: reducers.expansionCommandOrArithmetic, 20 | nextState: state.appendChar(char) 21 | }; 22 | } 23 | 24 | if (char.match(/[a-zA-Z_]/)) { 25 | return { 26 | nextReduction: reducers.expansionParameter, 27 | nextState: state.appendChar(char).replaceLastExpansion({ 28 | parameter: char, 29 | type: 'parameter_expansion' 30 | }) 31 | }; 32 | } 33 | 34 | if (isSpecialParameter(char)) { 35 | return reducers.expansionSpecialParameter(state, [char].concat(source)); 36 | } 37 | 38 | return state.previousReducer(state, [char].concat(source)); 39 | }; 40 | -------------------------------------------------------------------------------- /src/modes/posix/rules/identify-maybe-simple-commands.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // const hasOwnProperty = require('has-own-property'); 3 | const values = require('object-values'); 4 | const compose = require('compose-function'); 5 | const map = require('map-iterable'); 6 | const lookahead = require('iterable-lookahead'); 7 | 8 | module.exports = function identifyMaybeSimpleCommands(options, mode) { 9 | return compose(map((tk, idx, iterable) => { 10 | const last = iterable.behind(1) || {EMPTY: true, is: type => type === 'EMPTY'}; 11 | 12 | // evaluate based on last token 13 | tk._.maybeStartOfSimpleCommand = Boolean( 14 | last.is('EMPTY') || last.is('SEPARATOR_OP') || last.is('OPEN_PAREN') || 15 | last.is('CLOSE_PAREN') || last.is('NEWLINE') || last.is('NEWLINE_LIST') || 16 | last.is('TOKEN') === ';' || last.is('PIPE') || 17 | last.is('DSEMI') || last.is('OR_IF') || last.is('PIPE') || last.is('AND_IF') || 18 | (!last.is('For') && !last.is('In') && !last.is('Case') && values(mode.enums.reservedWords).some(word => last.is(word))) 19 | ); 20 | 21 | return tk; 22 | }), lookahead); 23 | }; 24 | -------------------------------------------------------------------------------- /src/modes/posix/rules/linebreak-in.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | const map = require('map-iterable'); 4 | const lookahead = require('iterable-lookahead'); 5 | const tokens = require('../../../utils/tokens'); 6 | const filterNonNull = require('../../../utils/non-null'); 7 | 8 | const ReplaceWithLineBreakIn = { 9 | NEWLINE_LIST(tk, iterable) { 10 | const nextToken = iterable.ahead(1) || tokens.mkToken('EMPTY'); 11 | 12 | if (nextToken.is('In')) { 13 | return tokens.changeTokenType( 14 | tk, 15 | 'LINEBREAK_IN', 16 | '\nin' 17 | ); 18 | } 19 | 20 | return tk; 21 | }, 22 | 23 | In(tk, iterable) { 24 | const lastToken = iterable.behind(1) || tokens.mkToken('EMPTY'); 25 | 26 | if (lastToken.is('NEWLINE_LIST')) { 27 | return null; 28 | } 29 | 30 | return tk; 31 | } 32 | }; 33 | 34 | /* resolve a conflict in grammar by tokenize linebreak+in 35 | tokens as a new linebreak_in */ 36 | module.exports = () => compose( 37 | filterNonNull, 38 | map( 39 | tokens.applyTokenizerVisitor(ReplaceWithLineBreakIn) 40 | ), 41 | lookahead 42 | ); 43 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 vorpaljs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 20 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 21 | OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const end = require('./end'); 4 | const operator = require('./operator'); 5 | const comment = require('./comment'); 6 | const singleQuoting = require('./single-quoting'); 7 | const doubleQuoting = require('./double-quoting'); 8 | const expansionStart = require('./expansion-start'); 9 | const expansionCommandTick = require('./expansion-command-tick'); 10 | const start = require('./start'); 11 | const expansionArithmetic = require('./expansion-arithmetic'); 12 | const expansionSpecialParameter = require('./expansion-special-parameter'); 13 | const expansionParameter = require('./expansion-parameter'); 14 | const expansionCommandOrArithmetic = require('./expansion-command-or-arithmetic'); 15 | const expansionParameterExtended = require('./expansion-parameter-extended'); 16 | 17 | module.exports = { 18 | end, 19 | operator, 20 | comment, 21 | singleQuoting, 22 | doubleQuoting, 23 | expansionStart, 24 | expansionCommandTick, 25 | start, 26 | expansionArithmetic, 27 | expansionSpecialParameter, 28 | expansionParameter, 29 | expansionCommandOrArithmetic, 30 | expansionParameterExtended 31 | }; 32 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-parameter-extended.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | const t = require('../../../../utils/tokens'); 5 | 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function expansionParameterExtended(state, source, reducers) { 9 | const char = source && source.shift(); 10 | 11 | const xp = last(state.expansion); 12 | 13 | if (char === '}') { 14 | return { 15 | nextReduction: state.previousReducer, 16 | nextState: state.appendChar(char).replaceLastExpansion({ 17 | type: 'parameter_expansion', 18 | loc: Object.assign({}, xp.loc, {end: state.loc.current}) 19 | }) 20 | }; 21 | } 22 | 23 | if (char === undefined) { 24 | return { 25 | nextReduction: state.previousReducer, 26 | tokensToEmit: [continueToken('${')], 27 | nextState: state.replaceLastExpansion({ 28 | loc: Object.assign({}, xp.loc, {end: state.loc.previous}) 29 | }) 30 | }; 31 | } 32 | 33 | return { 34 | nextReduction: reducers.expansionParameterExtended, 35 | nextState: state 36 | .appendChar(char) 37 | .replaceLastExpansion({parameter: (xp.parameter || '') + char}) 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /test/fixtures/command-substitution-assignment.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "variable=$(echo \\`echo ciao\\`)", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "variable=$(echo \\`echo ciao\\`)", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 29 20 | }, 21 | command: "echo \\`echo ciao\\`", 22 | type: "CommandExpansion", 23 | commandAST: { 24 | type: "Script", 25 | commands: [ 26 | { 27 | type: "SimpleCommand", 28 | name: { 29 | text: "echo", 30 | type: "Word" 31 | }, 32 | suffix: [ 33 | { 34 | text: "`echo", 35 | type: "Word" 36 | }, 37 | { 38 | text: "ciao`", 39 | type: "Word" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | } 46 | ], 47 | type: "AssignmentWord" 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/tilde-expanding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const map = require('map-iterable'); 3 | const tokens = require('../../../utils/tokens'); 4 | 5 | const replace = (text, resolveHomeUser) => { 6 | let replaced = false; 7 | let result = text.replace(/^~([^\/]*)\//, (match, p1) => { 8 | replaced = true; 9 | return resolveHomeUser(p1 || null) + '/'; 10 | }); 11 | // console.log({result, replaced}) 12 | if (!replaced) { 13 | result = text.replace(/^~(.*)$/, (match, p1) => { 14 | return resolveHomeUser(p1 || null); 15 | }); 16 | } 17 | 18 | return result; 19 | }; 20 | 21 | module.exports = options => map(token => { 22 | if (token.is('WORD') && typeof options.resolveHomeUser === 'function') { 23 | return tokens.setValue(token, replace(token.value, options.resolveHomeUser)); 24 | } 25 | 26 | if (token.is('ASSIGNMENT_WORD') && typeof options.resolveHomeUser === 'function') { 27 | const parts = token.value.split('=', 2); 28 | const target = parts[0]; 29 | const sourceParts = parts[1]; 30 | 31 | const source = sourceParts 32 | .split(':') 33 | .map(text => replace(text, options.resolveHomeUser)) 34 | .join(':'); 35 | 36 | return tokens.setValue(token, target + '=' + source); 37 | } 38 | 39 | return token; 40 | }); 41 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-arithmetic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | const t = require('../../../../utils/tokens'); 5 | 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function expansionArithmetic(state, source) { 9 | const char = source && source.shift(); 10 | 11 | const xp = last(state.expansion); 12 | 13 | if (char === ')' && state.current.slice(-1)[0] === ')') { 14 | return { 15 | nextReduction: state.previousReducer, 16 | nextState: state 17 | .appendChar(char) 18 | .replaceLastExpansion({ 19 | type: 'arithmetic_expansion', 20 | expression: xp.value.slice(0, -1), 21 | loc: Object.assign({}, xp.loc, {end: state.loc.current}) 22 | }) 23 | .deleteLastExpansionValue() 24 | }; 25 | } 26 | 27 | if (char === undefined) { 28 | return { 29 | nextReduction: state.previousReducer, 30 | tokensToEmit: [continueToken('$((')], 31 | nextState: state.replaceLastExpansion({ 32 | loc: Object.assign({}, xp.loc, {end: state.loc.previous}) 33 | }) 34 | }; 35 | } 36 | 37 | return { 38 | nextReduction: expansionArithmetic, 39 | nextState: state.appendChar(char).replaceLastExpansion({value: (xp.value || '') + char}) 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /test/fixtures/command-substitution.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\"foo $(other) $(one) baz\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "foo bar bar baz", 10 | expansion: [ 11 | { 12 | loc: { 13 | start: 5, 14 | end: 12 15 | }, 16 | command: "other", 17 | type: "CommandExpansion", 18 | commandAST: { 19 | type: "Script", 20 | commands: [ 21 | { 22 | type: "SimpleCommand", 23 | name: { 24 | text: "other", 25 | type: "Word" 26 | } 27 | } 28 | ] 29 | }, 30 | resolved: true 31 | }, 32 | { 33 | loc: { 34 | start: 14, 35 | end: 19 36 | }, 37 | command: "one", 38 | type: "CommandExpansion", 39 | commandAST: { 40 | type: "Script", 41 | commands: [ 42 | { 43 | type: "SimpleCommand", 44 | name: { 45 | text: "one", 46 | type: "Word" 47 | } 48 | } 49 | ] 50 | }, 51 | resolved: true 52 | } 53 | ], 54 | originalText: "\"foo $(other) $(one) baz\"", 55 | type: "Word" 56 | } 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-command-or-arithmetic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | const t = require('../../../../utils/tokens'); 5 | 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function expansionCommandOrArithmetic(state, source, reducers) { 9 | const char = source && source.shift(); 10 | const xp = last(state.expansion); 11 | 12 | if (char === '(' && state.current.slice(-2) === '$(') { 13 | return { 14 | nextReduction: reducers.expansionArithmetic, 15 | nextState: state.appendChar(char) 16 | }; 17 | } 18 | 19 | if (char === undefined) { 20 | return { 21 | nextReduction: state.previousReducer, 22 | tokensToEmit: [continueToken('$(')], 23 | nextState: state.replaceLastExpansion({ 24 | loc: Object.assign({}, xp.loc, {end: state.loc.previous}) 25 | }) 26 | }; 27 | } 28 | 29 | if (char === ')') { 30 | return { 31 | nextReduction: state.previousReducer, 32 | nextState: state.appendChar(char).replaceLastExpansion({ 33 | type: 'command_expansion', 34 | loc: Object.assign({}, xp.loc, { 35 | end: state.loc.current 36 | }) 37 | }) 38 | }; 39 | } 40 | 41 | return { 42 | nextReduction: reducers.expansionCommandOrArithmetic, 43 | nextState: state.appendChar(char).replaceLastExpansion({command: (xp.command || '') + char}) 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/modes/posix/rules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.parameterExpansion = require('./parameter-expansion'); 4 | exports.commandExpansion = require('./command-expansion'); 5 | exports.arithmeticExpansion = require('./arithmetic-expansion'); 6 | exports.aliasSubstitution = require('./alias-substitution'); 7 | exports.defaultNodeType = require('./default-node-type'); 8 | exports.fieldSplitting = require('./field-splitting'); 9 | exports.tildeExpanding = require('./tilde-expanding'); 10 | exports.pathExpansion = require('./path-expansion'); 11 | exports.quoteRemoval = require('./quote-removal'); 12 | exports.identifySimpleCommandNames = require('./identify-simplecommand-names'); 13 | exports.identifyMaybeSimpleCommands = require('./identify-maybe-simple-commands'); 14 | exports.operatorTokens = require('./operator-tokens'); 15 | exports.reservedWords = require('./reserved-words'); 16 | exports.separator = require('./separator'); 17 | exports.linebreakIn = require('./linebreak-in'); 18 | exports.forNameVariable = require('./for-name-variable'); 19 | exports.functionName = require('./function-name'); 20 | exports.ioNumber = require('./io-number'); 21 | // exports.removeTempObject = require('./remove-temp-object'); 22 | exports.newLineList = require('./new-line-list'); 23 | exports.assignmentWord = require('./assignment-word'); 24 | exports.syntaxerrorOnContinue = require('./syntaxerror-oncontinue'); 25 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/expansion-command-tick.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const last = require('array-last'); 4 | const t = require('../../../../utils/tokens'); 5 | 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function expansionCommandTick(state, source, reducers) { 9 | const char = source && source.shift(); 10 | 11 | const xp = last(state.expansion); 12 | 13 | if (!state.escaping && char === '`') { 14 | return { 15 | nextReduction: state.previousReducer, 16 | nextState: state.appendChar(char).replaceLastExpansion({ 17 | type: 'command_expansion', 18 | loc: Object.assign({}, xp.loc, {end: state.loc.current}) 19 | }) 20 | }; 21 | } 22 | 23 | if (char === undefined) { 24 | return { 25 | nextReduction: state.previousReducer, 26 | tokensToEmit: [continueToken('`')], 27 | nextState: state.replaceLastExpansion({ 28 | loc: Object.assign({}, xp.loc, {end: state.loc.previous}) 29 | }) 30 | }; 31 | } 32 | 33 | if (!state.escaping && char === '\\') { 34 | return { 35 | nextReduction: reducers.expansionCommandTick, 36 | nextState: state.appendChar(char).setEscaping(true) 37 | }; 38 | } 39 | 40 | return { 41 | nextReduction: reducers.expansionCommandTick, 42 | nextState: state 43 | .setEscaping(false) 44 | .appendChar(char) 45 | .replaceLastExpansion({command: (xp.command || '') + char}) 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /test/test-loc-case.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | const mkloc = require('./_utils').mkloc2; 7 | 8 | test('case statement has loc', t => { 9 | const cmd = 10 | `case foo in 11 | * ) 12 | echo bar;; 13 | esac 14 | `; 15 | const result = bashParser(cmd, {insertLOC: true}); 16 | 17 | const expected = { 18 | type: 'Case', 19 | clause: { 20 | type: 'Word', 21 | text: 'foo', 22 | loc: mkloc(1, 6, 1, 8, 5, 7) 23 | }, 24 | cases: [ 25 | { 26 | type: 'CaseItem', 27 | pattern: [ 28 | { 29 | type: 'Word', 30 | text: '*', 31 | loc: mkloc(2, 2, 2, 2, 13, 13) 32 | } 33 | ], 34 | body: { 35 | type: 'CompoundList', 36 | commands: [ 37 | { 38 | type: 'Command', 39 | name: { 40 | type: 'Word', 41 | text: 'echo', 42 | loc: mkloc(3, 3, 3, 6, 19, 22) 43 | }, 44 | loc: mkloc(3, 3, 3, 10, 19, 26), 45 | suffix: [{ 46 | type: 'Word', 47 | text: 'bar', 48 | loc: mkloc(3, 8, 3, 10, 24, 26) 49 | }] 50 | } 51 | ], 52 | loc: mkloc(3, 3, 3, 10, 19, 26) 53 | }, 54 | loc: mkloc(2, 2, 3, 12, 13, 28) 55 | } 56 | ], 57 | loc: mkloc(1, 1, 4, 4, 0, 33) 58 | }; 59 | // utils.logResults(result.commands[0]); 60 | 61 | utils.checkResults(t, result.commands[0], expected); 62 | }); 63 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/double-quoting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('../../../../utils/tokens'); 4 | 5 | const tokenOrEmpty = t.tokenOrEmpty; 6 | const continueToken = t.continueToken; 7 | 8 | module.exports = function doubleQuoting(state, source, reducers) { 9 | const char = source && source.shift(); 10 | 11 | state = state.setPreviousReducer(doubleQuoting); 12 | 13 | if (char === undefined) { 14 | return { 15 | nextReduction: null, 16 | tokensToEmit: tokenOrEmpty(state).concat(continueToken('"')), 17 | nextState: state 18 | }; 19 | } 20 | 21 | if (!state.escaping && char === '\\') { 22 | return { 23 | nextReduction: doubleQuoting, 24 | nextState: state.setEscaping(true).appendChar(char) 25 | }; 26 | } 27 | 28 | if (!state.escaping && char === '"') { 29 | return { 30 | nextReduction: reducers.start, 31 | nextState: state.setPreviousReducer(reducers.start).appendChar(char) 32 | }; 33 | } 34 | 35 | if (!state.escaping && char === '$') { 36 | return { 37 | nextReduction: reducers.expansionStart, 38 | nextState: state.appendEmptyExpansion().appendChar(char) 39 | }; 40 | } 41 | 42 | if (!state.escaping && char === '`') { 43 | return { 44 | nextReduction: reducers.expansionCommandTick, 45 | nextState: state.appendEmptyExpansion().appendChar(char) 46 | }; 47 | } 48 | 49 | return { 50 | nextReduction: reducers.doubleQuoting, 51 | nextState: state.setEscaping(false).appendChar(char) 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /test/fixtures/subshell.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "(echo)", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Subshell", 8 | list: { 9 | type: "CompoundList", 10 | commands: [ 11 | { 12 | type: "SimpleCommand", 13 | name: { 14 | text: "echo", 15 | type: "Word", 16 | loc: { 17 | start: { 18 | col: 2, 19 | row: 1, 20 | char: 1 21 | }, 22 | end: { 23 | col: 5, 24 | row: 1, 25 | char: 4 26 | } 27 | } 28 | }, 29 | loc: { 30 | start: { 31 | col: 2, 32 | row: 1, 33 | char: 1 34 | }, 35 | end: { 36 | col: 5, 37 | row: 1, 38 | char: 4 39 | } 40 | } 41 | } 42 | ], 43 | loc: { 44 | start: { 45 | col: 2, 46 | row: 1, 47 | char: 1 48 | }, 49 | end: { 50 | col: 5, 51 | row: 1, 52 | char: 4 53 | } 54 | } 55 | }, 56 | loc: { 57 | start: { 58 | col: 1, 59 | row: 1, 60 | char: 0 61 | }, 62 | end: { 63 | col: 6, 64 | row: 1, 65 | char: 5 66 | } 67 | } 68 | } 69 | ], 70 | loc: { 71 | start: { 72 | col: 1, 73 | row: 1, 74 | char: 0 75 | }, 76 | end: { 77 | col: 6, 78 | row: 1, 79 | char: 5 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /test/test-while-until.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('parse while', t => { 9 | const result = bashParser('while true; do sleep 1; done'); 10 | 11 | utils.checkResults(t, 12 | result, { 13 | type: 'Script', 14 | commands: [{ 15 | type: 'While', 16 | clause: { 17 | type: 'CompoundList', 18 | commands: [{ 19 | type: 'Command', 20 | name: {type: 'Word', text: 'true'} 21 | }] 22 | }, 23 | do: { 24 | type: 'CompoundList', 25 | commands: [{ 26 | type: 'Command', 27 | name: {type: 'Word', text: 'sleep'}, 28 | suffix: [{type: 'Word', text: '1'}] 29 | }] 30 | } 31 | }] 32 | } 33 | ); 34 | }); 35 | 36 | test('parse until', t => { 37 | const result = bashParser('until true; do sleep 1; done'); 38 | // console.log(inspect(result, {depth:null})) 39 | utils.checkResults(t, 40 | result, { 41 | type: 'Script', 42 | commands: [{ 43 | type: 'Until', 44 | clause: { 45 | type: 'CompoundList', 46 | commands: [{ 47 | type: 'Command', 48 | name: {type: 'Word', text: 'true'} 49 | }] 50 | }, 51 | do: { 52 | type: 'CompoundList', 53 | commands: [{ 54 | type: 'Command', 55 | name: {type: 'Word', text: 'sleep'}, 56 | suffix: [{type: 'Word', text: '1'}] 57 | }] 58 | } 59 | }] 60 | } 61 | ); 62 | }); 63 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/operator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('../../../../utils/tokens'); 4 | 5 | const isPartOfOperator = t.isPartOfOperator; 6 | const operatorTokens = t.operatorTokens; 7 | const isOperator = t.isOperator; 8 | 9 | module.exports = function operator(state, source, reducers) { 10 | const char = source && source.shift(); 11 | 12 | // console.log('isOperator ', {state,char}) 13 | 14 | if (char === undefined) { 15 | if (isOperator(state.current)) { 16 | return { 17 | nextReduction: reducers.end, 18 | tokensToEmit: operatorTokens(state), 19 | nextState: state.resetCurrent().saveCurrentLocAsStart() 20 | }; 21 | } 22 | return reducers.start(state, char); 23 | } 24 | 25 | if (isPartOfOperator(state.current + char)) { 26 | return { 27 | nextReduction: reducers.operator, 28 | nextState: state.appendChar(char) 29 | }; 30 | } 31 | 32 | let tokens = []; 33 | if (isOperator(state.current)) { 34 | // console.log('isOperator ', state.current) 35 | tokens = operatorTokens(state); 36 | state = state.resetCurrent().saveCurrentLocAsStart(); 37 | } 38 | 39 | const ret = reducers.start(state, [char].concat(source), reducers); 40 | const nextReduction = ret.nextReduction; 41 | const tokensToEmit = ret.tokensToEmit; 42 | const nextState = ret.nextState; 43 | 44 | if (tokensToEmit) { 45 | tokens = tokens.concat(tokensToEmit); 46 | } 47 | return { 48 | nextReduction: nextReduction, 49 | tokensToEmit: tokens, 50 | nextState 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | const json = require('json5'); 2 | 3 | exports.mkloc = function mkloc(startLine, startColumn, endLine, endColumn) { 4 | return {startLine, startColumn, endLine, endColumn}; 5 | }; 6 | 7 | // eslint-disable-next-line max-params 8 | exports.mkloc2 = function mkloc(startLine, startColumn, endLine, endColumn, startChar, endChar) { 9 | return { 10 | start: {row: startLine, col: startColumn, char: startChar}, 11 | end: {row: endLine, col: endColumn, char: endChar} 12 | }; 13 | }; 14 | 15 | exports.logResults = function logResults(results) { 16 | console.log(json.stringify(results, null, '\t').replace(/"/g, '\'')); 17 | }; 18 | 19 | exports.checkResults = function check(t, actual, expected) { 20 | /* if (Array.isArray(actual)) { 21 | for (const item of actual) { 22 | console.log(item.constructor.name) 23 | if (item.constructor.name === 'Token') { 24 | console.log('tttt') 25 | Object.defineProperty(item, item.type, { 26 | enumerable: true, 27 | get() { 28 | const s = stack()[1]; 29 | 30 | if (s.getFileName() !== '/Users/parroit/Desktop/repos/bash-parser/src/utils/tokens.js' && 31 | s.getFileName() !== '/Users/parroit/Desktop/repos/bash-parser/src/modes/posix/rules/default-node-type.js') 32 | console.log(`${count++}: ${this.type} is deprectaed. Used ${s.getFileName()}:${s.getLineNumber()}`); 33 | return item.value; 34 | } 35 | }); 36 | } 37 | 38 | } 39 | }*/ 40 | // exports.logResults(actual); 41 | t.deepEqual(actual, expected); 42 | }; 43 | -------------------------------------------------------------------------------- /src/modes/posix/rules/alias-substitution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compose = require('compose-function'); 4 | const identity = require('identity-function'); 5 | const map = require('map-iterable'); 6 | const merge = require('transform-spread-iterable'); 7 | const tokens = require('../../../utils/tokens'); 8 | 9 | const expandAlias = (preAliasLexer, resolveAlias) => { 10 | function * tryExpandToken(token, expandingAliases) { 11 | if (expandingAliases.indexOf(token.value) !== -1 || !token._.maybeSimpleCommandName) { 12 | yield token; 13 | return; 14 | } 15 | 16 | const result = resolveAlias(token.value); 17 | if (result === undefined) { 18 | yield token; 19 | } else { 20 | for (const newToken of preAliasLexer(result)) { 21 | if (newToken.is('WORD')) { 22 | yield * tryExpandToken( 23 | newToken, 24 | expandingAliases.concat(token.value) 25 | ); 26 | } else if (!newToken.is('EOF')) { 27 | yield newToken; 28 | } 29 | } 30 | } 31 | } 32 | 33 | return { 34 | WORD: tk => { 35 | return Array.from(tryExpandToken(tk, [])); 36 | } 37 | }; 38 | }; 39 | 40 | module.exports = (options, mode, previousPhases) => { 41 | if (typeof options.resolveAlias !== 'function') { 42 | return identity; 43 | } 44 | 45 | const preAliasLexer = compose.apply(null, previousPhases.reverse()); 46 | const visitor = expandAlias(preAliasLexer, options.resolveAlias); 47 | 48 | return compose( 49 | merge, 50 | map( 51 | tokens.applyTokenizerVisitor(visitor) 52 | ) 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/shell-lexer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const compose = require('compose-function'); 3 | 4 | const posixShellLexer = (mode, options) => ({ 5 | lex() { 6 | const item = this.tokenizer.next(); 7 | const tk = item.value; 8 | const tkType = tk.originalType; 9 | const text = tk.value; 10 | 11 | this.yytext = {text}; 12 | if (tk.expansion) { 13 | this.yytext.expansion = tk.expansion; 14 | } 15 | 16 | if (tk.originalText) { 17 | this.yytext.originalText = tk.originalText; 18 | } 19 | 20 | if (tk.type) { 21 | this.yytext.type = tk.type; 22 | } 23 | 24 | if (tk.maybeSimpleCommandName) { 25 | this.yytext.maybeSimpleCommandName = tk.maybeSimpleCommandName; 26 | } 27 | 28 | if (tk.joined) { 29 | this.yytext.joined = tk.joined; 30 | } 31 | 32 | if (tk.fieldIdx !== undefined) { 33 | this.yytext.fieldIdx = tk.fieldIdx; 34 | } 35 | 36 | if (options.insertLOC && tk.loc) { 37 | this.yytext.loc = tk.loc; 38 | } 39 | 40 | if (tk.loc) { 41 | this.yylineno = tk.loc.start.row - 1; 42 | } 43 | 44 | return tkType; 45 | }, 46 | 47 | setInput(source) { 48 | const tokenizer = mode.tokenizer(options); 49 | let previousPhases = [tokenizer]; 50 | const phases = [tokenizer] 51 | .concat(mode.lexerPhases.map(phase => { 52 | const ph = phase(options, mode, previousPhases); 53 | previousPhases = previousPhases.concat(ph); 54 | return ph; 55 | })); 56 | 57 | const tokenize = compose.apply(null, phases.reverse()); 58 | this.tokenizer = tokenize(source); 59 | } 60 | }); 61 | 62 | module.exports = posixShellLexer; 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const shellLexer = require('./shell-lexer'); 4 | const utils = require('./utils'); 5 | 6 | // preload all modes to have them browserified 7 | const modes = { 8 | 'bash': require('./modes/bash'), 9 | 'posix': require('./modes/posix'), 10 | 'word-expansion': require('./modes/word-expansion') 11 | }; 12 | 13 | function loadPlugin(name) { 14 | const modePlugin = modes[name]; 15 | 16 | if (modePlugin.inherits) { 17 | return modePlugin.init(loadPlugin(modePlugin.inherits), utils); 18 | } 19 | return modePlugin.init(null, utils); 20 | } 21 | 22 | module.exports = function parse(sourceCode, options) { 23 | try { 24 | options = options || {}; 25 | options.mode = options.mode || 'posix'; 26 | 27 | const mode = loadPlugin(options.mode); 28 | const Parser = mode.grammar.Parser; 29 | const astBuilder = mode.astBuilder; 30 | const parser = new Parser(); 31 | parser.lexer = shellLexer(mode, options); 32 | parser.yy = astBuilder(options); 33 | 34 | const ast = parser.parse(sourceCode); 35 | 36 | /* 37 | const fixtureFolder = `${__dirname}/../test/fixtures`; 38 | const json = require('json5'); 39 | const {writeFileSync} = require('fs'); 40 | 41 | const fileName = require('node-uuid').v4(); 42 | const filePath = `${fixtureFolder}/${fileName}.js`; 43 | writeFileSync(filePath, 'module.exports = ' + json.stringify({ 44 | sourceCode, result: ast 45 | }, null, '\t')); 46 | */ 47 | return ast; 48 | } catch (err) { 49 | if (err instanceof SyntaxError) { 50 | throw err; 51 | } 52 | throw new Error(err.stack || err.message); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/modes/posix/rules/identify-simplecommand-names.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const lookahead = require('iterable-lookahead'); 3 | const compose = require('compose-function'); 4 | const map = require('map-iterable'); 5 | // const isOperator = require('../enums/io-file-operators').isOperator; 6 | const isValidName = require('../../../utils/is-valid-name'); 7 | 8 | function couldEndSimpleCommand(scTk) { 9 | return scTk && ( 10 | scTk.is('SEPARATOR_OP') || 11 | scTk.is('NEWLINE') || 12 | scTk.is('NEWLINE_LIST') || 13 | scTk.value === ';' || 14 | scTk.is('PIPE') || 15 | scTk.is('OR_IF') || 16 | scTk.is('PIPE') || 17 | scTk.is('AND_IF') 18 | ); 19 | } 20 | 21 | function couldBeCommandName(tk) { 22 | return tk && tk.is('WORD') && isValidName(tk.value); 23 | } 24 | 25 | module.exports = (options, mode) => compose( 26 | map((tk, idx, iterable) => { 27 | if (tk._.maybeStartOfSimpleCommand) { 28 | if (couldBeCommandName(tk)) { 29 | tk._.maybeSimpleCommandName = true; 30 | } else { 31 | const next = iterable.ahead(1); 32 | if (next && !couldEndSimpleCommand(next)) { 33 | next._.commandNameNotFoundYet = true; 34 | } 35 | } 36 | } 37 | 38 | if (tk._.commandNameNotFoundYet) { 39 | const last = iterable.behind(1); 40 | 41 | if (!mode.enums.IOFileOperators.isOperator(last) && couldBeCommandName(tk)) { 42 | tk._.maybeSimpleCommandName = true; 43 | } else { 44 | const next = iterable.ahead(1); 45 | if (next && !couldEndSimpleCommand(next)) { 46 | next._.commandNameNotFoundYet = true; 47 | } 48 | } 49 | delete tk._.commandNameNotFoundYet; 50 | } 51 | 52 | return tk; 53 | }), 54 | lookahead 55 | ); 56 | -------------------------------------------------------------------------------- /src/modes/posix/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const astBuilder = require('./ast-builder'); 4 | const tokenizer = require('./tokenizer'); 5 | const phaseCatalog = require('./rules'); 6 | const grammarSource = require('./grammar'); 7 | const enums = require('./enums'); 8 | 9 | const lexerPhases = () => [ 10 | phaseCatalog.newLineList, 11 | phaseCatalog.operatorTokens, 12 | phaseCatalog.separator, 13 | phaseCatalog.reservedWords, 14 | phaseCatalog.linebreakIn, 15 | phaseCatalog.ioNumber, 16 | phaseCatalog.identifyMaybeSimpleCommands, 17 | phaseCatalog.assignmentWord, 18 | phaseCatalog.parameterExpansion, 19 | phaseCatalog.arithmeticExpansion, 20 | phaseCatalog.commandExpansion, 21 | phaseCatalog.forNameVariable, 22 | phaseCatalog.functionName, 23 | phaseCatalog.identifySimpleCommandNames, 24 | // utils.loggerPhase('pre'), 25 | phaseCatalog.aliasSubstitution, 26 | // utils.loggerPhase('post'), 27 | phaseCatalog.tildeExpanding, 28 | phaseCatalog.parameterExpansion.resolve, 29 | phaseCatalog.commandExpansion.resolve, 30 | phaseCatalog.arithmeticExpansion.resolve, 31 | phaseCatalog.fieldSplitting.split, 32 | phaseCatalog.pathExpansion, 33 | phaseCatalog.quoteRemoval, 34 | phaseCatalog.syntaxerrorOnContinue, 35 | phaseCatalog.defaultNodeType 36 | // utils.loggerPhase('tokenizer'), 37 | ]; 38 | 39 | module.exports = { 40 | inherits: null, 41 | init: (posixMode, utils) => { 42 | let grammar = null; 43 | try { 44 | grammar = require('./built-grammar'); 45 | } catch (err) {} 46 | return { 47 | enums, 48 | phaseCatalog, 49 | lexerPhases: lexerPhases(utils), 50 | tokenizer, 51 | grammarSource, 52 | grammar, 53 | astBuilder 54 | }; 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: https://goel.io/joe 2 | 3 | #####=== SublimeText ===##### 4 | # cache files for sublime text 5 | *.tmlanguage.cache 6 | *.tmPreferences.cache 7 | *.stTheme.cache 8 | 9 | # workspace files are user-specific 10 | *.sublime-workspace 11 | 12 | # project files should be checked into the repository, unless a significant 13 | # proportion of contributors will probably not be using SublimeText 14 | # *.sublime-project 15 | 16 | # sftp configuration file 17 | sftp-config.json 18 | 19 | #####=== Node ===##### 20 | 21 | # Logs 22 | logs 23 | *.log 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | 30 | # Directory for instrumented libs generated by jscoverage/JSCover 31 | lib-cov 32 | 33 | # Coverage directory used by tools like istanbul 34 | coverage 35 | 36 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (http://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directory 46 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 47 | node_modules 48 | 49 | # Debug log from npm 50 | npm-debug.log 51 | 52 | #####=== OSX ===##### 53 | .DS_Store 54 | .AppleDouble 55 | .LSOverride 56 | 57 | # Icon must end with two \r 58 | Icon 59 | 60 | 61 | # Thumbnails 62 | ._* 63 | 64 | # Files that might appear on external disk 65 | .Spotlight-V100 66 | .Trashes 67 | 68 | # Directories potentially created on remote AFP share 69 | .AppleDB 70 | .AppleDesktop 71 | Network Trash Folder 72 | Temporary Items 73 | .apdisk 74 | 75 | yarn.lock 76 | out 77 | 78 | **/built-grammar.* 79 | **/.nyc_output 80 | 81 | package-lock.json 82 | -------------------------------------------------------------------------------- /src/modes/bash/rules/alias-substitution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compose = require('compose-function'); 4 | const identity = require('identity-function'); 5 | const map = require('map-iterable'); 6 | const values = require('object-values'); 7 | const merge = require('transform-spread-iterable'); 8 | const tokens = require('../../../utils/tokens'); 9 | 10 | const expandAlias = (preAliasLexer, resolveAlias, reservedWords) => { 11 | function * tryExpandToken(token, expandingAliases) { 12 | if (expandingAliases.indexOf(token.value) !== -1) { 13 | yield token; 14 | return; 15 | } 16 | const result = resolveAlias(token.value); 17 | if (result === undefined) { 18 | yield token; 19 | } else { 20 | for (const newToken of preAliasLexer(result)) { 21 | if (newToken.is('WORD') || reservedWords.some(word => newToken.is(word))) { 22 | yield * tryExpandToken( 23 | newToken, 24 | expandingAliases.concat(token.value) 25 | ); 26 | } else if (!newToken.is('EOF')) { 27 | yield newToken; 28 | } 29 | } 30 | } 31 | } 32 | 33 | function expandToken(tk) { 34 | return Array.from(tryExpandToken(tk, [])); 35 | } 36 | 37 | const visitor = { 38 | WORD: expandToken 39 | }; 40 | 41 | reservedWords.forEach(w => { 42 | visitor[w] = expandToken; 43 | }); 44 | return visitor; 45 | }; 46 | 47 | module.exports = (options, mode, previousPhases) => { 48 | if (typeof options.resolveAlias !== 'function') { 49 | return identity; 50 | } 51 | 52 | const preAliasLexer = compose.apply(null, previousPhases.reverse()); 53 | const visitor = expandAlias(preAliasLexer, options.resolveAlias, values(mode.enums.reservedWords)); 54 | 55 | return compose( 56 | merge, 57 | map( 58 | tokens.applyTokenizerVisitor(visitor) 59 | ) 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /test/fixtures/arithmetic-substitution.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "\"foo $((42 * 42)) baz\"", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "foo 43 baz", 10 | expansion: [ 11 | { 12 | loc: { 13 | start: 5, 14 | end: 16 15 | }, 16 | type: "ArithmeticExpansion", 17 | expression: "42 * 42", 18 | arithmeticAST: { 19 | type: "BinaryExpression", 20 | start: 0, 21 | end: 7, 22 | loc: { 23 | start: { 24 | line: 1, 25 | column: 0 26 | }, 27 | end: { 28 | line: 1, 29 | column: 7 30 | } 31 | }, 32 | left: { 33 | type: "NumericLiteral", 34 | start: 0, 35 | end: 2, 36 | loc: { 37 | start: { 38 | line: 1, 39 | column: 0 40 | }, 41 | end: { 42 | line: 1, 43 | column: 2 44 | } 45 | }, 46 | extra: { 47 | rawValue: 42, 48 | raw: "42" 49 | }, 50 | value: 42 51 | }, 52 | operator: "*", 53 | right: { 54 | type: "NumericLiteral", 55 | start: 5, 56 | end: 7, 57 | loc: { 58 | start: { 59 | line: 1, 60 | column: 5 61 | }, 62 | end: { 63 | line: 1, 64 | column: 7 65 | } 66 | }, 67 | extra: { 68 | rawValue: 42, 69 | raw: "42" 70 | }, 71 | value: 42 72 | } 73 | }, 74 | resolved: true 75 | } 76 | ], 77 | originalText: "\"foo $((42 * 42)) baz\"", 78 | type: "Word" 79 | } 80 | } 81 | ] 82 | } 83 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bash-parser", 3 | "version": "0.5.0", 4 | "main": "src/index.js", 5 | "description": "Standard compliant bash parser", 6 | "repository": "vorpaljs/bash-parser", 7 | "license": "MIT", 8 | "author": "vorpal-js", 9 | "scripts": { 10 | "doc": "doctoc", 11 | "test": "ava && xo --ignore **/built-grammar.js", 12 | "build": "mgb ./src/modes posix && mgb ./src/modes bash && mgb ./src/modes word-expansion", 13 | "cover-test": "nyc ava && xo --ignore **/built-grammar.js", 14 | "cover-publish": "nyc report --reporter=text-lcov | coveralls" 15 | }, 16 | "keywords": [], 17 | "engines": { 18 | "node": ">=4" 19 | }, 20 | "devDependencies": { 21 | "ava": "^0.15.2", 22 | "coveralls": "^2.11.11", 23 | "doctoc": "^1.2.0", 24 | "json5": "^0.5.0", 25 | "mode-grammar-builder": "^0.6.0", 26 | "nyc": "^7.0.0", 27 | "xo": "^0.16.0" 28 | }, 29 | "files": [ 30 | "src" 31 | ], 32 | "nyc": { 33 | "exclude": [ 34 | "src/modes/posix/built-grammar.js", 35 | "test/_utils.js" 36 | ] 37 | }, 38 | "dependencies": { 39 | "array-last": "^1.1.1", 40 | "babylon": "^6.9.1", 41 | "compose-function": "^3.0.3", 42 | "curry": "^1.2.0", 43 | "deep-freeze": "0.0.1", 44 | "filter-iterator": "0.0.1", 45 | "filter-obj": "^1.1.0", 46 | "has-own-property": "^0.1.0", 47 | "identity-function": "^1.0.0", 48 | "iterable-lookahead": "^1.0.0", 49 | "iterable-transform-replace": "^1.1.1", 50 | "magic-string": "^0.16.0", 51 | "map-iterable": "^1.0.1", 52 | "map-obj": "^2.0.0", 53 | "object-pairs": "^0.1.0", 54 | "object-values": "^1.0.0", 55 | "reverse-arguments": "^1.0.0", 56 | "shell-quote-word": "^1.0.1", 57 | "to-pascal-case": "^1.0.0", 58 | "transform-spread-iterable": "^1.1.0", 59 | "unescape-js": "^1.0.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/test-case.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable camelcase */ 3 | 4 | const test = require('ava'); 5 | const bashParser = require('../src'); 6 | const utils = require('./_utils'); 7 | 8 | test('parse case', t => { 9 | const result = bashParser('case foo in * ) echo bar;; esac'); 10 | // utils.logResults(result); 11 | const expected = { 12 | type: 'Script', 13 | commands: [{ 14 | type: 'Case', 15 | clause: { 16 | type: 'Word', 17 | text: 'foo' 18 | }, 19 | cases: [{ 20 | type: 'CaseItem', 21 | pattern: [{ 22 | type: 'Word', 23 | text: '*' 24 | }], 25 | body: { 26 | type: 'CompoundList', 27 | commands: [{ 28 | type: 'Command', 29 | name: {type: 'Word', text: 'echo'}, 30 | suffix: [{type: 'Word', text: 'bar'}] 31 | }] 32 | } 33 | }] 34 | }] 35 | }; 36 | utils.checkResults(t, result, expected); 37 | }); 38 | /* 39 | test.skip('parse case with compound list', t => { 40 | const result = bashParser('case foo in * ) echo foo;echo bar;; esac'); 41 | // utils.logResults(result); 42 | const expected = { 43 | type: 'Script', 44 | commands: [{ 45 | type: 'LogicalExpression', 46 | left: { 47 | type: 'Pipeline', 48 | commands: [{ 49 | type: 'Case', 50 | clause: { 51 | text: 'foo' 52 | }, 53 | cases: [{ 54 | type: 'CaseItem', 55 | pattern: [{ 56 | text: '*' 57 | }], 58 | body: { 59 | type: 'CompoundList', 60 | commands: [{ 61 | type: 'LogicalExpression', 62 | left: { 63 | type: 'Pipeline', 64 | commands: [{ 65 | type: 'Command', 66 | name: {text: 'echo'}, 67 | suffix: [{text: 'bar'}] 68 | }] 69 | } 70 | }] 71 | } 72 | }] 73 | }] 74 | } 75 | }] 76 | }; 77 | 78 | t.is(JSON.stringify(result), JSON.stringify(expected)); 79 | }); 80 | */ 81 | -------------------------------------------------------------------------------- /test/fixtures/single-quote.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "variable=`echo \\`echo ciao\\``", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "", 10 | type: "Word" 11 | }, 12 | prefix: [ 13 | { 14 | text: "variable=`echo \\`echo ciao\\``", 15 | expansion: [ 16 | { 17 | loc: { 18 | start: 9, 19 | end: 28 20 | }, 21 | command: "echo `echo ciao`", 22 | type: "CommandExpansion", 23 | commandAST: { 24 | type: "Script", 25 | commands: [ 26 | { 27 | type: "SimpleCommand", 28 | name: { 29 | text: "echo", 30 | type: "Word" 31 | }, 32 | suffix: [ 33 | { 34 | text: "`echo ciao`", 35 | expansion: [ 36 | { 37 | loc: { 38 | start: 0, 39 | end: 10 40 | }, 41 | command: "echo ciao", 42 | type: "CommandExpansion", 43 | commandAST: { 44 | type: "Script", 45 | commands: [ 46 | { 47 | type: "SimpleCommand", 48 | name: { 49 | text: "echo", 50 | type: "Word" 51 | }, 52 | suffix: [ 53 | { 54 | text: "ciao", 55 | type: "Word" 56 | } 57 | ] 58 | } 59 | ] 60 | } 61 | } 62 | ], 63 | type: "Word" 64 | } 65 | ] 66 | } 67 | ] 68 | } 69 | } 70 | ], 71 | type: "AssignmentWord" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bash-parser 2 | 3 | Parses bash source code to produce an AST 4 | 5 | [![Travis Build Status](https://img.shields.io/travis/vorpaljs/bash-parser/master.svg)](http://travis-ci.org/vorpaljs/bash-parser) 6 | [![Coveralls](https://img.shields.io/coveralls/vorpaljs/bash-parser.svg?maxAge=2592000)](https://coveralls.io/github/vorpaljs/bash-parser) 7 | [![NPM module](https://img.shields.io/npm/v/bash-parser.svg)](https://npmjs.org/package/bash-parser) 8 | [![NPM downloads](https://img.shields.io/npm/dt/bash-parser.svg)](https://npmjs.org/package/bash-parser) 9 | [![Try online](https://img.shields.io/badge/try_it-online!-yellow.svg)](https://vorpaljs.github.io/bash-parser-playground/) 10 | 11 | # Installation 12 | 13 | ```bash 14 | npm install --save bash-parser 15 | ``` 16 | 17 | # Usage 18 | 19 | ```js 20 | const parse = require('bash-parser'); 21 | const ast = parse('echo ciao'); 22 | ``` 23 | 24 | `ast` result is: 25 | 26 | ```js 27 | { 28 | type: "Script", 29 | commands: [ 30 | { 31 | type: "SimpleCommand", 32 | name: { 33 | text: "echo", 34 | type: "Word" 35 | }, 36 | suffix: [ 37 | { 38 | text: "ciao", 39 | type: "Word" 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | ``` 46 | 47 | # Related projects 48 | 49 | * [cash](https://github.com/dthree/cash) - This parser should become the parser used by `cash` (and also [vorpal](https://github.com/dthree/vorpal)) 50 | * [nsh](https://github.com/piranna/nsh) - This parser should become the parser used by `nsh` 51 | * [js-shell-parse](https://github.com/grncdr/js-shell-parse) - bash-parser was born as a fork of `js-shell-parse`, but was rewritten to use a `jison` grammar 52 | * [jison](https://github.com/zaach/jison) - Bison in JavaScript. 53 | 54 | # Documentation 55 | 56 | Look in [documents folder](https://github.com/vorpaljs/bash-parser/tree/master/documents) 57 | 58 | # License 59 | 60 | The MIT License (MIT) 61 | 62 | Copyright (c) 2016 vorpaljs 63 | -------------------------------------------------------------------------------- /src/modes/posix/rules/separator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const compose = require('compose-function'); 4 | const map = require('map-iterable'); 5 | const lookahead = require('iterable-lookahead'); 6 | const tokens = require('../../../utils/tokens'); 7 | const filterNonNull = require('../../../utils/non-null'); 8 | 9 | const isSeparator = tk => tk && ( 10 | tk.is('NEWLINE') || 11 | tk.is('NEWLINE_LIST') || 12 | tk.is('AND') || 13 | tk.is('SEMICOLON') || 14 | (tk.is('OPERATOR') && tk.value === ';') || 15 | (tk.is('OPERATOR') && tk.value === '&') 16 | ); 17 | 18 | function toSeparatorToken(tk, iterable) { 19 | if (skipJoined(tk) === null) { 20 | return null; 21 | } 22 | 23 | let newTk = tokens.changeTokenType( 24 | tk, 25 | 'SEPARATOR_OP', 26 | tk.value 27 | ); 28 | 29 | let i = 1; 30 | let nextTk = iterable.ahead(i); 31 | while (isSeparator(nextTk)) { 32 | nextTk._.joinedToSeparator = true; 33 | i++; 34 | newTk = newTk.appendTo(nextTk.value); 35 | 36 | nextTk = iterable.ahead(i); 37 | } 38 | return newTk; 39 | } 40 | 41 | function skipJoined(tk) { 42 | if (tk._.joinedToSeparator) { 43 | return null; 44 | } 45 | return tk; 46 | } 47 | 48 | const AccumulateSeparators = { 49 | NEWLINE: skipJoined, 50 | NEWLINE_LIST: skipJoined, 51 | SEMICOLON: toSeparatorToken, 52 | AND: toSeparatorToken, 53 | OPERATOR: (tk, iterable) => tk.value === '&' || tk.value === ';' ? 54 | toSeparatorToken(tk, iterable) : 55 | tk 56 | }; 57 | 58 | /* 59 | resolve a conflict in grammar by 60 | tokenize the former rule: 61 | 62 | separator_op : '&' 63 | | ';' 64 | ; 65 | separator : separator_op 66 | | separator_op NEWLINE_LIST 67 | | NEWLINE_LIST 68 | 69 | with a new separator_op token, the rule became: 70 | 71 | separator : separator_op 72 | | NEWLINE_LIST 73 | */ 74 | module.exports = () => compose( 75 | filterNonNull, 76 | map( 77 | tokens.applyTokenizerVisitor(AccumulateSeparators) 78 | ), 79 | lookahead.depth(10) 80 | ); 81 | -------------------------------------------------------------------------------- /src/modes/posix/rules/arithmetic-expansion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable camelcase */ 3 | 4 | const map = require('map-iterable'); 5 | const babylon = require('babylon'); 6 | const MagicString = require('magic-string'); 7 | const tokens = require('../../../utils/tokens'); 8 | const fieldSplitting = require('./field-splitting'); 9 | 10 | function parseArithmeticAST(xp) { 11 | let AST; 12 | try { 13 | AST = babylon.parse(xp.expression); 14 | } catch (err) { 15 | throw new SyntaxError(`Cannot parse arithmetic expression "${xp.expression}": ${err.message}`); 16 | } 17 | 18 | const expression = AST.program.body[0].expression; 19 | 20 | if (expression === undefined) { 21 | throw new SyntaxError(`Cannot parse arithmetic expression "${xp.expression}": Not an expression`); 22 | } 23 | 24 | return JSON.parse(JSON.stringify(expression)); 25 | } 26 | 27 | const arithmeticExpansion = () => map(token => { 28 | if (token.is('WORD') || token.is('ASSIGNMENT_WORD')) { 29 | if (!token.expansion || token.expansion.length === 0) { 30 | return token; 31 | } 32 | 33 | return tokens.setExpansions(token, token.expansion.map(xp => { 34 | if (xp.type === 'arithmetic_expansion') { 35 | return Object.assign({}, xp, {arithmeticAST: parseArithmeticAST(xp)}); 36 | } 37 | return xp; 38 | })); 39 | } 40 | return token; 41 | }); 42 | 43 | arithmeticExpansion.resolve = options => map(token => { 44 | if (options.runArithmeticExpression && token.expansion) { 45 | const value = token.value; 46 | 47 | const magic = new MagicString(value); 48 | 49 | for (const xp of token.expansion) { 50 | if (xp.type === 'arithmetic_expansion') { 51 | const result = options.runArithmeticExpression(xp); 52 | magic.overwrite( 53 | xp.loc.start, 54 | xp.loc.end + 1, 55 | fieldSplitting.mark(result, value, options) 56 | ); 57 | xp.resolved = true; 58 | } 59 | } 60 | 61 | return token.alterValue(magic.toString()); 62 | } 63 | return token; 64 | }); 65 | 66 | module.exports = arithmeticExpansion; 67 | -------------------------------------------------------------------------------- /test/test-tilde-expanding.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('resolve tilde to current user home', t => { 9 | const result = bashParser('echo ~/subdir', { 10 | resolveHomeUser() { 11 | return '/home/current'; 12 | } 13 | }); 14 | // utils.logResults(result); 15 | utils.checkResults(t, result, { 16 | type: 'Script', 17 | commands: [ 18 | { 19 | type: 'Command', 20 | name: {type: 'Word', text: 'echo'}, 21 | suffix: [{ 22 | type: 'Word', 23 | text: '/home/current/subdir' 24 | }] 25 | } 26 | ] 27 | }); 28 | }); 29 | 30 | test('resolve one tilde only in normal WORD tokens', t => { 31 | const result = bashParser('echo ~/subdir/~other/', { 32 | resolveHomeUser() { 33 | return '/home/current'; 34 | } 35 | }); 36 | 37 | utils.checkResults(t, result, { 38 | type: 'Script', 39 | commands: [ 40 | { 41 | type: 'Command', 42 | name: {type: 'Word', text: 'echo'}, 43 | suffix: [{ 44 | type: 'Word', 45 | text: '/home/current/subdir/~other/' 46 | }] 47 | } 48 | ] 49 | }); 50 | }); 51 | 52 | test('resolve multiple tilde in assignments', t => { 53 | const result = bashParser('a=~/subdir:~/othersubdir/ciao', { 54 | resolveHomeUser() { 55 | return '/home/current'; 56 | } 57 | }); 58 | // utils.logResults(result.commands[0].prefix[0]); 59 | utils.checkResults(t, result.commands[0].prefix[0], { 60 | type: 'AssignmentWord', 61 | text: 'a=/home/current/subdir:/home/current/othersubdir/ciao' 62 | }); 63 | }); 64 | 65 | test('resolve tilde to any user home', t => { 66 | const result = bashParser('echo ~username/subdir', { 67 | resolveHomeUser() { 68 | return '/home/username'; 69 | } 70 | }); 71 | 72 | utils.checkResults(t, result, { 73 | type: 'Script', 74 | commands: [ 75 | { 76 | type: 'Command', 77 | name: {type: 'Word', text: 'echo'}, 78 | suffix: [{ 79 | type: 'Word', 80 | text: '/home/username/subdir' 81 | }] 82 | } 83 | ] 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/fixtures/different-expansions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "variable=$((42 + 43)) $ciao", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "$ciao", 10 | expansion: [ 11 | { 12 | loc: { 13 | start: 0, 14 | end: 4 15 | }, 16 | parameter: "ciao", 17 | type: "ParameterExpansion" 18 | } 19 | ], 20 | type: "Word" 21 | }, 22 | prefix: [ 23 | { 24 | text: "variable=$((42 + 43))", 25 | expansion: [ 26 | { 27 | loc: { 28 | start: 9, 29 | end: 20 30 | }, 31 | type: "ArithmeticExpansion", 32 | expression: "42 + 43", 33 | arithmeticAST: { 34 | type: "BinaryExpression", 35 | start: 0, 36 | end: 7, 37 | loc: { 38 | start: { 39 | line: 1, 40 | column: 0 41 | }, 42 | end: { 43 | line: 1, 44 | column: 7 45 | } 46 | }, 47 | left: { 48 | type: "NumericLiteral", 49 | start: 0, 50 | end: 2, 51 | loc: { 52 | start: { 53 | line: 1, 54 | column: 0 55 | }, 56 | end: { 57 | line: 1, 58 | column: 2 59 | } 60 | }, 61 | extra: { 62 | rawValue: 42, 63 | raw: "42" 64 | }, 65 | value: 42 66 | }, 67 | operator: "+", 68 | right: { 69 | type: "NumericLiteral", 70 | start: 5, 71 | end: 7, 72 | loc: { 73 | start: { 74 | line: 1, 75 | column: 5 76 | }, 77 | end: { 78 | line: 1, 79 | column: 7 80 | } 81 | }, 82 | extra: { 83 | rawValue: 43, 84 | raw: "43" 85 | }, 86 | value: 43 87 | } 88 | } 89 | } 90 | ], 91 | type: "AssignmentWord" 92 | } 93 | ] 94 | } 95 | ] 96 | } 97 | } -------------------------------------------------------------------------------- /test/test-function.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | 9 | test('parse function declaration multiple lines', t => { 10 | const result = bashParser('foo () \n{\n command bar --lol;\n}'); 11 | // utils.logResults(result); 12 | utils.checkResults(t, 13 | result, { 14 | type: 'Script', 15 | commands: [{ 16 | type: 'Function', 17 | name: {type: 'Name', text: 'foo'}, 18 | body: { 19 | type: 'CompoundList', 20 | commands: [{ 21 | type: 'Command', 22 | name: {type: 'Word', text: 'command'}, 23 | suffix: [{type: 'Word', text: 'bar'}, {type: 'Word', text: '--lol'}] 24 | }] 25 | } 26 | }] 27 | } 28 | ); 29 | }); 30 | 31 | test('parse function declaration with redirections', t => { 32 | const src = `foo () { 33 | command bar --lol; 34 | } > file.txt`; 35 | 36 | const result = bashParser(src); 37 | // utils.logResults(result); 38 | utils.checkResults(t, 39 | result, { 40 | type: 'Script', 41 | commands: [{ 42 | type: 'Function', 43 | name: {type: 'Name', text: 'foo'}, 44 | redirections: [{ 45 | type: 'Redirect', 46 | op: {type: 'great', text: '>'}, 47 | file: {type: 'Word', text: 'file.txt'} 48 | }], 49 | body: { 50 | type: 'CompoundList', 51 | commands: [{ 52 | type: 'Command', 53 | name: {type: 'Word', text: 'command'}, 54 | suffix: [{type: 'Word', text: 'bar'}, {type: 'Word', text: '--lol'}] 55 | }] 56 | } 57 | }] 58 | } 59 | ); 60 | }); 61 | 62 | test('parse function declaration', t => { 63 | const result = bashParser('foo (){ command bar --lol; }'); 64 | 65 | utils.checkResults(t, 66 | result, { 67 | type: 'Script', 68 | commands: [{ 69 | type: 'Function', 70 | name: {type: 'Name', text: 'foo'}, 71 | body: { 72 | type: 'CompoundList', 73 | commands: [{ 74 | type: 'Command', 75 | name: {type: 'Word', text: 'command'}, 76 | suffix: [{type: 'Word', text: 'bar'}, {type: 'Word', text: '--lol'}] 77 | }] 78 | } 79 | }] 80 | } 81 | ); 82 | }); 83 | -------------------------------------------------------------------------------- /test/fixtures/numer-redirection.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "echo 2> 43", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "SimpleCommand", 8 | name: { 9 | text: "echo", 10 | type: "Word", 11 | loc: { 12 | start: { 13 | col: 1, 14 | row: 1, 15 | char: 0 16 | }, 17 | end: { 18 | col: 4, 19 | row: 1, 20 | char: 3 21 | } 22 | } 23 | }, 24 | loc: { 25 | start: { 26 | col: 1, 27 | row: 1, 28 | char: 0 29 | }, 30 | end: { 31 | col: 10, 32 | row: 1, 33 | char: 9 34 | } 35 | }, 36 | suffix: [ 37 | { 38 | type: "Redirect", 39 | op: { 40 | text: ">", 41 | type: "great", 42 | loc: { 43 | start: { 44 | col: 7, 45 | row: 1, 46 | char: 6 47 | }, 48 | end: { 49 | col: 7, 50 | row: 1, 51 | char: 6 52 | } 53 | } 54 | }, 55 | file: { 56 | text: "43", 57 | type: "Word", 58 | loc: { 59 | start: { 60 | col: 9, 61 | row: 1, 62 | char: 8 63 | }, 64 | end: { 65 | col: 10, 66 | row: 1, 67 | char: 9 68 | } 69 | } 70 | }, 71 | loc: { 72 | start: { 73 | col: 6, 74 | row: 1, 75 | char: 5 76 | }, 77 | end: { 78 | col: 10, 79 | row: 1, 80 | char: 9 81 | } 82 | }, 83 | numberIo: { 84 | text: "2", 85 | type: "io_number", 86 | loc: { 87 | start: { 88 | col: 6, 89 | row: 1, 90 | char: 5 91 | }, 92 | end: { 93 | col: 6, 94 | row: 1, 95 | char: 5 96 | } 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | ], 103 | loc: { 104 | start: { 105 | col: 1, 106 | row: 1, 107 | char: 0 108 | }, 109 | end: { 110 | col: 10, 111 | row: 1, 112 | char: 9 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/modes/posix/rules/command-expansion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const map = require('map-iterable'); 4 | const MagicString = require('magic-string'); 5 | const tokensUtils = require('../../../utils/tokens'); 6 | const fieldSplitting = require('./field-splitting'); 7 | 8 | function setCommandExpansion(xp, token) { 9 | let command = xp.command; 10 | 11 | if (token.value[xp.loc.start - 1] === '`') { 12 | command = command.replace(/\\`/g, '`'); 13 | } 14 | 15 | const bashParser = require('../../../index'); 16 | 17 | const commandAST = bashParser(command); 18 | 19 | // console.log(JSON.stringify({command, commandAST}, null, 4)) 20 | return Object.assign({}, xp, {command, commandAST}); 21 | } 22 | 23 | // RULE 5 - If the current character is an unquoted '$' or '`', the shell shall 24 | // identify the start of any candidates for parameter expansion (Parameter Expansion), 25 | // command substitution (Command Substitution), or arithmetic expansion (Arithmetic 26 | // Expansion) from their introductory unquoted character sequences: '$' or "${", "$(" 27 | // or '`', and "$((", respectively. 28 | 29 | const commandExpansion = () => map(token => { 30 | if (token.is('WORD') || token.is('ASSIGNMENT_WORD')) { 31 | if (!token.expansion || token.expansion.length === 0) { 32 | return token; 33 | } 34 | 35 | return tokensUtils.setExpansions(token, token.expansion.map(xp => { 36 | if (xp.type === 'command_expansion') { 37 | return setCommandExpansion(xp, token); 38 | } 39 | 40 | return xp; 41 | })); 42 | } 43 | return token; 44 | }); 45 | 46 | commandExpansion.resolve = options => map(token => { 47 | if (options.execCommand && token.expansion) { 48 | const value = token.value; 49 | 50 | const magic = new MagicString(value); 51 | 52 | for (const xp of token.expansion) { 53 | if (xp.type === 'command_expansion') { 54 | const result = options.execCommand(xp); 55 | // console.log({value, xp}) 56 | magic.overwrite( 57 | xp.loc.start, 58 | xp.loc.end + 1, 59 | fieldSplitting.mark(result.replace(/\n+$/, ''), value, options) 60 | ); 61 | xp.resolved = true; 62 | } 63 | } 64 | return token.alterValue(magic.toString()); 65 | } 66 | return token; 67 | }); 68 | 69 | module.exports = commandExpansion; 70 | -------------------------------------------------------------------------------- /test/test-loc-line-continuations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | // const mkloc = require('./_utils').mkloc2; 7 | 8 | test('empty line after line continuation', t => { 9 | const cmd = `echo \\\n\n\necho there`; 10 | const result = bashParser(cmd); 11 | // utils.logResults(result); 12 | const expected = { 13 | type: 'Script', 14 | commands: [ 15 | { 16 | type: 'Command', 17 | name: { 18 | text: 'echo', 19 | type: 'Word' 20 | } 21 | }, 22 | { 23 | type: 'Command', 24 | name: { 25 | text: 'echo', 26 | type: 'Word' 27 | }, 28 | suffix: [ 29 | { 30 | text: 'there', 31 | type: 'Word' 32 | } 33 | ] 34 | } 35 | ] 36 | }; 37 | utils.checkResults(t, result, expected); 38 | }); 39 | 40 | test('loc take into account line continuations', t => { 41 | const cmd = 'echo \\\nworld'; 42 | const result = bashParser(cmd, {insertLOC: true}); 43 | // utils.logResults(result); 44 | const expected = { 45 | type: 'Script', 46 | commands: [ 47 | { 48 | type: 'Command', 49 | name: { 50 | text: 'echo', 51 | type: 'Word', 52 | loc: { 53 | start: { 54 | col: 1, 55 | row: 1, 56 | char: 0 57 | }, 58 | end: { 59 | col: 4, 60 | row: 1, 61 | char: 3 62 | } 63 | } 64 | }, 65 | loc: { 66 | start: { 67 | col: 1, 68 | row: 1, 69 | char: 0 70 | }, 71 | end: { 72 | col: 5, 73 | row: 2, 74 | char: 11 75 | } 76 | }, 77 | suffix: [ 78 | { 79 | text: 'world', 80 | type: 'Word', 81 | loc: { 82 | start: { 83 | col: 1, 84 | row: 2, 85 | char: 7 86 | }, 87 | end: { 88 | col: 5, 89 | row: 2, 90 | char: 11 91 | } 92 | } 93 | } 94 | ] 95 | } 96 | ], 97 | loc: { 98 | start: { 99 | col: 1, 100 | row: 1, 101 | char: 0 102 | }, 103 | end: { 104 | col: 5, 105 | row: 2, 106 | char: 11 107 | } 108 | } 109 | }; 110 | 111 | // utils.logResults(result); 112 | 113 | utils.checkResults(t, result, expected); 114 | }); 115 | -------------------------------------------------------------------------------- /test/test-tokenization-rules.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const rules = require('../src/modes/posix/rules'); 5 | const enums = require('../src/modes/posix/enums'); 6 | const utils = require('../src/utils'); 7 | // const _utils = require('./_utils'); 8 | 9 | const token = utils.tokens.token; 10 | 11 | function check(t, rule, actual, expected) { 12 | // _utils.logResults({actual: Array.from(rule({}, mode)(actual)), expected}); 13 | t.is( 14 | JSON.stringify( 15 | Array.from(rule({}, {enums})(actual)) 16 | ), 17 | JSON.stringify(expected) 18 | ); 19 | } 20 | 21 | test('operatorTokens - identify operator with their tokens', t => { 22 | check(t, rules.operatorTokens, 23 | [token({type: 'OPERATOR', value: '<<', loc: 42})], 24 | [token({type: 'DLESS', value: '<<', loc: 42})] 25 | ); 26 | }); 27 | 28 | test('reservedWords - identify reserved words or WORD', t => { 29 | check( 30 | t, 31 | rules.reservedWords, [ 32 | token({type: 'TOKEN', value: 'while', loc: 42}), 33 | token({type: 'TOKEN', value: 'otherWord', loc: 42}) 34 | ], [ 35 | token({type: 'While', value: 'while', loc: 42}), 36 | token({type: 'WORD', value: 'otherWord', loc: 42}) 37 | ] 38 | ); 39 | }); 40 | 41 | test('functionName - replace function name token as NAME', t => { 42 | const input = [ 43 | token({type: 'WORD', value: 'test', loc: 42, _: {maybeStartOfSimpleCommand: true}}), 44 | token({type: 'OPEN_PAREN', value: '(', loc: 42}), 45 | token({type: 'CLOSE_PAREN', value: ')', loc: 42}), 46 | token({type: 'Lbrace', value: '{', loc: 42}), 47 | token({type: 'WORD', value: 'body', loc: 42}), 48 | token({type: 'WORD', value: 'foo', loc: 42}), 49 | token({type: 'WORD', value: '--lol', loc: 42}), 50 | token({type: ';', value: ';', loc: 42}), 51 | token({type: 'Rbrace', value: '}', loc: 42}) 52 | ]; 53 | // _utils.logResults(result); 54 | 55 | check(t, rules.functionName, input, 56 | [ 57 | token({type: 'NAME', value: 'test', loc: 42, _: {maybeStartOfSimpleCommand: true}}), 58 | token({type: 'OPEN_PAREN', value: '(', loc: 42}), 59 | token({type: 'CLOSE_PAREN', value: ')', loc: 42}), 60 | token({type: 'Lbrace', value: '{', loc: 42}), 61 | token({type: 'WORD', value: 'body', loc: 42}), 62 | token({type: 'WORD', value: 'foo', loc: 42}), 63 | token({type: 'WORD', value: '--lol', loc: 42}), 64 | token({type: ';', value: ';', loc: 42}), 65 | token({type: 'Rbrace', value: '}', loc: 42}) 66 | ] 67 | ); 68 | }); 69 | -------------------------------------------------------------------------------- /test/test-positional-parameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | test('positional parameter with word following', t => { 8 | const result = bashParser('echoword=$1ciao') 9 | .commands[0].prefix; 10 | 11 | // utils.logResults(result); 12 | 13 | utils.checkResults(t, result, [{ 14 | type: 'AssignmentWord', 15 | text: 'echoword=$1ciao', 16 | expansion: [{ 17 | type: 'ParameterExpansion', 18 | kind: 'positional', 19 | parameter: 1, 20 | loc: { 21 | start: 9, 22 | end: 10 23 | } 24 | }] 25 | }]); 26 | }); 27 | 28 | test('positional parameter in braces', t => { 29 | const result = bashParser('echoword=${11}test'); 30 | utils.checkResults(t, result, { 31 | type: 'Script', 32 | commands: [ 33 | { 34 | type: 'Command', 35 | prefix: [{ 36 | type: 'AssignmentWord', 37 | text: 'echoword=${11}test', 38 | expansion: [{ 39 | type: 'ParameterExpansion', 40 | parameter: 11, 41 | kind: 'positional', 42 | loc: { 43 | start: 9, 44 | end: 13 45 | } 46 | }] 47 | }] 48 | } 49 | ] 50 | }); 51 | }); 52 | 53 | test('positional parameter without braces', t => { 54 | const result = bashParser('echoword=$1'); 55 | // console.log(JSON.stringify(result, null, 5)) 56 | utils.checkResults(t, result, { 57 | type: 'Script', 58 | commands: [{ 59 | type: 'Command', 60 | prefix: [{ 61 | type: 'AssignmentWord', 62 | text: 'echoword=$1', 63 | expansion: [{ 64 | type: 'ParameterExpansion', 65 | parameter: 1, 66 | kind: 'positional', 67 | loc: { 68 | start: 9, 69 | end: 10 70 | } 71 | }] 72 | }] 73 | }] 74 | }); 75 | }); 76 | 77 | test('positional parameter without braces allow one digit only', t => { 78 | const result = bashParser('echoword=$11'); 79 | // console.log(JSON.stringify(result, null, 5)) 80 | utils.checkResults(t, result, { 81 | type: 'Script', 82 | commands: [{ 83 | type: 'Command', 84 | prefix: [{ 85 | type: 'AssignmentWord', 86 | text: 'echoword=$11', 87 | expansion: [{ 88 | type: 'ParameterExpansion', 89 | parameter: 1, 90 | kind: 'positional', 91 | loc: { 92 | start: 9, 93 | end: 10 94 | } 95 | }] 96 | }] 97 | }] 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /test/test-quote-removal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | test('remove double quote from string', t => { 8 | const result = bashParser('"echo"'); 9 | utils.checkResults(t, result.commands[0].name, { 10 | type: 'Word', 11 | text: 'echo' 12 | }); 13 | }); 14 | 15 | test('remove single quotes from string', t => { 16 | const result = bashParser('\'echo\''); 17 | utils.checkResults(t, result.commands[0].name, { 18 | type: 'Word', 19 | text: 'echo' 20 | }); 21 | }); 22 | 23 | test('remove unnecessary slashes from string', t => { 24 | const result = bashParser('ec\\%ho'); 25 | utils.checkResults(t, result.commands[0].name, { 26 | type: 'Word', 27 | text: 'ec%ho' 28 | }); 29 | }); 30 | 31 | test('not remove quotes from middle of string if escaped', t => { 32 | const result = bashParser('ec\\\'\\"ho'); 33 | 34 | utils.checkResults(t, result.commands[0].name, { 35 | type: 'Word', 36 | text: 'ec\'"ho' 37 | }); 38 | }); 39 | 40 | test('transform escaped characters', t => { 41 | const result = bashParser('"ec\\t\\nho"'); 42 | 43 | utils.checkResults(t, result.commands[0].name, { 44 | type: 'Word', 45 | text: 'ec\t\nho' 46 | }); 47 | }); 48 | 49 | test('not remove special characters', t => { 50 | const result = bashParser('"ec\tho"'); 51 | 52 | utils.checkResults(t, result.commands[0].name, { 53 | type: 'Word', 54 | text: 'ec\tho' 55 | }); 56 | }); 57 | 58 | test('remove quotes from middle of string', t => { 59 | const result = bashParser('ec\'h\'o'); 60 | // utils.logResults(result) 61 | utils.checkResults(t, result.commands[0].name, { 62 | type: 'Word', 63 | text: 'echo' 64 | }); 65 | }); 66 | 67 | test('remove quotes on assignment', t => { 68 | const result = bashParser('echo="ciao mondo"'); 69 | 70 | utils.checkResults(t, result.commands[0].prefix[0], { 71 | text: 'echo=ciao mondo', 72 | type: 'AssignmentWord' 73 | }); 74 | }); 75 | 76 | test('remove quotes followed by single quotes', t => { 77 | const result = bashParser('echo"ciao"\'mondo\''); 78 | 79 | utils.checkResults(t, result.commands[0].name, { 80 | text: 'echociaomondo', 81 | type: 'Word' 82 | }); 83 | }); 84 | 85 | test('remove single quotes followed by quotes', t => { 86 | const result = bashParser('echo\'ciao\'"mondo"'); 87 | 88 | utils.checkResults(t, result.commands[0].name, { 89 | text: 'echociaomondo', 90 | type: 'Word' 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /src/modes/posix/rules/reserved-words.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const hasOwnProperty = require('has-own-property'); 3 | const values = require('object-values'); 4 | const compose = require('compose-function'); 5 | const map = require('map-iterable'); 6 | const lookahead = require('iterable-lookahead'); 7 | // const words = require('../enums/reserved-words'); 8 | /* 9 | function defined(v) { 10 | return v !== undefined; 11 | } 12 | */ 13 | function isValidReservedWordPosition(tk, iterable, words) { 14 | const last = iterable.behind(1) || {EMPTY: true, is: type => type === 'EMPTY'}; 15 | const twoAgo = iterable.behind(2) || {EMPTY: true, is: type => type === 'EMPTY'}; 16 | 17 | // evaluate based on last token 18 | const startOfCommand = ( 19 | last.is('EMPTY') || last.is('SEPARATOR_OP') || last.is('OPEN_PAREN') || 20 | last.is('CLOSE_PAREN') || last.is('NEWLINE') || last.is('NEWLINE_LIST') || 21 | last.is('DSEMI') || last.value === ';' || last.is('PIPE') || 22 | last.is('OR_IF') || last.is('PIPE') || last.is('AND_IF') 23 | ); 24 | 25 | const lastIsReservedWord = (!last.value === 'for' && !last.value === 'in' && !last.value === 'case' && values(words).some(word => last.is(word))); 26 | 27 | const thirdInCase = twoAgo.value === 'case' && tk.is('TOKEN') && tk.value.toLowerCase() === 'in'; 28 | const thirdInFor = twoAgo.value === 'for' && tk.is('TOKEN') && 29 | (tk.value.toLowerCase() === 'in' || tk.value.toLowerCase() === 'do'); 30 | 31 | // console.log({tk, startOfCommand, lastIsReservedWord, thirdInFor, thirdInCase, twoAgo}) 32 | return tk.value === '}' || startOfCommand || lastIsReservedWord || thirdInFor || thirdInCase; 33 | } 34 | 35 | module.exports = function reservedWords(options, mode) { 36 | return compose(map((tk, idx, iterable) => { 37 | // console.log(tk, isValidReservedWordPosition(tk, iterable), hasOwnProperty(words, tk.value)) 38 | // TOKEN tokens consisting of a reserved word 39 | // are converted to their own token types 40 | // console.log({tk, v:isValidReservedWordPosition(tk, iterable)}) 41 | if (isValidReservedWordPosition(tk, iterable, mode.enums.reservedWords) && hasOwnProperty(mode.enums.reservedWords, tk.value)) { 42 | return tk.changeTokenType(mode.enums.reservedWords[tk.value], tk.value); 43 | } 44 | 45 | // otherwise, TOKEN tokens are converted to 46 | // WORD tokens 47 | if (tk.is('TOKEN')) { 48 | return tk.changeTokenType('WORD', tk.value); 49 | } 50 | 51 | // other tokens are amitted as-is 52 | return tk; 53 | }), lookahead.depth(2)); 54 | }; 55 | -------------------------------------------------------------------------------- /test/fixtures/for-new-line.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "for x\n\tin ; do\n\techo $x;\ndone\n", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "For", 8 | name: { 9 | text: "x", 10 | type: "Name", 11 | loc: { 12 | start: { 13 | col: 5, 14 | row: 1, 15 | char: 4 16 | }, 17 | end: { 18 | col: 5, 19 | row: 1, 20 | char: 4 21 | } 22 | } 23 | }, 24 | do: { 25 | type: "CompoundList", 26 | commands: [ 27 | { 28 | type: "SimpleCommand", 29 | name: { 30 | text: "echo", 31 | type: "Word", 32 | loc: { 33 | start: { 34 | col: 2, 35 | row: 3, 36 | char: 16 37 | }, 38 | end: { 39 | col: 5, 40 | row: 3, 41 | char: 19 42 | } 43 | } 44 | }, 45 | loc: { 46 | start: { 47 | col: 2, 48 | row: 3, 49 | char: 16 50 | }, 51 | end: { 52 | col: 8, 53 | row: 3, 54 | char: 22 55 | } 56 | }, 57 | suffix: [ 58 | { 59 | text: "$x", 60 | expansion: [ 61 | { 62 | loc: { 63 | start: 0, 64 | end: 1 65 | }, 66 | parameter: "x", 67 | type: "ParameterExpansion" 68 | } 69 | ], 70 | type: "Word", 71 | loc: { 72 | start: { 73 | col: 7, 74 | row: 3, 75 | char: 21 76 | }, 77 | end: { 78 | col: 8, 79 | row: 3, 80 | char: 22 81 | } 82 | } 83 | } 84 | ] 85 | } 86 | ], 87 | loc: { 88 | start: { 89 | col: 7, 90 | row: 2, 91 | char: 12 92 | }, 93 | end: { 94 | col: 4, 95 | row: 4, 96 | char: 28 97 | } 98 | } 99 | }, 100 | loc: { 101 | start: { 102 | col: 1, 103 | row: 1, 104 | char: 0 105 | }, 106 | end: { 107 | col: 4, 108 | row: 4, 109 | char: 28 110 | } 111 | } 112 | } 113 | ], 114 | loc: { 115 | start: { 116 | col: 1, 117 | row: 1, 118 | char: 0 119 | }, 120 | end: { 121 | col: 4, 122 | row: 4, 123 | char: 28 124 | } 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /test/test-loc-function-def.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('loc in function declaration', t => { 9 | const cmd = 10 | `foo () { 11 | command bar --lol; 12 | } 13 | `; 14 | const result = bashParser(cmd, {insertLOC: true}); 15 | // utils.logResults(result) 16 | const expected = { 17 | type: 'Function', 18 | name: { 19 | text: 'foo', 20 | type: 'Name', 21 | loc: { 22 | start: { 23 | col: 1, 24 | row: 1, 25 | char: 0 26 | }, 27 | end: { 28 | col: 3, 29 | row: 1, 30 | char: 2 31 | } 32 | } 33 | }, 34 | body: { 35 | type: 'CompoundList', 36 | commands: [ 37 | { 38 | type: 'Command', 39 | name: { 40 | text: 'command', 41 | type: 'Word', 42 | loc: { 43 | start: { 44 | col: 2, 45 | row: 2, 46 | char: 10 47 | }, 48 | end: { 49 | col: 8, 50 | row: 2, 51 | char: 16 52 | } 53 | } 54 | }, 55 | loc: { 56 | start: { 57 | col: 2, 58 | row: 2, 59 | char: 10 60 | }, 61 | end: { 62 | col: 18, 63 | row: 2, 64 | char: 26 65 | } 66 | }, 67 | suffix: [ 68 | { 69 | text: 'bar', 70 | type: 'Word', 71 | loc: { 72 | start: { 73 | col: 10, 74 | row: 2, 75 | char: 18 76 | }, 77 | end: { 78 | col: 12, 79 | row: 2, 80 | char: 20 81 | } 82 | } 83 | }, 84 | { 85 | text: '--lol', 86 | type: 'Word', 87 | loc: { 88 | start: { 89 | col: 14, 90 | row: 2, 91 | char: 22 92 | }, 93 | end: { 94 | col: 18, 95 | row: 2, 96 | char: 26 97 | } 98 | } 99 | } 100 | ] 101 | } 102 | ], 103 | loc: { 104 | start: { 105 | col: 8, 106 | row: 1, 107 | char: 7 108 | }, 109 | end: { 110 | col: 1, 111 | row: 3, 112 | char: 29 113 | } 114 | } 115 | }, 116 | loc: { 117 | start: { 118 | col: 1, 119 | row: 1, 120 | char: 0 121 | }, 122 | end: { 123 | col: 1, 124 | row: 3, 125 | char: 29 126 | } 127 | } 128 | }; 129 | 130 | utils.checkResults(t, result.commands[0], expected); 131 | }); 132 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/reducers/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const t = require('../../../../utils/tokens'); 4 | 5 | const tokenOrEmpty = t.tokenOrEmpty; 6 | const newLine = t.newLine; 7 | const isPartOfOperator = t.isPartOfOperator; 8 | 9 | module.exports = function start(state, source, reducers) { 10 | const char = source && source.shift(); 11 | 12 | if (char === undefined) { 13 | return { 14 | nextReduction: reducers.end, 15 | tokensToEmit: tokenOrEmpty(state), 16 | nextState: state.resetCurrent().saveCurrentLocAsStart() 17 | }; 18 | } 19 | 20 | if (state.escaping && char === '\n') { 21 | return { 22 | nextReduction: reducers.start, 23 | nextState: state.setEscaping(false).removeLastChar() 24 | }; 25 | } 26 | 27 | if (!state.escaping && char === '#' && state.current === '') { 28 | return { 29 | nextReduction: reducers.comment 30 | }; 31 | } 32 | 33 | if (!state.escaping && char === '\n') { 34 | return { 35 | nextReduction: reducers.start, 36 | tokensToEmit: tokenOrEmpty(state).concat(newLine()), 37 | nextState: state.resetCurrent().saveCurrentLocAsStart() 38 | }; 39 | } 40 | 41 | if (!state.escaping && char === '\\') { 42 | return { 43 | nextReduction: reducers.start, 44 | nextState: state.setEscaping(true).appendChar(char) 45 | }; 46 | } 47 | 48 | if (!state.escaping && isPartOfOperator(char)) { 49 | return { 50 | nextReduction: reducers.operator, 51 | tokensToEmit: tokenOrEmpty(state), 52 | nextState: state.setCurrent(char).saveCurrentLocAsStart() 53 | }; 54 | } 55 | 56 | if (!state.escaping && char === '\'') { 57 | return { 58 | nextReduction: reducers.singleQuoting, 59 | nextState: state.appendChar(char) 60 | }; 61 | } 62 | 63 | if (!state.escaping && char === '"') { 64 | return { 65 | nextReduction: reducers.doubleQuoting, 66 | nextState: state.appendChar(char) 67 | }; 68 | } 69 | 70 | if (!state.escaping && char.match(/\s/)) { 71 | return { 72 | nextReduction: reducers.start, 73 | tokensToEmit: tokenOrEmpty(state), 74 | nextState: state.resetCurrent().saveCurrentLocAsStart().setExpansion([]) 75 | }; 76 | } 77 | 78 | if (!state.escaping && char === '$') { 79 | return { 80 | nextReduction: reducers.expansionStart, 81 | nextState: state.appendChar(char).appendEmptyExpansion() 82 | }; 83 | } 84 | 85 | if (!state.escaping && char === '`') { 86 | return { 87 | nextReduction: reducers.expansionCommandTick, 88 | nextState: state.appendChar(char).appendEmptyExpansion() 89 | }; 90 | } 91 | 92 | return { 93 | nextReduction: reducers.start, 94 | nextState: state.appendChar(char).setEscaping(false) 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /test/fixtures/function.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "foo () {\n\tcommand bar --lol;\n}\n", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Function", 8 | name: { 9 | text: "foo", 10 | type: "Name", 11 | loc: { 12 | start: { 13 | col: 1, 14 | row: 1, 15 | char: 0 16 | }, 17 | end: { 18 | col: 3, 19 | row: 1, 20 | char: 2 21 | } 22 | } 23 | }, 24 | body: { 25 | type: "CompoundList", 26 | commands: [ 27 | { 28 | type: "SimpleCommand", 29 | name: { 30 | text: "command", 31 | type: "Word", 32 | loc: { 33 | start: { 34 | col: 2, 35 | row: 2, 36 | char: 10 37 | }, 38 | end: { 39 | col: 8, 40 | row: 2, 41 | char: 16 42 | } 43 | } 44 | }, 45 | loc: { 46 | start: { 47 | col: 2, 48 | row: 2, 49 | char: 10 50 | }, 51 | end: { 52 | col: 18, 53 | row: 2, 54 | char: 26 55 | } 56 | }, 57 | suffix: [ 58 | { 59 | text: "bar", 60 | type: "Word", 61 | loc: { 62 | start: { 63 | col: 10, 64 | row: 2, 65 | char: 18 66 | }, 67 | end: { 68 | col: 12, 69 | row: 2, 70 | char: 20 71 | } 72 | } 73 | }, 74 | { 75 | text: "--lol", 76 | type: "Word", 77 | loc: { 78 | start: { 79 | col: 14, 80 | row: 2, 81 | char: 22 82 | }, 83 | end: { 84 | col: 18, 85 | row: 2, 86 | char: 26 87 | } 88 | } 89 | } 90 | ] 91 | } 92 | ], 93 | loc: { 94 | start: { 95 | col: 8, 96 | row: 1, 97 | char: 7 98 | }, 99 | end: { 100 | col: 1, 101 | row: 3, 102 | char: 29 103 | } 104 | } 105 | }, 106 | loc: { 107 | start: { 108 | col: 1, 109 | row: 1, 110 | char: 0 111 | }, 112 | end: { 113 | col: 1, 114 | row: 3, 115 | char: 29 116 | } 117 | } 118 | } 119 | ], 120 | loc: { 121 | start: { 122 | col: 1, 123 | row: 1, 124 | char: 0 125 | }, 126 | end: { 127 | col: 1, 128 | row: 3, 129 | char: 29 130 | } 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /test/test-quoting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | function testUnclosed(cmd, char) { 8 | return t => { 9 | const err = t.throws(() => bashParser(cmd)); 10 | t.truthy(err instanceof SyntaxError); 11 | t.is(err.message, 'Unclosed ' + char); 12 | }; 13 | } 14 | 15 | test('throws on unclosed double quotes', testUnclosed('echo "TEST1', '"')); 16 | test('throws on unclosed single quotes', testUnclosed('echo \'TEST1', '\'')); 17 | test('throws on unclosed command subst', testUnclosed('echo $(TEST1', '$(')); 18 | test('throws on unclosed backtick command subst', testUnclosed('echo `TEST1', '`')); 19 | test('throws on unclosed arhit subst', testUnclosed('echo $((TEST1', '$((')); 20 | test('throws on unclosed param subst', testUnclosed('echo ${TEST1', '${')); 21 | 22 | test('quotes within double quotes', t => { 23 | const result = bashParser('echo "TEST1 \'TEST2"'); 24 | // utils.logResults(result) 25 | utils.checkResults(t, result, { 26 | type: 'Script', 27 | commands: [{ 28 | type: 'Command', 29 | name: {type: 'Word', text: 'echo'}, 30 | suffix: [{type: 'Word', text: 'TEST1 \'TEST2'}] 31 | }] 32 | }); 33 | }); 34 | 35 | test('escaped double quotes within double quotes', t => { 36 | const result = bashParser('echo "TEST1 \\"TEST2"'); 37 | // utils.logResults(result); 38 | utils.checkResults(t, result, { 39 | type: 'Script', 40 | commands: [{ 41 | type: 'Command', 42 | name: {type: 'Word', text: 'echo'}, 43 | suffix: [{type: 'Word', text: 'TEST1 "TEST2'}] 44 | }] 45 | }); 46 | }); 47 | 48 | test('double quotes within single quotes', t => { 49 | const result = bashParser('echo \'TEST1 "TEST2\''); 50 | utils.checkResults(t, result, { 51 | type: 'Script', 52 | commands: [{ 53 | type: 'Command', 54 | name: {type: 'Word', text: 'echo'}, 55 | suffix: [{type: 'Word', text: 'TEST1 "TEST2'}] 56 | }] 57 | }); 58 | }); 59 | 60 | test('Partially quoted word', t => { 61 | const result = bashParser('echo TEST1\' TEST2 \'TEST3'); 62 | utils.checkResults(t, result, { 63 | type: 'Script', 64 | commands: [{ 65 | type: 'Command', 66 | name: {type: 'Word', text: 'echo'}, 67 | suffix: [{type: 'Word', text: 'TEST1 TEST2 TEST3'}] 68 | }] 69 | }); 70 | }); 71 | 72 | test('Partially double quoted word', t => { 73 | const result = bashParser('echo TEST3" TEST4 "TEST5'); 74 | // utils.logResults(result); 75 | utils.checkResults(t, result, { 76 | type: 'Script', 77 | commands: [{ 78 | type: 'Command', 79 | name: {type: 'Word', text: 'echo'}, 80 | suffix: [{type: 'Word', text: 'TEST3 TEST4 TEST5'}] 81 | }] 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /test/test-for.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('parse for', t => { 9 | const result = bashParser('for x in a b c; do echo $x; done'); 10 | // utils.logResults(result) 11 | utils.checkResults(t, 12 | result, { 13 | type: 'Script', 14 | commands: [{ 15 | type: 'For', 16 | name: {type: 'Name', text: 'x'}, 17 | wordlist: [{type: 'Word', text: 'a'}, {type: 'Word', text: 'b'}, {type: 'Word', text: 'c'}], 18 | do: { 19 | type: 'CompoundList', 20 | commands: [{ 21 | type: 'Command', 22 | name: {type: 'Word', text: 'echo'}, 23 | suffix: [{ 24 | type: 'Word', 25 | text: '$x', 26 | expansion: [{ 27 | type: 'ParameterExpansion', 28 | parameter: 'x', 29 | loc: { 30 | start: 0, 31 | end: 1 32 | } 33 | }] 34 | }] 35 | }] 36 | } 37 | }] 38 | } 39 | ); 40 | }); 41 | 42 | test('parse for with default sequence', t => { 43 | const result = bashParser('for x\n do echo $x\n done'); 44 | // utils.logResults(result) 45 | utils.checkResults(t, 46 | result, { 47 | type: 'Script', 48 | commands: [{ 49 | type: 'For', 50 | name: {type: 'Name', text: 'x'}, 51 | do: { 52 | type: 'CompoundList', 53 | commands: [{ 54 | type: 'Command', 55 | name: {type: 'Word', text: 'echo'}, 56 | suffix: [{ 57 | type: 'Word', 58 | text: '$x', 59 | expansion: [{ 60 | type: 'ParameterExpansion', 61 | parameter: 'x', 62 | loc: { 63 | start: 0, 64 | end: 1 65 | } 66 | }] 67 | }] 68 | }] 69 | } 70 | }] 71 | } 72 | ); 73 | }); 74 | 75 | test('parse for with default sequence - on one line', t => { 76 | const result = bashParser('for x in; do echo $x; done'); 77 | // utils.logResults(result) 78 | utils.checkResults(t, 79 | result, { 80 | type: 'Script', 81 | commands: [{ 82 | type: 'For', 83 | name: {type: 'Name', text: 'x'}, 84 | do: { 85 | type: 'CompoundList', 86 | commands: [{ 87 | type: 'Command', 88 | name: {type: 'Word', text: 'echo'}, 89 | suffix: [{ 90 | type: 'Word', 91 | text: '$x', 92 | expansion: [{ 93 | type: 'ParameterExpansion', 94 | parameter: 'x', 95 | loc: { 96 | start: 0, 97 | end: 1 98 | } 99 | }] 100 | }] 101 | }] 102 | } 103 | }] 104 | } 105 | ); 106 | }); 107 | -------------------------------------------------------------------------------- /test/test-reserved-words.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | test('single quoted tokens are not parsed as reserved words', t => { 8 | const result = bashParser('\'if\' true'); 9 | // utils.logResults(result); 10 | 11 | utils.checkResults(t, result, { 12 | type: 'Script', 13 | commands: [ 14 | { 15 | type: 'Command', 16 | name: { 17 | text: 'if', 18 | type: 'Word' 19 | }, 20 | suffix: [ 21 | { 22 | text: 'true', 23 | type: 'Word' 24 | } 25 | ] 26 | } 27 | ] 28 | }); 29 | }); 30 | 31 | test('double quoted tokens are not parsed as reserved words', t => { 32 | const result = bashParser('"if" true'); 33 | // utils.logResults(result); 34 | 35 | utils.checkResults(t, result, { 36 | type: 'Script', 37 | commands: [ 38 | { 39 | type: 'Command', 40 | name: { 41 | text: 'if', 42 | type: 'Word' 43 | }, 44 | suffix: [ 45 | { 46 | text: 'true', 47 | type: 'Word' 48 | } 49 | ] 50 | } 51 | ] 52 | }); 53 | }); 54 | 55 | test('partially double quoted tokens are not parsed as reserved words', t => { 56 | const result = bashParser('i"f" true'); 57 | // utils.logResults(result); 58 | 59 | utils.checkResults(t, result, { 60 | type: 'Script', 61 | commands: [ 62 | { 63 | type: 'Command', 64 | name: { 65 | text: 'if', 66 | type: 'Word' 67 | }, 68 | suffix: [ 69 | { 70 | text: 'true', 71 | type: 'Word' 72 | } 73 | ] 74 | } 75 | ] 76 | }); 77 | }); 78 | 79 | test('partially single quoted tokens are not parsed as reserved words', t => { 80 | const result = bashParser('i\'f\' true'); 81 | // utils.logResults(result); 82 | 83 | utils.checkResults(t, result, { 84 | type: 'Script', 85 | commands: [ 86 | { 87 | type: 'Command', 88 | name: { 89 | text: 'if', 90 | type: 'Word' 91 | }, 92 | suffix: [ 93 | { 94 | text: 'true', 95 | type: 'Word' 96 | } 97 | ] 98 | } 99 | ] 100 | }); 101 | }); 102 | 103 | test('tokens in invalid positions are not parsed as reserved words', t => { 104 | const result = bashParser('echo if'); 105 | // utils.logResults(result); 106 | 107 | utils.checkResults(t, result, { 108 | type: 'Script', 109 | commands: [ 110 | { 111 | type: 'Command', 112 | name: { 113 | text: 'echo', 114 | type: 'Word' 115 | }, 116 | suffix: [ 117 | { 118 | text: 'if', 119 | type: 'Word' 120 | } 121 | ] 122 | } 123 | ] 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /test/test-fixtures.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | // various example taken from http://www.etalabs.net/sh_tricks.html 8 | 9 | test('2', t => { 10 | const result = bashParser('echo () { printf %s\\n "$*" ; }'); 11 | // utils.logResults(result); 12 | utils.checkResults(t, result, { 13 | type: 'Script', 14 | commands: [ 15 | { 16 | type: 'Function', 17 | name: { 18 | text: 'echo', 19 | type: 'Name' 20 | }, 21 | body: { 22 | type: 'CompoundList', 23 | commands: [ 24 | { 25 | type: 'Command', 26 | name: { 27 | text: 'printf', 28 | type: 'Word' 29 | }, 30 | suffix: [ 31 | { 32 | text: '%sn', 33 | type: 'Word' 34 | }, 35 | { 36 | text: '"$*"', 37 | expansion: [ 38 | { 39 | kind: 'positional-string', 40 | parameter: '*', 41 | loc: { 42 | start: 1, 43 | end: 2 44 | }, 45 | type: 'ParameterExpansion' 46 | } 47 | ], 48 | type: 'Word' 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | } 55 | ]}); 56 | }); 57 | 58 | test('3', t => { 59 | const result = bashParser('IFS= read -r var'); 60 | utils.checkResults(t, result, { 61 | type: 'Script', 62 | commands: [{ 63 | type: 'Command', 64 | name: {type: 'Word', text: 'read'}, 65 | prefix: [{type: 'AssignmentWord', text: 'IFS='}], 66 | suffix: [{type: 'Word', text: '-r'}, {type: 'Word', text: 'var'}] 67 | }] 68 | }); 69 | }); 70 | 71 | test('4', t => { 72 | const result = bashParser('foo | IFS= read var'); 73 | // console.log(inspect(result, {depth: null})); 74 | 75 | utils.checkResults(t, result, { 76 | type: 'Script', 77 | commands: [{ 78 | type: 'Pipeline', 79 | commands: [{ 80 | type: 'Command', 81 | name: {type: 'Word', text: 'foo'} 82 | }, { 83 | type: 'Command', 84 | name: {type: 'Word', text: 'read'}, 85 | prefix: [{type: 'AssignmentWord', text: 'IFS='}], 86 | suffix: [{type: 'Word', text: 'var'}] 87 | }] 88 | }] 89 | }); 90 | }); 91 | 92 | test('5', t => { 93 | const result = bashParser( 94 | `foo='hello ; rm -rf /' 95 | dest=bar 96 | eval "dest=foo"` 97 | ); 98 | 99 | utils.checkResults(t, result, { 100 | type: 'Script', 101 | commands: [{ 102 | type: 'Command', 103 | prefix: [{type: 'AssignmentWord', text: 'foo=hello ; rm -rf /'}] 104 | }, { 105 | type: 'Command', 106 | prefix: [{type: 'AssignmentWord', text: 'dest=bar'}] 107 | }, { 108 | type: 'Command', 109 | name: {type: 'Word', text: 'eval'}, 110 | suffix: [{type: 'Word', text: 'dest=foo'}] 111 | }] 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/modes/posix/enums/parameter-operators.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const name = '[a-zA-Z_][a-zA-Z0-9_]*'; 4 | 5 | const parameterOps = { 6 | [`^(${name}):\\-(.*)$`]: { 7 | op: 'useDefaultValue', 8 | parameter: m => m[1], 9 | word: m => m[2], 10 | expand: ['word'] 11 | }, 12 | 13 | [`^(${name}):\\=(.*)$`]: { 14 | op: 'assignDefaultValue', 15 | parameter: m => m[1], 16 | word: m => m[2], 17 | expand: ['word'] 18 | }, 19 | 20 | [`^(${name}):\\?(.*)$`]: { 21 | op: 'indicateErrorIfNull', 22 | parameter: m => m[1], 23 | word: m => m[2], 24 | expand: ['word'] 25 | }, 26 | 27 | [`^(${name}):\\+(.*)$`]: { 28 | op: 'useAlternativeValue', 29 | parameter: m => m[1], 30 | word: m => m[2], 31 | expand: ['word'] 32 | }, 33 | 34 | [`^(${name})\\-(.*)$`]: { 35 | op: 'useDefaultValueIfUnset', 36 | parameter: m => m[1], 37 | word: m => m[2], 38 | expand: ['word'] 39 | }, 40 | 41 | [`^(${name})\\=(.*)$`]: { 42 | op: 'assignDefaultValueIfUnset', 43 | parameter: m => m[1], 44 | word: m => m[2], 45 | expand: ['word'] 46 | }, 47 | 48 | [`^(${name})\\?(.*)$`]: { 49 | op: 'indicateErrorIfUnset', 50 | parameter: m => m[1], 51 | word: m => m[2], 52 | expand: ['word'] 53 | }, 54 | 55 | [`^(${name})\\+(.*)$`]: { 56 | op: 'useAlternativeValueIfUnset', 57 | parameter: m => m[1], 58 | word: m => m[2], 59 | expand: ['word'] 60 | }, 61 | 62 | [`^(${name})\\%\\%(.*)$`]: { 63 | op: 'removeLargestSuffixPattern', 64 | parameter: m => m[1], 65 | word: m => m[2], 66 | expand: ['word'] 67 | }, 68 | 69 | [`^(${name})\\#\\#(.*)$`]: { 70 | op: 'removeLargestPrefixPattern', 71 | parameter: m => m[1], 72 | word: m => m[2], 73 | expand: ['word'] 74 | }, 75 | 76 | [`^(${name})\\%(.*)$`]: { 77 | op: 'removeSmallestSuffixPattern', 78 | parameter: m => m[1], 79 | word: m => m[2], 80 | expand: ['word'] 81 | }, 82 | 83 | [`^(${name})\\#(.*)$`]: { 84 | op: 'removeSmallestPrefixPattern', 85 | parameter: m => m[1], 86 | word: m => m[2], 87 | expand: ['word'] 88 | }, 89 | 90 | [`^\\#(${name})$`]: { 91 | op: 'stringLength', 92 | parameter: m => m[1] 93 | }, 94 | 95 | [`^([1-9][0-9]*)$`]: { 96 | kind: 'positional', 97 | parameter: m => Number(m[1]) 98 | }, 99 | 100 | '^!$': { 101 | kind: 'last-background-pid' 102 | }, 103 | 104 | '^\\@$': { 105 | kind: 'positional-list' 106 | }, 107 | 108 | '^\\-$': { 109 | kind: 'current-option-flags' 110 | }, 111 | 112 | '^\\#$': { 113 | kind: 'positional-count' 114 | }, 115 | 116 | '^\\?$': { 117 | kind: 'last-exit-status' 118 | }, 119 | 120 | '^\\*$': { 121 | kind: 'positional-string' 122 | }, 123 | 124 | '^\\$$': { 125 | kind: 'shell-process-id' 126 | }, 127 | 128 | '^0$': { 129 | kind: 'shell-script-name' 130 | } 131 | }; 132 | 133 | module.exports = parameterOps; 134 | 135 | -------------------------------------------------------------------------------- /src/modes/word-expansion/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const map = require('map-iterable'); 4 | const tokenOrEmpty = require('../../utils/tokens').tokenOrEmpty; 5 | 6 | const convertToWord = () => map(tk => { 7 | // TOKEN tokens are converted to WORD tokens 8 | if (tk.is('TOKEN')) { 9 | return tk.changeTokenType('WORD', tk.value); 10 | } 11 | 12 | // other tokens are amitted as-is 13 | return tk; 14 | }); 15 | 16 | function start(state, source, reducers) { 17 | const char = source && source.shift(); 18 | 19 | if (char === undefined) { 20 | return { 21 | nextReduction: reducers.end, 22 | tokensToEmit: tokenOrEmpty(state), 23 | nextState: state.resetCurrent().saveCurrentLocAsStart() 24 | }; 25 | } 26 | 27 | if (state.escaping && char === '\n') { 28 | return { 29 | nextReduction: reducers.start, 30 | nextState: state.setEscaping(false).removeLastChar() 31 | }; 32 | } 33 | 34 | if (!state.escaping && char === '\\') { 35 | return { 36 | nextReduction: reducers.start, 37 | nextState: state.setEscaping(true).appendChar(char) 38 | }; 39 | } 40 | 41 | if (!state.escaping && char === '\'') { 42 | return { 43 | nextReduction: reducers.singleQuoting, 44 | nextState: state.appendChar(char) 45 | }; 46 | } 47 | 48 | if (!state.escaping && char === '"') { 49 | return { 50 | nextReduction: reducers.doubleQuoting, 51 | nextState: state.appendChar(char) 52 | }; 53 | } 54 | 55 | if (!state.escaping && char === '$') { 56 | return { 57 | nextReduction: reducers.expansionStart, 58 | nextState: state.appendChar(char).appendEmptyExpansion() 59 | }; 60 | } 61 | 62 | if (!state.escaping && char === '`') { 63 | return { 64 | nextReduction: reducers.expansionCommandTick, 65 | nextState: state.appendChar(char).appendEmptyExpansion() 66 | }; 67 | } 68 | 69 | return { 70 | nextReduction: reducers.start, 71 | nextState: state.appendChar(char).setEscaping(false) 72 | }; 73 | } 74 | 75 | module.exports = { 76 | inherits: 'posix', 77 | init: posixMode => { 78 | const phaseCatalog = posixMode.phaseCatalog; 79 | const lexerPhases = [ 80 | convertToWord, 81 | phaseCatalog.parameterExpansion, 82 | phaseCatalog.arithmeticExpansion, 83 | phaseCatalog.commandExpansion, 84 | phaseCatalog.tildeExpanding, 85 | phaseCatalog.parameterExpansion.resolve, 86 | phaseCatalog.commandExpansion.resolve, 87 | phaseCatalog.arithmeticExpansion.resolve, 88 | phaseCatalog.fieldSplitting.split, 89 | phaseCatalog.pathExpansion, 90 | phaseCatalog.quoteRemoval, 91 | phaseCatalog.defaultNodeType 92 | ]; 93 | const reducers = Object.assign({}, posixMode.tokenizer.reducers, {start}); 94 | 95 | const tokenizer = () => posixMode.tokenizer({}, reducers); 96 | 97 | return Object.assign({}, posixMode, {lexerPhases, tokenizer}); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /test/fixtures/case.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "case foo in\n\t* )\n\t\techo bar;;\nesac\n", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Case", 8 | clause: { 9 | text: "foo", 10 | type: "Word", 11 | loc: { 12 | start: { 13 | col: 6, 14 | row: 1, 15 | char: 5 16 | }, 17 | end: { 18 | col: 8, 19 | row: 1, 20 | char: 7 21 | } 22 | } 23 | }, 24 | cases: [ 25 | { 26 | type: "CaseItem", 27 | pattern: [ 28 | { 29 | text: "*", 30 | type: "Word", 31 | loc: { 32 | start: { 33 | col: 2, 34 | row: 2, 35 | char: 13 36 | }, 37 | end: { 38 | col: 2, 39 | row: 2, 40 | char: 13 41 | } 42 | } 43 | } 44 | ], 45 | body: { 46 | type: "CompoundList", 47 | commands: [ 48 | { 49 | type: "SimpleCommand", 50 | name: { 51 | text: "echo", 52 | type: "Word", 53 | loc: { 54 | start: { 55 | col: 3, 56 | row: 3, 57 | char: 19 58 | }, 59 | end: { 60 | col: 6, 61 | row: 3, 62 | char: 22 63 | } 64 | } 65 | }, 66 | loc: { 67 | start: { 68 | col: 3, 69 | row: 3, 70 | char: 19 71 | }, 72 | end: { 73 | col: 10, 74 | row: 3, 75 | char: 26 76 | } 77 | }, 78 | suffix: [ 79 | { 80 | text: "bar", 81 | type: "Word", 82 | loc: { 83 | start: { 84 | col: 8, 85 | row: 3, 86 | char: 24 87 | }, 88 | end: { 89 | col: 10, 90 | row: 3, 91 | char: 26 92 | } 93 | } 94 | } 95 | ] 96 | } 97 | ], 98 | loc: { 99 | start: { 100 | col: 3, 101 | row: 3, 102 | char: 19 103 | }, 104 | end: { 105 | col: 10, 106 | row: 3, 107 | char: 26 108 | } 109 | } 110 | }, 111 | loc: { 112 | start: { 113 | col: 2, 114 | row: 2, 115 | char: 13 116 | }, 117 | end: { 118 | col: 12, 119 | row: 3, 120 | char: 28 121 | } 122 | } 123 | } 124 | ], 125 | loc: { 126 | start: { 127 | col: 1, 128 | row: 1, 129 | char: 0 130 | }, 131 | end: { 132 | col: 4, 133 | row: 4, 134 | char: 33 135 | } 136 | } 137 | } 138 | ], 139 | loc: { 140 | start: { 141 | col: 1, 142 | row: 1, 143 | char: 0 144 | }, 145 | end: { 146 | col: 4, 147 | row: 4, 148 | char: 33 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/modes/posix/rules/parameter-expansion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const mapObj = require('map-obj'); 3 | const filter = require('filter-obj'); 4 | const map = require('map-iterable'); 5 | const pairs = require('object-pairs'); 6 | const MagicString = require('magic-string'); 7 | const tokens = require('../../../utils/tokens'); 8 | const fieldSplitting = require('./field-splitting'); 9 | 10 | const handleParameter = (obj, match) => { 11 | const ret = mapObj(obj, (k, v) => { 12 | if (typeof v === 'function') { 13 | const val = v(match); 14 | return [k, val]; 15 | } 16 | 17 | if (typeof v === 'object' && k !== 'expand') { 18 | return [k, handleParameter(v, match)]; 19 | } 20 | 21 | return [k, v]; 22 | }); 23 | 24 | if (ret.expand) { 25 | const bashParser = require('../../../index'); 26 | 27 | for (const prop of ret.expand) { 28 | const ast = bashParser(ret[prop], {mode: 'word-expansion'}); 29 | ret[prop] = ast.commands[0].name; 30 | } 31 | delete ret.expand; 32 | } 33 | 34 | return ret; 35 | }; 36 | 37 | function expandParameter(xp, enums) { 38 | let parameter = xp.parameter; 39 | 40 | for (const pair of pairs(enums.parameterOperators)) { 41 | const re = new RegExp(pair[0]); 42 | 43 | const match = parameter.match(re); 44 | 45 | if (match) { 46 | const opProps = handleParameter(pair[1], match); 47 | 48 | return filter(Object.assign( 49 | xp, 50 | opProps 51 | ), (k, v) => v !== undefined); 52 | } 53 | } 54 | 55 | return xp; 56 | } 57 | 58 | // RULE 5 - If the current character is an unquoted '$' or '`', the shell shall 59 | // identify the start of any candidates for parameter expansion (Parameter Expansion), 60 | // command substitution (Command Substitution), or arithmetic expansion (Arithmetic 61 | // Expansion) from their introductory unquoted character sequences: '$' or "${", "$(" 62 | // or '`', and "$((", respectively. 63 | const parameterExpansion = (options, mode) => map(token => { 64 | if (token.is('WORD') || token.is('ASSIGNMENT_WORD')) { 65 | if (!token.expansion || token.expansion.length === 0) { 66 | return token; 67 | } 68 | 69 | return tokens.setExpansions(token, token.expansion.map(xp => { 70 | if (xp.type === 'parameter_expansion') { 71 | return expandParameter(xp, mode.enums); 72 | } 73 | 74 | return xp; 75 | })); 76 | } 77 | return token; 78 | }); 79 | 80 | parameterExpansion.resolve = options => map(token => { 81 | if (token.is('WORD') || token.is('ASSIGNMENT_WORD')) { 82 | if (!options.resolveParameter || !token.expansion || token.expansion.length === 0) { 83 | return token; 84 | } 85 | 86 | const value = token.value; 87 | 88 | const magic = new MagicString(value); 89 | for (const xp of token.expansion) { 90 | if (xp.type === 'parameter_expansion') { 91 | const result = options.resolveParameter(xp); 92 | xp.resolved = true; 93 | magic.overwrite( 94 | xp.loc.start, 95 | xp.loc.end + 1, 96 | fieldSplitting.mark(result, value, options) 97 | ); 98 | } 99 | } 100 | return tokens.alterValue(token, magic.toString()); 101 | } 102 | return token; 103 | }); 104 | 105 | module.exports = parameterExpansion; 106 | -------------------------------------------------------------------------------- /test/test-alias-substitution.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable camelcase */ 4 | const test = require('ava'); 5 | const bashParser = require('../src'); 6 | const utils = require('./_utils'); 7 | 8 | test('alias with no argument', t => { 9 | const result = bashParser('thisIsAlias world', { 10 | resolveAlias: name => name === 'thisIsAlias' ? 'test-value' : undefined 11 | }); 12 | utils.checkResults(t, result, { 13 | type: 'Script', 14 | commands: [{ 15 | type: 'Command', 16 | name: {type: 'Word', text: 'test-value'}, 17 | suffix: [{type: 'Word', text: 'world'}] 18 | }] 19 | }); 20 | }); 21 | 22 | test('alias with duplicating stream redirection', t => { 23 | const result = bashParser('2>&1 world', { 24 | resolveAlias: name => name === 'world' ? 'test-value' : undefined 25 | }); 26 | // utils.logResults(result); 27 | utils.checkResults(t, 28 | result.commands[0].name, 29 | {type: 'Word', text: 'test-value'} 30 | ); 31 | }); 32 | 33 | test('alias with arguments', t => { 34 | const result = bashParser('thisIsAlias world', { 35 | resolveAlias: name => name === 'thisIsAlias' ? 'test-value earth' : undefined 36 | }); 37 | utils.checkResults(t, result, { 38 | type: 'Script', 39 | commands: [{ 40 | type: 'Command', 41 | name: {type: 'Word', text: 'test-value'}, 42 | suffix: [ 43 | {type: 'Word', text: 'earth'}, 44 | {type: 'Word', text: 'world'} 45 | ] 46 | }] 47 | }); 48 | }); 49 | 50 | test('alias with prefixes', t => { 51 | const result = bashParser('thisIsAlias world', { 52 | resolveAlias: name => name === 'thisIsAlias' ? 'a=42 test-value' : undefined 53 | }); 54 | utils.checkResults(t, result, { 55 | type: 'Script', 56 | commands: [{ 57 | prefix: [{type: 'AssignmentWord', text: 'a=42'}], 58 | type: 'Command', 59 | name: {type: 'Word', text: 'test-value'}, 60 | suffix: [{type: 'Word', text: 'world'}] 61 | }] 62 | }); 63 | }); 64 | 65 | test('recursive alias with prefixes', t => { 66 | const result = bashParser('thisIsAlias world', { 67 | resolveAlias: name => { 68 | if (name === 'thisIsAlias') { 69 | return 'a=42 recurse'; 70 | } 71 | if (name === 'recurse') { 72 | return 'echo other'; 73 | } 74 | } 75 | }); 76 | // utils.logResults(result) 77 | 78 | utils.checkResults(t, result, { 79 | type: 'Script', 80 | commands: [{ 81 | prefix: [{type: 'AssignmentWord', text: 'a=42'}], 82 | type: 'Command', 83 | name: {type: 'Word', text: 'echo'}, 84 | suffix: [ 85 | {type: 'Word', text: 'other'}, 86 | {type: 'Word', text: 'world'} 87 | ] 88 | }] 89 | }); 90 | }); 91 | 92 | test('guarded against infinite loops', t => { 93 | const result = bashParser('thisIsAlias world', { 94 | resolveAlias: name => { 95 | if (name === 'thisIsAlias') { 96 | return 'alias1'; 97 | } 98 | if (name === 'alias1') { 99 | return 'alias2'; 100 | } 101 | if (name === 'alias2') { 102 | return 'thisIsAlias ciao'; 103 | } 104 | } 105 | }); 106 | // utils.logResults(result) 107 | 108 | utils.checkResults(t, result, { 109 | type: 'Script', 110 | commands: [{ 111 | type: 'Command', 112 | name: {type: 'Word', text: 'thisIsAlias'}, 113 | suffix: [ 114 | {type: 'Word', text: 'ciao'}, 115 | {type: 'Word', text: 'world'} 116 | ] 117 | }] 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /documents/mode.md: -------------------------------------------------------------------------------- 1 | # Mode plugins internal API 2 | 3 | The parser is able to parse different shell flavours by using `mode` plugins. By now, the `posix` mode is almost complete, and we are implementing the `bash` mode. 4 | 5 | This document describe how a mode shall be defined. This is for now an internal API, it's possible that in future release this API will be exposed as a public one to allow the use of external defined modes. 6 | 7 | In this document we use flow syntax to define the type of expected object, but this only for documentation purpose, types are not checked at runtime in any way. 8 | 9 | ## `init` function 10 | 11 | A mode plugin consist of a module in `src/modes` folder. The module must exports 12 | a `ModePlugin` object with an optional `inherits` property and a required `init` factory function. 13 | 14 | ```flow 15 | type ModePlugin = { 16 | inherits?: string, 17 | init: (parentMode: Mode) => Mode 18 | }; 19 | ``` 20 | 21 | ## `Mode` object 22 | 23 | A mode could optionally inherit an existing one. It specify the mode to inherit in its 24 | `inherits` property. If it does, the `init` function will receive as argument the inherited mode. In this way the child mode could use the parent mode features. 25 | 26 | The `init` function must return a `Mode` object. 27 | 28 | ``` 29 | export type Mode = { 30 | tokenizer: (options: Object, utils: Object) => (code: String) => Iterable, 31 | lexerPhases: Array, 32 | phaseCatalog: {[id:string]: LexerPhase} 33 | grammar: Object, 34 | grammarSource: Object, 35 | astBuilder: Object, 36 | }; 37 | 38 | A `Mode` object consists has the following properties: 39 | 40 | * tokenizer - a function that receive parser options and utils object as argument, and return another function that, given shell source code, return an iterable oif parsed tokens. 41 | 42 | * lexerPhases - an array of transform functions that are applied, in order, to the iterable of tokens returned by the `tokenizer` function. Each phase must have the `LexerPhase` type described below. 43 | 44 | * phaseCatalog - a named map of all phases contained in the array. This could be used by children modes to access each phase by name and reuse them. 45 | 46 | * grammar - the grammar compiled function. This is usually imported from a Jison grammar, compiled using the `mode-grammar-builder` cli. 47 | 48 | * grammarSource - an object that describe the grammar, must be in the JSON format supported by Jison. This object could be reused by children modes and is read by the `mode-grammar-builder` cli to build the compiled grammar. 49 | 50 | * astBuilder - an object containing methods to build the final AST. This object is mixed-in in the Jison grammar, and any of its methods could be called directly from grammar EBNF source. 51 | 52 | 53 | ## `LexerPhase` functions 54 | 55 | `LexerPhase` functions are applyed, in order, to the iterable returned from `tokenizer` function. Each phase enhance or alter the tokens to produce a final token iterable, directly consumable by the grammar parser. 56 | 57 | Each phase is a function that accept as arguments the parser option object, 58 | an array of all phases that precede it in the pipeline, and the utils object. The function shall return another function that receive as argument the iterable produced by the previous phase, and return the iterable to give to the subsequent one. 59 | 60 | ```flow 61 | export type Token = Object; 62 | 63 | type LexerPhase = (options: Object, previousPhases: Array,utils: Object) => 64 | (tokens: Iterable) => Iterable; 65 | ``` 66 | 67 | 68 | ## `utils` object. 69 | 70 | This is an object containing various help methods that simplify the implementation of token phases. 71 | -------------------------------------------------------------------------------- /test/test-if.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('parse if', t => { 9 | const result = bashParser('if true; then echo 1; fi'); 10 | // console.log(inspect(result, {depth:null})) 11 | utils.checkResults(t, 12 | result, { 13 | type: 'Script', 14 | commands: [{ 15 | type: 'If', 16 | clause: { 17 | type: 'CompoundList', 18 | commands: [{ 19 | type: 'Command', 20 | name: {type: 'Word', text: 'true'} 21 | }] 22 | }, 23 | then: { 24 | type: 'CompoundList', 25 | commands: [{ 26 | type: 'Command', 27 | name: {type: 'Word', text: 'echo'}, 28 | suffix: [{type: 'Word', text: '1'}] 29 | }] 30 | } 31 | }] 32 | } 33 | ); 34 | }); 35 | 36 | test('parse if else', t => { 37 | const result = bashParser('if true; then echo 1; else echo 2; fi'); 38 | // utils.logResults(result); 39 | utils.checkResults(t, 40 | result, { 41 | type: 'Script', 42 | commands: [{ 43 | type: 'If', 44 | clause: { 45 | type: 'CompoundList', 46 | commands: [{ 47 | type: 'Command', 48 | name: {type: 'Word', text: 'true'} 49 | }] 50 | }, 51 | then: { 52 | type: 'CompoundList', 53 | commands: [{ 54 | type: 'Command', 55 | name: {type: 'Word', text: 'echo'}, 56 | suffix: [{type: 'Word', text: '1'}] 57 | }] 58 | }, 59 | else: { 60 | type: 'CompoundList', 61 | commands: [{ 62 | type: 'Command', 63 | name: {type: 'Word', text: 'echo'}, 64 | suffix: [{type: 'Word', text: '2'}] 65 | }] 66 | } 67 | }] 68 | } 69 | ); 70 | }); 71 | 72 | test('parse if else multiline', t => { 73 | const result = bashParser('if true; then \n echo 1;\n else\n echo 2;\n fi'); 74 | // console.log(inspect(result, {depth:null})) 75 | utils.checkResults(t, 76 | result, { 77 | type: 'Script', 78 | commands: [{ 79 | type: 'If', 80 | clause: { 81 | type: 'CompoundList', 82 | commands: [{ 83 | type: 'Command', 84 | name: {type: 'Word', text: 'true'} 85 | }] 86 | }, 87 | then: { 88 | type: 'CompoundList', 89 | commands: [{ 90 | type: 'Command', 91 | name: {type: 'Word', text: 'echo'}, 92 | suffix: [{type: 'Word', text: '1'}] 93 | }] 94 | }, 95 | else: { 96 | type: 'CompoundList', 97 | commands: [{ 98 | type: 'Command', 99 | name: {type: 'Word', text: 'echo'}, 100 | suffix: [{type: 'Word', text: '2'}] 101 | }] 102 | } 103 | }] 104 | } 105 | ); 106 | }); 107 | 108 | test('parse if elif else', t => { 109 | const result = bashParser('if true; then echo 1; elif false; then echo 3; else echo 2; fi'); 110 | // utils.logResults(result); 111 | const expected = { 112 | type: 'Script', 113 | commands: [{ 114 | type: 'If', 115 | clause: { 116 | type: 'CompoundList', 117 | commands: [{ 118 | type: 'Command', 119 | name: {type: 'Word', text: 'true'} 120 | }] 121 | }, 122 | then: { 123 | type: 'CompoundList', 124 | commands: [{ 125 | type: 'Command', 126 | name: {type: 'Word', text: 'echo'}, 127 | suffix: [{type: 'Word', text: '1'}] 128 | }] 129 | }, 130 | else: { 131 | type: 'If', 132 | clause: { 133 | type: 'CompoundList', 134 | commands: [{ 135 | type: 'Command', 136 | name: {type: 'Word', text: 'false'} 137 | }] 138 | }, 139 | then: { 140 | type: 'CompoundList', 141 | commands: [{ 142 | type: 'Command', 143 | name: {type: 'Word', text: 'echo'}, 144 | suffix: [{type: 'Word', text: '3'}] 145 | }] 146 | }, 147 | else: { 148 | type: 'CompoundList', 149 | commands: [{ 150 | type: 'Command', 151 | name: {type: 'Word', text: 'echo'}, 152 | suffix: [{type: 'Word', text: '2'}] 153 | }] 154 | } 155 | } 156 | }] 157 | }; 158 | 159 | // utils.logDiff(result, expected) 160 | utils.checkResults(t, result, expected); 161 | }); 162 | -------------------------------------------------------------------------------- /documents/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ## `parse(code, [options])` 4 | 5 | `parse` is the function exported by the module. You give it shell source code and an optional options arguments, and it return an object containing the AST representing it. 6 | 7 | ## Arguments 8 | 9 | ### code: String 10 | 11 | >The source code to parse 12 | 13 | ### Options: Object 14 | 15 | >An optional option objects, containing one or more of the following properties: 16 | 17 | - `insertLOC: Boolean = false` - if `true`, includes lines and columns information in the source file. 18 | 19 | - `resolveAlias: (name: String) => String` - a callback to resolve shell alias. If specified, the parser call it whenever it need to resolve an alias. It should return the resolved code if the alias exists, otherwise `null`. If the option is not specified, the parser won't try to resolve any alias. 20 | 21 | - `resolveEnv: (name: String) => String` - a callback to resolve environment variables. If specified, the parser call it whenever it need to resolve an environment variable. It should return the value if the variable is defined, otherwise `null`. If the option is not specified, the parser won't try to resolve any environment variable. 22 | 23 | - `resolvePath: (text: String) => String` - a callback to resolve path globbing. If specified, the parser call it whenever it need to resolve a path globbing. It should return the value if the expanded variable. If the option is not specified, the parser won't try to resolve any path globbing. 24 | 25 | - `resolveHomeUser: (username: String) => String` - a callback to resolve users home directories. If specified, the parser call it whenever it need to resolve a tilde expansion. If the option is not specified, the parser won't try to resolve any tilde expansion. When the callback is called with a null value for `username`, the callbackshould return the current user home directory. 26 | 27 | 28 | - `resolveParameter: (parameterAST: Object) => String` - a callback to resolve parameter expansion. If specified, the parser call it whenever it need to resolve a parameter expansion. It should return the result of the expansion. If the option is not specified, the parser won't try to resolve any parameter expansion. 29 | 30 | - `execCommand: (cmdAST: Object) => String` - a callback to execute a [simple_command](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#simple_command). If specified, the parser call it whenever it need to resolve a command substitution. It receive as argument the AST of a [simple_command node](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#simple_command), and shall return the output of the command. If the option is not specified, the parser won't try to resolve any command substitution. 31 | 32 | - `execShellScript: (scriptAST: Object) => String` - a callback to execute a [complete_command](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#complete_command) in a new shell process. If specified, the parser call it whenever it need to resolve a subshell statement. It receive as argument the AST of a [complete_command node](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#complete_command), and shall return the output of the command. If the option is not specified, the parser won't try to resolve any subshell statement. 33 | 34 | - `runArithmeticExpression: (arithmeticAST: Object) => String` - a callback to execute an [arithmetic_expansion](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#arithmetic_expansion). If specified, the parser call it whenever it need to resolve an arithmetic substitution. It receive as argument the AST of a [arithmetic_expansion node](https://github.com/vorpaljs/bash-parser/blob/master/docs/ast.md#arithmetic_expansion), and shall return the result of the calculation. If the option is not specified, the parser won't try to resolve any arithmetic_expansion substitution. Please note that the aritmethic expression AST is built using [babylon](https://github.com/babel/babylon), you cand find there it's AST specification. 35 | 36 | ## Return value 37 | 38 | The function return an object containing the AST on successfull parsing. Please refer to [AST documentation](https://github.com/vorpaljs/bash-parser/blob/master/documents/ast.md) for details on the AST format. 39 | 40 | -------------------------------------------------------------------------------- /src/utils/tokens.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const hasOwnProperty = require('has-own-property'); 3 | const filter = require('filter-obj'); 4 | const operators = require('../modes/posix/enums/operators'); 5 | 6 | class Token { 7 | constructor(fields) { 8 | const definedFields = filter(fields, (key, value) => value !== undefined); 9 | Object.assign(this, definedFields); 10 | 11 | if (this._ === undefined) { 12 | this._ = {}; 13 | } 14 | } 15 | 16 | is(type) { 17 | return this.type === type; 18 | } 19 | 20 | appendTo(chunk) { 21 | return new Token(Object.assign({}, this, {value: this.value + chunk})); 22 | } 23 | changeTokenType(type, value) { 24 | return new Token({type, value, loc: this.loc, _: this._, expansion: this.expansion}); 25 | } 26 | setValue(value) { 27 | return new Token(Object.assign({}, this, {value})); 28 | } 29 | alterValue(value) { 30 | return new Token(Object.assign({}, this, {value, originalText: this.originalText || this.value})); 31 | } 32 | addExpansions() { 33 | return new Token(Object.assign({}, this, {expansion: []})); 34 | } 35 | setExpansions(expansion) { 36 | return new Token(Object.assign({}, this, {expansion})); 37 | } 38 | } 39 | 40 | exports.token = args => new Token(args); 41 | 42 | function mkToken(type, value, loc, expansion) { 43 | const tk = new Token({type, value, loc}); 44 | if (expansion && expansion.length) { 45 | tk.expansion = expansion; 46 | } 47 | 48 | return tk; 49 | } 50 | 51 | exports.mkToken = mkToken; 52 | 53 | exports.mkFieldSplitToken = function mkFieldSplitToken(joinedTk, value, fieldIdx) { 54 | const tk = new Token({ 55 | type: joinedTk.type, 56 | value, 57 | joined: joinedTk.value, 58 | fieldIdx, 59 | loc: joinedTk.loc, 60 | expansion: joinedTk.expansion, 61 | originalText: joinedTk.originalText 62 | }); 63 | 64 | return tk; 65 | }; 66 | 67 | exports.appendTo = (tk, chunk) => tk.appendTo(chunk); 68 | exports.changeTokenType = (tk, type, value) => tk.changeTokenType(type, value); 69 | exports.setValue = (tk, value) => tk.setValue(value); 70 | exports.alterValue = (tk, value) => tk.alterValue(value); 71 | exports.addExpansions = tk => tk.addExpansions(); 72 | exports.setExpansions = (tk, expansion) => tk.setExpansions(expansion); 73 | 74 | exports.tokenOrEmpty = function tokenOrEmpty(state) { 75 | if (state.current !== '' && state.current !== '\n') { 76 | const expansion = (state.expansion || []).map(xp => { 77 | // console.log('aaa', {token: state.loc, xp: xp.loc}); 78 | return Object.assign({}, xp, {loc: { 79 | start: xp.loc.start.char - state.loc.start.char, 80 | end: xp.loc.end.char - state.loc.start.char 81 | }}); 82 | }); 83 | const token = mkToken('TOKEN', state.current, { 84 | start: Object.assign({}, state.loc.start), 85 | end: Object.assign({}, state.loc.previous) 86 | }, expansion); 87 | 88 | /* if (state.expansion && state.expansion.length) { 89 | token.expansion = state.expansion; 90 | }*/ 91 | 92 | return [token]; 93 | } 94 | return []; 95 | }; 96 | 97 | exports.operatorTokens = function operatorTokens(state) { 98 | const token = mkToken( 99 | operators[state.current], 100 | state.current, { 101 | start: Object.assign({}, state.loc.start), 102 | end: Object.assign({}, state.loc.previous) 103 | } 104 | ); 105 | 106 | return [token]; 107 | }; 108 | 109 | exports.newLine = function newLine() { 110 | return mkToken('NEWLINE', '\n'); 111 | }; 112 | 113 | exports.continueToken = function continueToken(expectedChar) { 114 | return mkToken('CONTINUE', expectedChar); 115 | }; 116 | 117 | exports.eof = function eof() { 118 | return mkToken('EOF', ''); 119 | }; 120 | 121 | exports.isPartOfOperator = function isPartOfOperator(text) { 122 | return Object.keys(operators).some(op => op.slice(0, text.length) === text); 123 | }; 124 | 125 | exports.isOperator = function isOperator(text) { 126 | return hasOwnProperty(operators, text); 127 | }; 128 | 129 | exports.applyTokenizerVisitor = visitor => (tk, idx, iterable) => { 130 | if (hasOwnProperty(visitor, tk.type)) { 131 | const visit = visitor[tk.type]; 132 | 133 | return visit(tk, iterable); 134 | } 135 | 136 | if (hasOwnProperty(visitor, 'defaultMethod')) { 137 | const visit = visitor.defaultMethod; 138 | return visit(tk, iterable); 139 | } 140 | 141 | return tk; 142 | }; 143 | -------------------------------------------------------------------------------- /test/test-special-parameter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('positional list paramter', t => { 9 | const result = bashParser('echoword=$@'); 10 | // console.log(JSON.stringify(result, null, 5)) 11 | utils.checkResults(t, result, { 12 | type: 'Script', 13 | commands: [{ 14 | type: 'Command', 15 | prefix: [{ 16 | type: 'AssignmentWord', 17 | text: 'echoword=$@', 18 | expansion: [{ 19 | type: 'ParameterExpansion', 20 | parameter: '@', 21 | kind: 'positional-list', 22 | loc: { 23 | start: 9, 24 | end: 10 25 | } 26 | }] 27 | }] 28 | }] 29 | }); 30 | }); 31 | 32 | test('positional string paramter', t => { 33 | const result = bashParser('echoword=$*'); 34 | utils.checkResults(t, result, { 35 | type: 'Script', 36 | commands: [{ 37 | type: 'Command', 38 | prefix: [{ 39 | type: 'AssignmentWord', 40 | text: 'echoword=$*', 41 | expansion: [{ 42 | type: 'ParameterExpansion', 43 | parameter: '*', 44 | kind: 'positional-string', 45 | loc: { 46 | start: 9, 47 | end: 10 48 | } 49 | }] 50 | }] 51 | }] 52 | }); 53 | }); 54 | 55 | test('positional count paramter', t => { 56 | const result = bashParser('echoword=$#'); 57 | utils.checkResults(t, result, { 58 | type: 'Script', 59 | commands: [{ 60 | type: 'Command', 61 | prefix: [{ 62 | type: 'AssignmentWord', 63 | text: 'echoword=$#', 64 | expansion: [{ 65 | type: 'ParameterExpansion', 66 | parameter: '#', 67 | kind: 'positional-count', 68 | loc: { 69 | start: 9, 70 | end: 10 71 | } 72 | }] 73 | }] 74 | }] 75 | }); 76 | }); 77 | 78 | test('last exit status', t => { 79 | const result = bashParser('echoword=$?'); 80 | utils.checkResults(t, result, { 81 | type: 'Script', 82 | commands: [{ 83 | type: 'Command', 84 | prefix: [{ 85 | type: 'AssignmentWord', 86 | text: 'echoword=$?', 87 | expansion: [{ 88 | type: 'ParameterExpansion', 89 | parameter: '?', 90 | kind: 'last-exit-status', 91 | loc: { 92 | start: 9, 93 | end: 10 94 | } 95 | }] 96 | }] 97 | }] 98 | }); 99 | }); 100 | 101 | test('current option flags', t => { 102 | const result = bashParser('echoword=$-'); 103 | utils.checkResults(t, result, { 104 | type: 'Script', 105 | commands: [{ 106 | type: 'Command', 107 | prefix: [{ 108 | type: 'AssignmentWord', 109 | text: 'echoword=$-', 110 | expansion: [{ 111 | type: 'ParameterExpansion', 112 | parameter: '-', 113 | kind: 'current-option-flags', 114 | loc: { 115 | start: 9, 116 | end: 10 117 | } 118 | }] 119 | }] 120 | }] 121 | }); 122 | }); 123 | 124 | test('shell process id', t => { 125 | const result = bashParser('echoword=$$'); 126 | utils.checkResults(t, result, { 127 | type: 'Script', 128 | commands: [{ 129 | type: 'Command', 130 | prefix: [{ 131 | type: 'AssignmentWord', 132 | text: 'echoword=$$', 133 | expansion: [{ 134 | type: 'ParameterExpansion', 135 | parameter: '$', 136 | kind: 'shell-process-id', 137 | loc: { 138 | start: 9, 139 | end: 10 140 | } 141 | }] 142 | }] 143 | }] 144 | }); 145 | }); 146 | 147 | test('last background pid', t => { 148 | const result = bashParser('echoword=$!'); 149 | utils.checkResults(t, result, { 150 | type: 'Script', 151 | commands: [{ 152 | type: 'Command', 153 | prefix: [{ 154 | type: 'AssignmentWord', 155 | text: 'echoword=$!', 156 | expansion: [{ 157 | type: 'ParameterExpansion', 158 | parameter: '!', 159 | kind: 'last-background-pid', 160 | loc: { 161 | start: 9, 162 | end: 10 163 | } 164 | }] 165 | }] 166 | }] 167 | }); 168 | }); 169 | 170 | test('shell script name', t => { 171 | const result = bashParser('echoword=$0'); 172 | // logResults(result); 173 | utils.checkResults(t, result, { 174 | type: 'Script', 175 | commands: [{ 176 | type: 'Command', 177 | prefix: [{ 178 | type: 'AssignmentWord', 179 | text: 'echoword=$0', 180 | expansion: [{ 181 | type: 'ParameterExpansion', 182 | parameter: '0', 183 | kind: 'shell-script-name', 184 | loc: { 185 | start: 9, 186 | end: 10 187 | } 188 | }] 189 | }] 190 | }] 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /test/test-loc-while.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | /* eslint-disable camelcase */ 7 | 8 | test('loc in while statement', t => { 9 | const result = bashParser('while true && 1; do sleep 1;echo ciao; done', {insertLOC: true}); 10 | // utils.logResults(result.commands[0]); 11 | const expected = { 12 | type: 'While', 13 | clause: { 14 | type: 'CompoundList', 15 | commands: [ 16 | { 17 | type: 'LogicalExpression', 18 | op: 'and', 19 | left: { 20 | type: 'Command', 21 | name: { 22 | text: 'true', 23 | type: 'Word', 24 | loc: { 25 | start: { 26 | col: 7, 27 | row: 1, 28 | char: 6 29 | }, 30 | end: { 31 | col: 10, 32 | row: 1, 33 | char: 9 34 | } 35 | } 36 | }, 37 | loc: { 38 | start: { 39 | col: 7, 40 | row: 1, 41 | char: 6 42 | }, 43 | end: { 44 | col: 10, 45 | row: 1, 46 | char: 9 47 | } 48 | } 49 | }, 50 | right: { 51 | type: 'Command', 52 | name: { 53 | text: '1', 54 | type: 'Word', 55 | loc: { 56 | start: { 57 | col: 15, 58 | row: 1, 59 | char: 14 60 | }, 61 | end: { 62 | col: 15, 63 | row: 1, 64 | char: 14 65 | } 66 | } 67 | }, 68 | loc: { 69 | start: { 70 | col: 15, 71 | row: 1, 72 | char: 14 73 | }, 74 | end: { 75 | col: 15, 76 | row: 1, 77 | char: 14 78 | } 79 | } 80 | }, 81 | loc: { 82 | start: { 83 | col: 7, 84 | row: 1, 85 | char: 6 86 | }, 87 | end: { 88 | col: 15, 89 | row: 1, 90 | char: 14 91 | } 92 | } 93 | } 94 | ], 95 | loc: { 96 | start: { 97 | col: 7, 98 | row: 1, 99 | char: 6 100 | }, 101 | end: { 102 | col: 15, 103 | row: 1, 104 | char: 14 105 | } 106 | } 107 | }, 108 | do: { 109 | type: 'CompoundList', 110 | commands: [ 111 | { 112 | type: 'Command', 113 | name: { 114 | text: 'sleep', 115 | type: 'Word', 116 | loc: { 117 | start: { 118 | col: 21, 119 | row: 1, 120 | char: 20 121 | }, 122 | end: { 123 | col: 25, 124 | row: 1, 125 | char: 24 126 | } 127 | } 128 | }, 129 | loc: { 130 | start: { 131 | col: 21, 132 | row: 1, 133 | char: 20 134 | }, 135 | end: { 136 | col: 27, 137 | row: 1, 138 | char: 26 139 | } 140 | }, 141 | suffix: [ 142 | { 143 | text: '1', 144 | type: 'Word', 145 | loc: { 146 | start: { 147 | col: 27, 148 | row: 1, 149 | char: 26 150 | }, 151 | end: { 152 | col: 27, 153 | row: 1, 154 | char: 26 155 | } 156 | } 157 | } 158 | ] 159 | }, 160 | { 161 | type: 'Command', 162 | name: { 163 | text: 'echo', 164 | type: 'Word', 165 | loc: { 166 | start: { 167 | col: 29, 168 | row: 1, 169 | char: 28 170 | }, 171 | end: { 172 | col: 32, 173 | row: 1, 174 | char: 31 175 | } 176 | } 177 | }, 178 | loc: { 179 | start: { 180 | col: 29, 181 | row: 1, 182 | char: 28 183 | }, 184 | end: { 185 | col: 37, 186 | row: 1, 187 | char: 36 188 | } 189 | }, 190 | suffix: [ 191 | { 192 | text: 'ciao', 193 | type: 'Word', 194 | loc: { 195 | start: { 196 | col: 34, 197 | row: 1, 198 | char: 33 199 | }, 200 | end: { 201 | col: 37, 202 | row: 1, 203 | char: 36 204 | } 205 | } 206 | } 207 | ] 208 | } 209 | ], 210 | loc: { 211 | start: { 212 | col: 18, 213 | row: 1, 214 | char: 17 215 | }, 216 | end: { 217 | col: 43, 218 | row: 1, 219 | char: 42 220 | } 221 | } 222 | }, 223 | loc: { 224 | start: { 225 | col: 1, 226 | row: 1, 227 | char: 0 228 | }, 229 | end: { 230 | col: 43, 231 | row: 1, 232 | char: 42 233 | } 234 | } 235 | }; 236 | utils.checkResults(t, result.commands[0], expected); 237 | }); 238 | -------------------------------------------------------------------------------- /test/test-loc-until.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('loc in until statement', t => { 9 | const result = bashParser('until true || 1; do sleep 1;echo ciao; done', {insertLOC: true}); 10 | // utils.logResults(result.commands[0]); 11 | const expected = { 12 | type: 'Until', 13 | clause: { 14 | type: 'CompoundList', 15 | commands: [ 16 | { 17 | type: 'LogicalExpression', 18 | op: 'or', 19 | left: { 20 | type: 'Command', 21 | name: { 22 | text: 'true', 23 | type: 'Word', 24 | loc: { 25 | start: { 26 | col: 7, 27 | row: 1, 28 | char: 6 29 | }, 30 | end: { 31 | col: 10, 32 | row: 1, 33 | char: 9 34 | } 35 | } 36 | }, 37 | loc: { 38 | start: { 39 | col: 7, 40 | row: 1, 41 | char: 6 42 | }, 43 | end: { 44 | col: 10, 45 | row: 1, 46 | char: 9 47 | } 48 | } 49 | }, 50 | right: { 51 | type: 'Command', 52 | name: { 53 | text: '1', 54 | type: 'Word', 55 | loc: { 56 | start: { 57 | col: 15, 58 | row: 1, 59 | char: 14 60 | }, 61 | end: { 62 | col: 15, 63 | row: 1, 64 | char: 14 65 | } 66 | } 67 | }, 68 | loc: { 69 | start: { 70 | col: 15, 71 | row: 1, 72 | char: 14 73 | }, 74 | end: { 75 | col: 15, 76 | row: 1, 77 | char: 14 78 | } 79 | } 80 | }, 81 | loc: { 82 | start: { 83 | col: 7, 84 | row: 1, 85 | char: 6 86 | }, 87 | end: { 88 | col: 15, 89 | row: 1, 90 | char: 14 91 | } 92 | } 93 | } 94 | ], 95 | loc: { 96 | start: { 97 | col: 7, 98 | row: 1, 99 | char: 6 100 | }, 101 | end: { 102 | col: 15, 103 | row: 1, 104 | char: 14 105 | } 106 | } 107 | }, 108 | do: { 109 | type: 'CompoundList', 110 | commands: [ 111 | { 112 | type: 'Command', 113 | name: { 114 | text: 'sleep', 115 | type: 'Word', 116 | loc: { 117 | start: { 118 | col: 21, 119 | row: 1, 120 | char: 20 121 | }, 122 | end: { 123 | col: 25, 124 | row: 1, 125 | char: 24 126 | } 127 | } 128 | }, 129 | loc: { 130 | start: { 131 | col: 21, 132 | row: 1, 133 | char: 20 134 | }, 135 | end: { 136 | col: 27, 137 | row: 1, 138 | char: 26 139 | } 140 | }, 141 | suffix: [ 142 | { 143 | text: '1', 144 | type: 'Word', 145 | loc: { 146 | start: { 147 | col: 27, 148 | row: 1, 149 | char: 26 150 | }, 151 | end: { 152 | col: 27, 153 | row: 1, 154 | char: 26 155 | } 156 | } 157 | } 158 | ] 159 | }, 160 | { 161 | type: 'Command', 162 | name: { 163 | text: 'echo', 164 | type: 'Word', 165 | loc: { 166 | start: { 167 | col: 29, 168 | row: 1, 169 | char: 28 170 | }, 171 | end: { 172 | col: 32, 173 | row: 1, 174 | char: 31 175 | } 176 | } 177 | }, 178 | loc: { 179 | start: { 180 | col: 29, 181 | row: 1, 182 | char: 28 183 | }, 184 | end: { 185 | col: 37, 186 | row: 1, 187 | char: 36 188 | } 189 | }, 190 | suffix: [ 191 | { 192 | text: 'ciao', 193 | type: 'Word', 194 | loc: { 195 | start: { 196 | col: 34, 197 | row: 1, 198 | char: 33 199 | }, 200 | end: { 201 | col: 37, 202 | row: 1, 203 | char: 36 204 | } 205 | } 206 | } 207 | ] 208 | } 209 | ], 210 | loc: { 211 | start: { 212 | col: 18, 213 | row: 1, 214 | char: 17 215 | }, 216 | end: { 217 | col: 43, 218 | row: 1, 219 | char: 42 220 | } 221 | } 222 | }, 223 | loc: { 224 | start: { 225 | col: 1, 226 | row: 1, 227 | char: 0 228 | }, 229 | end: { 230 | col: 43, 231 | row: 1, 232 | char: 42 233 | } 234 | } 235 | }; 236 | 237 | utils.checkResults(t, result.commands[0], expected); 238 | }); 239 | -------------------------------------------------------------------------------- /test/fixtures/or.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceCode: "until true || 1; do sleep 1;echo ciao; done", 3 | result: { 4 | type: "Script", 5 | commands: [ 6 | { 7 | type: "Until", 8 | clause: { 9 | type: "CompoundList", 10 | commands: [ 11 | { 12 | type: "LogicalExpression", 13 | op: "or", 14 | left: { 15 | type: "SimpleCommand", 16 | name: { 17 | text: "true", 18 | type: "Word", 19 | loc: { 20 | start: { 21 | col: 7, 22 | row: 1, 23 | char: 6 24 | }, 25 | end: { 26 | col: 10, 27 | row: 1, 28 | char: 9 29 | } 30 | } 31 | }, 32 | loc: { 33 | start: { 34 | col: 7, 35 | row: 1, 36 | char: 6 37 | }, 38 | end: { 39 | col: 10, 40 | row: 1, 41 | char: 9 42 | } 43 | } 44 | }, 45 | right: { 46 | type: "SimpleCommand", 47 | name: { 48 | text: "1", 49 | type: "Word", 50 | loc: { 51 | start: { 52 | col: 15, 53 | row: 1, 54 | char: 14 55 | }, 56 | end: { 57 | col: 15, 58 | row: 1, 59 | char: 14 60 | } 61 | } 62 | }, 63 | loc: { 64 | start: { 65 | col: 15, 66 | row: 1, 67 | char: 14 68 | }, 69 | end: { 70 | col: 15, 71 | row: 1, 72 | char: 14 73 | } 74 | } 75 | }, 76 | loc: { 77 | start: { 78 | col: 7, 79 | row: 1, 80 | char: 6 81 | }, 82 | end: { 83 | col: 15, 84 | row: 1, 85 | char: 14 86 | } 87 | } 88 | } 89 | ], 90 | loc: { 91 | start: { 92 | col: 7, 93 | row: 1, 94 | char: 6 95 | }, 96 | end: { 97 | col: 15, 98 | row: 1, 99 | char: 14 100 | } 101 | } 102 | }, 103 | do: { 104 | type: "CompoundList", 105 | commands: [ 106 | { 107 | type: "SimpleCommand", 108 | name: { 109 | text: "sleep", 110 | type: "Word", 111 | loc: { 112 | start: { 113 | col: 21, 114 | row: 1, 115 | char: 20 116 | }, 117 | end: { 118 | col: 25, 119 | row: 1, 120 | char: 24 121 | } 122 | } 123 | }, 124 | loc: { 125 | start: { 126 | col: 21, 127 | row: 1, 128 | char: 20 129 | }, 130 | end: { 131 | col: 37, 132 | row: 1, 133 | char: 36 134 | } 135 | }, 136 | suffix: [ 137 | { 138 | text: "1", 139 | type: "Word", 140 | loc: { 141 | start: { 142 | col: 27, 143 | row: 1, 144 | char: 26 145 | }, 146 | end: { 147 | col: 27, 148 | row: 1, 149 | char: 26 150 | } 151 | } 152 | }, 153 | { 154 | text: "echo", 155 | type: "Word", 156 | loc: { 157 | start: { 158 | col: 28, 159 | row: 1, 160 | char: 27 161 | }, 162 | end: { 163 | col: 32, 164 | row: 1, 165 | char: 31 166 | } 167 | } 168 | }, 169 | { 170 | text: "ciao", 171 | type: "Word", 172 | loc: { 173 | start: { 174 | col: 34, 175 | row: 1, 176 | char: 33 177 | }, 178 | end: { 179 | col: 37, 180 | row: 1, 181 | char: 36 182 | } 183 | } 184 | } 185 | ] 186 | } 187 | ], 188 | loc: { 189 | start: { 190 | col: 18, 191 | row: 1, 192 | char: 17 193 | }, 194 | end: { 195 | col: 43, 196 | row: 1, 197 | char: 42 198 | } 199 | } 200 | }, 201 | loc: { 202 | start: { 203 | col: 1, 204 | row: 1, 205 | char: 0 206 | }, 207 | end: { 208 | col: 43, 209 | row: 1, 210 | char: 42 211 | } 212 | } 213 | } 214 | ], 215 | loc: { 216 | start: { 217 | col: 1, 218 | row: 1, 219 | char: 0 220 | }, 221 | end: { 222 | col: 43, 223 | row: 1, 224 | char: 42 225 | } 226 | } 227 | } 228 | } -------------------------------------------------------------------------------- /test/test-loc-simple-command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // const json = require('json5'); 4 | // const {diff} = require('rus-diff'); 5 | const test = require('ava'); 6 | const bashParser = require('../src'); 7 | const mkloc = require('./_utils').mkloc2; 8 | const utils = require('./_utils'); 9 | 10 | /* eslint-disable camelcase */ 11 | test('simple command with prefixes and name', t => { 12 | const result = bashParser('a=1 b=2 echo', {insertLOC: true}); 13 | utils.checkResults(t, result.commands[0], { 14 | type: 'Command', 15 | name: { 16 | type: 'Word', 17 | text: 'echo', 18 | loc: mkloc(1, 9, 1, 12, 8, 11) 19 | }, 20 | loc: mkloc(1, 1, 1, 12, 0, 11), 21 | prefix: [{ 22 | type: 'AssignmentWord', 23 | text: 'a=1', 24 | loc: mkloc(1, 1, 1, 3, 0, 2) 25 | }, { 26 | type: 'AssignmentWord', 27 | text: 'b=2', 28 | loc: mkloc(1, 5, 1, 7, 4, 6) 29 | }] 30 | }); 31 | }); 32 | 33 | test('simple command with only name', t => { 34 | const result = bashParser('echo', {insertLOC: true}); 35 | utils.checkResults(t, result.commands[0], { 36 | type: 'Command', 37 | name: { 38 | type: 'Word', 39 | text: 'echo', 40 | loc: mkloc(1, 1, 1, 4, 0, 3) 41 | }, 42 | loc: mkloc(1, 1, 1, 4, 0, 3) 43 | }); 44 | }); 45 | 46 | test('simple command with pipeline', t => { 47 | const result = bashParser('echo | grep', {insertLOC: true}); 48 | // console.log(JSON.stringify(result, null, 4)); 49 | utils.checkResults(t, result.commands[0], { 50 | type: 'Pipeline', 51 | commands: [{ 52 | type: 'Command', 53 | name: { 54 | type: 'Word', 55 | text: 'echo', 56 | loc: mkloc(1, 1, 1, 4, 0, 3) 57 | }, 58 | loc: mkloc(1, 1, 1, 4, 0, 3) 59 | }, { 60 | type: 'Command', 61 | name: { 62 | type: 'Word', 63 | text: 'grep', 64 | loc: mkloc(1, 8, 1, 11, 7, 10) 65 | }, 66 | loc: mkloc(1, 8, 1, 11, 7, 10) 67 | }], 68 | loc: mkloc(1, 1, 1, 11, 0, 10) 69 | }); 70 | }); 71 | 72 | test('simple command with suffixes', t => { 73 | const result = bashParser('echo 42 43', {insertLOC: true}); 74 | utils.checkResults(t, result.commands[0], { 75 | type: 'Command', 76 | name: { 77 | type: 'Word', 78 | text: 'echo', 79 | loc: mkloc(1, 1, 1, 4, 0, 3) 80 | }, 81 | loc: mkloc(1, 1, 1, 10, 0, 9), 82 | suffix: [{ 83 | type: 'Word', 84 | text: '42', 85 | loc: mkloc(1, 6, 1, 7, 5, 6) 86 | }, { 87 | type: 'Word', 88 | text: '43', 89 | loc: mkloc(1, 9, 1, 10, 8, 9) 90 | }] 91 | }); 92 | }); 93 | 94 | test('simple command with IO redirection', t => { 95 | const result = bashParser('echo > 43', {insertLOC: true}); 96 | // utils.logResults(result) 97 | 98 | utils.checkResults(t, result.commands[0], { 99 | type: 'Command', 100 | name: { 101 | type: 'Word', 102 | text: 'echo', 103 | loc: mkloc(1, 1, 1, 4, 0, 3) 104 | }, 105 | loc: mkloc(1, 1, 1, 9, 0, 8), 106 | suffix: [{ 107 | type: 'Redirect', 108 | op: { 109 | type: 'great', 110 | text: '>', 111 | loc: mkloc(1, 6, 1, 6, 5, 5) 112 | }, 113 | file: { 114 | type: 'Word', 115 | text: '43', 116 | loc: mkloc(1, 8, 1, 9, 7, 8) 117 | }, 118 | loc: mkloc(1, 6, 1, 9, 5, 8) 119 | }] 120 | }); 121 | }); 122 | 123 | test('simple command with numbered IO redirection', t => { 124 | const result = bashParser('echo 2> 43', {insertLOC: true}); 125 | // utils.logResults(result); 126 | const expected = { 127 | type: 'Command', 128 | name: { 129 | type: 'Word', 130 | text: 'echo', 131 | loc: mkloc(1, 1, 1, 4, 0, 3) 132 | }, 133 | loc: mkloc(1, 1, 1, 10, 0, 9), 134 | suffix: [{ 135 | type: 'Redirect', 136 | op: { 137 | type: 'great', 138 | text: '>', 139 | loc: mkloc(1, 7, 1, 7, 6, 6) 140 | }, 141 | file: { 142 | type: 'Word', 143 | text: '43', 144 | loc: mkloc(1, 9, 1, 10, 8, 9) 145 | }, 146 | loc: mkloc(1, 6, 1, 10, 5, 9), 147 | numberIo: { 148 | text: '2', 149 | type: 'io_number', 150 | loc: mkloc(1, 6, 1, 6, 5, 5) 151 | } 152 | }] 153 | }; 154 | // console.log(json.stringify(diff(result.commands[0].left.commands[0], expected), null, 4)); 155 | 156 | utils.checkResults(t, result.commands[0], expected); 157 | }); 158 | 159 | test('simple command with suffixes & prefixes', t => { 160 | const result = bashParser('a=1 b=2 echo 42 43', {insertLOC: true}); 161 | utils.checkResults(t, result.commands[0], { 162 | type: 'Command', 163 | name: { 164 | type: 'Word', 165 | text: 'echo', 166 | loc: mkloc(1, 9, 1, 12, 8, 11) 167 | }, 168 | loc: mkloc(1, 1, 1, 18, 0, 17), 169 | prefix: [{ 170 | type: 'AssignmentWord', 171 | text: 'a=1', 172 | loc: mkloc(1, 1, 1, 3, 0, 2) 173 | }, { 174 | type: 'AssignmentWord', 175 | text: 'b=2', 176 | loc: mkloc(1, 5, 1, 7, 4, 6) 177 | }], 178 | suffix: [{ 179 | type: 'Word', 180 | text: '42', 181 | loc: mkloc(1, 14, 1, 15, 13, 14) 182 | }, { 183 | type: 'Word', 184 | text: '43', 185 | loc: mkloc(1, 17, 1, 18, 16, 17) 186 | }] 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/test-loc-for.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('ava'); 4 | const bashParser = require('../src'); 5 | const utils = require('./_utils'); 6 | 7 | /* eslint-disable camelcase */ 8 | test('loc in for statement', t => { 9 | const cmd = 10 | `for x in a b c; do 11 | echo $x; 12 | done 13 | `; 14 | const result = bashParser(cmd, {insertLOC: true}); 15 | const expected = { 16 | type: 'For', 17 | name: { 18 | text: 'x', 19 | type: 'Name', 20 | loc: { 21 | start: { 22 | col: 5, 23 | row: 1, 24 | char: 4 25 | }, 26 | end: { 27 | col: 5, 28 | row: 1, 29 | char: 4 30 | } 31 | } 32 | }, 33 | wordlist: [ 34 | { 35 | text: 'a', 36 | type: 'Word', 37 | loc: { 38 | start: { 39 | col: 10, 40 | row: 1, 41 | char: 9 42 | }, 43 | end: { 44 | col: 10, 45 | row: 1, 46 | char: 9 47 | } 48 | } 49 | }, 50 | { 51 | text: 'b', 52 | type: 'Word', 53 | loc: { 54 | start: { 55 | col: 12, 56 | row: 1, 57 | char: 11 58 | }, 59 | end: { 60 | col: 12, 61 | row: 1, 62 | char: 11 63 | } 64 | } 65 | }, 66 | { 67 | text: 'c', 68 | type: 'Word', 69 | loc: { 70 | start: { 71 | col: 14, 72 | row: 1, 73 | char: 13 74 | }, 75 | end: { 76 | col: 14, 77 | row: 1, 78 | char: 13 79 | } 80 | } 81 | } 82 | ], 83 | do: { 84 | type: 'CompoundList', 85 | commands: [ 86 | { 87 | type: 'Command', 88 | name: { 89 | text: 'echo', 90 | type: 'Word', 91 | loc: { 92 | start: { 93 | col: 2, 94 | row: 2, 95 | char: 20 96 | }, 97 | end: { 98 | col: 5, 99 | row: 2, 100 | char: 23 101 | } 102 | } 103 | }, 104 | loc: { 105 | start: { 106 | col: 2, 107 | row: 2, 108 | char: 20 109 | }, 110 | end: { 111 | col: 8, 112 | row: 2, 113 | char: 26 114 | } 115 | }, 116 | suffix: [ 117 | { 118 | text: '$x', 119 | expansion: [ 120 | { 121 | loc: { 122 | start: 0, 123 | end: 1 124 | }, 125 | parameter: 'x', 126 | type: 'ParameterExpansion' 127 | } 128 | ], 129 | type: 'Word', 130 | loc: { 131 | start: { 132 | col: 7, 133 | row: 2, 134 | char: 25 135 | }, 136 | end: { 137 | col: 8, 138 | row: 2, 139 | char: 26 140 | } 141 | } 142 | } 143 | ] 144 | } 145 | ], 146 | loc: { 147 | start: { 148 | col: 17, 149 | row: 1, 150 | char: 16 151 | }, 152 | end: { 153 | col: 4, 154 | row: 3, 155 | char: 32 156 | } 157 | } 158 | }, 159 | loc: { 160 | start: { 161 | col: 1, 162 | row: 1, 163 | char: 0 164 | }, 165 | end: { 166 | col: 4, 167 | row: 3, 168 | char: 32 169 | } 170 | } 171 | }; 172 | 173 | utils.checkResults(t, result.commands[0], expected); 174 | }); 175 | 176 | test('loc in default for statement', t => { 177 | const cmd = 178 | `for x do 179 | echo $x; 180 | done 181 | `; 182 | const result = bashParser(cmd, {insertLOC: true}); 183 | // utils.logResults(result.commands[0]) 184 | const expected = { 185 | type: 'For', 186 | name: { 187 | text: 'x', 188 | type: 'Name', 189 | loc: { 190 | start: { 191 | col: 5, 192 | row: 1, 193 | char: 4 194 | }, 195 | end: { 196 | col: 5, 197 | row: 1, 198 | char: 4 199 | } 200 | } 201 | }, 202 | do: { 203 | type: 'CompoundList', 204 | commands: [ 205 | { 206 | type: 'Command', 207 | name: { 208 | text: 'echo', 209 | type: 'Word', 210 | loc: { 211 | start: { 212 | col: 2, 213 | row: 2, 214 | char: 10 215 | }, 216 | end: { 217 | col: 5, 218 | row: 2, 219 | char: 13 220 | } 221 | } 222 | }, 223 | loc: { 224 | start: { 225 | col: 2, 226 | row: 2, 227 | char: 10 228 | }, 229 | end: { 230 | col: 8, 231 | row: 2, 232 | char: 16 233 | } 234 | }, 235 | suffix: [ 236 | { 237 | text: '$x', 238 | expansion: [ 239 | { 240 | loc: { 241 | start: 0, 242 | end: 1 243 | }, 244 | parameter: 'x', 245 | type: 'ParameterExpansion' 246 | } 247 | ], 248 | type: 'Word', 249 | loc: { 250 | start: { 251 | col: 7, 252 | row: 2, 253 | char: 15 254 | }, 255 | end: { 256 | col: 8, 257 | row: 2, 258 | char: 16 259 | } 260 | } 261 | } 262 | ] 263 | } 264 | ], 265 | loc: { 266 | start: { 267 | col: 7, 268 | row: 1, 269 | char: 6 270 | }, 271 | end: { 272 | col: 4, 273 | row: 3, 274 | char: 22 275 | } 276 | } 277 | }, 278 | loc: { 279 | start: { 280 | col: 1, 281 | row: 1, 282 | char: 0 283 | }, 284 | end: { 285 | col: 4, 286 | row: 3, 287 | char: 22 288 | } 289 | } 290 | }; 291 | 292 | utils.checkResults(t, result.commands[0], expected); 293 | }); 294 | -------------------------------------------------------------------------------- /src/modes/posix/tokenizer/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const deepFreeze = require('deep-freeze'); 3 | const last = require('array-last'); 4 | 5 | const defaultFields = reducers => ({ 6 | current: '', 7 | escaping: false, 8 | previousReducer: reducers.start, 9 | loc: { 10 | start: {col: 1, row: 1, char: 0}, 11 | previous: null, 12 | current: {col: 1, row: 1, char: 0} 13 | } 14 | }); 15 | 16 | const mkImmutableState = reducers => class ImmutableState { 17 | constructor(fields) { 18 | Object.assign(this, fields || defaultFields(reducers)); 19 | deepFreeze(this); 20 | } 21 | 22 | setLoc(loc) { 23 | return new ImmutableState(Object.assign({}, this, {loc})); 24 | } 25 | 26 | setEscaping(escaping) { 27 | return new ImmutableState(Object.assign({}, this, {escaping})); 28 | } 29 | 30 | setExpansion(expansion) { 31 | return new ImmutableState(Object.assign({}, this, {expansion})); 32 | } 33 | 34 | setPreviousReducer(previousReducer) { 35 | return new ImmutableState(Object.assign({}, this, {previousReducer})); 36 | } 37 | 38 | setCurrent(current) { 39 | return new ImmutableState(Object.assign({}, this, {current})); 40 | } 41 | 42 | appendEmptyExpansion() { 43 | const expansion = (this.expansion || []).concat({ 44 | loc: {start: Object.assign({}, this.loc.current)} 45 | }); 46 | return this.setExpansion(expansion); 47 | } 48 | 49 | appendChar(char) { 50 | return new ImmutableState(Object.assign({}, this, {current: this.current + char})); 51 | } 52 | 53 | removeLastChar() { 54 | return new ImmutableState(Object.assign({}, this, {current: this.current.slice(0, -1)})); 55 | } 56 | 57 | saveCurrentLocAsStart() { 58 | return new ImmutableState(Object.assign({}, this, {loc: Object.assign({}, this.loc, {start: this.loc.current})})); 59 | } 60 | 61 | resetCurrent() { 62 | return new ImmutableState(Object.assign({}, this, {current: ''})); 63 | } 64 | 65 | advanceLoc(char) { 66 | const loc = Object.assign({}, 67 | this.loc, { 68 | current: Object.assign({}, this.loc.current), 69 | previous: Object.assign({}, this.loc.current) 70 | } 71 | ); 72 | 73 | if (char === '\n') { 74 | loc.current.row++; 75 | loc.current.col = 1; 76 | } else { 77 | loc.current.col++; 78 | } 79 | 80 | loc.current.char++; 81 | 82 | if (char && char.match(/\s/) && this.current === '') { 83 | loc.start = Object.assign({}, loc.current); 84 | } 85 | 86 | return this.setLoc(loc); 87 | } 88 | }; 89 | 90 | const mkMutableState = reducers => class { 91 | constructor(fields) { 92 | Object.assign(this, fields || defaultFields(reducers)); 93 | } 94 | 95 | setLoc(loc) { 96 | this.loc = loc; 97 | return this; 98 | } 99 | 100 | setEscaping(escaping) { 101 | this.escaping = escaping; 102 | return this; 103 | } 104 | 105 | setExpansion(expansion) { 106 | this.expansion = expansion; 107 | return this; 108 | } 109 | 110 | setPreviousReducer(previousReducer) { 111 | this.previousReducer = previousReducer; 112 | return this; 113 | } 114 | 115 | setCurrent(current) { 116 | this.current = current; 117 | return this; 118 | } 119 | 120 | appendEmptyExpansion() { 121 | this.expansion = (this.expansion || []); 122 | this.expansion.push({ 123 | loc: {start: Object.assign({}, this.loc.current)} 124 | }); 125 | return this; 126 | } 127 | 128 | appendChar(char) { 129 | this.current = this.current + char; 130 | return this; 131 | } 132 | 133 | removeLastChar() { 134 | this.current = this.current.slice(0, -1); 135 | return this; 136 | } 137 | 138 | saveCurrentLocAsStart() { 139 | this.loc.start = Object.assign({}, this.loc.current); 140 | return this; 141 | } 142 | 143 | resetCurrent() { 144 | this.current = ''; 145 | return this; 146 | } 147 | 148 | replaceLastExpansion(fields) { 149 | const xp = last(this.expansion); 150 | Object.assign(xp, fields); 151 | return this; 152 | } 153 | 154 | deleteLastExpansionValue() { 155 | const xp = last(this.expansion); 156 | delete xp.value; 157 | return this; 158 | } 159 | 160 | advanceLoc(char) { 161 | const loc = JSON.parse(JSON.stringify(this.loc)); 162 | loc.previous = Object.assign({}, this.loc.current); 163 | 164 | if (char === '\n') { 165 | loc.current.row++; 166 | loc.current.col = 1; 167 | } else { 168 | loc.current.col++; 169 | } 170 | 171 | loc.current.char++; 172 | 173 | if (char && char.match(/\s/) && this.current === '') { 174 | loc.start = Object.assign({}, loc.current); 175 | } 176 | 177 | return this.setLoc(loc); 178 | } 179 | }; 180 | 181 | module.exports = (options, reducers) => function * tokenizer(src) { 182 | reducers = reducers || require('./reducers'); 183 | 184 | const State = process.env.NODE_NEV === 'development' ? mkImmutableState(reducers) : mkMutableState(reducers); 185 | 186 | let state = new State(); 187 | 188 | let reduction = reducers.start; 189 | const source = Array.from(src); 190 | 191 | while (typeof reduction === 'function') { 192 | const char = source[0]; 193 | const r = reduction(state, source, reducers); 194 | const nextReduction = r.nextReduction; 195 | const tokensToEmit = r.tokensToEmit; 196 | const nextState = r.nextState; 197 | if (tokensToEmit) { 198 | yield * tokensToEmit; 199 | } 200 | 201 | /* if (char === undefined && nextReduction === reduction) { 202 | throw new Error('Loop detected'); 203 | } */ 204 | 205 | if (nextState) { 206 | state = nextState.advanceLoc(char); 207 | } else { 208 | state = state.advanceLoc(char); 209 | } 210 | 211 | reduction = nextReduction; 212 | } 213 | }; 214 | 215 | module.exports.reducers = require('./reducers'); 216 | -------------------------------------------------------------------------------- /test/test-loc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require("ava"); 4 | const bashParser = require("../src"); 5 | const mkloc = require("./_utils").mkloc2; 6 | const utils = require("./_utils"); 7 | 8 | /* eslint-disable camelcase */ 9 | test("syntax error contains line number", async t => { 10 | const error = t.throws(() => bashParser("ecoh\necho <")); 11 | t.true( 12 | error.message.startsWith( 13 | "Error: Parse error on line 2: Unexpected 'EOF'" 14 | ) 15 | ); 16 | }); 17 | 18 | test("AST can include loc", t => { 19 | const result = bashParser("echo", { insertLOC: true }); 20 | // utils.logResults(result) 21 | utils.checkResults(t, result.commands[0].name, { 22 | type: "Word", 23 | text: "echo", 24 | loc: mkloc(1, 1, 1, 4, 0, 3) 25 | }); 26 | }); 27 | 28 | test("subshell can include loc", t => { 29 | const result = bashParser("(echo)", { insertLOC: true }); 30 | // utils.logResults(result); 31 | utils.checkResults(t, result, { 32 | type: "Script", 33 | commands: [ 34 | { 35 | type: "Subshell", 36 | list: { 37 | type: "CompoundList", 38 | commands: [ 39 | { 40 | type: "Command", 41 | name: { 42 | text: "echo", 43 | type: "Word", 44 | loc: mkloc(1, 2, 1, 5, 1, 4) 45 | }, 46 | loc: mkloc(1, 2, 1, 5, 1, 4) 47 | } 48 | ], 49 | loc: mkloc(1, 2, 1, 5, 1, 4) 50 | }, 51 | loc: mkloc(1, 1, 1, 6, 0, 5) 52 | } 53 | ], 54 | loc: mkloc(1, 1, 1, 6, 0, 5) 55 | }); 56 | }); 57 | 58 | test("double command with only name", t => { 59 | const result = bashParser("echo; ciao;", { insertLOC: true }); 60 | // utils.logResults(result); 61 | utils.checkResults(t, result, { 62 | type: "Script", 63 | loc: mkloc(1, 1, 1, 10, 0, 9), 64 | commands: [ 65 | { 66 | type: "Command", 67 | name: { 68 | type: "Word", 69 | text: "echo", 70 | loc: mkloc(1, 1, 1, 4, 0, 3) 71 | }, 72 | loc: mkloc(1, 1, 1, 4, 0, 3) 73 | }, 74 | { 75 | type: "Command", 76 | name: { 77 | type: "Word", 78 | text: "ciao", 79 | loc: mkloc(1, 7, 1, 10, 6, 9) 80 | }, 81 | loc: mkloc(1, 7, 1, 10, 6, 9) 82 | } 83 | ] 84 | }); 85 | }); 86 | 87 | test("loc are composed by all tokens", t => { 88 | const result = bashParser("echo 42", { insertLOC: true }); 89 | // console.log(JSON.stringify(result, null, 4)); 90 | utils.checkResults(t, result.commands[0], { 91 | type: "Command", 92 | name: { 93 | type: "Word", 94 | text: "echo", 95 | loc: mkloc(1, 1, 1, 4, 0, 3) 96 | }, 97 | loc: mkloc(1, 1, 1, 7, 0, 6), 98 | suffix: [ 99 | { 100 | type: "Word", 101 | text: "42", 102 | loc: mkloc(1, 6, 1, 7, 5, 6) 103 | } 104 | ] 105 | }); 106 | }); 107 | 108 | test("loc works with multiple newlines", t => { 109 | const result = bashParser("\n\n\necho 42", { insertLOC: true }); 110 | utils.checkResults(t, result.commands[0], { 111 | type: "Command", 112 | name: { 113 | type: "Word", 114 | text: "echo", 115 | loc: mkloc(4, 1, 4, 4, 3, 6) 116 | }, 117 | loc: mkloc(4, 1, 4, 7, 3, 9), 118 | suffix: [ 119 | { 120 | type: "Word", 121 | text: "42", 122 | loc: mkloc(4, 6, 4, 7, 8, 9) 123 | } 124 | ] 125 | }); 126 | }); 127 | 128 | test("loc with LINEBREAK_IN statement", t => { 129 | const cmd = `for x 130 | in ; do 131 | echo $x; 132 | done 133 | `; 134 | 135 | const result = bashParser(cmd, { insertLOC: true }); 136 | // utils.logResults(result) 137 | const expected = { 138 | type: "For", 139 | name: { 140 | text: "x", 141 | type: "Name", 142 | loc: { 143 | start: { 144 | col: 5, 145 | row: 1, 146 | char: 4 147 | }, 148 | end: { 149 | col: 5, 150 | row: 1, 151 | char: 4 152 | } 153 | } 154 | }, 155 | do: { 156 | type: "CompoundList", 157 | commands: [ 158 | { 159 | type: "Command", 160 | name: { 161 | text: "echo", 162 | type: "Word", 163 | loc: { 164 | start: { 165 | col: 2, 166 | row: 3, 167 | char: 16 168 | }, 169 | end: { 170 | col: 5, 171 | row: 3, 172 | char: 19 173 | } 174 | } 175 | }, 176 | loc: { 177 | start: { 178 | col: 2, 179 | row: 3, 180 | char: 16 181 | }, 182 | end: { 183 | col: 8, 184 | row: 3, 185 | char: 22 186 | } 187 | }, 188 | suffix: [ 189 | { 190 | text: "$x", 191 | expansion: [ 192 | { 193 | loc: { 194 | start: 0, 195 | end: 1 196 | }, 197 | parameter: "x", 198 | type: "ParameterExpansion" 199 | } 200 | ], 201 | type: "Word", 202 | loc: { 203 | start: { 204 | col: 7, 205 | row: 3, 206 | char: 21 207 | }, 208 | end: { 209 | col: 8, 210 | row: 3, 211 | char: 22 212 | } 213 | } 214 | } 215 | ] 216 | } 217 | ], 218 | loc: { 219 | start: { 220 | col: 7, 221 | row: 2, 222 | char: 12 223 | }, 224 | end: { 225 | col: 4, 226 | row: 4, 227 | char: 28 228 | } 229 | } 230 | }, 231 | loc: { 232 | start: { 233 | col: 1, 234 | row: 1, 235 | char: 0 236 | }, 237 | end: { 238 | col: 4, 239 | row: 4, 240 | char: 28 241 | } 242 | } 243 | }; 244 | 245 | utils.checkResults(t, result.commands[0], expected); 246 | }); 247 | 248 | test("loc in multi line commands", t => { 249 | const result = bashParser("echo;\nls;\n", { insertLOC: true }); 250 | // utils.logResults(result); 251 | utils.checkResults(t, result, { 252 | loc: mkloc(1, 1, 2, 2, 0, 7), 253 | type: "Script", 254 | commands: [ 255 | { 256 | type: "Command", 257 | name: { 258 | type: "Word", 259 | text: "echo", 260 | loc: mkloc(1, 1, 1, 4, 0, 3) 261 | }, 262 | loc: mkloc(1, 1, 1, 4, 0, 3) 263 | }, 264 | { 265 | type: "Command", 266 | name: { 267 | type: "Word", 268 | text: "ls", 269 | loc: mkloc(2, 1, 2, 2, 6, 7) 270 | }, 271 | loc: mkloc(2, 1, 2, 2, 6, 7) 272 | } 273 | ] 274 | }); 275 | }); 276 | --------------------------------------------------------------------------------