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

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

pending completion
4843776952

Pull #67

github

GitHub
Merge 4765397c2 into d1012a531
Pull Request #67: feat: command expansion - fixes #66

65 of 83 branches covered (78.31%)

Branch coverage included in aggregate %.

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

136 of 147 relevant lines covered (92.52%)

16.91 hits per line

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

87.01
/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 {
23✔
17

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

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

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

35
        switch (command.type) {
107✔
36
            case 'Command':
37
                if (command.prefix && command.prefix.length && (!command.name || !command.name.text)) { // simple variable assignment
38!
38
                    return command.prefix.map(c => this.convertCommand(c)).join('\n');
5✔
39
                }
40
                if (command.name && command.name.text) {
33✔
41
                    if (this.userDefinedFunctionNames.has(command.name.text)) {
33✔
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(' ')}` : '';
41!
46
                    switch (command.name.text) {
31!
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}`;
27✔
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);
3✔
68
                this.userDefinedFunctions.push(command);
3✔
69
                return '';
3✔
70
            case 'Word':
71
                const expandedWord = this.performExpansions(command.text, command.expansion);
50✔
72
                const textWord = convertPaths(expandedWord);
50✔
73

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

83
                const [variableName, variableValue] = textAssignmentWord.split('=', 2);
5✔
84
                const unquotedValue = variableValue.replace(/^"(.*)"$/, '$1');
5✔
85
                return `SET "${variableName}=${unquotedValue}"`;
5✔
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 {
22✔
119
        if (!this.userDefinedFunctions.length) {
22✔
120
            return '';
19✔
121
        }
122

123
        return `\n\nEXIT /B %ERRORLEVEL%\n\n${this.userDefinedFunctions.map(f => {
3✔
124
            const innerCommands = f.body.commands.map(c => this.convertCommand(c)).join('\n');
3✔
125
            return `:${f.name.text}\n${innerCommands}\nEXIT /B 0\n`;
3✔
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 || '';
55✔
132
        const sortedExpansions = [...(expansions || [])];
55✔
133
        sortedExpansions.sort((a, b) => a.loc.start > b.loc.start ? -1 : 1);
55!
134
        for (const expansion of sortedExpansions) {
110✔
135
            switch (expansion.type) {
14✔
136
                case 'CommandExpansion':
137
                    this.delayedExpansionActive = true;
3✔
138
                    const interpolationVar = `_INTERPOLATION_${this.interpolationCounter++}`;
3✔
139
                    this.preStatements.push(`SET ${interpolationVar}=`);
3✔
140
                    this.preStatements.push(`FOR /f "delims=" %%a in ('${expansion.command}') DO (SET "${interpolationVar}=!${interpolationVar}! %%a")`);
3✔
141
                    result = `${result.substring(0, expansion.loc.start)}!${interpolationVar}!${result.substring(expansion.loc.end + 1)}`;
3✔
142
                    break;
3✔
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}%`);
11✔
146
                    result = `${result.substring(0, expansion.loc.start)}${expandedValue}${result.substring(expansion.loc.end + 1)}`;
11✔
147
                    break;
11✔
148
            }
149
        }
150
        return result;
55✔
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;
22✔
159
    }
160

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

168

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

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

188

189

190

191

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