游侠网云服务,免实名免备案服务器 游侠云域名,免实名免备案域名

统一声明:

1.本站联系方式
QQ:709466365
TG:@UXWNET
官方TG频道:@UXW_NET
如果有其他人通过本站链接联系您导致被骗,本站一律不负责!

2.需要付费搭建请联系站长QQ:709466365 TG:@UXWNET
3.免实名域名注册购买- 游侠云域名
4.免实名国外服务器购买- 游侠网云服务
PHP-CLI命令行模式开发从新手到高手:实战入门与进阶全攻略

从0到1:PHP-CLI入门,先搞定这3件事

先别急着写复杂脚本,入门阶段把“环境配置、第一个脚本、参数解析”搞懂,就能解决80%的基础需求——我当初就是靠这3件事,从“对着命令行发懵”变成“能写出能用的脚本”。

先确认:你的PHP已经支持CLI模式

其实PHP默认就带CLI模式,不用额外装插件。你打开终端(Linux/Mac是Terminal,Windows是CMD),输php -v,如果能显示类似这样的结果,就说明没问题:

PHP 8.2.12 (cli) (built: Oct 25 2023 11:49:53) (NTS)

Copyright (c) The PHP Group

Zend Engine v4.2.12, Copyright (c) Zend Technologies

要是没反应,就得装PHP-CLI组件了。不同系统的安装方法我整理了个表格,照着做就行:

系统 安装命令 验证方式
Ubuntu/Debian sudo apt install php-cli php -v
CentOS/RHEL sudo yum install php-cli php -v
Windows 下载PHP安装包,勾选“CLI”组件 cmd中输入php.exe -v

写第一个CLI脚本:比你想的还简单

环境搞定后,写第一个脚本只需要3步。我第一次写的是“计算文件大小”的脚本——想知道某个文件有多大,不用打开文件管理器,终端输个命令就行。

步骤1:新建一个file_size.php文件,写代码:

<?php 

// 从命令行获取文件路径($argv[1]是第一个参数)

$filePath = $argv[1];

// 检查文件是否存在

if (!file_exists($filePath)) {

echo "Error:文件 {$filePath} 不存在!n";

exit(1); // 非0退出表示脚本执行失败

}

// 计算文件大小(转换为KB,保留2位小数)

$fileSize = filesize($filePath);

$sizeInKB = round($fileSize / 1024, 2);

// 输出结果

echo "文件 {$filePath} 的大小是:{$sizeInKB} KBn";

?>

步骤2:在终端运行脚本:

php file_size.php test.txt

如果test.txt存在,就能看到类似“文件 test.txt 的大小是:12.34 KB”的输出;如果不存在,会提示错误。

这里要重点说下$argv——这是PHP预定义的命令行参数数组$argv[0]永远是脚本本身的文件名(比如file_size.php),$argv[1]才是你输入的第一个参数。我之前犯过傻,把$argv[0]当成用户输入的文件路径,结果脚本一直输出“file_size.php不存在”,后来查了文档才反应过来——原来$argv的结构是固定的,第一个元素永远是脚本自己。

处理复杂参数:用getopt()搞定选项式输入

光用$argv处理简单参数没问题,但要是碰到带选项的参数(比如php script.php name=张三 age=20),就得用getopt()函数了。我写批量发送邮件的脚本时,就用getopt()处理过“收件人邮箱”“邮件主题”这类参数。

比如写一个send_email.php脚本:

<?php 

// 定义可接受的参数:短选项(-e=邮箱、-s=主题)、长选项(email、subject)

$options = getopt('e:s:', ['email:', 'subject:']);

// 解析参数(优先用短选项,没有就用长选项,再没有就用默认值)

$recipient = $options['e'] ?? $options['email'] ?? 'default@example.com';

$subject = $options['s'] ?? $options['subject'] ?? '测试邮件';

$content = "这是一封来自PHP-CLI的测试邮件!";

// 模拟发送邮件(实际项目用PHPMailer等库)

echo "正在给 {$recipient} 发送主题为『{$subject}』的邮件...n";

echo "发送成功!n";

?>

运行脚本时,可以用短选项:

php send_email.php -e test@example.com -s "CLI测试邮件"

也可以用长选项:

php send_email.php email test@example.com subject "CLI测试邮件"

甚至混合用:

php send_email.php -e test@example.com subject "CLI测试邮件"

