Компания
223,69
рейтинг
24 января 2012 в 13:17

Разное → Написание системных утилит на PHP CLI

Для большинства специалистов PHP не является языком, который бы всерьёз использовался для написания консольных утилит, и для этого есть много причин. PHP изначально разрабатывался как язык для создания веб-сайтов, но, начиная с PHP 4.3, в 2002-ом году появилась официальная поддержка режима CLI, поэтому он уже давно перестал быть таковым. Разработчики Badoo на протяжении нескольких лет вполне успешно используют множество интерактивных CLI-утилит на PHP.

В данной статье нам хотелось бы поделиться своим опытом работы с CLI-режимом в PHP и дать несколько рекомендаций тем, кто собирается писать скрипты на PHP, при условии, что они будут запускаться в *nix-системе (впрочем, почти всё верно и для Windows).

Рекомендации


Скорость работы

Распространено мнение, что PHP — язык медленный, и таковым он является на самом деле. Для PHP CLI рекомендуется не использовать тяжелые фреймворки и даже просто большие библиотеки на PHP по двум причинам:
  1. Время работы include/require в CLI-режиме будет всегда включать в себя парсинг и исполнение, т.к. байткод в этом режиме не кэшируется (по крайней мере — по умолчанию), а значит, инициализация займет много времени, даже если из-под веб-сервера всё работает достаточно быстро.
  2. Пользователи веб-сайтов привыкли ждать некоторое количество времени для загрузки страницы (около 1-ой секунды, а иногда и чуть больше пользователем воспринимается вполне нормально), а вот сказать то же самое про CLI нельзя: даже задержка в 100 мс уже будет ощутимой, а в 1-у секунду и более может раздражать.
Вывод на экран

В CLI- и в веб-режиме вывод на экран значительно отличается. В веб-режиме вывод, как правило, буферизуется, у пользователя нельзя ничего спросить во время исполнения скрипта; отсутствует как класс понятие вывода в поток ошибок. В CLI-режиме, естественно, неприемлем вывод HTML, а также крайне нежелателен вывод длинных строк. В CLI echo по умолчанию вызывает flush() (подробнее можно посмотреть здесь) — это удобно тем, что можно не заботиться о вызове flush() вручную, если, к примеру, вывод перенаправляется в файл.

Также для CLI-скриптов имеет смысл выводить ошибки не в STDOUT (используя echo), а в STDERR: таким образом, даже если вывод программы будет перенаправлен куда-либо еще (например, в /dev/null или grep), пользователь не пропустит текст ошибки в случае ее появления. Это стандартное поведение для большинства «родных» *nix'овых консольных утилит, и STDERR существует именно по причине, описанной выше. В PHP для записи в STDERR можно пользоваться, к примеру, fwrite(STDERR, $message) или error_log($message).

Использование кодов возврата

Код возврата — это число, которое равно 0 в случае успешного выполнения команды и не равно 0 в противном случае. Код возврата, равный 1, часто применяется в случае некритичных ошибок (например, если указаны неправильные аргументы командной строки), а 2 — в случае критичных системных ошибок (например, при ошибке сети или диска). Значения наподобие 127 или 255 обычно используются для каких-либо специальных случаев, которые отражаются отдельно в документации.

По умолчанию при простом завершении PHP-скрипта предполагается, что все команды отработали успешно и возвращается 0. Чтобы выйти с определенным кодом возврата, нужно явно вызвать exit(NUM), где NUM — это и есть код возврата (помним, что он равен 0 в случае успеха и имеет другое значение в случае ошибок).

Чтобы понять, что внешняя команда, исполняемая с помощью exec() или system(), завершилась неуспешно, нужно передавать переменную $return_var в качестве параметров соответствующих функций и проверять значение на равенство нулю.

Внимание! Если вы собираетесь написать exec('some_cmd … 2>&1', $output), чтобы ошибки тоже попали в $output, рекомендуем ознакомиться с причинами разделения STDOUT и STDERR и убрать явное перенаправление потока ошибок в STDOUT (2>&1). Такое перенаправление требуется намного реже, чем может показаться. Единственный случай, когда его использование хоть немного оправдано (в PHP-скрипте) — необходимость распечатать на веб-странице (не в CLI!) результат выполнения команды, включая ошибки, которые произошли (иначе они попадут в лог веб-сервера или вообще уйдут в /dev/null).

«Маскировка» под встроенные команды системы

Хорошая консольная утилита должна себя вести стандартным образом и пользователи могут даже и не знать, что она на PHP. Для этого в *nix-системах предусмотрен механизм, который многим известен по запуску скриптов на Perl/Python/Ruby, но в равной степени применимый и к PHP.

Если добавить в начало PHP-файла, к примеру, #!/usr/bin/env php и перенос строки, дать ему права на исполнение (chmod 755 myscript.php) и убрать расширение .php (последнее не обязательно), то файл можно будет исполнить, как и любой другой исполняемый файл (./myscript). Можно добавить директорию со скриптом в PATH или переместить его в одну из стандартных директорий PATH, например, /usr/local/bin, и тогда скрипт можно будет вызывать простым набором «myscript», как и любые другие системные утилиты.

Обработка аргументов командной строки

