Skip to content

Commit

Permalink
fix backslash handling on windows
Browse files Browse the repository at this point in the history
  • Loading branch information
alextekartik committed Jan 2, 2024
1 parent 00442ff commit a0eb456
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 7 deletions.
101 changes: 101 additions & 0 deletions packages/process_run/lib/src/io/shell_words.dart
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,107 @@ List<String> shellSplitImpl(String command) {
return results;
}

/// Splits [command] into tokens according to [the POSIX shell
/// specification][spec] slightly modified for windows.
///
/// [spec]: http://pubs.opengroup.org/onlinepubs/9699919799/utilities/contents.html
///
/// This returns the unquoted values of quoted tokens. For example,
/// `shellSplit('foo "bar baz"')` returns `["foo", "bar baz"]`. It does not
/// currently support here-documents. It does *not* treat dynamic features such
/// as parameter expansion specially. For example, `shellSplit("foo $(bar
/// baz)")` returns `["foo", "$(bar", "baz)"]`.
///
/// This will discard any comments at the end of [command].
///
/// Throws a [FormatException] if [command] isn't a valid shell command.
List<String> shellSplitWindowsImpl(String command) {
final scanner = StringScanner(command);
final results = <String>[];
final token = StringBuffer();

// Whether a token is being parsed, as opposed to a separator character. This
// is different than just [token.isEmpty], because empty quoted tokens can
// exist.
var hasToken = false;

while (!scanner.isDone) {
final next = scanner.readChar();
switch (next) {
case charcodeBackslash:
// We don't escape backslashes in Windows paths.
hasToken = true;
token.writeCharCode(next);
break;

case charcodeSingleQuote:
hasToken = true;
// Section 2.2.2: Enclosing characters in single-quotes ( '' ) shall
// preserve the literal value of each character within the
// single-quotes. A single-quote cannot occur within single-quotes.
final firstQuote = scanner.position - 1;
while (!scanner.scanChar(charcodeSingleQuote)) {
_checkUnmatchedQuote(scanner, firstQuote);
token.writeCharCode(scanner.readChar());
}
break;

case charcodeDoubleQuote:
hasToken = true;
// Section 2.2.3: Enclosing characters in double-quotes ( "" ) shall
// preserve the literal value of all characters within the
// double-quotes, with the exception of the characters backquote,
// <dollar-sign>, and <backslash>.
//
// (Note that this code doesn't preserve special behavior of backquote
// or dollar sign within double quotes, since those are dynamic
// features.)
final firstQuote = scanner.position - 1;
while (!scanner.scanChar(charcodeDoubleQuote)) {
_checkUnmatchedQuote(scanner, firstQuote);

// We don't escape backslashes in Windows paths.

token.writeCharCode(scanner.readChar());
}
break;

case charcodeHash:
// Section 2.3: If the current character is a '#' [and the previous
// characters was not part of a word], it and all subsequent characters
// up to, but excluding, the next <newline> shall be discarded as a
// comment. The <newline> that ends the line is not considered part of
// the comment.
if (hasToken) {
token.writeCharCode(charcodeHash);
break;
}

while (!scanner.isDone && scanner.peekChar() != charcodeLf) {
scanner.readChar();
}
break;

case charcodeSpace:
case charcodeTab:
case charcodeLf:
// ignore: invariant_booleans
if (hasToken) results.add(token.toString());
hasToken = false;
token.clear();
break;

default:
hasToken = true;
token.writeCharCode(next);
break;
}
}

if (hasToken) results.add(token.toString());
return results;
}

/// Throws a [FormatException] if [scanner] is done indicating that a closing
/// quote matching the one at position [openingQuote] is missing.
void _checkUnmatchedQuote(StringScanner scanner, int openingQuote) {
Expand Down
7 changes: 5 additions & 2 deletions packages/process_run/lib/src/shell_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import 'dart:convert';
import 'package:path/path.dart';
import 'package:process_run/shell.dart';
import 'package:process_run/src/common/constant.dart';
import 'package:process_run/src/io/shell_words.dart' as io show shellSplitImpl;
import 'package:process_run/src/io/shell_words.dart' as io
show shellSplitImpl, shellSplitWindowsImpl;
import 'package:process_run/src/shell_environment.dart';

import 'bin/shell/import.dart';
Expand Down Expand Up @@ -207,7 +208,9 @@ bool fixRunInShell(bool? runInShell, String executable) {
}

/// Use io package shellSplit implementation
List<String> shellSplit(String command) => io.shellSplitImpl(command);
List<String> shellSplit(String command) => context.style == windows.style
? io.shellSplitWindowsImpl(command)
: io.shellSplitImpl(command);

/// Inverse of shell split
String shellJoin(List<String> parts) =>
Expand Down
7 changes: 6 additions & 1 deletion packages/process_run/test/echo_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ library process_run.echo_test;
import 'dart:convert';
import 'dart:io';

import 'package:path/path.dart';
import 'package:process_run/process_run.dart';
import 'package:process_run/src/common/import.dart';
import 'package:process_run/src/dartbin_impl.dart';
import 'package:test/test.dart';

import 'process_run_test_common.dart';

var echo = '$resolvedDartExecutable run example/echo.dart';
var echo =
'$resolvedDartExecutable run ${shellArgument(join("example", "echo.dart"))}';

void main() {
group('echo', () {
test('run echo', () async {
await run('$echo --stdout test');
});
Future runCheck(
Object? Function(ProcessResult result) check,
String executable,
Expand Down
15 changes: 12 additions & 3 deletions packages/process_run/test/shell_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,13 @@ dart example/echo.dart -o ${shellArgument(weirdText)}
''');

expect(results[0].stdout.toString().trim(), r'a/bc/d');
expect(results[1].stdout.toString().trim(), r'a/\b c/\d');
if (Platform.isWindows) {
expect(results[0].stdout.toString().trim(), r'a/\bc/\d');
expect(results[1].stdout.toString().trim(), r'a/\b c/\d');
} else {
expect(results[0].stdout.toString().trim(), r'a/bc/d');
expect(results[1].stdout.toString().trim(), r'a/\b c/\d');
}
expect(results.length, 2);
});
test('dart', () async {
Expand Down Expand Up @@ -400,7 +405,11 @@ dart current_dir.dart
test('escape backslash', () async {
var shell = Shell(verbose: debug);
var results = await shell.run(r'echo "\\"');
expect(results[0].stdout.toString().trim(), '\\');
if (Platform.isWindows) {
expect(results[0].stdout.toString().trim(), r'\\');
} else {
expect(results[0].stdout.toString().trim(), r'\');
}
});
test('others', () async {
try {
Expand Down
6 changes: 5 additions & 1 deletion packages/process_run/test/src_shell_utils_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ void main() {

test('shellSplit', () {
// We differ from io implementation
expect(shellSplit(r'"\\"'), [r'\']);
if (Platform.isWindows) {
expect(shellSplit(r'"\\"'), [r'\\']);
} else {
expect(shellSplit(r'"\\"'), [r'\']);
}
expect(shellSplit('Hello world'), ['Hello', 'world']);
expect(shellSplit('"Hello world"'), ['Hello world']);
expect(shellSplit("'Hello world'"), ['Hello world']);
Expand Down

0 comments on commit a0eb456

Please sign in to comment.