Writing System Utilities in the PHP CLI
For most professionals, PHP is not a language that would be seriously used to write console utilities, and there are many reasons for this. PHP was originally developed as a language for creating websites, but, starting with PHP 4.3, in 2002 there was official support for the CLI mode, so it has long ceased to be such. Over the course of several years, Badoo developers have been successfully using many interactive CLI utilities in PHP. In this article, we would like to share our experience with the CLI mode in PHP and give some recommendations to those who are going to write scripts in PHP, provided that they will run in a * nix system (however, almost everything is also true for Windows )
Recommendations
Work speed
It is widely believed that PHP is a slow language, and it really is. For the PHP CLI, it is recommended not to use heavy frameworks or even just large PHP libraries for two reasons:
- The run time of include / require in the CLI mode will always include parsing and execution, as Bytecode in this mode is not cached (at least by default), which means initialization will take a lot of time, even if everything works quite quickly from under the web server.
- Website users are used to waiting a certain amount of time to load a page (about 1 second, and sometimes a little more, the user perceives it quite normally), but the same cannot be said about the CLI: even a delay of 100 ms will already be noticeable, and in 1 second or more can be annoying.
Output on display
In CLI and web mode, the output to the screen is significantly different. In web mode, the output is usually buffered; you cannot ask the user anything while the script is executing; missing as a class is the concept of output to the error stream. In the CLI mode, naturally, HTML output is unacceptable, and long string output is highly undesirable. In the CLI, echo by default calls flush () (more details can be found here ) - this is convenient because you do not have to worry about calling flush () manually if, for example, the output is redirected to a file.
Also for CLI scripts, it makes sense to display errors not in STDOUT (using echo), but in STDERR: in this way, even if the output of the program is redirected somewhere else (for example, to / dev / null or grep), the user will not miss the text errors in case of its occurrence. This is standard behavior for most native * nix console utilities, and STDERR exists for exactly the reason described above. In PHP, for writing to STDERR, you can use, for example, fwrite (STDERR, $ message) or error_log ($ message).
Using Return Codes
The return code is a number that is 0 if the command succeeds and not 0 otherwise. A return code of 1 is often used in case of non-critical errors (for example, if incorrect command line arguments are specified), and 2 in case of critical system errors (for example, if a network or disk error). Values like 127 or 255 are usually used for any special cases that are shown separately in the documentation.
By default, when you simply end a PHP script, it is assumed that all the commands worked successfully and returns 0. To exit with a specific return code, you must explicitly call exit (NUM), where NUM is the return code (remember that it is 0 in success and has a different meaning in case of errors).
To understand that an external command executed with exec () or system () did not succeed, you need to pass the variable $ return_var as parameters of the corresponding functions and check the value for equality to zero.
Attention! If you are going to write exec ('some_cmd ... 2> & 1', $ output) so that the errors also fall into $ output, we recommend that you familiarize yourself with the reasons for separating STDOUT and STDERR and remove the explicit redirection of the error stream to STDOUT (2> & 1). Such redirection is required much less often than it might seem. The only case when its use is even a little justified (in a PHP script) is the need to print the result of the command execution on a web page (not in the CLI!), Including errors that occurred (otherwise they will end up in the web server’s log or even go to / dev / null).
"Masking" for the built-in system commands
A good console utility should behave in a standard way and users may not even know that it is in PHP. To do this, * nix-systems provide a mechanism that many people know about running scripts in Perl / Python / Ruby, but equally applicable to PHP.
If you add, for example, #! / Usr / bin / env php to the beginning of the PHP file and line break, give it execute rights (chmod 755 myscript.php) and remove the .php extension (the latter is optional), then the file will execute, like any other executable (./myscript). You can add the directory with the script in PATH or move it to one of the standard PATH directories, for example, / usr / local / bin, and then the script can be called with a simple set of "myscript", like any other system utility.
Handling Command Line Arguments
There is an agreement on the format of command line arguments that most built-in system utilities follow, and we recommend that you follow it and your scripts.
Write a brief help for your script if it received the wrong number of arguments.
To find out the name of the called script, use $ argv [0]:
if($argc != 2) {
// не забывайте \n на конце
echo "Usage: ".$argv[0]." \n";
// возвращаем ненулевой код возврата, что свидетельствует об ошибке
exit(1);
} To facilitate flag handling, you can use getopt (). Getopt () is one of the built-in functions for processing command line arguments. On the other hand, it’s not difficult to process part of the arguments manually, as in PHP this is not a big deal. You may need this method if you need to process arguments in the style of ssh or sudo (sudo -u nobody echo Hello world will execute echo Hello world from the user nobody specified after the -u flag before the command).
Recommendations for a more difficult level
Calling the "correct" system () for the CLI
The implementation of system () has already been written here . The point is that the standard system () in PHP is not a call to system () in C, but a wrapper over popen (), respectively, "spoils" STDIN and STDOUT of the called script. To prevent this from happening, you need to use the following function:
// функция совместима по аргументам с system() в С
function cSystem($cmd) {
$pp = proc_open($cmd, array(STDIN,STDOUT,STDERR), $pipes);
if(!$pp) return 127;
return proc_close($pp);
}Work with the file system
To the possible surprise, we recommend not writing your own implementation of recursive deletion (copying, moving) of files, but instead use the built-in commands mv, rm, cp (for Windows - the corresponding analogues). This is not portable between Windows / * nix, but it avoids some of the problems described below.
Let's look at a simple example of implementing a recursive directory deletion in 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);
}At first glance, that's right, right? Moreover, even in well-known file managers in PHP (for example, in eXtplorer and in the comments to the documentation), deleting a folder is implemented in this way. Now create a symbolic link to a nonexistent file (ln -s some_test other_test) and try to delete it. Or create a symbolic link in the folder to ourselves, or to the root of the FS (we recommend not to test this option) ... Specifically for recursiveDelete (), the fix is, of course, trivial, but it’s clear that it’s better not to reinvent the wheel and use the built-in commands, albeit losing a bit in performance.
Error Cleaning
If your script does some operations with files (database, sockets, etc.), then you often need to correctly shut down the program in case of unexpected errors: it can be writing to the log, clearing temporary files, removing file locks, etc. .d.
In PHP web mode, this is implemented using register_shutdown_function (), which is triggered even when the script ends with a fatal error (this method, by the way, is suitable for catching almost any errors, including memory shortage errors). In the CLI mode, everything is a little more complicated, because the user, for example, can send your script Ctrl + C, and register_shutdown_function () will not work.
But the explanation is simple: PHP by default does not process UNIX signals at all, so receiving any signal immediately causes the script to terminate. This can be fixed by adding declare (ticks = 1), to the top of the file after here):
pcntl_signal(SIGINT, function() { exit(1); }); // Ctrl+C
pcntl_signal(SIGTERM, function() { exit(1); }); // killall myscript / kill
pcntl_signal(SIGHUP, function() { exit(1); }); // обрыв связи The functions for signal processing do not have to be the same for everyone. You can not call exit () inside the signal handler - then the script will continue to run after the signal is processed.
Working with a database in several processes (after fork ())
The recommendation is very simple: you should close all connections to the database before executing fork () (ideally, even open files with fopen () should not be present), because executing fork () in these cases can lead to very strange consequences, and for connecting to the database it simply leads to closing the connection after the completion of any of the forked processes. The same SQLite manual explicitly states that a resource opened before fork () cannot be used in forked processes because it does not support multithreaded access in this way. In any case, pcntl_fork () in PHP just makes fork () and logs errors, so you need to handle it as carefully as in C.
Using ncurses for complex rendering
The ncurses library was created specifically so that you don’t have to worry about esc sequences for controlling the position of the cursor in the terminal and that a program that uses, for example, color, is portable between systems and terminals. On the other hand, even for such simple things as color output, you need to keep in mind that STDOUT does not always support colors. We know one primitive, but unreliable, way to find out without ncurses whether the terminal supports color - to check if the STDOUT is a terminal (posix_isatty (1)).
The number of displayed on the screen
Most standard programs display almost nothing on the screen, unless they are specifically asked for it by specifying the -v switch (verbose, chatty). Indeed, do not clog the screen for no reason. Finding a balance can be difficult, but there are a few simple recommendations:
- If the operation does not take much time (less than 10 seconds), do not display anything at all;
- If you are doing something non-trivial (for example, mounting temporary devices using sudo), on the contrary, inform the user about this so that he knows what to do in case of an error;
- If the operation is lengthy and it is possible to show the progress for it, it is better to show this very progress (the cSystem function mentioned above may be useful for this);
- If the program can work as a filter (for example, cat, grep, gzip ...), check that only data gets into STDOUT, and errors, prompts for input, etc. go to STDERR so that the next programs in the chain do not receive any unnecessary garbage.
for($i = 0; $i <= 100; $i++) {
printf("\r%3d%%", $i);
sleep(1);
}
echo "\n";Determining the name of the user who called the script
The username is contained in the USER environment variable ($ _ENV ['USER']), but there is one catch - this method uses environment variables that can report incorrect data (the user can execute a script, say, like USER = root myscript, and the script will assume that the username is root).
Therefore, you need to use the posix functions:
// getuid() вернет пользователя, который вызывал скрипт, а не эффективный uid – в данном случае нам это и нужно
$info = posix_getpwuid(posix_getuid());
$login = $info['name'];Conclusion
In this article, we tried to provide recommendations that are not immediately obvious directly to the PHP developers than to all programmers writing console utilities in general. Although much of the above can be applied to other programming languages, and perhaps some points will be useful to those who are not going to write in PHP.
Yuri youROCK Nasretdinov, developer Badoo