Существует соглашение о формате аргументов командной строки, которому следуют большинство встроенных системных утилит, и мы рекомендуем следовать ему и ваших скриптах.

Пишите краткую справку для своего скрипта, если он получил неверное количество аргументов.

Чтобы узнать имя вызываемого скрипта, используйте $argv[0]:

if($argc != 2) {
// не забывайте \n на конце
echo "Usage: ".$argv[0]." <filename>\n"; 
// возвращаем ненулевой код возврата, что свидетельствует об ошибке
exit(1);
}

Для облегчения обработки флагов можно использовать getopt(). Getopt() — одна из встроенных функций для обработки аргументов командной строки. С другой стороны, нет ничего сложного в том, чтобы обрабатывать часть аргументов вручную, т.к. на PHP это не представляет особого труда. Такой способ может понадобиться, если нужно обработать аргументы в стиле ssh или sudo (sudo -u nobody echo Hello world выполнит echo Hello world из-под пользователя nobody, который указан после флага -u перед командой).

Рекомендации для более сложного уровня


Вызов «правильного» system() для CLI

О реализации system() уже было написано здесь. Речь идет о том, что стандартный system() в PHP является не вызовом system() в С, а оберткой над popen(), соответственно, «портит» STDIN и STDOUT у вызываемого скрипта. Чтобы этого не происходило, нужно пользоваться следующей функцией:

// функция совместима по аргументам с system() в С
function cSystem($cmd) {
$pp = proc_open($cmd, array(STDIN,STDOUT,STDERR), $pipes);
if(!$pp) return 127;
return proc_close($pp);
}


Работа с файловой системой