getopt()的规则很简单:

  • 短选项用单个字母,比如'e:s:'表示-e-s后面必须跟参数(冒号:表示“需要参数”);
  • 长选项用数组,比如['email:', 'subject:']表示emailsubject后面必须跟参数;
  • 解析后的参数会存在$options数组里,键是短选项或长选项的名称。
  • 提醒下:如果参数里有空格(比如subject=Hello World),一定要用引号把参数包起来——否则HelloWorld会被当成两个不同的参数,$options['subject']只会拿到Hello,后面的World会跑到$argv的其他位置。

    从会用到好用:PHP-CLI进阶,这4个技巧让脚本更稳

    入门之后,你可能会发现——写出来的脚本能跑,但不够、不够:比如定时脚本重复运行导致数据重复,批量处理时内存爆炸,跑异步任务占满CPU。我帮朋友优化过一个导入10万条CSV数据到MySQL的脚本,原来要跑30分钟,优化后只要5分钟;还修复过一个监控脚本,因为没处理进程冲突,导致同一时间跑了5个实例,把数据库连接池占满。下面这4个技巧,是我从这些经历里 出来的“稳脚本秘诀”。

  • 用PID文件防止脚本重复运行
  • 很多CLI脚本是定时执行的(比如每分钟跑一次同步任务),如果前一次脚本还没跑完,下一次又启动,就会导致重复处理任务(比如重复同步数据到数据库)。我解决这个问题的办法是——创建PID文件:脚本启动时,检查是否有一个叫script.pid的文件,如果有,就查这个文件里的进程ID(PID)是不是还在运行;如果在,就退出脚本;如果不在(比如上一次脚本崩溃没删PID文件),就删掉旧的PID文件,再把当前进程的PID写进去。

    代码大概长这样(以同步数据库的脚本为例):

    <?php 

    // 定义PID文件路径(用__DIR__确保路径正确)

    $pidFile = __DIR__ . '/sync_db.pid';

    // 检查PID文件是否存在

    if (file_exists($pidFile)) {

    $existingPid = trim(file_get_contents($pidFile));

    // 检查进程是否在运行(posix_kill(pid, 0)不发送信号,只检查进程存在性)

    if (function_exists('posix_kill') && posix_kill($existingPid, 0)) {

    echo "Error:脚本已在运行(PID:{$existingPid}),请勿重复启动!n";

    exit(1);

    }

    // 进程不存在,删除旧PID文件

    unlink($pidFile);

    }

    // 写入当前进程的PID

    file_put_contents($pidFile, getmypid());

    //

  • // 核心任务:同步数据库(示例代码)
  • //

  • echo "开始同步数据库...n";
  • // 模拟同步过程(实际项目用PDO或mysqli)

    sleep(5); // 代替真实的同步操作

    echo "数据库同步完成!n";

    // 任务完成,删除PID文件

    unlink($pidFile);

    ?>

    这里要注意:

  • posix_kill()函数需要PHP安装posix扩展(Linux系统一般默认装了,Windows没有);
  • __DIR__来定义PID文件路径,避免“工作目录”的问题(后面会讲);
  • 脚本结束后一定要删除PID文件,否则下次启动会认为进程还在运行。
  • 优化内存:避免脚本“吃垮”服务器
  • CLI脚本跑批量任务时,最容易踩的坑就是内存泄漏。我之前写过一个导入10万条CSV数据到MySQL的脚本,没做任何内存优化,结果导入到第5万条时,内存占用从100M涨到1.2G,直接把服务器的2G内存吃满,导致其他服务卡崩。后来我用了3个优化技巧,把内存占用降到了300M以内:

    技巧1:分批处理,每批清理变量

    比如导入CSV数据时,不要一次性把10万条数据读进内存,而是每1000条处理一次,处理完就清理变量,再手动触发垃圾回收(GC)。代码示例:

    <?php 

    // 打开CSV文件

    $csvFile = fopen('users.csv', 'r');

    // 跳过表头(如果有的话)

    fgetcsv($csvFile);

    $batch = [];

    $batchSize = 1000; // 每批处理1000条

    while (($row = fgetcsv($csvFile)) !== false) {

    $batch[] = [

    'name' => $row[0],

    'email' => $row[1],

    'age' => $row[2]

    ];

    // 达到批次大小,插入数据库

    if (count($batch) >= $batchSize) {

    insertIntoDatabase($batch); // 插入数据库的函数

    unset($batch); // 释放变量内存

    $batch = []; // 重置批次数组

    gc_collect_cycles(); // 手动触发垃圾回收

    }

    }

    // 处理剩余的不足1000条数据

    if (!empty($batch)) {

    insertIntoDatabase($batch);

    }

    fclose($csvFile);

    echo "数据导入完成!n";

    ?>

    这里的关键是unset($batch)gc_collect_cycles()——unset()会释放变量占用的内存,gc_collect_cycles()会强制PHP回收那些“不再使用的内存”,避免内存越积越多。

    技巧2:限制内存使用上限

    ini_set('memory_limit', '512M')给脚本设一个内存上限——比如我把导入脚本的内存限制设为512M,这样即使有内存泄漏,也不会吃满服务器的内存。代码开头加:

    <?php 

    // 限制脚本最多使用512M内存

    ini_set('memory_limit', '512M');

    ?>

    技巧3:关闭不必要的扩展

    CLI脚本不需要Web相关的扩展(比如curlgdsession),可以在脚本开头关闭这些扩展,减少内存占用:

    <?php 

    // 关闭不需要的扩展

    ini_set('extension=curl.so', 0);

    ini_set('extension=gd.so', 0);

    ini_set('session.auto_start', 0);

    ?>

  • 用Redis做异步任务:让Web端更快
  • CLI脚本最适合做异步任务——比如用户注册后发送验证邮件、下单后发送短信通知、生成用户账单PDF。这些任务如果放在Web请求里做,会让用户等好几秒(比如发送邮件要连接SMTP服务器,可能得1-2秒),但用CLI脚本异步处理,Web端只要把任务丢进队列,就能立即响应。

    我帮朋友的社区网站做过一个“用户注册通知”的异步任务,流程是这样的:

    步骤1:Web端把任务推到Redis队列

    用户注册成功后,Web端将“收件人邮箱”“验证token”等信息JSON编码,推到Redis的列表(List)里:

    <?php 

    // 连接Redis

    $redis = new Redis();

    $redis->connect('127.0.0.1', 6379);

    // 构造任务数据

    $taskData = [

    'email' => $user['email'],

    'username' => $user['username'],

    'verify_token' => $user['verify_token']

    ];

    // 把任务推到Redis列表(lpush:从左边插入)

    $redis->lpush('register_notify_queue', json_encode($taskData));

    echo "注册成功!验证邮件将很快发送到你的邮箱~n";

    ?>

    步骤2:CLI脚本从队列里取任务并处理

    写一个process_notify.php脚本,用brpop()阻塞读取Redis列表(没有任务时会等待,不会占CPU):

    <?php 

    // 连接Redis

    $redis = new Redis();

    $redis->connect('127.0.0.1', 6379);

    echo "开始监听注册通知队列...n";

    while (true) {

    // brpop:阻塞读取队列(从右边弹出,没有任务时等待0秒=一直等)

    $result = $redis->brpop('register_notify_queue', 0);

    // $result[0]是队列名,$result[1]是任务数据

    $taskData = json_decode($result[1], true);

    // 处理任务:发送验证邮件(实际项目用PHPMailer)

    sendVerifyEmail($taskData['email'], $taskData['username'], $taskData['verify_token']);

    echo "处理任务成功:给 {$taskData['email']} 发送了验证邮件n";

    }

    ?>

    这里的关键是brpop()——它是阻塞式的,比用sleep(1)轮询队列高效多了(轮询会每隔1秒查一次Redis,占CPU;brpop()只有当队列有数据时才会唤醒进程)。

  • 避坑!这3个错误我踩过,你别再犯
  • 最后说几个我踩过的高频坑,帮你省点时间:

    坑1:权限问题:脚本执行用户要和目标目录匹配

    CLI脚本的执行用户是终端登录的用户(比如root、www-data),如果脚本要读写Web目录(比如/var/www/html),得确保执行用户有权限。我之前写的脚本用root运行,生成的文件权限是700(只有root能读),结果Web


    本文常见问题(FAQ)

    怎么知道自己的PHP有没有开启CLI模式?

    其实PHP默认就带CLI模式,不用额外装插件。你打开终端(Linux/Mac是Terminal,Windows是CMD),输入php -v,如果显示的结果里有“cli”字样(比如PHP 8.2.12 (cli)),就说明已经支持了。

    要是没反应,可能是没装PHP-CLI组件,得按对应系统的命令安装,比如Ubuntu/Debian用sudo apt install php-cli,Windows下载安装包时勾选“CLI”组件就行。

    第一次写PHP-CLI脚本,有什么容易踩的坑?

    最要注意的是$argv数组的结构——$argv[0]永远是脚本本身的文件名(比如file_size.php),$argv[1]才是你输入的第一个参数。比如你写计算文件大小的脚本,别把$argv[0]当成用户输入的文件路径,不然会提示“file_size.php不存在”,我之前就犯过这错。

    脚本执行失败时最好用exit(1)退出,非0状态码能让系统或定时任务知道执行出错了,比如文件不存在时exit(1),这样运维能及时发现问题,别用exit(0)(0表示成功)。

    CLI脚本要处理带选项的参数(比如name=张三),用什么方法?

    可以用PHP的getopt()函数,它专门用来处理带选项的参数,能同时支持短选项(比如-e=邮箱、-s=主题)和长选项(比如email=邮箱、subject=主题)。比如定义$options = getopt(‘e:s:’, [’email:’, ‘subject:’]),就能解析-e/-s这样的短选项,或者email/subject这样的长选项。

    解析的时候可以用“??”运算符兼容不同输入,比如$recipient = $options[‘e’] ?? $options[’email’] ?? ‘default@example.com’,优先用短选项,没有就用长选项,再没有就用默认值,这样用户怎么输入都能兼容。

    CLI脚本定时运行,怕重复启动导致数据冲突怎么办?

    可以用PID文件解决——脚本启动时先检查有没有对应的PID文件(比如sync_db.pid),如果有就取出里面的进程ID,用posix_kill(pid, 0)检查进程是不是还在运行;如果在,就输出错误退出;如果不在(比如上一次脚本崩溃没删文件),就删掉旧PID文件。

    然后把当前进程的ID写进PID文件(用getmypid()获取),任务完成后再删掉这个文件。要注意用__DIR__定义PID文件路径,避免“工作目录”的问题,比如__DIR__ . ‘/sync_db.pid’,这样不管脚本在哪运行,路径都是对的。