Статьи

PHP в командной строке — часть 2

В первой части этого руководства мы рассмотрели командную строку PHP SAPI (серверный API). В этой статье мы пойдем дальше и увидим, как вы можете подключить PHP-скрипт командной строки к существующим инструментам командной строки, предоставляемым вашей операционной системой.

В отличие от предыдущей статьи, в центре внимания этой статьи будут системы на основе Unix — то, чего нельзя избежать. Чтобы украсть цитату из CLI Linux.com для серии Noobies :

«[Сравнение] среды командной строки DOS / Windows с Linux — это все равно, что сравнивать тачку с 18-колесным колесом».

Я предполагаю, что вы используете PHP 4.3.x + с CLI SAPI для выполнения сценариев PHP в командной строке. Представленные примеры предполагают найти двоичный файл CLI в /usr/local/bin/php . Если ваша настройка отличается от этой, изменение первой строки должно исправить ситуацию в каждом случае. Обратите внимание, что вы можете скачать полный архив кода для этого урока здесь .

Сегодняшние параметры командной строки:

  • Shell Execution: запускать внешние программы из PHP
  • Вопросы безопасности: террористы угрожают командной строке!
  • Работа с окружающей средой: поговорите с Mother Operating System
  • Управление процессом: это убивает меня
Shell Execution

Одним из преимуществ работы на Unix-системах является множество инструментов командной строки, которые доступны для решения большинства распространенных проблем, о которых вы только можете подумать.

Это становится особенно важным, когда вы пишете сценарии PHP для командной строки. Например, для поиска и замены по группе текстовых файлов лучше всего использовать такие инструменты, как grep и sed , как для повышения производительности, так и для экономии времени на разработку. Это не значит, что вы не должны использовать PHP в качестве «внешнего интерфейса» для выполнения операций поиска и замены, но вы должны по возможности делегировать «реальную работу» существующим инструментам.

PHP предоставляет ряд функций для выполнения внешних программ через оболочку, а именно: shell_exec () , passthru () , exec () , popen () , которые в сущности выполняют одно и то же, но предоставляют разные «интерфейсы» для внешней программы. , Каждая из этих функций может захватывать только выходные данные, записанные в канал STDOUT ; каждый будет порождать дочерний процесс, в рамках которого будет выполняться внешняя программа.

Существуют еще две функции для выполнения внешних программ: proc_open () и pcntl_exec () . Их использование и поведение отличается от предыдущих функций, поэтому мы рассмотрим их отдельно.

Какое у меня расширение?

Чтобы дать нам «известное количество», которое мы можем выполнить с другими сценариями PHP, сначала мы рассмотрим сценарий, который проверяет расширения PHP, доступные в любой данной установке. Я позволю коду объясниться; все, что вы увидите здесь, вы видели в прошлой статье:

 #!/usr/local/bin/php  <?php  # Include PEAR::Console_Getopt  require_once 'Console/Getopt.php';   // Define error return codes  define('INVALID_PHP_SAPI',3);  define('UNKNOWN_EXTENSION',4);   //--------------------------------------------------------------------------------------  /**  * Displays the usage of this script  * Called when -h option specified or on illegal option  */  function usage() {  $usage = <<<EOD  Usage: ./extensions.php [OPTION]  Lists the loaded PHP extensions or shows the functions  a single extension makes available  -e=EXTENSION  Name of extension to list functions for  -h    Display usage   EOD;  fwrite(STDOUT,$usage);  exit(0);  }   //--------------------------------------------------------------------------------------  /**  * Gets the name of an extension from -e option  * (defaults to NULL)  */  function getExtension() {   $args = Console_Getopt::readPHPArgv();   // Could be an error with older PHP versions and the CGI SAPI  if ( PEAR::isError($args) ) {    fwrite(STDERR,$args->getMessage()."n");    exit(INVALID_PHP_SAPI);  }   // Compatibility between "php extensions.php" and "./extensions.php"  if ( realpath($_SERVER['argv'][0]) == __FILE__ ) {    $options = Console_Getopt::getOpt($args,'he:');  } else {    $options = Console_Getopt::getOpt2($args,'he:');  }   // Check for invalid options  if ( PEAR::isError($options) ) {    fwrite(STDERR,$options->getMessage()."n");    usage();  }   // Set default length  $extension = NULL;   // Loop through the user provided options  foreach ( $options[0] as $option ) {    switch ( $option[0] ) {      case 'h':        usage();      break;      case 'e':        $extension = $option[1];      break;    }  }   return $extension;  }   //--------------------------------------------------------------------------------------  // Get a list of extensions  $extensions = get_loaded_extensions();   $extension = getExtension();   // If it's not null, the -e option was used  if ( !is_null($extension) ) {   // Does the extension actually exist?  if ( in_array($extension,$extensions) ) {     // Display the list of functionsthe extension provides    $funcs = get_extension_funcs($extension);    foreach ( $funcs as $func ) {      fwrite(STDOUT,$func."n");    }   } else {     fwrite(STDERR,"Unknown extension $extensionn");    exit(UNKNOWN_EXTENSION);   }   } else {   // Display the list of extension  foreach ( $extensions as $extension ) {    fwrite(STDOUT, $extension."n");  }   }   exit(0);  ?> 

