Однажды, когда я запустил один из моих скриптов, я получил исключение Missing argument in sprintf at ...
(отсутствующий аргумент в sprintf ...).
В общем я написал небольшой пример, демонстрирующий проблему. Взглянем на пример:
В корне проекта у меня находятся два файла, скрипт и модуль, который представляет текущий обрабатываемый файл:
dir/
printf_interpolate.pl
lib/App/File.pm
Скрипт printf_interpolate.pl
содержит следующий код:
use strict;
use warnings FATAL => 'all';
use App::File;
my $LIMIT = 80;
my $file = read_params(@ARGV);
_log("START");
check_file($file);
_log("DONE");
sub read_params {
my $filename = shift or die "Usage: $0 FILENAME\n";
my $file = App::File->new($filename);
return $file;
}
sub check_file {
my ($file) = @_;
open my $fh, '<', $file->filename or die;
while (my $line = <$fh>) {
my $actual = length $line;
if ($actual > $LIMIT) {
_log(sprintf "Line is too long ($actual > $LIMIT) ($line) (file %s)", $file->filename);
}
}
}
sub _log {
my $str = shift;
say $str;
}
Всё, что делает код в этом примере, это построчное чтение файла с проверкой длины строк. Если строка слишком длинная, вызывается функция _log
и выводится предупреждение.
Модуль /lib/App/File.pm
содержит следующий код:
package App::File;
use strict;
use warnings;
sub new {
my ($class, $filename) = @_;
return bless {filename => $filename}, $class;
}
sub filename {
my ($self) = @_;
return $self->{filename};
}
1;
Это простой модуль, который представляет файл. В реальном коде реализация класса будет значительно сложнее, но в нашем случае такого примитивного класса достаточно.
Итак, что произойдёт, если я запущу скрипт, передав этот скрипт в качестве параметра? То есть, что произойдёт, если скрипт не будет содержать ни одной строки, длиннее 80 символво?
cd dir/
perl printf_interpolate.pl printf_interpolate.pl
После вывода слова "START" мы получим следующее исключение:
Missing argument in sprintf at printf_interpolate.pl line 27, <$fh> line 27.
pointing us to the line where we call sprintf
.
На самом деле это предупреждение, т.к. код use warnings FATAL => 'all'
включает обработку всех предупреждений как фатальные исключения.
Проблема заключается в том, что некоторые строки содержат фрагменты, которые похожи на заполнители, использующиеся в функции sprintf.
Т.к. sprintf
содержит некоторые встроенные (интерполированные) переменные (в частности, переменную $line
), после интерполяции строка будет содержать более одного заполнителя %s
.
Если заменить проблемный код на следующий:
_log(sprintf "Line is too long ($actual > $LIMIT) (%s) (file %s)", $line, $file->filename);
то это решит указанную выше проблему, т.к. мы вставили ещё один форматтер %s
, который будет раскрыт во время вызова sprintf
.
Если запустить на выполнение код, то теперь мы увидим следующее:
START
Line is too long (104 > 80) ( _log(sprintf "Line is too long ($actual > $LIMIT) (%s) (file %s)", $line, $file->filename);
) (file printf_interpolate.pl)
DONE
Ну вот, теперь лучше.
Конечно, если мы вынесли одну переменную из строки, то мы должны поступить таким же образом с двумя оставишимися переменными:
_log(sprintf "Line is too long (%s > %s) (%s) (file %s)", $actual, $LIMIT, $line, $file->filename);
Короткий пример
Следующий фрагмент кода может воспроизвести предупреждение:
use strict;
use warnings;
my $name = 'Foo';
#my $smalltalk = 'how are you?';
my $smalltalk = '%s hi?';
printf "Hello %s, $smalltalk\n", $name;
Missing argument in printf at printf_interpolate_short line 8.
Hello Foo, hi?
Итог
Не встраивайте переменные в функции printf
и sprintf
, т.к. переменные могут содержать специальные символы, которые могут управлять поведением этих функций.