Вопрос или проблема
У меня есть CSV файл, и я хочу выполнить команду для каждой строки, используя поля файла как отдельные аргументы.
Например, учитывая следующий файл:
foo,42,red
bar,13,blue
baz,27,green
Я хочу выполнить следующие команды одну за другой:
my_cmd --arg1 42 --arg2 foo --arg3 red
my_cmd --arg1 13 --arg2 bar --arg3 blue
my_cmd --arg1 27 --arg2 baz --arg3 green
Какой самый простой способ этого достичь? Кажется, что это возможно с помощью xargs, но я не смог понять, как именно.
GNU parallel
может читать csv напрямую и обладает встроенной заменой элементов.
Большей частью взято из man parallel
:
parallel --csv 'my_cmd --arg1 {2} --arg2 {1} --arg3 {3}' :::: file.csv
Добавьте -j1
перед my_cmd
, чтобы эти вызовы выполнялись один за другим. Или не добавляйте, чтобы выполнять их параллельно.
(на debian и fedora, это находится в пакете под названием parallel
, а не в moreutils
или moreutils-parallel
)
Спасибо, Ole Tange!
Следующее сначала использует Miller (mlr
) для конвертации входного CSV без заголовка в JSONL выход (строки одиночных JSON объектов). Затем JSON процессор jq
читает эти объекты и выводит их части как аргументы к команде. Выходные данные — это код оболочки, который может быть выполнен через eval
.
$ cat file
foo,42,red
bar,13,blue
baz,27,green
"""mute"" swan","1,23",* * green * *
$ mlr --c2l -N cat file
{"1": "foo", "2": 42, "3": "red"}
{"1": "bar", "2": 13, "3": "blue"}
{"1": "baz", "2": 27, "3": "green"}
{"1": "\"mute\" swan", "2": "1,23", "3": "* * green * *"}
$ mlr --c2l -N cat file | jq -r '["my_cmd", "--arg1", ."1", "--arg2", ."2", "--arg3", ."3"] | @sh'
'my_cmd' '--arg1' 'foo' '--arg2' 42 '--arg3' 'red'
'my_cmd' '--arg1' 'bar' '--arg2' 13 '--arg3' 'blue'
'my_cmd' '--arg1' 'baz' '--arg2' 27 '--arg3' 'green'
'my_cmd' '--arg1' '"mute" swan' '--arg2' '1,23' '--arg3' '* * green * *'
Оператор вывода @sh
пытается обернуть данные, подходящие для оболочки. Это не всегда безошибочно, но обычно работает хорошо.
$ eval "$(mlr --c2l -N cat file | jq -r '["my_cmd", "--arg1", ."1", "--arg2", ."2", "--arg3", ."3"] | @sh')"
zsh: command not found: my_cmd
zsh: command not found: my_cmd
zsh: command not found: my_cmd
zsh: command not found: my_cmd
$ my_cmd () { echo ""; printf 'arg: %s\n' "$@"; }
$ eval "$(mlr --c2l -N cat file | jq -r '["my_cmd", "--arg1", ."1", "--arg2", ."2", "--arg3", ."3"] | @sh')"
arg: --arg1
arg: foo
arg: --arg2
arg: 42
arg: --arg3
arg: red
arg: --arg1
arg: bar
arg: --arg2
arg: 13
arg: --arg3
arg: blue
arg: --arg1
arg: baz
arg: --arg2
arg: 27
arg: --arg3
arg: green
arg: --arg1
arg: "mute" swan
arg: --arg2
arg: 1,23
arg: --arg3
arg: * * green * *
Вы также можете запускать команды прямо из Miller, но я не знаю, насколько хорошо его функция exec()
справляется со значениями, требующими кавычек в оболочке (и является ли это проблемой вообще). Я могу вернуться позже и пересмотреть это, когда найду время для тестирования.
Мне удобнее использовать awk, чем возиться с xargs, поэтому я обычно собираю аргументы с помощью awk, а затем передаю их в xargs:
$ awk -F ',' '{ print "--arg1", $2, "--arg2", $1, "--arg3", $3 }' csv.txt | xargs -L1 echo
--arg1 42 --arg2 foo --arg3 red
--arg1 13 --arg2 bar --arg3 blue
--arg1 27 --arg2 baz --arg3 green
Здесь -L1
означает “выполнять одну команду на строку ввода”.
Использование Raku (ранее известного как Perl_6)
…с модулем Text::CSV
из Raku:
~$ raku -MText::CSV -e '
my $fh = open "luator.txt", :r;
my $parser = Text::CSV.new;
until $fh.eof {
$_ = $parser.getline($fh);
run "echo", .[0], .[1], .[2] given $_;
}
$fh.close;'
Raku — это язык программирования из семейства Perl, который предоставляет простые функции для вызова внешних команд. Существует два варианта: использование shell
или run
. Согласно документации, использование run
более безопасно.
В примере выше, при объявлении объекта $parser
, можно задать различные параметры, такие как использование нестандартного разделителя (например: my $parser = Text::CSV.new(sep => "|");
). Затем файл считывается/анализируется построчно с помощью getline()
. В примере выше демонстрируется простой вывод с использованием echo
.
Пример ввода:
~$ cat luator.txt
foo,42,red
bar,13,blue
baz,27,green
Пример вывода (с echo
):
foo 42 red
bar 13 blue
baz 27 green
Ниже, использование run "printf", "%s\t", .[0].uc, .[1], .[2].uc given $_; run "printf", "\n";
, для разделения вывода колонок с помощью вкладок \t
. Обратите внимание, что здесь мы добавляем .uc
для преобразования первой и третьей колонок в верхний регистр, чтобы показать, что можно очистить текст при необходимости (до вызова вашего my_cmd
):
Пример вывода (с printf
):
FOO 42 RED
BAR 13 BLUE
BAZ 27 GREEN
Наконец, вы можете брать входные файлы с командной строки, используя динамическую переменную $*ARGFILES
из Raku. Очевидно, что вы замените свой my_cmd
на printf
ниже:
~$ raku -MText::CSV -e '
my $parser = Text::CSV.new;
until $*ARGFILES.eof {
$_ = $parser.getline($*ARGFILES);
run "printf", "%s ", "--arg1", .[0], "--arg2", .[1], "--arg3", .[2] given $_;
run "printf", "\n";
};' luator.txt
--arg1 foo --arg2 42 --arg3 red
--arg1 bar --arg2 13 --arg3 blue
--arg1 baz --arg2 27 --arg3 green
В противном случае, смотрите первую ссылку ниже, как сохранить вывод в объект Raku “Proc” (процесс), или вторую ссылку ниже, для использования “Proc::Async” (асинхронный интерфейс процесса).
https://docs.raku.org/type/Proc
https://docs.raku.org/type/Proc/Async
https://raku.org
Разделение выбора/упорядочивания полей (f=...
ниже) от добавления --argN
(цикл), чтобы было легко изменять поля и/или порядок и потенциально использовать одно и то же поле несколько раз, с использованием любого awk
и POSIX xargs
:
$ awk -F, -v f="2,1,3" '
{
n = split(f, flds)
for (i = 1; i <= n; i++) {
printf " --arg%d \"%s\"", i, $(flds[i])
}
print ""
}
' file | xargs -L1 echo my_cmd
my_cmd --arg1 42 --arg2 foo --arg3 red
my_cmd --arg1 13 --arg2 bar --arg3 blue
my_cmd --arg1 27 --arg2 baz --arg3 green
Исходя из этого, изменять порядок и дублировать поля так же просто, как изменить f="..."
:
$ awk -F, -v f="3,1,3,2,1" '
{
n = split(f, flds)
for (i = 1; i <= n; i++) {
printf " --arg%d \"%s\"", i, $(flds[i])
}
print ""
}
' file | xargs -L1 echo my_cmd
my_cmd --arg1 red --arg2 foo --arg3 red --arg4 42 --arg5 foo
my_cmd --arg1 blue --arg2 bar --arg3 blue --arg4 13 --arg5 bar
my_cmd --arg1 green --arg2 baz --arg3 green --arg4 27 --arg5 baz
Символы \"
вокруг %s
предназначены для того, чтобы xargs
правильно обрабатывал поля, содержащие пробелы.
Ответ или решение
Вопрос о том, как выполнить команду для каждой строки CSV-файла, используя поля файла в качестве отдельных аргументов, является достаточно распространенной задачей среди IT-специалистов, работающих с данными. Данная задача может быть решена несколькими способами на разных платформах и с использованием различных инструментов. В этом ответе мы рассмотрим несколько подходов к решению этой задачи, воспользовавшись инструментами командной строки, такими как GNU parallel
, awk
, xargs
, а также языками программирования, как Raku.
Теория
Когда у нас есть CSV-файл, каждая строка которого должна быть обработана отдельно, можно рассматривать этот файл как набор команд, параметры которых варьируются в зависимости от значений в строках. Основная задача здесь — это преобразовать каждую строку в соответствующую команду, где элементы строки становятся аргументами этой команды. Это типичная задача автоматизации, позволяющая существенно сэкономить время при обработке больших объемов данных.
Пример
Рассмотрим CSV-файл со следующим содержимым:
foo,42,red
bar,13,blue
baz,27,green
Необходимо выполнить команду my_cmd
таким образом, чтобы для каждой строки формировалась уникальная команда:
my_cmd --arg1 42 --arg2 foo --arg3 red
my_cmd --arg1 13 --arg2 bar --arg3 blue
my_cmd --arg1 27 --arg2 baz --arg3 green
Применение
Использование GNU parallel
GNU parallel
— это мощный инструмент, который позволяет параллельно выполнять команды. Он особенно полезен, когда нужно обрабатывать большие объемы данных быстро. Преимущество parallel
состоит в его способности непосредственно читать CSV-файлы и обрабатывать подстановку элементов.
Команда будет выглядеть следующим образом:
parallel --csv 'my_cmd --arg1 {2} --arg2 {1} --arg3 {3}' :::: file.csv
Здесь {1}
, {2}
, {3}
обозначают подстановку элементов строк CSV в соответствующие позиции в команде. Это мощный и лаконичный способ автоматизировать выполнение команд.
Использование awk
и xargs
Если parallel
вам недоступен, можно использовать awk
совместно с xargs
. Этот метод немного более сложный и требует предварительной обработки данных, но он имеет широкую платформенную совместимость.
Сначала awk
извлекает поля из каждой строки, затем xargs
формирует и выполняет команды:
awk -F ',' '{ print "--arg1", $2, "--arg2", $1, "--arg3", $3 }' file.csv | xargs -L1 my_cmd
Здесь -F ','
задает запятую в качестве разделителя полей, а -L1
указывает xargs
выполнять команду для каждой строки отдельно.
Использование Raku
Raku (ранее Perl 6) является мощным языком программирования, поддерживающим расширенные операции работы с CSV и интеграцию с системными командами. В этом языке также можно использовать модуль Text::CSV
для разбора CSV и выполнения команд с использованием встроенных функций.
Пример использования:
use Text::CSV;
my $fh = open "file.csv", :r;
my $parser = Text::CSV.new;
until $fh.eof {
$_ = $parser.getline($fh);
run 'my_cmd', '--arg1', .[1], '--arg2', .[0], '--arg3', .[2] given $_;
}
$fh.close;
Этот код читает каждый элемент строки в отдельные переменные, которые затем передаются в команду my_cmd
.
Заключение
При выборе метода выполнения команд на данных из CSV-файла важно учитывать масштабы и требования вашего проекта. GNU parallel
предлагает мощное решение для параллельного выполнения, в то время как awk
и xargs
обеспечивают гибкость и простоту развертывания на множествах платформ. Использование языков программирования, таких как Raku, предоставляет расширенные возможности обработки данных и интеграции команд, что является преимуществом в более сложных сценариях. Каждое из представленных решений имеет свои уникальные преимущества, которые могут быть адаптированы к специфическим требованиям вашего рабочего процесса.