Skip to content

Unable to pass & in an argument to a shell script started with Process.start #59604

@DanTup

Description

@DanTup

I'm trying to fix a bug where we mishandle some characters like & in user-provided arguments because of having to spawn flutter.bat through a shell. There is a little info on Process.start() about this:

NOTE: On Windows, if executable is a batch file ('.bat' or '.cmd'), it may be launched by the operating system in a system shell regardless of the value of runInShell. This could result in arguments being parsed according to shell rules. For example:

  // Will launch notepad.
  Process.start('test.bat', ['test&notepad.exe']);
}

The issue I'm having is that I can't find any way to escape a & that comes through correctly.

Below is a script to reproduce the issue. It creates a temp folder with a space in the name, and then writes a simple .bat file that forwards all args to a Dart script (similar to what flutter.bat does). The Dart script it writes just prints all the arguments out to stdout. The test reads stdout and compares what's printed to what it sent as arguments to ensure they match.

If I remove the & from testCharacters, the test passes. If I add the & then it fails because the last argument is truncated, and it tried to execute after:

'after\""' is not recognized as an internal or external command,
operable program or batch file.
Expected: ['beforeaafter', 'beforebafter', 'beforecafter', 'before&after']
  Actual: ['beforeaafter', 'beforebafter', 'beforecafter', '"before']

The _escapeAndQuoteArg function needs to escape & in some way, but I've tried many combinations (including backslashes, the ^ character and combinations of quoting/not quoting the args) (I'm assuming https://ss64.com/nt/syntax-esc.html is a reasonable source), but none of them work. Based on @derekxu16 comment at #50076 (comment) it's not clear to me if Dart is also trying to do some of this escaping.

I'm not sure if this is a bug, or I'm doing it wrong. I'm hoping someone that understands the code in createProcess may be able to verify one way or the other.

import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';

const testCharacters = r'abc&'; // r' "&:\<>|%=+-_@~#<>?/';

final testArgs = [
  for (var char in testCharacters.split('')) 'before${char}after',
];

void main() {
  test('test escaping', () async {
    var tempDir =
        Directory.systemTemp.createTempSync('flutter dap args escape test');
    print('Writing scripts to $tempDir');

    // Write a shell script to simulate the Flutter .bat file and a Dart script
    // that just prints the arguments sent to it one-per-line.
    var tempShellScript = File(path.join(tempDir.path, 'fake_flutter.bat'));
    var tempDartScript = File(path.join(tempDir.path, 'print_args.dart'));
    var command = [
      '"${Platform.resolvedExecutable}"',
      '"${tempDartScript.path}"',
      "%*",
    ];
    tempShellScript.writeAsStringSync('@echo off\n${command.join(" ")}');
    tempDartScript.writeAsStringSync(r'''
void main(List<String> args) {
  print(args.join('\n'));
}
''');

    var executable = tempShellScript.path;
    var args = testArgs.map(_escapeAndQuoteArg).toList();

    print('''
Executing:
  executable: $executable
  args: ${args.join(' ')}
  runInShell: true
''');
    var proc = await Process.start(
      '"$executable"',
      args,
    );

    var stdoutFuture = proc.stdout.transform(utf8.decoder).toList();
    var stderrFuture = proc.stderr.transform(utf8.decoder).toList();
    await proc.exitCode;

    var stdout = (await stdoutFuture).join().trim();
    var stderr = (await stderrFuture).join().trim();

    if (stderr.isNotEmpty) {
      print(stderr);
    }

    var actual = stdout
        .split('\n')
        .map((l) => l.trim())
        .where((l) => l.isNotEmpty)
        .toList();

    expect(actual, testArgs);
  });
}

String _escapeAndQuoteArg(String input) {
  // What is the correct thing to do here to escape &?
  // return input.replaceAll('&', '^&');
  return input;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-dart-cliUse area-dart-cli for issues related to the 'dart' command like tool.triage-automationSee https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot.type-bugIncorrect behavior (everything from a crash to more subtle misbehavior)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions