LINUX.ORG.RU

Как экранировать пробелы во вложенных командах?

 ,


2

1

Есть однострочник:

for f in oldpath/*.png; do convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp"; done

Он работает в 1 поток. Можно запускать каждую команду отдельным процессом:

for f in oldpath/*.png; do convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp" & done

Но памяти хватает файлов на 300, если больше — всё прибивается по OOM.

Я хотел использовать GNU parallel, точнее sem:

for f in oldpath/*.png; do sem -j8 convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp" & done

Но как выяснилось, если в $f есть пробелы, convert получит его без кавычек и не сможет работать.

Вопрос: как принято передавать имена с пробелами пробелы таким утилитам, как sem?

Ответ: У sem и parallel есть параметр --quote или -q.

for f in oldpath/*.png; do sem -q -j8 convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp" & done

Но я всё ещё жду ответов о возможности более общего решения. Например, утилиты вроде tr или sed, автоматически экранирующей строки.

★★★★★

Последнее исправление: question4 (всего исправлений: 2)

for f in oldpath/*.png; do sem -j8 convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp" & done

Но как выяснилось, если в $f есть пробелы, convert получит его без кавычек и не сможет работать.

Судя по фразе «convert получит его без кавычек» ты не в теме.

Я бы сказал что это проблема в sem:

Уже sem получит его (имя файла) без кавычек, но одним параметром (в котором есть пробел). Далее sem лажает и передаёт convert уже два параметра (или больше, зависит от количества и расстановки пробелов). Скорее всего, sem запускает команду посредством шелла, и не заботится об экранировании спецсимволов (тех же самых пробелов).

Универсального решения проблемы здесь нет — надо курить или документацию sem (он же parallel), или исходники, и либо править багу, либо документировать поведение.

debugger ★★★★★
()
Последнее исправление: debugger (всего исправлений: 1)

У parallel есть аргумент -q, заключающий все аргументы передаваемых ему команд в кавычки. У sem наличие такого аргумента не документировано, но, судя по коду, он есть.

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

annulen ★★★★★
()
Ответ на: комментарий от annulen

У parallel есть аргумент -q, заключающий все аргументы передаваемых ему команд в кавычки.

Спасибо! Пропустил.

У sem наличие такого аргумента не документировано, но, судя по коду, он есть.

Согласно документации, sem эквивалентен parallel с 1 дополнительным аргументом. Поэтому ман неполный.

question4 ★★★★★
() автор топика
Ответ на: комментарий от debugger

Универсального решения проблемы здесь нет

А жалко.

Некоторые программы, включая ls, умеют задавать кастомные разделители или 0x00, но для parallel я такое искал и не нашёл.

Кто-нибудь задумывался об универсальном решении? По каким словам искать обсуждения?

Есть ли какой-то инструмент, чтобы экранировать произвольную строку? Без велосипеденья выражений для sed-а.

Судя по фразе «convert получит его без кавычек» ты не в теме.

Уже sem получит его (имя файла) без кавычек, но одним параметром (в котором есть пробел).

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

Далее sem лажает и передаёт convert уже два параметра (или больше, зависит от количества и расстановки пробелов). Скорее всего, sem запускает команду посредством шелла, и не заботится об экранировании спецсимволов (тех же самых пробелов).

Оказалось, экранирование нужно явно включать аргументом, документированным в середине другого мануала из того же пакета. Уже подсказали.

question4 ★★★★★
() автор топика
Ответ на: комментарий от versetty777

ls без параметра -1 выдает вывод по умолч. в виде неск. строк, а ls -1 выдает вывод в виде одной строки.

Строк? А не столбцов?

В итоге работает одинаково.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Согласно документации, sem эквивалентен parallel с 1 дополнительным аргументом. Поэтому ман неполный.

И правда, sem оказался симлинком на parallel. А я думал там тонна копипасты)

annulen ★★★★★
()

Ну как-то так:

#!/bin/bash

PARALLEL=2

Parallel_convert() {
	local f="${1##*/}"
	f="${f%.png}.webp"
	convert $PARAMETERS "$f"
}

declare -i j=0

for f in *.png; do
   Parallel_convert "$f" &
   [[ ++j -eq PARALLEL ]] && { wait -n; ((j--)); }
done
wait

vodz ★★★★★
()
Ответ на: комментарий от question4

В винде передача аргументов в программу — врождённый дефект. Гугли описание функции CreateProcess — процессу предаётся одна командная строка, содержащая все аргументы, и каждая программа вольна разбираться с этой строкой как хочет. Может положиться на стандартную сишную библиотеку, а может и не полагаться — виндовые (в смысле «не консольные», те, у которых исполнение начинается с функции WinMain, а не main) программы так и делают.

В юникс-подобных системах с этим полегче, т. к. программе передаётся массив строк, каждый аргумент — отдельная строка (man 2 execve). В отличие от винды юниксовой программе нет нужды думать как разбивать строчку на отдельные параметры.

Однако, одна программа может запустить другую программу не непосредственно (через execve), а, скажем, при помощи функции system (man 3 system), которая принимает одну строку, и запускающая программа должна сформировать эту строку правильно, чтобы шелл эту строчку побил на отдельные аргументы так, как и было задумано. В принципе, поведение шелла (причём не какого-нибудь, а /bin/sh) документировано и даже стандартизовано, поэтому подготовить строку для него не так трудно. (Но «не так трудно» не значит, что можно каждый аргумент тупо заключать в кавычки, т. к. аргумент может содержать кавычки.) Дальше распространяться на эту тему мне лень.

Когда я говорил «универсального решения нет», это относилось к взаимодействию с не вполне корректными программами, которые собирают командную строку абы как, не парясь. Обсуждать здесь особо нечего, просто надо писать коррекный код и нормальную документацию на программы.

debugger ★★★★★
()
Последнее исправление: debugger (всего исправлений: 1)
Ответ на: комментарий от debugger

Если имеется командная строка, уже разбитая на части, можно добавить бэкслэш перед каждым пробелом и спецсимволом. Средствами bash или какими-то стандартными утилитами так сделать можно? Помимо sed.

question4 ★★★★★
() автор топика
Ответ на: комментарий от question4

Средствами bash…

Bash — почти полноценный язык программирования. Сделать можно всё, особенно, если прочитать хотя бы раз инструкцию. Запусти man bash и поищи «Pattern substitution».

debugger ★★★★★
()
Ответ на: комментарий от t184256

А вот дикость ssh не обойти.

Элементарно. Закидываешь на таргет с помощью scp скрипт, который делает всё, что тебе надо и запускается без параметров, при помощи ssh запускаешь скрипт.

debugger ★★★★★
()
Ответ на: комментарий от t184256

Осталось заэкранировать все так в одну строку в этом скрипте, чтобы все пра- а, стоп, это ж изначальная задача.

Какие бесы заставляют тебя делать всё в одну строку, чудик?

debugger ★★★★★
()
Ответ на: комментарий от t184256
$ cat <<END > t.sh
set -e
# Do whatever you want, using the well-documented shell syntax.
find . -delete
END

$ scp t.sh remote:/tmp

$ ssh remote /bin/bash /tmp/t.sh

Всё. Нет никаких проблем с пробелами, кавычками и спецсимволами. Не бей себя по голове, это плохо сказывается на твоих умственных способностях.

debugger ★★★★★
()
Ответ на: комментарий от debugger

Разупорись сам, а.

Вот у тебя задача: выполнить argv0 argv1 argv2 … argvN

В каждом из них могут быть пробелы, кавычки, переводы строки и вообще любая оргия чего угодно в любой комбинации.

Интерфейс здорового человека выглядит как exec(argv). Интерфейс курильщика выглядит как ssh(str). У тебя массив аргументов, как положено, а запустить команду надо по ssh.

Как ты решил эту задачу? Как выглядит твой волшебный скрипт?

Я серьёзно спрашиваю, у меня есть место в коде, где вместо решения — изолента. С огромной радостью заменю её на правильное, но правильного не смог мне подсказать даже мейтейнер openssh, вздохнув, обвинив сам протокол и благословив вместо этого изоленту.

Do whatever you want, using the well-documented shell syntax, я и без скрипта могу.

t184256 ★★★★★
()
Последнее исправление: t184256 (всего исправлений: 1)
Ответ на: комментарий от t184256

Вот у тебя задача: выполнить argv0 argv1 argv2 … argvN

Что ж ты тупой-то такой? Тебе идеи мало, тебе ещё разжевать надо, чтоб ты проглотить смог? На, держи:

$ cat remote.sh
#!/bin/bash
{
    for a in "$@"; do
        echo -n "'${a//\'/\'\\\'\'}' "
    done
    echo
} > t.sh
scp t.sh remote:/tmp
ssh remote /bin/bash /tmp/t.sh

Скрипт remote.sh выполняет на удалённой системе remote ту командную строку, которая была передана скрипту, один-в-один, во всеми пробелами, кавычками, переводами строк и вообще любой оргией в любой комбинации, какую ты только сможешь придумать.

Пример использования:

$ ./remote.sh "with space" "with'apostrophe" 'with"quote' "with
newline"

В примере спецсимволы использованы по одному, но ничто не мешает тебе использовать любую оргию в любой комбинации, если у тебя хватит ума её правильно записать в шелле. Если не хватит — используй сишечку и execve — там ума надо поменьше.

P. S. Посмотрел на «синюю изоленту»:

cmd = (' '.join(["'" + a.replace("'", r"\'") + "'" for a in cmd])

По мне это не синяя изолента, а кто-то man bash не осилил. Я ж говорю: не бей себя по голове — выглядит глупо.

debugger ★★★★★
()
Последнее исправление: debugger (всего исправлений: 3)
Ответ на: комментарий от debugger

Пример использования:

$ ./remote.sh "with space" "with'apostrophe" 'with"quote' "with
newline"

Гениально! И вот ты предложил вместо ssh(quote_args(argv)) сделать ssh('/script ' + quote_args(argv)). То есть для самого использования твоего скрипта нужно предварительно произвести экранирование и превратить a'b"c в какое-нибудь "a'b\"c".

Какую проблему решил твой скрипт? Правильно, никакую, добавил геморроя. Зачем ты предлагаешь какой-то скрипт?

В примере спецсимволы использованы по одному, но ничто не мешает тебе использовать любую оргию в любой комбинации, если у тебя хватит ума её правильно записать в шелле.

В общем случае — не хватит, только в частных.

Если не хватит — используй сишечку и execve — там ума надо поменьше.

Так нет execve-like интерфейса у ssh-библиотек из-за дебильности протокола, услышь ты уже наконец.

t184256 ★★★★★
()
Последнее исправление: t184256 (всего исправлений: 3)
Ответ на: комментарий от t184256

И вот ты предложил вместо ssh(quote_args(argv)) сделать ssh(’/script ’ + quote_args(argv)).

Нет, ты ничего не понял. Абсолютно ничего.

То есть для самого использования твоего скрипта нужно предварительно произвести экранирование и превратить a’b"c в какое-нибудь «a’b"c».

Производить экранирование для запуска моего скрипта нужно если ты запускаешь его из шелла, т. к. в шелле апостроф, кавычки, пробел и конец строки — специальные символы. Это естественно.

Если ты запускаешь его из программы на сях:

char * cmd_with_args[] = {
    "with space",
    "with'apostrophe",
    "with\"quote",
    "with\nnewline",
    NULL,
};
execve(".../remote.sh", cmd_with_args, NULL);

Как видишь, даже в сях приходится экранировать кавычки и конец сроки — это обычные правила того языка, на котором ты пишешь программу. Но при этом тебе не надо знать правила шелла, и, например, экранировать пробелы и апострофы, т. к. remote.sh сам выполнит эту задачу, и передаст все параметры на удалённую систему ровно в том виде, в котором он их сам получил.

Все параметры из массива args будут переданы на удалённую машину как есть, с сохранением пробелов, кавычек, апострофов, концов строк и любой оргии в любой комбинации.

Говорят, можно дать человеку книгу, можно даже заставить её прочитать, но невозможно заставить её понять.

Так нет execve-like интерфейса у ssh-библиотек из-за дебильности протокола, услышь ты уже наконец.

Извини, но это не у ssh-библиотек дебильность.

debugger ★★★★★
()
Последнее исправление: debugger (всего исправлений: 2)

По прочтению треда вспомнилась нетленка: https://habr.com/ru/post/321652/

Особенно, это:

Как зайти на хост a@a, с него — на b@b, с него — на c@c, с него — на d@d, а с него удалить файл /foo? Ну, это легко:

ssh a@a "ssh b@b \"ssh c@c \\\"ssh d@d \\\\\\\"rm /foo\\\\\\\"\\\"\""

Слишком много бекслешей, да? Ну, не нравится так, давайте чередовать одинарные и двойные кавычки, будет не так скучно:

ssh a@a 'ssh b@b "ssh c@c '\''ssh d@d \"rm /foo\"'\''"'

А между прочим, если бы вместо shell’а был Lisp, и там функция ssh передавала бы на удалённую сторону не строку (вот она, повёрнутость UNIX на тексте!), а уже распарсенный AST (abstract syntax tree), то такого ада бекслешей не было бы:

(ssh "a@a" '(ssh "b@b" '(ssh "c@c" '(ssh "d@d" '(rm "foo")))))

Когда наконец в Linux появится нормальный Shell, а не вот это вот?

EXL ★★★★★
()
Последнее исправление: EXL (всего исправлений: 1)
Ответ на: комментарий от debugger

А, я, наконец, понял.

remote.sh, названный так, чтобы враг не догадался, кладется локально. remote.sh дергается exec-style, поэтому ему передается массив аргументов. remote.sh то ли нафиг бесполезен, то ли коряво экранирует, чтобы вызвать ssh shell-style. Затем шелл переделывает это в массив аргументов, который exec-style передается команде ssh, которая опять их экранирует и передает серверу строкой shell-style, где они опять разбираются в массив.

Так как если бы я мог дергать ssh вместо использования библиотеки, я бы просто дергал ssh, remote.sh бесполезен, ты тоже.

t184256 ★★★★★
()

если в $f есть пробелы, convert получит его без кавычек и не сможет работать

В данном случае можно передать дополнительные кавычки convert’у: for f in oldpath/*.png; do sem -j8 convert \'"$f"\' $PARAMETERS \'newpath/`basename "$f" .png`.webp\' & done. А общего решения, думаю, нет.

undef ★★
()
14 апреля 2023 г.

Оптимальные, вроде, параметры:

for ex in {png,jpg}; do for f in oldpath/*.$ex; do sem -q -j7 convert "$f" -quality 76 -define webp:method=6:thread-level=1:pass=4:sns-strength=100  newpath/"`basename "$f" .$ex`.webp" ; done ; done &
question4 ★★★★★
() автор топика
19 марта 2024 г.

Напоминание: sem и parallel глючат от имён файлов, содержащих одинарную кавычку (апостроф). Если есть шанс встретить такое имя, необходим ключ -q он же --quote.

question4 ★★★★★
() автор топика

Я проще через GNU Parallel делал, хз правда что там с пробелами, вроде ни разу не косячило. Идея простая. Пишешь на баше функцию (да они есть) которая делает то что тебе надо с твоими многострадальными файлами. В твоём случае это будет как-то так

ConvertImages
{
    f="$1"
    PARAMETERS="$2"
    convert "$f" $PARAMETERS "newpath/`basename "$f" .png`.webp"
}
export -f ConvertImages
find -type f -iname '*.png' | parallel --no-notice opt_optipng '{}'
Ну а дальше сам смотри что и где там у тебя теряется. Вместо '*.png' смотри может oldpath/*.png тебе надо писать, мне лениво, я с головы пишу, так что можешь проверять, мог и накосячить

peregrine ★★★★★
()
Ответ на: комментарий от peregrine

Пишешь на баше функцию (да они есть)

Будет ли принципиальное отличие, если то же самое оформить в виде скрипта?

Потому что я на проблему из-за отсутствия -q нарвался именно при запуске такого скрипта sem -j 8 png2webp_and_rm "$filename"

Но за совет спасибо. Может, пригодится.

question4 ★★★★★
() автор топика

утилиты вроде tr или sed, автоматически экранирующей строки

Если нет восклицательных знаков, можно пользоваться питоновским re.escape():

ls -1 | python -c "import re, sys; [print(re.escape(f)) for f in sys.stdin.read().split('\n') if f]"

Общий случай:

ls -1 | python -c "import sys; s = ' \!\"$&\'()*<>?[\\]\`{|}~'; d = {c : '\\\\' + c for c in s}; t = str.maketrans(d); [print(f.translate(t)) for f in sys.stdin.read().split('\n') if f]"

или

ls -1 | python -c "import sys; t = str.maketrans({c:'\\\\' + c for c in ' \!\"$&\'()*<>?[\\]\`{|}~'}); [print(f.translate(t)) for f in sys.stdin.read().split('\n') if f]"

Или отдельным скриптом:

#!/usr/bin/python3
import sys
# s = ' !"$&\'()*<>?[\\]`{|}~'; d = {c : '\\' + c for c in s}; t = str.maketrans(d)
t = {32: '\\ ', 33: '\\!', 34: '\\"', 36: '\\$', 38: '\\&', 39: "\\'", 40: '\\(', 41: '\\)', 42: '\\*', 60: '\\<', 62: '\\>', 63: '\\?', 91: '\\[', 92: '\\\\', 93: '\\]', 96: '\\`', 123: '\\{', 124: '\\|', 125: '\\}', 126: '\\~'}
[print(f.translate(t)) for f in sys.stdin.read().split('\n')[:-1]]
question4 ★★★★★
() автор топика
Последнее исправление: question4 (всего исправлений: 1)
Ответ на: комментарий от question4

Или чтобы не дожидалось окончания ввода:

#!/usr/bin/python3
import sys
# symbols = ' !"$&\'()*<>?[\\]`{|}~'; trans_dict = {c : '\\' + c for c in s}; t = str.maketrans(trans_dict)
t = {32: '\\ ', 33: '\\!', 34: '\\"', 36: '\\$', 38: '\\&', 39: "\\'", 40: '\\(', 41: '\\)', 42: '\\*', 60: '\\<', 62: '\\>', 63: '\\?', 91: '\\[', 92: '\\\\', 93: '\\]', 96: '\\`', 123: '\\{', 124: '\\|', 125: '\\}', 126: '\\~'}
try:
    l = sys.stdin.readline()
    while l:
        print(l.translate(t), end='');
        l = sys.stdin.readline()
except KeyboardInterrupt:
    pass
question4 ★★★★★
() автор топика