Имя файла: extensions.php

Код выполняется с помощью этой команды:

 $ ./extensions.php 

При выполнении вышеприведенный скрипт по умолчанию отображает список загруженных расширений PHP. Однако, если указана опция командной строки ‘-e’ вместе с именем расширения, в нем перечислены функции, которые предоставляет расширение.

 $ ./extensions.php -e mysql 

Приведенная выше команда отображает список функций, предоставляемых расширением MySQL (или любым именем расширения, которое вы указали).

Не забывайте, что вам нужно сделать исполняемый скрипт, установив разрешения на исправление следующим образом:

 $ chmod +x extensions.php 

Теперь, когда у меня есть простая программа для работы, я могу выполнить ее из другого скрипта.

shell_exec ()

Команда shell_exec () фактически является псевдонимом для оператора backtick . Это позволяет вам выполнять внешнюю программу через оболочку и получать результаты в виде строки.

Используете ли вы оператор backtick или команду shell_exec() зависит только от вас.

Лично я предпочитаю последнее, так как оно четко указывает, что делает ваш код, и его легко увидеть в исходном коде.

Вот основной пример:

 #!/usr/local/bin/php  <?php  $result = shell_exec('./extensions.php');  fwrite(STDOUT,$result);  exit(0);  ?> 

Имя файла: shell_exec1.php

Как вы можете видеть, используя shell_exec() , я могу просто назвать внешнюю программу, которую я хочу выполнить, и получить результаты обратно в возвращаемой переменной. Для работы приведенного выше примера есть одно важное требование — сценарий shell_exec1.php должен выполняться из каталога, в котором находится файл extensions.php . Мы посмотрим на этот момент дальше, когда будем рассматривать окружающую среду ниже.

Мы также можем передать параметры командной строки через shell_exec (). Давайте посмотрим на пример:

 #!/usr/local/bin/php  <?php  $result = shell_exec('./extensions.php -e mysql');  fwrite(STDOUT,$result);  exit(0);  ?> 

Имя файла: shell_exec2.php

Другими словами, команда, которую мы говорим shell_exec() для выполнения, может быть чем угодно, что мы можем набрать сами из командной строки. Например, я могу направить вывод grep extensions.php через grep , отфильтровывая все расширения, начинающиеся с буквы «p»:

 #!/usr/local/bin/php  <?php  $result = shell_exec('./extensions.php | grep -w "^p.*"');  fwrite(STDOUT,$result);  exit(0);  ?> 

Имя файла: shell_exec3.php

Это возвращает следующий список (из моей системы):

 posix  pgsql  pcre  pcntl 

Одна важная вещь, которую следует отметить в отношении shell_exec() это то, что он возвращает только вывод из STDOUT . Посмотрите на этот пример:

 #!/usr/local/bin/php  <?php  $result = shell_exec('./extensions.php -e hair 2>/dev/null');   // Assumes that nothing was written to STDOUT if there was an error  if ( !empty($result) ) {  fwrite(STDOUT,$result);  } else {  fwrite(STDERR,"An error occurredn");  }  exit(0);  ?> 

Имя файла: shell_exec4.php

Здесь я перенаправил поток STDERR на NULL-устройство Unix (я его выбрасываю). Если я выполняю свой скрипт extensions.php с именем расширения, которое не существует, в STDOUT ничего не записывается, поэтому приведенный выше скрипт может отображать (несколько бесполезное) сообщение о том, что произошла ошибка.

Обратите внимание, что если я не перенаправлю STDERR на нулевое устройство, я все равно увижу сообщение об ошибке («Неизвестное расширение волоска») из моей оболочки, ошибка с «обходом» shell_exec4.php .

пройти через()

Функция passthru() позволяет вам запускать внешнюю программу и напрямую отображать ее результаты:

 #!/usr/local/bin/php  <?php  passthru('./extensions.php');  exit(0);  ?> 

Имя файла: passthru1.php

Это все, что вам нужно сделать ( passthru() ничего не возвращает).

Однако для этого требуется второй аргумент — переменная, которая заполняется кодом возврата внешней программы. Например:

 #!/usr/local/bin/php  <?php  $return_code = 0;   passthru('./extensions.php -e hair 2>/dev/null',$return_code);   if ( $return_code != 0 ) {  fwrite(STDERR,"There was an error!n");  }  exit(0);  ?> 

Имя файла: passthru2.php

Это дает мне более точный механизм проверки ошибок, чем shell_exec() , с помощью которого я был вынужден предположить, что в STDOUT ничего не будет записано в случае возникновения ошибки.

Exec ()

Функция exec () предоставляет еще один вариант темы. Он возвращает последнюю строку вывода из внешней программы, но также (необязательно) заполняет массив полным выводом и делает код возврата доступным. Если я предполагаю, что большинство программ будет возвращать однострочное сообщение об ошибке, если оно действительно было, exec() может быть очень удобным. Давайте посмотрим на другой пример:

 #!/usr/local/bin/php  <?php  $return_code = 0;  $result = array();   $error = exec('./extensions.php -e hair 2>&1',$result, $return_code);   if ( $return_code != 0 ) {  fwrite(STDERR,"Error: $errorn");  } else {  $result = implode("n",$result);  fwrite(STDOUT,$result);  }  exit(0);  ?> 

Имя файла: exec1.php

Сначала я перенаправляю поток STDERR в STDOUT , чтобы можно было захватить оба из моего скрипта. Я использую переменную $error только если $return_code не равен нулю. В противном случае я могу отобразить содержимое массива $result .

Система ()

Функция system () предоставляет еще один вариант темы — что-то между passthru() и exec() . Как и passthru() , он также выводит все, что получает от внешней программы, в потоке STDOUT . Однако, как и exec() , он также возвращает вывод последней строки и делает код возврата доступным. Например:

 #!/usr/local/bin/php  <?php  $return_code = 0;  $result = array();   $error = system('./extensions.php -e hair 2>&1',$return_code);   if ( $return_code != 0 ) {  // Fictional logger  # Logger::log($error);  }  exit(0);  ?> 

Имя файла: system1.php

Важным отличием в этом коде является то, что, хотя сообщение об ошибке все равно будет немедленно отображаться в оболочке, у меня также есть возможность записать сообщение об ошибке.

POPEN ()

Функция popen () позволяет мне работать с внешней программой, как если бы я работал с файлом. Например:

 #!/usr/local/bin/php  <?php  $fp = popen('./extensions.php','r');   while ( !feof($fp) ) {  fwrite(STDOUT, fgets($fp));  }   pclose($fp);  exit(0);  ?> 

Имя файла: popen1.php

Как и во всех предыдущих функциях, popen() читает только из STDOUT . Таким образом, следующий пример ничего не отобразит:

 #!/usr/local/bin/php  <?php  $fp = popen('./extensions.php -e hair 2>/dev/null','r');   while ( !feof($fp) ) {  fwrite(STDOUT, fgets($fp));  }   pclose($fp);  exit(0);  ?> 

Имя файла: popen2.php

Особенностью popen() является то, что она также позволяет вам записывать в поток программы STDIN , как мы видели в прошлом уроке.

Давайте представим, что у меня есть такой простой скрипт:

 #!/usr/local/bin/php  <?php  while ( trim($line = fgets(STDIN)) != 'exit' ) {   fwrite(STDOUT,$line);   }   exit(0);  ?> 

Имя файла: reader.php

Если я сам выполняю этот скрипт, он просто отображает все, что я печатаю, построчно, пока я не введу строку «exit».

Теперь я могу написать в эту программу с помощью popen() следующим образом:

 #!/usr/local/bin/php  <?php  $colors = array('red','green','blue');   // Open program in write mode  $fp = popen('./reader.php','w');   foreach ( $colors as $color ) {  fwrite($fp,$color."n");  }   fwrite($fp,"exitn");   pclose($fp);  exit(0);  ?> 

Имя файла: popen3.php

Если я запустите этот скрипт, три цвета в массиве $colors будут отображаться на новых строках в моем терминале. Поскольку я открыл программу с помощью popen() в режиме записи , вместо захвата потока STDOUT , он использует вместо него STDIN , что позволяет результатам оставаться видимыми.

Обратите внимание, что popen() является однонаправленным. Вы можете только читать или писать во внешнюю программу, но не в обе (хотя в руководстве по PHP есть несколько интересных представленных хаков, чтобы обойти это).

В PHP 4.3.x + функция proc_open() позволяет работать сразу с несколькими proc_open() ввода-вывода, как мы увидим позже.

Проблемы с безопасностью

Программирование не доставило бы удовольствия, если бы не было постоянной угрозы «haXor ownZ you», не так ли? Что ж, хорошая новость заключается в том, что при выполнении внешних программ из PHP есть много возможностей, чтобы предоставить миру доступ к вашему внутреннему святилищу.

Инъекция команд

Выполнение внешних программ на PHP, в которых пользовательский ввод влияет на выполняемую программу, имеет параллель с выполнением операторов SQL: вы рискуете ввести команду .

Рассмотрим следующий пример, как этого не делать!

 #!/usr/local/bin/php  <?php  fwrite(STDOUT,"This is insecurity in action!n");   fwrite(STDOUT,"Enter a filename to list: ");   $file = trim(fgets(STDIN));   $result = shell_exec("ls -l $file");   fwrite(STDOUT, $result);   exit(0);  ?> 

Имя файла: security1.php

Видишь проблему? Представьте, что я, как пользователь сценария, ввожу что-то подобное в приглашении «Введите имя файла в список:»:

 Enter a filename to list: whatever.php; rm -rf ./; 

К сожалению, в этом каталоге больше нет файлов! Команда, которую я выполнил с помощью shell_exec() , на самом деле будет выглядеть так:

 $result = shell_exec("ls -l whatever.php; rm -rf ./;"); 

На самом деле я выполняю две команды!

Один из способов предотвратить возникновение этой проблемы в этом примере — поместить переменную $file в одинарные кавычки и удалить любые одиночные кавычки, которые мог добавить пользователь, например, так:

 $file = str_replace(''','',$file);  $result = shell_exec("ls -l '$file'"); 

Важное примечание: не используйте двойные кавычки для экранирования введенного пользователем значения, потому что это позволяет вставлять переменные окружения и тому подобное. См. Когда командная строка не строка? для обсуждения расширения параметров и общих проблем безопасности CLI, связанных с смешением с пользовательским вводом.

Гораздо лучший подход заключается в использовании PHP-функции escapeshellarg , которая обеспечивает правильное экранирование всех одинарных кавычек в строке и помещает одинарные кавычки в начало и конец строки.

Если мы возьмем такой скрипт:

 <?php  $arg = "foo 'bar' foo";  echo escapeshellarg($arg)."n";  ?> 

Отображаемый вывод будет выглядеть так, как показано ниже:

 'foo '''bar''' foo' 

Вы можете безопасно использовать этот вывод в качестве единственного аргумента для внешней программы, которую вы выполняете.

Другими словами, скрипт security1.php выше должен стать:

 #!/usr/local/bin/php  <?php  fwrite(STDOUT,"Enter a filename to list: ");   $file = trim(fgets(STDIN));   $file = escapeshellarg($file);   $result = shell_exec("ls -l $file");   fwrite(STDOUT, $result);   exit(0);  ?> 

Имя файла: security2.php

Другая функция, предоставляемая PHP — это escapeshellcmd () , которая экранирует метасимволы, такие как; с обратной косой чертой. Это обеспечивает альтернативу использованию одинарных кавычек, которые могут потребоваться в некоторых обстоятельствах. В целом, однако, лучше использовать escapeshellarg() если у вас нет особых потребностей.

Предупреждение для пользователей Windows: с PHP версии 4.3.6 и ниже небезопасно использовать escapeshellarg() и escapeshellcmd() . Смотрите это предупреждение на Security Tracker.

В целом, как и в случае с веб-приложениями, мы не должны просто полагаться на функции escape здесь; мы должны ограничивать доступ пользователей только к тому контенту, который они обязаны предоставлять. Например, если они должны вводить только строку, содержащую символы из алфавита, проверьте ввод с помощью простого регулярного выражения, например так:

 if ( !preg_match('/^[a-zA-Z]*$/', $userinput) ) {  exit(INVALID_INPUT);  } 

Общие хосты

Если вы уже не чувствуете достаточно паранойи, подумайте, что может произойти, если аргументы, передаваемые сценарию командной строки, содержат конфиденциальную информацию. Например, рассмотрите возможность выполнения следующего:

 $ mysqldump --user=harryf -password=secret somedatabase > somedatabase.sql 

Это делает мои имя пользователя и пароль доступными в списке процессов, запущенных в системе, и видимыми для всех с помощью команды ps . Для кого-то нетрудно написать программу, которая автоматически собирает такую ​​информацию.

Та же самая проблема также применяется, когда мы выполняем программы из сценария PHP.

Чтобы продемонстрировать, вот пример, который генерирует «долгое ожидание» (первое назначение для большинства механиков-стажеров состоит в том, чтобы попросить своего руководителя о долгом ожидании…); в основном этот сценарий будет «висеть» достаточно долго, чтобы вы могли увидеть его с помощью команды ps . Это также хорошая возможность увидеть PEAR :: Console_ProgressBar в действии:

 #!/usr/local/bin/php  <?php  # Include PEAR::Console_Getopt  require_once 'Console/Getopt.php';   # Include PEAR::Console_ProgressBar  require_once 'Console/ProgressBar.php';   # Exit codes  define('INVALID_PHP_SAPI',3);  define('INVALID_LOGIN',4);   # Valid username / password  define('USERNAME','harryf');  define('PASSWORD','secret');   //--------------------------------------------------------------------------------------  /**  * Displays the usage of this script  * Called when -h option specified or on illegal option  */  function usage() {  $usage = <<<EOD  Usage: ./longweight.php [OPTION]  Builds long weights.  -l=LENGTH  Length of weight  -u=USERNAME  Username  -p=PASSWORD  Password  -h    Display usage   EOD;  fwrite(STDOUT,$usage);  exit(0);  }   //--------------------------------------------------------------------------------------  /**  * Gets the options specified by the user  */  function getOptions() {   $args = Console_Getopt::readPHPArgv();   // Could be an error with older PHP versions and the CGI SAPI  if ( PEAR::isError($args) ) {    fwrite(STDERR,$args->getMessage()."n");    exit(INVALID_PHP_SAPI);  }   // Compatibility between "php longweight.php" and "./longweight.php"  if ( realpath($_SERVER['argv'][0]) == __FILE__ ) {    $options = Console_Getopt::getOpt($args,'hl:u:p:');  } else {    $options = Console_Getopt::getOpt2($args,'hl:u:p:');  }   // Check for invalid options  if ( PEAR::isError($options) ) {    fwrite(STDERR,$options->getMessage()."n");    usage();  }   // Set defaults for length, username and password  $ret_options = array(    'length' => 10,    'username'=> NULL,    'password'=> NULL  );   // Loop through the user provided options  foreach ( $options[0] as $option ) {    switch ( $option[0] ) {      case 'h':        usage();      break;      case 'l':        $ret_options['length'] = $option[1];      break;      case 'u':        $ret_options['username'] = $option[1];      break;      case 'p':        $ret_options['password'] = $option[1];      break;    }  }   return $ret_options;  }   //--------------------------------------------------------------------------------------  /**  * Validates the user. If not username and password was provided  * by options, prompts user to enter them  */  function validateUser($username,$password) {  if (is_null($username) ) {    fwrite(STDOUT,'Enter username: ');    $username = trim(fgets(STDIN));  }  if (is_null($password) ) {    fwrite(STDOUT,'Enter password: ');    $password = trim(fgets(STDIN));  }  if ( !(($username == USERNAME) & ($password == PASSWORD)) ) {    exit(INVALID_LOGIN);  }  }   //--------------------------------------------------------------------------------------  /**  * Returns an instance of PEAR::Console_ProgressBar  */  function & getProgressBar($length) {  // Progress bar display (see documentation)  $display = 'Weight preparation %fraction% [%bar%] %percent% complete';   // Indicator for progress  $progress = '+';   // Indicator for what's remaining  $remaining = '-';   // Width of the progress bar  $width = '50';   return new Console_ProgressBar($display,$progress,$remaining,$width,$length);  };   //--------------------------------------------------------------------------------------  // Get the command line options  $options = getOptions();   // Make sure we have a valid user (exits if not)  validateUser($options['username'],$options['password']);   fwrite(STDOUT, "Preparing Long Weight of length {$options['length']}n");   $PBar = & getProgressBar($options['length']);   // Loop for the length of the weight  for ($i=0;$i<$options['length'];$i++) {   // Update the progress bar  $PBar->update($i+1);   sleep(1);  }   fwrite(STDOUT,"nDid you enjoy your long weight?n");   exit(0);  ?> 

Имя файла: longweight.php

Если вы чувствуете необходимость длительного ожидания, вы можете получить его, введя в командной строке следующее:

 $ ./longweight.php -l 30 -u harryf -p secret 

Сценарий будет зависать в списке процессов в течение 30 секунд, с именем пользователя и паролем, доступными для всеобщего обозрения.

Теперь я выполняю эту команду с другим скриптом, таким как:

 #!/usr/local/bin/php  <?php  shell_exec('./longweight.php -l 30 -u harryf -p secret');  fwrite(STDOUT,"Long weight finishedn");  exit(0);  ?> 

Имя файла: execlongweight1.php

Теперь из командной строки запустите скрипт в фоновом режиме и изучите список процессов:

 $ ./execlongweight1.php &  $ ps -eo pid,ppid,cmd | grep longweight 

Я получаю вывод, как это:

 26466  1589 /usr/local/bin/php ./execlongweight1.php  26467 26466 /usr/local/bin/php ./longweight.php -l 30 -u harryf -p secret 

То, как я отформатировал список процессов, отображает идентификатор процесса в первом столбце, идентификатор родительского процесса во втором столбце и команду выполнения в третьем столбце. Вы можете видеть, что сценарию execlongweight1.php был присвоен идентификатор процесса 26466. Вы также можете видеть, что longweight.php имеет 26466 в качестве идентификатора родительского процесса; он был execlongweight1.php скриптом execlongweight1.php . И есть имя пользователя и пароль в полном объеме для тех, кто ищет …

Поскольку мой скрипт longweight.php также принимает ввод имени пользователя и пароля в интерактивном режиме, через STDIN лучше всего выполнить сценарий с помощью popen() в режиме записи:

 #!/usr/local/bin/php  <?php  $fp = popen('./longweight.php -l 30','w');   fwrite($fp,"harryfn");  fwrite($fp,"secretn");   fclose($fp);   exit(0);  ?> 

Имя файла: execlongweight2.php

Теперь, когда мы выполняем команду cps , мы видим следующее:

 26512  1589 /usr/local/bin/php ./execlongweight2.php  26513 26512 /usr/local/bin/php ./longweight.php -l 30 

Имя пользователя и пароль больше не видны.

К сожалению, то же самое не будет работать с mysqldump , который читает пользовательский ввод из другого источника, чем STDIN . Следующее лучшее решение — поместить файл с именем .my.cnf , содержащий ваши имя пользователя и пароль MySQL, в домашнюю директорию вашей учетной записи Unix.

 [client]  user=harryf  password=secret 

Имя файла: ~ / .my.cnf

Утилита mysqldump будет автоматически считывать имя пользователя и пароль из .my.cnf , избавляя вас от необходимости выставлять их в командной строке.

Работа с окружающей средой

Никакой PHP-скрипт не является островом, особенно когда он используется для выполнения других программ. Оболочка Unix предоставляет собственную «память», в которой вы можете хранить переменные окружения. Ряд переменных предопределен, а некоторые, такие как переменная PATH, необходимы для того, чтобы ваш PHP-скрипт мог выполнять внешние программы «наивно».

Отличное руководство и описание LINUX: Rute User содержит краткое описание общих переменных среды.

Когда вы выполняете скрипт PHP из командной строки, он наследует переменные среды, определенные в вашей оболочке. Это означает, что вы можете установить переменную окружения с помощью команды export следующим образом:

 $ export SOMEVAR='Hello World!' 

Обратите внимание, что мы используем одинарные кавычки, а не двойные!

Теперь выполним следующий скрипт PHP:

 #!/usr/local/bin/php  <?php  fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");   exit(0);  ?> 

Имя файла: env1.php

Вы увидите значение, которое вы присвоили SOMEVAR . Функция getenv () позволяет вам читать переменные окружения.

Вы также можете создавать или изменять переменные среды из скрипта PHP с помощью функции putenv () , но с одной важной оговоркой: вы не можете изменять среду родительских процессов, только локальную копию, к которой имеет доступ ваш скрипт.

Продолжая последний пример, давайте выполним следующий скрипт PHP:

 #!/usr/local/bin/php  <?php  fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");   putenv("SOMEVAR='Goodbye World!'");   fwrite(STDOUT,"The value of SOMEVAR is ".getenv('SOMEVAR')."n");   exit(0);  ?> 

Имя файла: env2.php

Выполнив этот код, мы увидим измененное значение SOMEVAR при втором вызове fwrite() . Но если я наберу следующее из своей оболочки после того, как скрипт завершит выполнение, я увижу исходное значение «Hello World!»

 $ echo $SOMEVAR 

На первый взгляд вы можете подумать, что делает putenv() довольно бесполезным. Это становится важным, когда ваш PHP-скрипт должен настроить среду для другой программы, которую он будет выполнять. Посмотрите на этот пример:

 #!/usr/local/bin/php  <?php  putenv("SOMEVAR='Goodbye World!'");   fwrite(STDOUT,"Executing env1.phpn");  fwrite(STDOUT,shell_exec('./env1.php'));   exit(0);  ?> 

Имя файла: env3.php

Поскольку сценарий env1.php наследует среду от своего родительского процесса env1.php , он видит измененное значение SOMEVAR и отображает «Goodbye World!»

Другими словами, переменные среды могут использоваться для обмена информацией между программами. Вы можете определить переменную среды INSTALL_DIR , которая указывает на каталог, в который несколько скриптов должны что-то установить. Какой-то основной сценарий, например install.php , устанавливает эту переменную, а затем выполняет другие сценарии, которые выполняют дополнительные задачи установки, такие как извлечение архивов исходного кода в каталог установки или создание документации API с помощью phpDocumentor .

Также важно знать о переменных среды при выполнении внешних программ из сценариев PHP, работающих под Apache. Как правило, сценарии запускаются в среде специального пользователя Unix («nobody», «wwwrun» или аналогичного), с очень небольшим количеством привилегий файловой системы и, возможно, нестандартной пользовательской средой.

Это может означать, что, например, когда вы пытаетесь выполнить под Apache программу, с помощью которой вы обычно находите свой собственный PATH , ничего не происходит. Возможно, вам просто нужно указать полный путь к программе или временно обновить переменную PATH с помощью putenv() . Это также может быть проблема с разрешениями, это отдельная история.

Обратите внимание, что если вы управляете сервером и вам необходимо выполнять внешние программы из сценариев PHP, работающих под Apache, возможно, стоит изучить sudo , который предоставляет механизм, с помощью которого один пользователь может выполнить одну команду с той же средой и разрешениями, что и у другого пользователя ( очевидно, вы должны быть предельно осторожны при этом).

Контроль над процессом

Ранее мы видели функцию popen() , которая позволяет нам либо читать, либо писать во внешнюю программу. Иногда вам нужно сделать и то и другое, и именно здесь функция proc_open () пригодится.
Вот простая программа, которая читает строку за строкой из STDIN и преобразует первые буквы каждого слова в верхний регистр, а затем записывает строку обратно в STDOUT . Поскольку это несколько темпераментная программа, ей не нравится вводить слова в верхнем регистре. Если он находит слово в верхнем регистре, он жалуется STDERR и удаляет это слово:

 #!/usr/local/bin/php  <?php  while ( ($line = trim(fgets(STDIN))) != 'exit' ) {  $words = explode(' ',$line);  foreach ( $words as $index => $word ) {    if ( strcmp($word,STRTOUPPER($word)) != 0 ) {      $words[$index] = ucfirst($word);    } else {      fwrite(STDERR, "Bleugh! $word is all upper case!n");      unset($words[$index]);    }  }  $line = implode(' ',$words);  fwrite(STDOUT, $line."n");  }  exit(0);  ?> 

Имя файла: ucfirst.php

Возможность общаться с этой программой из другого PHP-скрипта, очевидно, сложнее. Нам нужно подключиться к STDIN , STDOUT и STDERR одновременно. Введите: proc_open

 #!/usr/local/bin/php  <?php  // Describes how proc_open should open the external program  $Spec = array (  0 => array('pipe','r'), /* STDIN */  1 => array('pipe','w'), /* STDOUT */  2 => array('file','./badwords.log','a'), /* STDERR */  ); 

Во-первых, мне нужно определить «спецификацию дескриптора», которая представляет собой массив со специальной структурой. Он сообщает proc_open() как подключиться к STDIN , STDOUT и STDERR внешней программы. Это должен быть массив массивов, родительский массив, содержащий три элемента, которые соответствуют STDIN , STDOUT и STDERR соответственно. Каждый из дочерних массивов определяет, как каждый из них должен использоваться, идентифицируя «тип ресурса»: либо «канал» для ввода-вывода оболочки, либо «файл», а также другие параметры, описывающие, как следует использовать канал или файл. Поначалу это может показаться немного «неуклюжим», но вы к этому привыкнете.

Здесь я определил спецификацию дескриптора, которая говорит: «сделайте STDIN доступным для чтения внешней программой (спецификация рассматривается с точки зрения внешней программы, даже если этот скрипт будет записывать в канал), сделайте STDOUT доступным Таким образом, внешняя программа может записать в нее выходные данные, затем указать STDERR в файл ‘badwords.log’ и добавить к нему любые сообщения об ошибках. «

Давайте перейдем к остальной части программы, где цель «спецификации дескриптора» должна стать более понятной:

 // Handles to external programs STDIN, STDOUT and STDERR placed here  $handles = array();   // Open the process using the descriptor spec  $process = proc_open('./ucfirst.php', $Spec, $handles);   // Some lines to input to ucfirst.php  $lines = array (  'hello world!',  'a bad EXAMPLE of upper case.',  'goodbye world!',  );   foreach ( $lines as $line ) {  // Write the line to the STDIN of ucfirst.php  fwrite($handles[0],$line."n");   $response = fgets($handles[1]);   // Display the response to the user  fwrite(STDOUT, $response);  }   // Issue the exit command to the ucfirst.php program  fwrite($handles[0],"exitn");   // Clean up  fclose($handles[0]);  fclose($handles[1]);  proc_close($process);   exit(0);  ?> 

Имя файла: proc_open.php

Здесь есть несколько вещей, на которые стоит обратить внимание. Выполнение proc_open() возвращает ресурс, который представляет процесс , но для взаимодействия с процессом, из которого нужно читать или записывать дескрипторы, хранящиеся в массиве $handles , элементы массива соответствуют элементам в массиве спецификаций дескриптора.

Обратите внимание, что вам нужно быть особенно осторожным, чтобы закрыть ручки на трубах, как я делал в разделе очистки выше. Более разумный подход может состоять в том, чтобы зарегистрировать функцию выключения, чтобы позаботиться об очистке — смотрите register_shutdown_function () .

Теперь, когда мы выполняем proc_open.php , вывод будет выглядеть так, как показано ниже:

 Hello World!  A Bad Of Upper Case.  Goodbye World! 

Файл badwords.log содержит:

 Bleugh! EXAMPLE is all upper case! 

В PHP5 proc_open() поддерживает новый дескриптор «pty», который позволит ему управлять программами, которые читают из других мест, кроме STDIN и STDOUT (это должно решить проблему с отправкой пароля в mysqldump , например).

PHP5 также вводит еще три функции: proc_get_status () , которая предоставляет информацию о том, что в данный момент делает внешняя программа (наиболее важно, предоставляя свой идентификатор процесса), proc_terminate () , которая позволяет отправлять процессу сигнал завершения (подробнее об этом ниже) и proc_nice () , которые позволяют изменить приоритет, который операционная система назначает программе (сколько процессорного времени она получает); обычно это число от 20 (самый низкий приоритет) до -20 (самый высокий приоритет). Обратите внимание, что proc_nice() на самом деле не имеет отношения к proc_open() ; он предназначен для управления приоритетом текущего скрипта PHP, а не приоритетом внешней программы. Из оболочки команда renice может использоваться для того же эффекта.

сигналы

Функции Posix предоставляют различные инструменты для получения полезной информации из операционной системы (обратите внимание, что расширение Posix недоступно в Windows). Возможно, наиболее полезной с точки зрения работы с внешними программами является функция posix_kill () , которая позволяет отправлять сигналы программе во время ее выполнения.

Если вы использовали Unix чуть больше, вы, вероятно, столкнулись с командой kill , возможно, в ее наиболее известном воплощении:

 $ kill -9 <pid> 

«-9» говорит, что посылает сигнал «SIGKILL» (немедленно завершается) процессу, идентифицированному по его идентификатору. Учебник по Rute Users дает некоторые общие сигналы здесь (на самом деле, стоит прочитать всю главу о процессах и переменных среды ).

Функция posix_kill() работает так же, как и команда kill : вы идентифицируете процесс по его идентификатору и посылаете ему сигнал. Как правило, вы должны послать сигнал номер 15 ( SIGTERM ), который дает программе, которую вы убиваете, возможность убрать перед выходом. Сигнал SIGKILL (9) не может быть перехвачен программой; это немедленно умирает, что может означать, что это оставляет беспорядок позади.

Для получения дополнительной информации о сигналах также стоит взглянуть на PHP Process Control Extension (по умолчанию отключен), который предоставляет функции, позволяющие вашим сценариям работать с сигналами (среди прочего).

Предупреждение: не используйте функции pcntl при запуске PHP под Apache! Странные вещи произойдут, если вы это сделаете.

Вот скрипт, который «ловит» сигнал SIGTERM :

 #!/usr/local/bin/php  <?php  // Required from PHP 4.3.0+: see manual  declare(ticks = 1);   function cleanUp() {  fwrite(STDOUT,"Performing clean up...n");  exit(0);  }   // Map the SIGTERM signal to the cleanup callback function  pcntl_signal(SIGTERM, "cleanUp");   // Illegal - you cant catch the KILL signal  # pcntl_signal(SIGKILL, "cleanUp");   while (1) {  // Loop forever  }  ?> 

Имя файла: pcntl1.php

Я выполняю это из командной строки следующим образом:

 $ ./pcntl1.php & 

Это указывает оболочке запускать процесс в фоновом режиме. Сразу после выполнения он сообщает мне идентификатор процесса, под которым выполняется мой скрипт, например:

 [6] 2840 

«2840» является идентификатором процесса. Я сейчас набираю следующее:

 $ kill 2840 

И я вижу ниже:

 Performing clean up... 

Затем процесс завершается (обратите внимание, что команда kill умолчанию принимает желаемый сигнал SIGTERM , если вам интересно).

Может оказаться полезным ловить сигналы типа SIGINT , что соответствует нажатию клавишами CTRL + C для выхода из вашей программы; это даст вам возможность откатить все, что сделал ваш скрипт.

Другие трюки PCNTL

Еще одна заметная функция — pcntl_exec () . В отличие от любой из функций выполнения программы, которые мы видели до сих пор, использование pcntl_exec() для выполнения внешней программы приводит к тому, что программа заменяет скрипт, которым она была выполнена, вместо того, чтобы запускаться как дочерний процесс.

Например, представьте, что я выполняю следующий скрипт:

 #!/usr/local/bin/php  <?php  fwrite(STDOUT,"My process ID is ".posix_getpid()."n");  pcntl_exec('./longweight.php',array('-l15','-uharryf','-psecret'));  ?> 

Имя файла: pcntl2.php

Затем я выполняю приведенный ниже код из отдельной оболочки:

 $ ps -ef | grep longweight.php 

Идентификатор процесса longweight.php совпадает с идентификатором процесса, полученным из pcntl2.php . Более того, если я выполню следующее, я ничего не вижу:

 $ ps -ef | grep pcntl2.php 

Причина, по которой я ничего не вижу, потому что скрипт был заменен longweight.php .

Такое поведение может быть полезно при написании сценариев-оболочек, которые устанавливают среду для другого сценария, а затем тихо исчезают.

Еще одна область, в которой функции PCNTL полезны, — это если вы пишете какой-то «демонический» процесс с помощью PHP (сервер). Хотите верьте, хотите нет, но существует целый ряд HTTP-серверов, полностью написанных на PHP, таких как Nanoweb и PEAR :: HTTP_Server .

Используя функцию pcntl_fork () , вы можете написать скрипт, который действует как сервер, но создает дочерние процессы для обработки входящих запросов почти так же, как веб-сервер Apache (1.x). Это позволяет моему серверу справляться с несколькими задачами параллельно, уменьшая задержки. К сожалению, pxntl_fork() не поддается краткому обсуждению, поэтому я оставлю вам дальнейшее расследование, если оно вам понадобится.

К вам

Теперь у вас должно быть довольно хорошее представление о том, как заставить ваши PHP-сценарии командной строки «настраиваться» на множество инструментов и утилит, доступных в системах на основе Unix. Есть много чего узнать, если вы новичок в Unix, и в этой короткой статье я только смог представить темы. Надеюсь, однако, у вас есть ощущение того, сколько энергии в вашем распоряжении.

Если вы хотите узнать больше, я очень рекомендую Учебное пособие и экспозицию Пола Шиера для пользователей Rute , также доступное на Amazon .