К возможному удивлению, мы рекомендуем не писать свои реализации рекурсивного удаления (копирования, перемещения) файлов, а вместо этого использовать встроенные команды mv, rm, cp (под Windows — соответствующие аналоги). Такое не переносимо между Windows/*nix, но зато позволяет избежать некоторых проблем, описанных ниже.

Давайте рассмотрим простой пример реализации рекурсивного удаления директории на PHP:

// неправильный пример! используйте rm -r
function recursiveDelete($path) {
if(is_file($path)) return unlink($path);
$dh = opendir($path);
while(false !== ($file = readdir($dh))) {
if($file != '.' && $file != '..') recursiveDelete($path.'/'.$file);
}
closedir($dh);
return rmdir($path);
}


На первый взгляд всё верно, так? Более того, даже в известных файловых менеджерах на PHP (например, в eXtplorer и в комментариях к документации) удаление папки реализовано именно таким способом. Теперь создадим символическую ссылку на несуществующий файл (ln -s some_test other_test) и попробуем её удалить. Или создадим в папке символическую ссылку на себя, или на корень ФС (рекомендуем не тестировать такой вариант)… Конкретно для recursiveDelete() фикс, конечно же, тривиален, но понятно, что лучше не изобретать велосипед и использовать встроенные команды, пусть и теряя немного в производительности.

Очистка в случае ошибок

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

В веб-режиме PHP это реализуется с помощью register_shutdown_function(), которая срабатывает даже тогда, когда скрипт завершился с фатальной ошибкой (этот способ, кстати, годится для отлова почти любых ошибок, в том числе ошибок нехватки памяти). В CLI-режиме всё немного сложнее, поскольку пользователь, к примеру, может послать вашему скрипту Ctrl+C, и register_shutdown_function() при этом не сработает.

Но объясняется это просто: PHP по умолчанию вообще не обрабатывает UNIX-сигналы, поэтому получение любого сигнала немедленно вызывает завершение скрипта. Это можно исправить путем добавления declare(ticks=1), в начало файла после <?php и регистрации своих обработчиков интересующих нас сигналов (более подробно здесь):

pcntl_signal(SIGINT, function() { exit(1); }); // Ctrl+C
pcntl_signal(SIGTERM, function() { exit(1); }); // killall myscript / kill <PID>
pcntl_signal(SIGHUP, function() { exit(1); }); // обрыв связи

Функции для обработки сигналов не обязаны быть одинаковыми для всех. Можно не вызывать exit() внутри обработчика сигнала — тогда выполнение скрипта будет продолжено после того, как сигнал обработан.

Работа с базой данных в нескольких процессах (после fork())

Рекомендация очень простая: следует закрывать все соединения с базой перед тем, как выполнить fork() (в идеале даже открытые файлы с помощью fopen() не должны присутствовать), т.к. выполнение fork() в этих случаях может привести к весьма странным последствиям, а для соединения с базой данных это просто приведет к закрытию соединения после завершения любого из «форкнутых» процессов. В том же руководстве по SQLite прямо сказано, что ресурс, открытый до fork(), нельзя использовать в «форкнутых» процессах, потому что он не поддерживает многопоточный доступ таким способом. В любом случае, pcntl_fork() в PHP просто делает fork() и логирует ошибки, поэтому обращаться с ним нужно столь же осторожно, как и в С.

Использование ncurses для сложной отрисовки на экран

Библиотека ncurses была создана специально для того, чтобы можно было не заботиться об esc-последовательностях для управления положением курсора в терминале и чтобы программа, которая использует, например, цвет, была переносима между системами и терминалами. С другой стороны, даже для таких простых вещей как цветной вывод нужно иметь в виду, что STDOUT не всегда поддерживает цвета. Нам известен один примитивный, но ненадежный, способ узнать без ncurses, поддерживает ли терминал цвет — проверить, является ли STDOUT терминалом (posix_isatty(1)).

Количество выводимого на экран

Большинство стандартных программ почти ничего не выводят на экран, только если их не попросить об этом специально, указав ключ -v (verbose, болтливый). Действительно, не стоит засорять экран без причины. Найти баланс бывает непросто, но есть несколько простых рекомендаций:
  1. Если операция не займет много времени (меньше 10-ти секунд), не выводите вообще ничего;
  2. Если вы делаете что-то нетривиальное (например, монтируете временные устройства с использованием sudo), наоборот, сообщите пользователю об этом, чтобы он знал, что делать в случае ошибки;
  3. Если операция длительная и для неё возможно показывать прогресс выполнения, лучше этот самый прогресс показывать (для этого может пригодиться функция cSystem, указанная выше);
  4. Если программа может работать как фильтр (например cat, grep, gzip...), проверьте, что в STDOUT попадают только данные, а ошибки, приглашения ко вводу и др. идут в STDERR, чтобы следующие программы в цепочке не получили какой-нибудь ненужный мусор.
Чтобы показывать прогресс выполнения, можно делать так, как это делает git: пользоваться предположением, что у всех терминалов ширина как минимум 80 символов, и печатать строку фиксированной ширины. Если учесть, что символ возврата каретки (\r) возвращает курсор в начало строки (и следующий вывод переписывает то, что было в строке до этого), очень легко написать код, который выводит, к примеру, процент выполнения операции от 0 до 100, занимая, при этом, всего одну строку на экране пользователя:

for($i = 0; $i <= 100; $i++) {
printf("\r%3d%%", $i);
sleep(1);
}
echo "\n";


Определение имени пользователя, вызвавшего скрипт

Имя пользователя содержится в переменной окружения USER ($_ENV['USER']), но есть одна загвоздка — этот способ использует переменные окружения, которые могут сообщать неверные данные (пользователь может выполнить скрипт, скажем, как USER=root myscript, и скрипт будет считать, что имя пользователя — root).

Поэтому нужно использовать функции posix:

// getuid() вернет пользователя, который вызывал скрипт, а не эффективный uid – в данном случае нам это и нужно
$info = posix_getpwuid(posix_getuid()); 
$login = $info['name'];


Заключение


В статье мы постарались привести рекомендации, которые не совсем очевидны непосредственно разработчикам PHP, нежели вообще всем программистам, пишущим консольные утилиты. Хотя многое из вышеизложенного можно применить и к другим языкам программирования, и, возможно, некоторые пункты будут полезны и тем, кто не собирается писать на РНР.

Юрий youROCK Насретдинов, разработчик Badoo
Автор: @Badoo
Badoo
рейтинг 223,69

Похожие публикации

Комментарии (107)

  • –35
    Простите, но тролейбус.jpg
    Для каждого языка есть свое место применения. На perl можно написать 3D игру, только зачем?
    • +15
      Не являюсь поклонником PHP, но хотелось бы вспомнить про язык JavaScript. Его применение в прежние годы было крайне ограниченно. А вот что этот язык перерос сейчас? Еще стоит упомянуть ActionScript. Помните те годы, когда AS был языком, позволяющим самую малость добавить интерактивности в flash компоненты? А сейчас на нем пишут enterprise приложения. Т.ч. время меняет представление о «применении» той или иной технологии/языка.
      • 0
        Ваша точка зрения понятна, но хочется заметить вот что — такое «превращение» и перенацеливание ActionScript вызвано в том числе и стремительным развитием самого ActionScript.
        AS1 и AS3 (последний актуальный) — это два совершенно разных языка, код на одном не компилируется в другом. В AS3 теперь есть строгая (в основном статическая) типизация, появились классы и интерфейсы, есть даже геттеры — он больше похож на Java, а не на JS, как AS1.

        Без всякого сарказма или потаённого смысла, было бы очень интересно посмотреть на то, как в этом отношении развивался php
        • +1
          habrahabr.ru/blogs/php/136800/

          Ну вот, можете посмотреть здесь. PHP тоже развивается и обзаводится новыми полезными качествами. Взрослеет, в общем. В том числе, есть и движение к более строгой типизации.
        • 0
          возьмите и посмотрите, на сайте все есть у РНР. Он мало чем (на самом деле, не могу придумать даже чем) отличаеться от других языков. И есть у меня вся веб-система на РНР, зачем выносить какую-то одну процедуру и городить зачастую огород из RPC (между двух платформ) чтобы воспользоваться уже существующим кодом
    • –14
      Активно минусующие прояснят свою позицию?
    • +4
      Назовите, пожалуйста, хотя бы некоторые из серьезных причин, по которым PHP не следовало бы использовать для написания консольных утилит.
      • +3
        Пожалуйста, автор сам ее озвучил — скорость работы. Я, например, для парсинга большого текстового файла возьму что-то другое.
        Для мелких прикладных задач вполне возможно использовать PHP, тут сомнений и возражений нет.
        • +9
          В том же Badoo консольные утилиты пишутся на PHP просто потому, что почти весь сайт написан на PHP, и засчет того, что консольные приложения тоже пишутся на PHP, мы используем большую часть существующего и отлаженного кода, вместо того, чтобы писать его заново. Этот же факт помогает исключить дублирование кода и поддержание нескольких реализаций на разных языках.
          • +1
            +100500, делаем так же, пачки кронджобов на php, которые реюзаюя классы с сайта, выполняют различные обновления, парсят xml данные, генерят картинки и т.п.
          • 0
            У меня тоже самое что и у вас, только я использую Yii у которого есть CConsoleApplication и CConsoleCommand — это позволяет мне писать код не думая о том, WEB у меня или консоль (естественно за исключением очевидных вещей, что у консольного могут быть параметры, а на WEB приходят GET/POST/etc). Крайне удобно и кодовая база идентична (модели, компоненты, модули, поведения).
        • 0
          А если вся компания на PHP пишет? У Фликр, например, есть PHP-сервисы для работы с фото, которые сжимают фото, ресайзят, складывают как надо и т.д. Зачем мне что-то другое, если меня устраивает скорость и я могу написать PHP скрипт (да еще и учитывая, что у меня уже есть кодвая база на нем) в разы быстрее, чем Java или C++, например?
        • 0
          Скорость работы это еще не показатель. Сомневаюсь, что пара лишних секунд работы скрипта так уж критична, зато скорость написания и отладка на php значительно быстрее, чем на том же Си.
        • 0
          Согласен что скорость штатных функций работы с текстом у PHP может уступать нативному, допустим, grep-у в Linux, но не всегда это основополагающий фактор, из-за которого стоит отвергнуть на корню вообще идею использования PHP для консольных приложений. Доводы, приведенные выше, в особенности уже существующий код проектов, связанных с будущей утилиткой на PHP, который можно повторно использовать, на мой взгляд, более существенны, если речь не идет, конечно же, о какой-то задаче, требующей реально высокой скорости обработки.
      • +1
        единственная причина, по которой я пишу мелкие прикладные скрипты на РНР — все коллеги знают этот язык. Задачи парсинга файлов значительно проще и быстрее делаются на Перле, очень многие задачи можно сделать даже на шелле.
        РНР из коробки весьма беден функциями, нужными сисадмину, я это по своему опыту пишу.
        • 0
          Позвольте не согласиться.

          Что именно из коробки в ПХП (PHP CLI)
          не хватает в отличии от перла? Навскидку могу вспомнить только многопоточность.
          • 0
            например, парсинг тем писем в необработанных почтовых сообщениях. Правда, я в последнее время обычно питон для этого использую. А что касается перла, то в нём удобно текст разбирать. Например, мой скрипт для парсинга конфигов (вырезает комментарии):
            #!/usr/bin/perl
            while (<>)
            { chomp;
            s/^\s//;
            next if /^#/;
            next if /^$/;
            print "$_\n";
            }

            А представьте как он будет выглядеть на РНР?
            • +3
              Так и хочется сказать, что будет выглядеть читабельно :). Я вот хоть и немного знаю Perl, но мне пришлось немного подумать, чтобы понять, что делает ваш скрипт :).

              #!/usr/bin/env php
              <?php
              while(false !== ($ln=fgets(STDIN))) { // while(<>) короче, согласен :))
              $ln = trim($ln);
              if(!strlen($ln)) continue;
              if($ln[0]=='#') continue;
              echo "$ln\n";
              }


              Вывод, вроде как, одинаков (я, кстати, исправил заодно ошибку с тем, что вы только один пробел в начале вырезаете :)):

              $ cat test.txt
              Hello

              # comment
              Just test
              a
              0

              Hello world
              $ cat test.txt | ./skip_comments.pl
              Hello
              Just test
              a
              0
              Hello world
              $ cat test.txt | ./skip_comments.php
              Hello
              Just test
              a
              0
              Hello world
              • 0
                Кстати говоря, я не утверждаю, что скрипт на PHP меньше:
                PHP-скрипт весит 197B, Perl-скрипт весит 89B
                • 0
                  Кстати, это я посчитал длину файла с комментарием на русском языке :). Если без моего комментария, то получается, что скрипт на PHP весит 149B
              • –4
                и всё-таки чисто субъективно код на Перле выглядит изящнее. Особенно если использовать регулярные выражения по назначению, а не для сокращения кода, как я в этом случае.
                Про ошибку интересно подметили.
                • 0
                  Регулярные выражения — это нечитабельный код прежде всего. Поэтому меня совершенно не расстроило, что PHP весит 149 байт, зато я прочесть и понять его смог сразу же.
                • 0
                  Вы же говорили, что вам чего-то в пхп не хватает из коробки, а в итоге говорите об изящности кода.

                  Практика показывает, что осуждающие PHP и хвалящие Perl в 90% просто не знают, как сделать то, что они хотели в этом языке, т.к. почти всегда это можно сделать почти так же, но удобнее конечно думать, что перл особенный, в нем можно, а в PHP — нет (или надо что-то доставлять).
                  • 0
                    я не говорил, что Перл из коробки богаче РНР. Кроме того, я использую не только Перл, но и Питон.
                    Аргумент про не знают просто не катит, т.к. выше я написал, что сейчас делаю всё на РНР, потому что коллеги только его знают.
                    Я достаточно знаю Перл, РНР и немного Питон, чтобы делать свой выбор. А недостаточное знание другими РНР-программистами Перла совсем не говорит о том, что РНР лучше для конкретных задач.
                    Я понял, чего мне нехватает в РНР: быстрого старта. Когда пишешь консольное приложение на РНР, надо предусмотреть вывод ошибок на экран на время отладки (в других языках это сразу работает, а РНР обычно пишет в error_log, который еще найти надо). Надо проверить, чтобы cgi-версия РНР вообще была, а не только модуль апача. Вызов сторонних приложений из РНР сделан через жопу, о чём даже в этом топике говорится. Использование регулярных выражений в Перле удобнее, ну а если кому-то не не нравится — пусть пишут свои парсеры. Многие перечисленные нюансы возникают от того, что РНР создавался как язык для веба, а не универсальный для всего, и глупо отрицать это наследие.
                    PS: в дальнейшем споре я не буду участвовать из-за системы оценок, но почитаю.
    • 0
      У нас например некоторым крон-скриптам можно передать флаги. Например:
      --force -f форсировать генерацию данных, даже если они уже есть
      --ignore-lock — игнорировать лок скрипта, если такой же уже выполняется (у нас часто скрипт не может запуститься, если уже выполняется такой же)
      --print-output — тут понятно
      если скрипт работает с большим ассивом данных, можно пердавать ему то сколько элементов нужно обработать флагом --limit -l
      и т.д.

      Можно использоваться как консольные интерфейсы для существующего софта… Применений много.
    • +2
      А если консольная утилита обслуживает веб-приложение, которое написано на PHP? Такая утилита может использовать уже написанные классы, например.
      • +1
        Да, как писал youROCK выше, в этом и преимущество, не нужно дублировать код.

        Единственное что нужно помнить что у вас нету таких переменных как $_GET, $_POST, $_COOKIE, а $_SERVER отличается по содержимому. Но появляются как getopt(), $argv, и $argc чтобы работать с переданными параметрами
        • +1
          Даже добавлю некоторые фраймворки имеют встроенную поддрежку из браузера и консоли.
          Например в CodeIgniter есть рассширешие CLI и тогда то что у вас в урле как

          yourdomain.com/module/controller/action/param1/param2/param3/…

          можно запустить вот так

          $php index.php module controller action param1 param2 param3…

          Только надо следить за выводом и не выдать кучу HTML кода в Консоль.
          • 0
            В kohana тоже такое есть.
            • 0
              да и в zf есть. Я думаю эта фича есть почти во всех фреймворках.
    • 0
      На упомянутом Perl отлично пишутся мелкие мультиплатформенные парсеры, как в прочем и на php. Perl зато с помощью PerlApp можно загнать в snandalode exe и дать человеку у которого перла нет
      • +1
        perl? а еще есть python, на нем тоже очень спокойно пишутся парсеры веб страничек, и его можно загнать в приложение (даже несколькими способами вроде).
    • +1
      позвольте не согласиться. зачастую консольные приложения на пхп — это всякие кроны, которые обслуживают основной проект. это импорт всяких внешних данных, сборка javascript'ов и css, очистка всяких фаловых кешей, генерация каких-то кешей заново, рассылка почты и прочее. если у меня есть большой проект со сложной структурой и мне надо раз в сутки стягивать обновление данных из внешних источников — вы прикажете мне писать на Си или перле заново все модели? но моделями ведь приложение не ограничивается и потому мне надо еще и управляющую логику нарисовать. и тоже на каком-то другом языке. но зачем, если существует уже работающий моторчик, в котором работы-то — всего-лишь докрутить запуск оного из консольки. да, быстродействие страдает. но процессорное время дешевле времени программистов и дешевле времени в бизнесе.
    • –1
      на php тоже можно написать 3D игру.
  • 0
    Не могли бы вы подробнее описать обработку пользовательского ввода в процессе выполнения скрипта?
      • 0
        Это опции запуска. Я имел в виду пользовательский ввод во время выполнения скрипта: чтение из stdin, ввод данных с возможностью редактирования, ожидание нажатия клавиши (например y/n или стрелки вверх) и т.д.
        • +2
          Ну, для большинства задач fgets(STDIN) хватает за глаза ;)
        • 0
          Вот функция, которая у нас используется и отлично работает:
          function askValue()
          {
              $stream = fopen('php://stdin', 'r');
          
              return trim(fgets($stream));
          }
          
          • +5
            В PHP уже довольно давно есть заранее открытые дескрипторы STDIN, STDOUT и STDERR: php.net/manual/ru/features.commandline.io-streams.php
            • 0
              Конечно, да.

              Только вот не надо забывать про замечание:
              Эти константы недоступны, если PHP считывает запускаемый скрипт из stdin.

              В моём случае обычно так и получается и эти константы мне не доступны.
              • +2
                Если PHP считывает запускаемый скрипт из stdin, тогда попросить пользователя ввести что-либо тоже не получится ;), ибо в STDIN поступает текст исполняемой программы
                • 0
                  Хм… тогда странно, что скрипт отрабатывает, но у меня нет этих констант.
                  Может подскажете?

                  grevus@grevus ~ $ php -v
                  PHP 5.3.9 with Suhosin-Patch (cli) (built: Jan 11 2012 10:25:51)


                  Выполнение скрипта:
                  php projects/common/auto/runner/run.php some_our_script.php
                  • +2
                    «Скрипт поступает из STDIN» означает, что вы запускаете следующим образом:
                    php <projects/common/auto/runner/run.php (обратите внимание на "<" :))
                    Если вы запускаете не через STDIN и у вас этих констант нет, значит это баг, и в этом отдельном случае нужно разбираться более детально.
                • +1
                  я идиот, простите :(
                  Вы были правы.
                  Я не правильно проверял :(
          • 0
            Спасибо. Однако, при таком чтении не получается отредактировать строку с помощтю стрелок во время ввода (хотя она приходит отредактированной). Получается как-то так: abc^[[D^[[Dde, что не всегда удобно.
            • +5
              • 0
                Спасибо, это то, что нужно!
                Жаль только, что расширение не работает на Windows.
                • +5
                  Консольные скрипты для винды с юзерским вводом из консоли? О_О
                  • +1
                    На PHP.
                  • 0
                    Что в этом плохого?
      • 0
        У меня велосипед для этих целей есть. Век живи — век луркай. Спасибо.
    • 0
      для а-ля консоль рекомендую использовать readline расширение которое идёт по умолчанию в PHP. или же STDIN константу, которая ресура, на самом деле.
  • 0
    Огромное спасибо.
    Давно хотел что-то эдакое попробовать.
    Но есть пара вопросов:
    1. На сколько я знаю, в *nix и MacOS php встроенный. А в Windows разве встроенный?
    2. Может вы знаете какие-то хитрые тулзы, при помощи которых можно обернуть php в gui?
    • 0
      1. В Linux обычно PHP из коробки нет. В Mac OS X есть, в Windows тоже нет.
      2. php-gtk разве что :). Но лучше их не использовать, ИМХО
      • 0
        1. Совсем беда. Т.е. еще нужно заставить юзера поставить php…
        2. А почему лучше не испоьзовать?
        • 0
          соберите инсталлер
      • 0
        О, оказывается есть и php-qt!
    • 0
      php-qt
    • 0
      2. PHPDock, например.
      В качестве UI — страница, отображаемая в интегрированном браузере.
      • 0
        интересная штука, спасибо, но windows-only (
  • 0
    Отличная статья, в свое время тоже до всего доходил самостоятельно.

    Хотелось бы коснуться темы сокетов — был опыт работы с ними? Вы пишите, что перед fork'ами надо все ресурсы закрывать, что невозможно сделать когда, например, у нас есть сервер, который всегда ждет входных данных на свой порт, а все полученные запросы к нему раскидывает по дочерним воркерам.

    Как показали эксперименты, связка socket_create/socket_bind очень сильно ела память, стал использовать stream_socket_server, память течет значительно меньше, но течет. Помогают перезапуски сервера. Как я выявил, проблема в закрытии ресурса после вызова stream_socket_accept из сервера-родителя.

    Был ли такой опыт и как вы это бороли? :)
    • +1
      Если вы пишете свой апач, то, конечно, без этого вы обойтись не сможете :). Я честно скажу — я не помню, как я решал проблемы с утечкой памяти при работе с сокетами, когда писал что-то аналогичное :). По-моему, у меня таких проблем вообще не было.
      • 0
        Спасибо за ответ.

        Я сам эту проблему заметил не сразу, после активной работы демона в течение месяца увидел, что тот отъел порядка 100 лишних мб памяти.
        Тут же был написан брут-скрипт, который в 3х запущенных вариантах набирал на сервере порядка 500 лишних мб за 5 минут, вот и висит вопрос с тех пор :)
        • 0
          Я не вижу проблемы с авто-перезапуском каждые 10000 запросов, как умеет тот же апач :)
          • 0
            Решал проблему аналогично, только через проверку занятой памяти. memory_get_usage Как только съел больше определенного порога — перезапуск
            • 0
              так опасно, уж лучше совместно со счетчиком запросов, всё-таки нужна какая-то граница снизу.
              • 0
                У меня был *nix service реализованный который из мониторил сокет и писал в базу.
                Открывал соеденение через сокет к IBM Webshere и ждал запись. Как только, что-то приходило писал в базу запрос. Тек как разтаки модуль для php-cgi от IBM ;( они видилите его с 2007 для ПХП больше не поддерживают. Поэтому у меня память убегала быстрее чем все мыслеммые счетчики, даже на 1000-10000 запросов :))

                Правада в итоге мы выбили С код для модуля и его фиксили.
                • 0
                  Память по идее должна убегать условно-линейно с ростом счётчика. Иначе по закону сохранения получается, что она убегает даже в том случае, если в сокете ничего нет и никогда не будет. Так написать — надо очень постараться.
                  • 0
                    Почти. Скажем так я читаю из сокета строку типа JSON и которая прогонялась через внутрений билдер и создавала объект (что-то похожее на PDOStatement после запроса)

                    Так проблема была, что даже после unset объект криво удалялся из памяти, а объект был не всегда одиканого размера :( где-то внутри открытого соеденения оставались ссылки на него.

                    П.С. это был мой первый опыт написания php-cgi / php-cli скрипта который должен был больше чем Крон и быть с максимальным аптаймом. Так что я там много дров наломал и граблей насобирал. На избушку точно хватит :)
                    • 0
                      Но ведь то, что не удалялись ссылки — это как раз и есть причина утечек памяти. А если в сокете ничего нет — то и память не течёт. Значит, мы выбираем память условно-пропорционально числу запросов (с поправкой как раз на «не всегда одинакового размера»).
                      • 0
                        а чем счетчик будет лучше чем наблюдением за паматью.
                        Я знаю сколько могу занять Х. знаю сколько уже занял памяти Y. и если
                        Y/X < скажем 90% то все ок.
                        Чем такой вариант хуже? Особенно при условии что мне скрипт должен быть все время запущен. Или просто как второй критерий проверки и защиты?
                        • 0
                          Кстати, после того, как код был уже написан и прошли набитые шишки, из недр гугла удалось достать библиотеку (phpsocketdaemon), которая имеет очень положительные отзывы в плане работы с памятью.

                          Может кому пригодиться, а может у кого уже был опыт использования?
                          • 0
                            был, да и на хабре были статьи о нем, почитайте.
    • 0
      мы не форкались, но активно использовали сокеты. как показал наш опыт — утечки связаны с кодом, а не с языком — наверняка есть циклические ссылки, например.
      совет по отлову — логируйте сколько было памяти до обработки запроса и после. если дельта больше нуля — смотрите всю цепочку обработки, удаляйте не нужные объекты, избавляйтесь от циклических ссылок. особое внимание — foreach'ам с объектами.
  • –4
    #! — называет шибэнг.

    непонятно зачем использовать php, если есть bash для этих целей. Особенно exec и system, которые по-хорошему нужно запретить использовать.
    • 0
      Думаю, вы не совсем поняли сочетание «PHP CLI» в названии топика.

      1. Основной плюс PHP CLI — работа с объектами (моделями) уже описанные в самом проекте, без написания уже существующего функционала, следовательно экономим время на разработке.

      2. Bash врядли сможет легко манипулировать данными как в базе, так и в nosql демонах (memcached/mongo/redis). А вызов system в php cli зачастую необходим только для того, чтобы сделать ротацию логов, сделать ресайз нескольких фотографий, ну и удалить какой-то мусор.
      • –1
        > 1. Основной плюс PHP CLI — работа с объектами (моделями) уже описанные в самом проекте, без написания уже существующего функционала, следовательно экономим время на разработке.

        никогда не приходилось использовать никаких моделей в скриптах. Все что запускалось по крону работало напрямую с БД

        > 2. Bash врядли сможет легко манипулировать данными как в базе, так и в nosql демонах (memcached/mongo/redis). А вызов system в php cli зачастую необходим только для того, чтобы сделать ротацию логов, сделать ресайз нескольких фотографий, ну и удалить какой-то мусор.

        с mysql/pgsql никаких проблем не возникало. Для memcached:

        echo «stats» | nc 127.0.0.1 11211

        • 0
          > никогда не приходилось использовать никаких моделей в скриптах.
          вам не приходилось. мне приходилось. это вопрос личного опыта, а не аргумент.

          > с mysql/pgsql никаких проблем не возникало.
          так зачем же вы писали скрипт на php, который работает с базой, если одними и теми же данными может с успехом манипулировать и bash?
          • 0
            > так зачем же вы писали скрипт на php, который работает с базой, если одними и теми же данными может с успехом манипулировать и bash?

            у меня есть системные CGI скрипты на bash/awk, которые выводят статистику по БД, на php я не пишу. Все либо FCGI на C, либо WSGI на Python. Как только FCGI реализуют на bash, обязательно перейду на него.
  • 0
    Было бы неплохо осветить работу с расширением readline, многим было бы интересно.
  • +1
    скрипт на картинке зачотный!
  • –4
    Я могу понять CLI-скрипт на PHP в том случае, если у вас есть какой-нибудь сложный веб-проект на PHP с уже готовой объектной моделью, с которой надо работать.

    Я могу понять, если человек просто не знает (или знает недостаточно хорошо) другие скриптовые языки, вроде перла или питона, и есть какая-нибудь несерьезная или учебная задачка.

    Во всех же остальных случаях… ну, это все равно что какать на улице при людях — вроде бы и покакали (решили задачу), но ни вам, ни окружающим удовольствия это не принесло. Ну если только что вы большой шутник да затейник.
    • +2
      А если без туалетных метафор, чем PHP хуже перла или питона?
      • –3
        Да на нем же одна школота пишет! *sarcasm*
      • 0
        PHP достаточно универсален, чтобы на нем можно было написать все, что можно написать на Perl, Python, bash, whatever.

        Но видите ли в чем дело, PHP не предназначен для того, чтобы писать на нем системные утилиты. Это доказывает хотя бы эта вот статья. Согласитесь, что если бы вы писали на bash, вам бы не пришлось указывать на тонкости рекурсивного удаления директории или запуска другой программы и получения данных от нее, не так ли?

        Используйте язык для решения тех задач, для которых он разрабатывался, а если перед вами стоит задача из другой области — воспользуйтесь другим.
        • +2
          > Согласитесь, что если бы вы писали на bash, вам бы не пришлось указывать на тонкости рекурсивного удаления директории или запуска другой программы и получения данных от нее, не так ли?

          Согласитесь, что если бы вы писали на bash, вам бы пришлось указывать на тонкости выборки из БД, кэширования, объектной модели, существующего кода etc.
          • –2
            Я вижу, вы уловили суть — использовать тот язык программирования, который предназначен для решения стоящей перед вами задачи, а не тот, который вы знаете или который вам нравится.
            • +1
              И использовать 10 языков вместо одного? Нет, понятно, иногда есть явные ограничения языка (скорость, возможности работы с памятью, многопоточность), которые заставляют писать на чем-то другом. А тут то что не так? php вполне нормально работает в качестве средства для написания консольных утилит. Особенно, если есть подходящее окружение (framework).
              Писать на перле и питоне только потому, что в айтишной тусовке их считают более крутыми? Нормально для вспыльчивого студента, не более.
              • 0
                Для начала, давайте вы не будете заниматься демагогией и приписывать мне высказывания, которых я не говорил (я про крутость языков).

                Затем, насчет «вполне нормально работает» — возможно, если бы он просто нормально работал, не было бы необходимости в этой статье, разве нет?

                А это коварное «вполне» впоследствии может вылиться в большие проблемы, как по времени, так и по деньгам — я, например, сталкивался.

                • 0
                  Вы сталкивались с кривыми руками (если были проблемы конкретно с cli на php). php позволяет писать криво, это его проблема. Но только для тех, кто пишет криво. Грамотно он тоже позволяет делать.

                  Касательно того, зачем статья — в учебниках по Си подобные вещи тоже описывают. Они элементарны, в основном, просто почему-то в учебниках по php это обходится стороной, обычно. Но ничего необычного или сложного в этой статье не описывается, подобным образом работают и на других языках консольные программы.
                  Единственный описанный ньюанс — реализация system, остальное — практически цитаты из учебника.
  • 0
    Да ладно, каждый работает с чем умеет. Вон у Zeus'а makefile был php написан :)
  • 0
    Консоль forever!

    Спасибо, определенно полезная статья с учетом того что часто проще написать консольную утилиту на более медленном языке, чем посвящать разработчика на другом языке во всю архитектуру существующего веб-приложения.
  • –1
    А почему не воспользоваться PHP_EOL вместо \n?
    • 0
      По-моему, echo "Hello world\n"; в Windows тоже работает, а для всех остальных ОС (в том числе Mac OS X) строки итак заканчиваются на "\n" по умолчанию. Поэтому я не вижу смысла в использовании PHP_EOL конкретно в этом месте :)
      • 0
        Windows \r\n
        Mac OS X \r
        Остальной *nix \n
        AFAIK
        • +1
          Нет. В MacOS 9 и ниже — действительно "\r", поскольку MacOS 9 — это не *nix. Как подсказывает вики (http://en.wikipedia.org/wiki/MacOS), последняя версия классической MacOS была выпущена более 10 лет назад (в 2001 году).
          • 0
            Так а зачем разбираться в этих тонкостях, если PHP_EOL предназначена именно для решения этих вопросов? А если завтра появится новая ОС и в ней будет другой символ для перевода строки?
            • –1
              Да нет здесь никаких тонкостей :). Сейчас \n работает везде, и я не вижу причин, почему кто-то в обозримом будущем будет создавать ОС, где перевод строки — это что-то ещё.
    • 0
      Правильное предложение, на мой взгляд.
  • 0
    // не забывайте \n на конце
    echo "Usage: ".$argv[0]." \n";
    А мне нравится так писать:
    echo "Usage: {$argv[0]} ", PHP_EOL;
    Хотя это не принципиально.

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

    2>&1 использую для вывода ошибок в стандартный поток, когда по ssh команду какую-нить у другого сервера прошу выполнить, иначе ошибок при выполнении команды на удалённом сервере не видать.
    • 0
      Ой, че-то с тэгами накосячил, всё как код теперь :(
  • 0
    кстати, всегда думал, что на «неправильные аргументы командной строки», принято возвращать все-таки двойку:
    $ ls -z; echo $?
    ls: invalid option -- 'z'
    Try `ls --help' for more information.
    2

    хотя, традиция — не закон.
    • 0
      $ ls --afdafdsaf
      ls: illegal option — -
      usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
      $ echo $?
      1
    • 0
      Пример в linux:

      $ git dfadsfdsf
      git: 'dfadsfdsf' is not a git command. See 'git --help'.
      $ echo $?
      1
      $ cp --afdfadfa adfadsaf
      cp: ключ --afdfadfa не распознан
      Попробуйте `cp --help' для получения более подробного описания.
      $ echo $?
      1
  • 0
    вполне годные можно скрипты писать на bash с применением всех консольных утилит

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

Самое читаемое Разное