diff --git a/commands/core/core.drush.inc b/commands/core/core.drush.inc index b6d137e..380f61a 100644 --- a/commands/core/core.drush.inc +++ b/commands/core/core.drush.inc @@ -882,7 +882,7 @@ function drush_core_cli() { // Make sure that we call drush the same way that we were called. // In --pipe mode, just use 'drush'. - $drush_command = $interactive_mode ? DRUSH_COMMAND.' --in-cli' : 'drush'; + $drush_command = $interactive_mode ? drush_build_drush_command() . ' --in-cli' : 'drush'; $bashrc_data = implode("\n\n", drush_command_invoke_all('cli_bashrc', $drush_command, $interactive_mode)); @@ -904,7 +904,7 @@ function drush_core_cli() { // control-d to exit. The temp file will be deleted after // we exit. $bashrc = drush_save_data_to_temp_file($bashrc_data); - return drush_op_system('bash --rcfile ' . $bashrc . ' > `tty`'); + return drush_shell_exec_interactive('bash --rcfile %s', $bashrc); } // Implement our own hook_cli_bashrc() @@ -1176,7 +1176,7 @@ EOD; $bashrc_data .= "builtin cd $site_root\n"; } if ($drush_command != 'drush') { - $bashrc_data .= "alias drush=\"$drush_command\"\n"; + $bashrc_data .= "alias drush=" . drush_wrap_with_quotes($drush_command) . "\n"; } } // Add some additional statements that should only appear in non-interactive mode. diff --git a/commands/core/drupal/batch.inc b/commands/core/drupal/batch.inc index 1165926..2823dd6 100644 --- a/commands/core/drupal/batch.inc +++ b/commands/core/drupal/batch.inc @@ -14,7 +14,7 @@ * The command to call to process the batch. * */ -function _drush_backend_batch_process($command = 'batch-process') { +function _drush_backend_batch_process($command = 'batch-process', $args) { global $user; $batch =& batch_get(); @@ -53,10 +53,10 @@ function _drush_backend_batch_process($command = 'batch-process') { while (!$finished) { if ($user->uid) { - $data = drush_backend_invoke($command, array($batch['id'], '-u', $user->uid)); + $data = drush_invoke_process_args($command, $args, array($batch['id'], '-u', $user->uid)); } else { - $data = drush_backend_invoke($command, array($batch['id'])); + $data = drush_invoke_process_args($command, $args, array($batch['id'])); } $finished = drush_get_error() || !$data || ($data['context']['drush_batch_process_finished'] == TRUE); } diff --git a/commands/core/drupal/batch_6.inc b/commands/core/drupal/batch_6.inc index f3cccf1..4caba3b 100644 --- a/commands/core/drupal/batch_6.inc +++ b/commands/core/drupal/batch_6.inc @@ -14,7 +14,7 @@ * The command to call to process the batch. * */ -function _drush_backend_batch_process($command = 'batch-process') { +function _drush_backend_batch_process($command = 'batch-process', $args) { global $user; $batch =& batch_get(); @@ -36,10 +36,10 @@ function _drush_backend_batch_process($command = 'batch-process') { while (!$finished) { if ($user->uid) { - $data = drush_backend_invoke($command, array($batch['id'], '-u', $user->uid)); + $data = drush_invoke_process_args($command, $args, array($batch['id'], '-u', $user->uid)); } else { - $data = drush_backend_invoke($command, array($batch['id'])); + $data = drush_invoke_process_args($command, $args, array($batch['id'])); } $finished = drush_get_error() || !$data || (isset($data['context']['drush_batch_process_finished']) && $data['context']['drush_batch_process_finished'] == TRUE); diff --git a/commands/core/search.drush.inc b/commands/core/search.drush.inc index 5a2818f..e521ca2 100644 --- a/commands/core/search.drush.inc +++ b/commands/core/search.drush.inc @@ -44,7 +44,7 @@ function _drush_core_search_index() { register_shutdown_function('search_update_totals'); while ($remaining > 0) { drush_log(dt('Remaining items to be indexed: ' . $remaining), 'ok'); - // Use drush_backend_invoke() to start subshell. Avoids out of memory issue. + // Use drush_invoke_process_args() to start subshell. Avoids out of memory issue. $eval = "register_shutdown_function('search_update_totals');"; if (drush_drupal_major_version() >= 7) { foreach (variable_get('search_active_modules', array('node', 'user')) as $module) { @@ -54,7 +54,7 @@ function _drush_core_search_index() { else { $eval .= " module_invoke_all('update_index');"; } - drush_backend_invoke('php-eval', array($eval)); + drush_invoke_process_args('php-eval', array($eval)); list($remaining, ) = _drush_core_search_status(); } } diff --git a/commands/pm/package_handler/wget.inc b/commands/pm/package_handler/wget.inc index 1d2766e..76c3dcf 100644 --- a/commands/pm/package_handler/wget.inc +++ b/commands/pm/package_handler/wget.inc @@ -90,7 +90,7 @@ function package_handler_download_project(&$request, $release) { // the output is not the same in Mac. drush_shell_exec("tar -tf %s", $tarpath); $output = drush_shell_exec_output(); - $project_dir = rtrim($output[0], DIRECTORY_SEPARATOR); + $project_dir = drush_trim_path($output[0]); if ($request['project_dir'] != $project_dir) { $path = $request['base_project_path']; drush_move_dir($path . '/'. $project_dir, $path . '/' . $request['project_dir']); diff --git a/commands/pm/pm.drush.inc b/commands/pm/pm.drush.inc index 65bfc97..ed3e972 100644 --- a/commands/pm/pm.drush.inc +++ b/commands/pm/pm.drush.inc @@ -1542,9 +1542,9 @@ function drush_pm_update() { * Execute updatedb command after an updatecode - user requested `update`. */ function drush_pm_post_pm_update() { - // Use drush_backend_invoke to start a subprocess. Cleaner that way. + // Use drush_invoke_process_args to start a subprocess. Cleaner that way. if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) { - drush_backend_invoke('updatedb'); + drush_invoke_process('updatedb'); } } @@ -1575,7 +1575,7 @@ function drush_pm_post_pm_updatecode() { $command = drush_get_command(); if ($command['command'] != 'pm-update') { if (drush_get_context('DRUSH_PM_UPDATED', FALSE) !== FALSE) { - drush_backend_invoke('pm-updatecode-postupdate'); + drush_invoke_process('pm-updatecode-postupdate'); } } } diff --git a/commands/pm/updatecode.pm.inc b/commands/pm/updatecode.pm.inc index 9a29234..939b3d6 100644 --- a/commands/pm/updatecode.pm.inc +++ b/commands/pm/updatecode.pm.inc @@ -395,6 +395,7 @@ function pm_update_packages($update_info, $tmpfile) { return FALSE; } $project['base_project_path'] = dirname($project['full_project_path']); + // Check we have a version control system, and it clears its pre-flight. if (!$version_control->pre_update($project)) { return FALSE; diff --git a/commands/pm/version_control/backup.inc b/commands/pm/version_control/backup.inc index d46d490..1bcf4a5 100644 --- a/commands/pm/version_control/backup.inc +++ b/commands/pm/version_control/backup.inc @@ -43,7 +43,7 @@ class drush_pm_version_control_backup implements drush_pm_version_control { if (drush_get_option('no-backup', FALSE)) { return; } - if (drush_move_dir($project['backup_target'], $project['full_project_path'])) { + if (drush_move_dir($project['backup_target'], $project['full_project_path'], TRUE)) { return drush_log(dt("Backups were restored successfully."), 'ok'); } return drush_set_error('DRUSH_PM_BACKUP_ROLLBACK_FAILED', dt('Could not restore backup and rollback from failed upgrade. You will need to resolve manually.')); diff --git a/commands/sql/sql.drush.inc b/commands/sql/sql.drush.inc index 56c1e30..0bcdee1 100644 --- a/commands/sql/sql.drush.inc +++ b/commands/sql/sql.drush.inc @@ -375,6 +375,9 @@ function drush_sql_build_dump_command($table_selection, $db_spec = NULL) { $exec .= ' > '. $file; } break; + case 'sqlsrv': + $exec = "echo 'sqlsrv sql-dump not yet supported'"; + break; } if (drush_get_option('gzip')) { @@ -625,7 +628,7 @@ function _drush_sql_get_spec_from_options($prefix, $default_to_self = TRUE) { $remote_host = drush_get_option($prefix . 'remote-host'); if (!drush_is_local_host($remote_host)) { $db_spec['remote-host'] = $remote_host; - $db_spec['port'] = drush_get_option($prefix . 'remote-port', $db_spec['port']); + $db_spec['port'] = drush_get_option($prefix . 'remote-port', (isset($db_spec['port']) ? $db_spec['port'] : NULL)); } } @@ -753,7 +756,7 @@ function _drush_sql_get_credentials($db_spec = NULL) { $parameter_strings = array(); foreach ($parameters as $key => $value) { // Only escape the values, not the keys or the rest of the string. - $value = escapeshellcmd($value); + $value = drush_escapeshellarg($value); $parameter_strings[] = "--$key=$value"; } diff --git a/drush b/drush index 5bcd294..cfb7133 100755 --- a/drush +++ b/drush @@ -34,7 +34,7 @@ esac # we redirect stderr in any way, e.g. $(tput cols 2>/dev/null), then the # error message is suppressed, but tput cols becomes confused about the # terminal and prints out the default value (80). -if [ -z $COLUMNS ] && [ -n "$TERM" ] && [ "$TERM" != dumb ] ; then +if [ -z $COLUMNS ] && [ -n "$TERM" ] && [ "$TERM" != dumb ] && [ ! -z "`which tput`" ] ; then # Note to cygwin users: install the ncurses package to get tput command. if COLUMNS=$(tput cols); then export COLUMNS @@ -55,6 +55,10 @@ else php=`which php-cli` fi + # On MSYSGIT, we need to use "php", not the full path to php + if [ "x${MSYSTEM:0:5}" = "xMINGW" ] ; then + php="php" + fi fi # Check to see if the user has provided a php.ini file or drush.ini file in any conf dir @@ -70,13 +74,17 @@ done # Add in the php file location and/or the php override variables as appropriate if [ "x$drush_php_ini" != "x" ] ; then - php="$php --php-ini $drush_php_ini" + php_options="--php-ini $drush_php_ini" fi if [ "x$drush_php_override" != "x" ] ; then - drush_override_vars=`grep '^[a-z_A-Z0-9]\+ *=' $drush_php_override | sed -e 's|\([^ =]*\) *= *\(.*\)|\1="\2"|' -e 's| ||g' -e 's|^|-d |' | tr '\n\r' ' '` - php="$php $drush_override_vars" + php_options=`grep '^[a-z_A-Z0-9]\+ *=' $drush_php_override | sed -e 's|\([^ =]*\) *= *\(.*\)|\1="\2"|' -e 's| ||g' -e 's|^|-d |' | tr '\n\r' ' '` fi # Pass in the path to php so that drush knows which one -# to use if it re-launches itself to run subcommands -exec $php "$SCRIPT_PATH" "$@" --php="$php" +# to use if it re-launches itself to run subcommands. We +# will also pass php options if any are defined. +if [ -z "$php_options" ] ; then + exec "$php" $php_options "$SCRIPT_PATH" "$@" --php="$php" +else + exec "$php" $php_options "$SCRIPT_PATH" "$@" --php="$php" --php-options="$php_options" +fi diff --git a/drush.bat b/drush.bat index d2583a9..dcfcf49 100644 --- a/drush.bat +++ b/drush.bat @@ -1,3 +1,3 @@ @echo off REM See http://drupal.org/node/506448 for more information. -@php.exe "%~dp0drush.php" %* +@php.exe "%~dp0drush.php" %* --php="php.exe" diff --git a/includes/backend.inc b/includes/backend.inc index 4e5e372..f26743a 100644 --- a/includes/backend.inc +++ b/includes/backend.inc @@ -268,7 +268,9 @@ function _drush_proc_open($cmd, $data = NULL, $context = NULL) { * Invoke a drush backend command. * * @param command - * A defined drush command such as 'cron', 'status' or any of the available ones such as 'drush pm'. + * A defined drush command such as 'cron', 'status' or so on. + * @param args + * An array of command arguments. * @param data * Optional. An array containing options to pass to the call. Common options would be 'uri' if you want to call a command * on a different site, or 'root', if you want to call a command using a different Drupal installation. @@ -293,27 +295,14 @@ function _drush_proc_open($cmd, $data = NULL, $context = NULL) { * @param username * Optional. Defaults to the current user. If you specify this, you can choose which module to send. * - * @deprecated Command name includes arguments, and these are not quote-escaped in any way. - * @see drush_backend_invoke_args and @see drush_invoke_process for better options. + * @deprecated Prefer wrapper function @see drush_invoke_process when possible. + * n.b. the function drush_backend_invoke is now obsolete. * * @return * If the command could not be completed successfully, FALSE. * If the command was completed, this will return an associative array containing the data from drush_backend_output(). */ -function drush_backend_invoke($command, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL) { - $args = explode(" ", $command); - $command = array_shift($args); - return drush_backend_invoke_args($command, $args, $data, $method, $integrate, $drush_path, $hostname, $username); -} - -/** - * A variant of drush_backend_invoke() which specifies command and arguments separately. - * - * @deprecated Is not as extensible as drush_backend_invoke_sitealias; @see http://drupal.org/node/766080 - * @see drush_invoke_sitealias_args() and @see drush_invoke_process and - * @see drush_backend_invoke_sitealias() for better options. - */ -function drush_backend_invoke_args($command, $args, $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL, $ssh_options = NULL) { +function drush_backend_invoke_args($command, $args = array(), $data = array(), $method = 'GET', $integrate = TRUE, $drush_path = NULL, $hostname = NULL, $username = NULL, $ssh_options = NULL) { $cmd = _drush_backend_generate_command($command, $args, $data, $method, $drush_path, $hostname, $username, $ssh_options); return _drush_backend_invoke($cmd, $data, array_key_exists('#integrate', $data) ? $data['#integrate'] : $integrate); } @@ -519,10 +508,12 @@ function _drush_backend_generate_command($command, $args, &$data, $method = 'GET */ function _drush_backend_generate_command_sitealias($site_record, $command, $args, &$data, $method = 'GET') { $drush_path = null; + $php = ''; $hostname = array_key_exists('remote-host', $site_record) ? $site_record['remote-host'] : null; $username = array_key_exists('remote-user', $site_record) ? $site_record['remote-user'] : null; $ssh_options = array_key_exists('ssh-options', $site_record) ? $site_record['ssh-options'] : null; + $os = drush_os($site_record); $drush_path = NULL; if (array_key_exists('path-aliases', $site_record)) { @@ -535,11 +526,16 @@ function _drush_backend_generate_command_sitealias($site_record, $command, $args $hostname = null; } - $drush_path = !is_null($drush_path) ? $drush_path : (is_null($hostname) ? DRUSH_COMMAND : 'drush'); // Call own drush.php file on local machines, or 'drush' on remote machines. + // If the caller did not pass in a specific path to drush, then we will + // use a default value. For commands that are being executed on the same + // machine, we will use DRUSH_COMMAND, which is the path to the drush.php + // that is running right now. For remote commands, we will run a wrapper + // script instead of drush.php -- drush.bat on Windows, or drush on Linux. + $drush_command = drush_build_drush_command($drush_path, array_key_exists('php', $data) ? $data['php'] : NULL, $os, !empty($hostname)); + $data['root'] = array_key_exists('root', $data) ? $data['root'] : drush_get_context('DRUSH_DRUPAL_ROOT'); $data['uri'] = array_key_exists('uri', $data) ? $data['uri'] : drush_get_context('DRUSH_URI'); - $os = drush_os($site_record); $option_str = _drush_backend_argument_string($data, $method, $os); foreach ($data as $key => $arg) { if (is_numeric($key)) { @@ -550,16 +546,15 @@ function _drush_backend_generate_command_sitealias($site_record, $command, $args foreach ($args as $arg) { $command .= ' ' . drush_escapeshellarg($arg, $os); } - $interactive = ' ' . (empty($data['#interactive']) ? '' : ' > `tty`') . ' 2>&1'; - // @TODO: Implement proper multi platform / multi server support. - $cmd = escapeshellcmd($drush_path) . " " . $option_str . " " . $command . (empty($data['#interactive']) ? " --backend" : ""); - + $cmd = $drush_command . " " . $option_str . " " . $command . (empty($data['#interactive']) ? " --backend" : ""); if (!is_null($hostname)) { - $username = (!is_null($username)) ? drush_escapeshellarg($username, $os) . "@" : ''; + $username = (!is_null($username)) ? drush_escapeshellarg($username, "LOCAL") . "@" : ''; $ssh_options = (!is_null($ssh_options)) ? $ssh_options : drush_get_option('ssh-options', "-o PasswordAuthentication=no"); - $cmd = "ssh " . $ssh_options . " " . $username . drush_escapeshellarg($hostname, $os) . " " . drush_escapeshellarg($cmd . ' 2>&1', $os) . $interactive; + $cmd = "ssh " . $ssh_options . " " . $username . drush_escapeshellarg($hostname, "LOCAL") . " " . drush_escapeshellarg($cmd . ' 2>&1', "LOCAL") . ' 2>&1'; } else { + // TODO: `tty` is not usable on Windows. Is this necessary at all, and if so, is there a better way to do it? + $interactive = ' ' . ((drush_is_windows() || empty($data['#interactive'])) ? '' : ' > `tty`') . ' 2>&1'; $cmd .= $interactive; } @@ -664,6 +659,43 @@ function _drush_escape_option($key, $value = TRUE, $os = NULL) { */ function _drush_backend_get_stdin() { $fp = fopen('php://stdin', 'r'); + // Windows workaround: we cannot count on stream_get_contents to + // return if STDIN is reading from the keyboard. We will therefore + // check to see if there are already characters waiting on the + // stream (as there always should be, if this is a backend call), + // and if there are not, then we will exit. + // This code prevents drush from hanging forever when called with + // --backend from the commandline; however, overall it is still + // a futile effort, as it does not seem that backend invoke can + // successfully write data to that this function can read, + // so the argument list and command always come out empty. :( + // Perhaps stream_get_contents is the problem, and we should use + // the technique described here: + // http://bugs.php.net/bug.php?id=30154 + // n.b. the code in that issue passes '0' for the timeout in stream_select + // in a loop, which is not recommended. + // Note that the following DOES work: + // drush ev 'print(json_encode(array("test" => "XYZZY")));' | drush status --backend + // So, redirecting input is okay, it is just the proc_open that is a problem. + if (drush_is_windows()) { + // Note that stream_select uses reference parameters, so we need variables (can't pass a constant NULL) + $read = array($fp); + $write = NULL; + $except = NULL; + // Question: might we need to wait a bit for STDIN to be ready, + // even if the process that called us immediately writes our parameters? + // Passing '100' for the timeout here causes us to hang indefinitely + // when called from the shell. + $changed_streams = stream_select($read, $write, $except, 0); + // Return on error (FALSE) or no changed streams (0). + // Oh, according to http://php.net/manual/en/function.stream-select.php, + // stream_select will return FALSE for streams returned by proc_open. + // That is not applicable to us, is it? Our stream is connected to a stream + // created by proc_open, but is not a stream returned by proc_open. + if ($changed_streams < 1) { + return FALSE; + } + } stream_set_blocking($fp, FALSE); $string = stream_get_contents($fp); fclose($fp); diff --git a/includes/batch.inc b/includes/batch.inc index f0fd8d9..5260046 100644 --- a/includes/batch.inc +++ b/includes/batch.inc @@ -48,9 +48,9 @@ * use their own command. * */ -function drush_backend_batch_process($command = 'batch-process') { +function drush_backend_batch_process($command = 'batch-process', $args = array()) { drush_include_engine('drupal', 'batch', drush_drupal_major_version()); - _drush_backend_batch_process($command); + _drush_backend_batch_process($command, $args); } /** diff --git a/includes/command.inc b/includes/command.inc index beebf6a..b6a586a 100644 --- a/includes/command.inc +++ b/includes/command.inc @@ -220,6 +220,7 @@ function drush_dispatch($command, $arguments = array()) { /** * Invoke a command in a new process. + * Prefer this to drush_backend_invoke("command") [n.b. drush_backend_invoke is obsolete] * * @param command_name * The drush command to execute. @@ -237,6 +238,7 @@ function drush_invoke_process($command_name) { /** * Invoke a command in a new process. + * Prefer this to drush_backend_invoke_args(...); * * @param command_name * The drush command to execute. diff --git a/includes/drush.inc b/includes/drush.inc index 4957f63..6be7a31 100644 --- a/includes/drush.inc +++ b/includes/drush.inc @@ -699,21 +699,6 @@ function drush_op($function) { } /** - * Check if the operating system is Windows. - */ -function drush_is_windows($os = NULL) { - if (!isset($os)) { - $os = PHP_OS; - } - if (strtoupper(substr($os, 0, 3)) == 'WIN') { - return TRUE; - } - else { - return FALSE; - } -} - -/** * Download a file using wget or curl. * * @param string $url diff --git a/includes/environment.inc b/includes/environment.inc index 08850f5..9cf732b 100644 --- a/includes/environment.inc +++ b/includes/environment.inc @@ -1284,27 +1284,123 @@ function drush_valid_db_credentials() { * Note that the $_ global is defined only in bash and therefore cannot * be relied upon. * - * We will therefore assume PHP is available in the path and is named - * "php" for execute ourselves. That is, the #!/usr/bin/env php is - * working and valid, unless a PHP constant is defined, which can be - * done by the shell wrapper. - * * The DRUSH_COMMAND constant is initialised to the value of this * function when environment.inc is loaded. * * @see DRUSH_COMMAND */ function drush_find_drush() { - $php = drush_get_option('php'); - if (isset($php)) { - $drush = $php . " " . realpath($_SERVER['argv'][0]) . " --php='$php'"; - } else { - $drush = realpath($_SERVER['argv']['0']); - } + $drush = realpath($_SERVER['argv']['0']); + // TODO: On Windows, if we leave $drush as-is, then callbacks will + // be done just as we were called by the batch file: php.exe C:\path\drush.php + // We could also convert drush.php to drush.bat to run the batch file again, + // but this works just as well. return $drush; } /** + * Build a drush command suitable for use for drush to call itself + * e.g. in backend_invoke. + */ +function drush_build_drush_command($drush_path = NULL, $php = NULL, $os = NULL, $remote_command = FALSE) { + $os = _drush_get_os($os); + $additional_options = ''; + if (is_null($drush_path)) { + if (!$remote_command) { + $drush_path = DRUSH_COMMAND; + } + else { + $drush_path = drush_is_windows($os) ? 'drush.bat' : 'drush'; + } + } + // If the path to drush points to drush.php, then we will need to + // run it via php rather than direct execution. By default, we + // will use 'php' unless something more specific was passed in + // via the --php flag. + if (substr($drush_path, -4) == ".php") { + if (!isset($php)) { + $php = drush_get_option('php'); + if (!isset($php)) { + $php = 'php'; + } + } + if (isset($php) && ($php != "php")) { + $additional_options .= ' --php=' . drush_escapeshellarg($php, $os); + } + // We will also add in the php options from --php-options + $php = drush_escapeshellarg($php, $os) . ' '; + $php_options = drush_get_option('php-options',''); + if (!empty($php_options)) { + $php .= $php_options . ' '; + $additional_options .= ' --php-options=' . drush_escapeshellarg($php_options, $os); + } + } + else { + $php = ''; + } + return $php . drush_escapeshellarg($drush_path, $os) . $additional_options; +} + +/** + * Check if the operating system is Windows. + * This will return TRUE under DOS, Powershell + * Cygwin and MSYSGIT shells, so test for the + * Windows variant FIRST if you care. + */ +function drush_is_windows($os = NULL) { + return _drush_test_os($os, array("WIN","CYGWIN","MINGW")); +} + +/** + * Check if the operating system is Winodws + * running some variant of cygwin -- either + * Cygwin or the MSYSGIT shell. If you care + * which is which, test mingw first. + */ +function drush_is_cygwin($os = NULL) { + return _drush_test_os($os, array("CYGWIN","MINGW")); +} + +function drush_is_mingw($os = NULL) { + return _drush_test_os($os, array("MINGW")); +} + +/** + * Return the OS we are running under. + * + * @return string + * Linux + * WIN* (e.g. WINNT) + * CYGWIN + * MINGW* (e.g. MINGW32) + */ +function _drush_get_os($os = NULL) { + // We allow "LOCAL" to document, in instances where some parameters are being escaped + // for use on a remote machine, that one particular parameter will always be used on + // the local machine (c.f. drush_backend_invoke). + if (isset($os) && ($os != "LOCAL")) { + return $os; + } + if (_drush_test_os(getenv("MSYSTEM"), array("MINGW"))) { + return getenv("MSYSTEM"); + } + // QUESTION: Can we differentiate between DOS and POWERSHELL? They appear to have the same environment. + // At the moment, it does not seem to matter; they behave the same from PHP. + // At this point we will just return PHP_OS. + return PHP_OS; +} + +function _drush_test_os($os, $os_list_to_check) { + $os = _drush_get_os($os); + foreach ($os_list_to_check as $test) { + if (strtoupper(substr($os, 0, strlen($test))) == strtoupper($test)) { + return TRUE; + } + } + return FALSE; +} + +/** * Read the drush info file. */ function drush_read_drush_info() { diff --git a/includes/exec.inc b/includes/exec.inc index d7bfe63..c00154a 100644 --- a/includes/exec.inc +++ b/includes/exec.inc @@ -159,7 +159,8 @@ function drush_os($site_record = NULL) { $os = $site_record['os']; } // Otherwise, we will assume that all remote machines are Linux - elseif (isset($site_record) && array_key_exists('remote-host', $site_record)) { + // (or whatever value 'remote-os' is set to in drushrc.php). + elseif (isset($site_record) && array_key_exists('remote-host', $site_record) && !empty($site_record['remote-host'])) { $os = drush_get_option('remote-os', 'Linux'); } @@ -167,48 +168,102 @@ function drush_os($site_record = NULL) { } /** - * Platform-independent version of escapeshellarg(). - * This only works for local commands. - * TODO: Make a unified drush_escapeshellarg - * that works on Linux and Windows. + * Make an attempt to simply wrap the arg with the + * kind of quote characters it does not already contain. + * If it contains both kinds, or if it contains no quote + * characters, then this function reverts to drush_escapeshellarg. + * + * Note that this routine is only useful in certain very + * specific circumstances (e.g. core-cli), as in general, + * Windows -must- use double-quotes to escape a shell arg. + */ +function drush_wrap_with_quotes($arg) { + $has_double = strpos($arg, '"') !== FALSE; + $has_single = strpos($arg, "'") !== FALSE; + // If there are both kinds of quotes ($has_double == TRUE && $has_single == TRUE) + // or there are neither kind of quotes ($has_double == FALSE && $has_single == FALSE) + // then we call drush_escapeshellarg. The relations above logically reduce to: + if ($has_double == $has_single) { + return drush_escapeshellarg($arg); + } + elseif ($has_double) { + return "'" . $arg . "'"; + } + else { + return '"' . $arg . '"'; + } +} + +/** + * Platform-dependent version of escapeshellarg(). + * Given the target platform, return an appropriately-escaped + * string. The target platform may be omitted for args that + * are /known/ to be for the local machine. */ function drush_escapeshellarg($arg, $os = NULL) { - if (drush_is_windows($os)) { + // Short-circuit escaping for simple params (keep stuff readable) + if (preg_match('|^[a-zA-Z0-9.:/_-]*$|', $arg)) { + return $arg; + } + elseif (drush_is_windows($os)) { return _drush_escapeshellarg_windows($arg); } else { - return escapeshellarg($arg); + $escaped = _drush_escapeshellarg_linux($arg); + // We expect that our escapeshellarg should return exactly the + // same thing that escapeshellarg does on Linux; to be conservative, + // though, we will compare and correct against the reference implementation, + // and remove this check once everything works perfectly. + if (!drush_is_windows()) { + $compare_escaped = escapeshellarg($arg); + if ($compare_escaped != $escaped) { + drush_log(dt("Error in _drush_escapeshellarg_linux;\nReturned: !escaped\nExpected: !expected", array('!escaped' => $escaped, '!expected' => $compare_escaped)), 'debug'); + $escaped = $compare_escaped; + } + } + return $escaped; } } /** * Windows version of escapeshellarg(). - * - * @deprecated escapeshellarg needs to be cross-platform, - * because drush does not always know in advance whether an - * escaped arg will be used locally or on a remote system. - * See http://drupal.org/node/766080 */ function _drush_escapeshellarg_windows($arg) { - // Double the backslashes before any double quotes. Escape the double quotes. - // (\" => \\\") && (" => \") = - // (\" => \\") + - $arg = preg_replace('/\\\"/', '\\\\\\"', $arg); - // + (" => \") + // Double up existing backslashes + $arg = preg_replace('/\\\/', '\\\\\\\\', $arg); + + // Escape double quotes. $arg = preg_replace('/"/', '\\"', $arg); - // The same with single quotes. - // (\' => \\\') && (' => \') = - // (\' => \\') + - $arg = preg_replace('/\\\'/', '\\\\\\\'', $arg); - // + (' => \') + // Escape single quotes. $arg = preg_replace('/\'/', '\\\'', $arg); + // Add surrounding quotes. + $arg = '"' . $arg . '"'; + + return $arg; +} + +/** + * Linux version of escapeshellarg(). + * + * This is intended to work the same way that escapeshellarg() does on + * Linux. If we need to escape a string that will be used remotely on + * a Linux system, then we need our own implementation of escapeshellarg, + * because the Windows version behaves differently. + */ +function _drush_escapeshellarg_linux($arg) { + // For single quotes existing in the string, we will "exit" + // single-quote mode, add a \' and then "re-enter" + // single-quote mode. The result of this is that + // 'quote' becomes '\''quote'\'' + $arg = preg_replace('/\'/', '\'\\\'\'', $arg); + // Replace "\t", "\n", "\r", "\0", "\x0B" with a whitespace. $arg = str_replace(array("\t", "\n", "\r", "\0", "\x0B"), ' ', $arg); // Add surrounding quotes. - $arg = '"' . $arg . '"'; + $arg = "'" . $arg . "'"; return $arg; } diff --git a/includes/filesystem.inc b/includes/filesystem.inc index bced915..07082f9 100644 --- a/includes/filesystem.inc +++ b/includes/filesystem.inc @@ -5,6 +5,63 @@ * @{ */ + /** + * Determines whether the provided path is absolute or not + * on the specified O.S. -- starts with "/" on *nix, or starts + * with "[A-Z]:\" or "[A-Z]:/" on Windows. + */ +function drush_is_absolute_path($path, $os = NULL) { + if (drush_is_cygwin($os)) { + return preg_match('@/cygdrive/@', $path) || drush_is_absolute_path($path, "WINDOWS"); + } + elseif (drush_is_windows($os)) { + return preg_match('@^[a-zA-Z]:[\\\/]@', $path); + } + else { + return $path[0] == '/'; + } +} + +/** + * If we are going to pass a path to exec or proc_open, + * then we need to fix it up under CYGWIN or MINGW. In + * both of these environments, PHP works with absolute paths + * such as "C:\path". CYGWIN expects these to be converted + * to "/cygdrive/c/path" and MINGW expects these to be converted + * to "/c/path"; otherwise, the exec will not work. + * + * This call does nothing if the parameter is not an absolute + * path, or we are not running under CYGWIN / MINGW. + * + * UPDATE: It seems I was mistaken; this is only necessary if we + * are using cwRsync. We do not need to correct every path to + * exec or proc_open (thank god). + */ +function drush_correct_absolute_path_for_exec($path, $os = NULL) { + if (drush_is_windows() && drush_is_absolute_path($path, "WINNT")) { + if (drush_is_mingw($os)) { + $path = preg_replace('/(\w):/', '/${1}', str_replace('\\', '/', $path)); + } + elseif (drush_is_cygwin($os)) { + $path = preg_replace('/(\w):/', '/cygdrive/${1}', str_replace('\\', '/', $path)); + } + } + return $path; +} + +/** + * Remove the trailing DIRECTORY_SEPARATOR from a path. + * Will actually remove either / or \ on Windows. + */ +function drush_trim_path($path, $os = NULL) { + if (drush_is_windows($os)) { + return rtrim($path, '/\\'); + } + else { + return rtrim($path, '/'); + } +} + /** * Deletes the provided file or folder and everything inside it. * @@ -161,9 +218,13 @@ function drush_move_dir($src, $dest, $overwrite = FALSE) { * @see http://theserverpages.com/php/manual/en/function.mkdir.php#50383 */ function drush_mkdir($path) { + // On Windows, the directory separator must be a backslash for creating directories. + if (drush_is_windows()) { + $path = str_replace('/', '\\', $path); + } return is_dir($path) || (drush_mkdir(dirname($path)) && drush_shell_exec('mkdir %s', $path)); } - + /** * Save a string to a temporary file. Does not depend on Drupal's API. * The temporary file will be automatically deleted when drush exits. @@ -253,7 +314,7 @@ function drush_tempnam($pattern, $tmp_dir = NULL) { * Creates a temporary directory and return its path. */ function drush_tempdir() { - $tmp_dir = rtrim(drush_find_tmp(), DIRECTORY_SEPARATOR); + $tmp_dir = drush_trim_path(drush_find_tmp()); $tmp_dir .= '/' . 'drush_tmp_' . time(); drush_mkdir($tmp_dir); @@ -324,7 +385,7 @@ function drush_preflight_backup_dir($subdir = NULL) { $date = gmdate('YmdHis', $_SERVER['REQUEST_TIME']); $backup_dir = drush_get_option('backup-dir', drush_server_home() . '/' . 'drush-backups'); - $backup_dir = rtrim($backup_dir, DIRECTORY_SEPARATOR) . '/' . $subdir . '/' . $date; + $backup_dir = drush_trim_path($backup_dir) . '/' . $subdir . '/' . $date; drush_set_context('DRUSH_BACKUP_DIR', $backup_dir); } return $backup_dir; diff --git a/includes/sitealias.inc b/includes/sitealias.inc index ee5be71..f526df4 100644 --- a/includes/sitealias.inc +++ b/includes/sitealias.inc @@ -1419,8 +1419,23 @@ function drush_sitealias_set_alias_context($site_alias_settings, $prefix = '') { * local site in $path are defined. */ function _drush_sitealias_preflight_path($path) { + // Special handling of absolute paths on Windows: if + // we are on Windows, then we must be using cwRsync. + // Convert absolute paths from "C:\path" to "/cygdrive/C/path", + // because cwRsync expects this, even when running under + // DOS or Powershell. + if (drush_is_windows() && drush_is_absolute_path($path)) { + $path = preg_replace('/(\w):/', '/cygdrive/${1}', str_replace('\\', '/', $path)); + } $alias = NULL; // Parse site aliases if there is a colon in the path + // We allow: + // @alias:/path + // machine.domain.com:/path + // machine:/path + // However, we assume that + // c:/path + // is a Windows full path (no single-letter machine names allowed) $colon_pos = strpos($path, ':'); if ($colon_pos !== FALSE) { $alias = substr($path, 0, $colon_pos); @@ -1574,20 +1589,18 @@ function drush_sitealias_evaluate_path($path, &$additional_options, $local_only if (empty($drupal_root)) { $drupal_root = ''; } - // Add a slash to the end of the drupal root, as below. - elseif ($drupal_root[strlen($drupal_root)-1] != '/') { - $drupal_root = $drupal_root . '/'; + else { + // Add a slash to the end of the drupal root, as below. + $drupal_root = drush_trim_path($drupal_root) . "/"; } $full_path_aliases = $path_aliases; foreach ($full_path_aliases as $key => $value) { // Expand all relative path aliases to be based off of the Drupal root - if ((substr($value, 0, 1) != '/') && ($key != '%root')) { + if (!drush_is_absolute_path($value, $os) && ($key != '%root')) { $full_path_aliases[$key] = $drupal_root . $value; } // We do not want slashes on the end of our path aliases. - if (substr($value, 0, -1) == '/') { - $full_path_aliases[$key] = substr($full_path_aliases[$key], -1); - } + $full_path_aliases[$key] = drush_trim_path($full_path_aliases[$key]); } // Fill in path aliases in the path, the include path and the exclude path. @@ -1602,7 +1615,9 @@ function drush_sitealias_evaluate_path($path, &$additional_options, $local_only // Next make the rsync path, which includes the machine // and path components together. // First make empty paths or relative paths start from the drupal root. - if (empty($path) || ($path[0] != '/')) { + // running on cygwin because we might have converted a Windows C:/path + // into a /cygdrive path. @see _drush_sitealias_preflight_path. + if (empty($path) || (!drush_is_absolute_path($path, $os) && (!drush_is_absolute_path($path, "cygwin")))) { $path = $drupal_root . $path; }