• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daniel-sc / bash-shell-to-bat-converter / 4845675714

pending completion
4845675714

push

github

GitHub
Merge pull request #67 from daniel-sc/66-command-expansion

65 of 83 branches covered (78.31%)

Branch coverage included in aggregate %.

58 of 58 new or added lines in 3 files covered. (100.0%)

137 of 148 relevant lines covered (92.57%)

17.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.08
/src/convert-bash.ts
1
// patch for irrelevant node dependency of bash-parser:
2
if (typeof window === 'undefined') {
1!
3
    // @ts-ignore
4
    let window = {process: {env: {NODE_NEV: 'mock'}}}; // lgtm [js/unused-local-variable]
1✔
5
    // @ts-ignore
6
} else if (!window.process) {
×
7
    // @ts-ignore
8
    window.process = {env: {NODE_NEV: 'mock'}};
×
9
}
10
import {convertPaths} from './convert-paths';
1✔
11
import {Expansion, Expression, isStatement, Script} from './types';
1✔
12
import parse from 'bash-parser'
1✔
13
import {RmHandler} from './rm-handler';
1✔
14
import {CpHandler} from './cp-handler';
1✔
15

16
class ConvertBash {
24✔
17

18
    private readonly rmHandler = new RmHandler(c => this.convertCommand(c));
23✔
19
    private readonly cpHandler = new CpHandler(c => this.convertCommand(c));
23✔
20
    private readonly userDefinedFunctionNames = new Set<string>();
23✔
21
    private readonly userDefinedFunctions: any[] = [];
23✔
22
    private delayedExpansionActive = false;
23✔
23
    private preStatements: string[] = [];
23✔
24
    private interpolationCounter = 0;
23✔
25

26
    public convertCommand(command: Expression): string {
1✔
27
        const result = this.convertCommandCore(command);
112✔
28
        const preStatements = isStatement(command) ? this.drainPreStatements() : '';
112✔
29
        return preStatements + result;
112✔
30
    }
31

32
    private convertCommandCore(command: Expression): string {
112✔
33
        console.log('convertCommand', command);
112✔
34

35
        switch (command.type) {
112✔
36
            case 'Command':
37
                if (command.prefix && command.prefix.length && (!command.name || !command.name.text)) { // simple variable assignment
40!
38
                    return command.prefix.map(c => this.convertCommand(c)).join('\n');
6✔
39
                }
40
                if (command.name && command.name.text) {
34✔
41
                    if (this.userDefinedFunctionNames.has(command.name.text)) {
34✔
42
                        const params = command.suffix && command.suffix.length ? ' ' + command.suffix.map(x => this.convertCommand(x)).join(' , ') : '';
2✔
43
                        return `CALL :${command.name.text}${params}`;
2✔
44
                    }
45
                    const suffix = command.suffix ? ` ${command.suffix.map(c => this.convertCommand(c)).join(' ')}` : '';
42!
46
                    switch (command.name.text) {
32!
47
                        case 'set':
48
                            if (command.suffix && command.suffix.length === 1 && command.suffix[0].type === 'Word' && command.suffix[0].text === '-e') {
×
49
                                console.log('skipping "set -e"');
×
50
                                return '';
×
51
                            } else {
52
                                return `${command.name.text}${suffix}`;
×
53
                            }
54
                        case 'rm':
55
                            return this.rmHandler.handle(command);
3✔
56
                        case 'cp':
57
                            return this.cpHandler.handle(command);
1✔
58

59
                        default:
60
                            return `${command.name.text}${suffix}`;
28✔
61
                    }
62
                }
63
                return 'unknown command: ' + JSON.stringify(command);
×
64
            case 'Function':
65
                // NOTE: bash: definition before call. batch: call before definition
66
                // --> append function definitions at the end
67
                this.userDefinedFunctionNames.add(command.name.text);
4✔
68
                this.userDefinedFunctions.push(command);
4✔
69
                return '';
4✔
70
            case 'Word':
71
                const expandedWord = this.performExpansions(command.text, command.expansion);
51✔
72
                const textWord = convertPaths(expandedWord);
51✔
73

74
                if (textWord.startsWith('"') || ['==', '!='].includes(textWord)) {
51✔
75
                    return textWord;
9✔
76
                } else {
77
                    return `"${textWord}"`;
42✔
78
                }
79
            case 'AssignmentWord':
80
                const expandedAssignmentWord = this.performExpansions(command.text, command.expansion);
6✔
81
                const textAssignmentWord = convertPaths(expandedAssignmentWord);
6✔
82

83
                const [variableName, variableValue] = textAssignmentWord.split('=', 2);
6✔
84
                const unquotedValue = variableValue.replace(/^"(.*)"$/, '$1');
6✔
85
                return `SET "${variableName}=${unquotedValue}"`;
6✔
86
            case 'LogicalExpression':
87
                switch (command.op) {
2!
88
                    case 'and':
89
                        return `${(this.convertCommand(command.left))} && ${(this.convertCommand(command.right))}`;
1✔
90
                    case 'or':
91
                        return `${(this.convertCommand(command.left))} || ${(this.convertCommand(command.right))}`;
1✔
92
                    default:
93
                        return `REM UNKNOWN operand "${command.op}" in: ${JSON.stringify(command)}`;
×
94
                }
95
            case 'CompoundList':
96
                return command.commands.map(c => this.convertCommand(c)).join('\n');
7✔
97
            case 'If':
98
                // note: AND/OR is not supported with batch IF (https://stackoverflow.com/a/2143203/2544163)
99
                const condition = this.convertCommand(command.clause.commands[0]).replace(/^\[ |^"\[" | "]"$/g, '');
2✔
100
                const elseBranch = command.else ? ` ELSE (\n${this.indent(this.convertCommand(command.else))}\n)` : '';
2✔
101
                return `IF ${condition} (\n${this.indent(this.convertCommand(command.then))}\n)${elseBranch}`;
2✔
102
            case 'Case':
103
                const caseStatement = this.convertCommand(command.clause);
1✔
104
                return command.cases.map((c, i) => {
1✔
105
                    const pattern = c.pattern[0]; // this is a list for unclear reason..
3✔
106
                    // simple heuristic: '*' is default case:
107
                    if (pattern.text === '*') {
3✔
108
                        return ` ELSE (\n${this.indent(this.convertCommand(c.body))}\n)`;
1✔
109
                    }
110
                    const caseCondition = `${caseStatement}==${this.convertCommand(pattern)}`;
2✔
111
                    const prefix = i === 0 ? 'IF' : ' ELSE IF';
2✔
112
                    return `${prefix} ${caseCondition} (\n${this.indent(this.convertCommand(c.body))}\n)`;
2✔
113
                }).join('');
114
        }
115
        return 'REM UNKNOWN: ' + JSON.stringify(command);
×
116
    }
117

118
    public getFunctionDefinitions(): string {
23✔
119
        if (!this.userDefinedFunctions.length) {
23✔
120
            return '';
19✔
121
        }
122

123
        return `\n\nEXIT /B %ERRORLEVEL%\n\n${this.userDefinedFunctions.map(f => {
4✔
124
            const innerCommands = f.body.commands.map(c => this.convertCommand(c)).join('\n');
5✔
125
            return `:${f.name.text}\n${innerCommands}\nEXIT /B 0\n`;
4✔
126
        }).join('\n')}`
127
    }
128

129
    private performExpansions(text?: string, expansions?: Expansion[]): string {
1✔
130
        // currently assumes only ParameterExpansions (TODO ArithmeticExpansion)
131
        let result = text || '';
57✔
132
        const sortedExpansions = [...(expansions || [])];
57✔
133
        sortedExpansions.sort((a, b) => a.loc.start > b.loc.start ? -1 : 1);
57!
134
        for (const expansion of sortedExpansions) {
114✔
135
            switch (expansion.type) {
16✔
136
                case 'CommandExpansion':
137
                    this.delayedExpansionActive = true;
4✔
138
                    const interpolationVar = `_INTERPOLATION_${this.interpolationCounter++}`;
4✔
139
                    this.preStatements.push(`SET ${interpolationVar}=`);
4✔
140
                    this.preStatements.push(`FOR /f "delims=" %%a in ('${expansion.command}') DO (SET "${interpolationVar}=!${interpolationVar}! %%a")`);
4✔
141
                    result = `${result.substring(0, expansion.loc.start)}!${interpolationVar}!${result.substring(expansion.loc.end + 1)}`;
4✔
142
                    break;
4✔
143
                case 'ParameterExpansion':
144
                    // expand function parameters such as `$1` (-> `%~1`) different to regular variables `$MY`(-> `%MY%` or `!MY!` if delayed expansion is active):
145
                    const expandedValue = /^\d+$/.test(`${expansion.parameter}`) ? `%~${expansion.parameter}` : (this.delayedExpansionActive ? `!${expansion.parameter}!` : `%${expansion.parameter}%`);
12✔
146
                    result = `${result.substring(0, expansion.loc.start)}${expandedValue}${result.substring(expansion.loc.end + 1)}`;
12✔
147
                    break;
12✔
148
            }
149
        }
150
        return result;
57✔
151
    }
152

153
    private indent(s: string): string {
1✔
154
        return s.split('\n').map(line => `  ${line}`).join('\n');
7✔
155
    }
156

157
    delayedExpansion(): boolean {
1✔
158
        return this.delayedExpansionActive;
23✔
159
    }
160

161
    private drainPreStatements() {
1✔
162
        const result = this.preStatements.join('\n');
49✔
163
        this.preStatements = [];
49✔
164
        return result + (result ? '\n' : '');
49✔
165
    }
166
}
1✔
167

168

169
function preprocess(script: string): string {
170
    return script.replace(/(^|\n)\s*function /g, '$1');
23✔
171
}
172

173
export function convertBashToWin(script: string) {
1✔
174
    const preprocessedScript = preprocess(script);
23✔
175
    const ast: Script = parse(preprocessedScript, {mode: 'bash'});
23✔
176
    const converter = new ConvertBash();
23✔
177
    const convertedCommands = ast.commands
23✔
178
        .map(c => converter.convertCommand(c))
31✔
179
        .filter((c: any) => !!c) // filter empty commands
31✔
180
        .join('\n');
181
    const functionDefinitions = converter.getFunctionDefinitions();
23✔
182
    return '@echo off' +
23✔
183
        (converter.delayedExpansion() ? '\nsetlocal EnableDelayedExpansion' : '') +
23✔
184
        '\n\n' +
185
        convertedCommands +
186
        functionDefinitions;
187
}
188

189

190

191

192

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc