Я пытаюсь создать команду, которая перенаправляет результаты одной команды grep в другую команду grep в функции bash. В конечном итоге я хочу, чтобы выполненная команда выглядела так:
grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:
Функция, которую я пишу, сохраняет первую часть команды в строке, а затем добавляет вторую часть:
grep_cmd="grep -I -r $pattern $@"
if (( ${#file_types[@]} > 0 )); then
file_types="${file_types[@]}"
file_types=.${file_types// /':\|.'}:
grep_cmd="$grep_cmd | grep $file_types"
fi
echo "$grep_cmd"
${grep_cmd}
Это выдает ошибку после вывода первой части:
grep: |: No such file or directory
grep: grep: No such file or directory
grep: .c:\|.h:: No such file or directory
Изменение последней строки с ${grep_cmd}
на просто "$grep_cmd"
не отображает вывод первой части и выдает другую ошибку:
bash: grep -I -r FooBar /code/internal/dev/ /code/public/dev/ | grep .c:\|.h:: No such file or directory
Заэтот ТАК ответ, я попробовал изменить последнюю строку на $(grep_cmd)
. Это выдает еще одну ошибку:
bash: grep_cmd: command not found
Этот ответ ТАКпредлагает использовать eval $grep_cmd
. Это подавляет ошибки, но также подавляет вывод.
Вот этотпредлагает использовать eval ${grep_cmd}
. Это дает тот же результат (подавляет ошибки и вывод). Я попробовал включить отладку в bash (с set -x
), что дает мне это:
+ eval grep -I -r FooBar /code/internal/dev/ /code/public/dev/ '|' grep '.c:\|.h:'
++ grep -I -r FooBar /code/internal/dev/ /code/public/dev/
++ grep '.c:|.h:'
Похоже, что канал экранируется, поэтому оболочка интерпретирует команду как две команды. Как правильно экранировать символ канала, чтобы он интерпретировал его как одну команду?
решение1
Как уже упоминалось в комментарии, большая часть ваших трудностей возникает из-за того, что вы пытаетесь сохранить команду в переменной, а затем выполнить эту команду позже.
Вам повезет гораздо больше, если вы просто выполните команду немедленно, а не будете пытаться ее сохранить.
Например, это должно сделать то, чего вы пытаетесь добиться:
if (( ${#file_types[@]} > 0 )); then
regex="${file_types[*]}"
regex="\.\(${regex// /\|}\):"
grep -I -r "$pattern" "$@" | grep "$regex"
else
grep -I -r "$pattern" "$@"
fi
решение2
При программировании оболочки следует помнить одну вещь, которая часто не объясняется четко в учебных пособиях: существует два типа данных:строки и списки строк. Список строк — это не то же самое, что строки с разделителем в виде новой строки или пробела, это нечто отдельное.
Еще одна вещь, которую следует иметь в виду, заключается в том, что большинство расширений применяются только тогда, когда оболочка анализирует файл. Выполнение команды не подразумевает никаких расширений.
Некоторое расширение происходит со значением переменной: $foo
означает «взять значение переменной foo
, разбить его на список строк, используя пробелы¹ в качестве разделителей, и интерпретировать каждый элемент списка как шаблон подстановочных знаков, который затем расширяется». Это расширение происходит только в том случае, если переменная используется в контексте, который требует списка. В контексте, который требует строки, $foo
означает «взять значение переменной foo
». Двойные кавычки накладывают контекст строки, отсюда и совет:всегда используйте подстановки переменных и подстановки команд в двойных кавычках: "$foo"
,"$(somecommand)"
². (Те же расширения происходят с незащищенными подстановками команд, что и с переменными.)
Следствием различия между разбором и выполнением является то, что вы не можете просто вставить команду в строку и выполнить ее. Когда вы пишете ${grep_cmd}
, происходит только разделение и подстановка, а не разбор, поэтому символ типа |
не имеет особого значения.
Если вам абсолютно необходимо вставить команду оболочки в строку, вы можете eval
это сделать:
eval "$grep_cmd"
Обратите внимание на двойные кавычки — значение переменной содержит команду оболочки, поэтому нам нужно ее точное строковое значение. Однако этот подход, как правило, сложен: вам действительно нужно что-то в синтаксисе исходного кода оболочки. Если вам нужно, скажем, имя файла, то это имя файла должно быть правильно заключено в кавычки. Поэтому вы не можете просто вставить $pattern
и $@
туда, вам нужно построить строку, которая при синтаксическом анализе даст одно слово, содержащее шаблон, и список слов, содержащих аргументы.
Обобщить:не вставляйте команды оболочки в переменные. Вместо,использовать функции. Если вам нужна простая команда с аргументами, а не что-то более сложное, например конвейер, вы можете использовать массив (переменная массива хранит список строк).
Вот один из возможных подходов. Функция run_grep
на самом деле не нужна в коде, который вы показали; я включаю ее сюда, предполагая, что это небольшая часть более крупного скрипта и есть гораздо больше промежуточного кода. Если это действительно весь скрипт, просто запустите grep в точке, где вы знаете, куда его передать. Я также исправил код, который создает фильтр, который казался неправильным (например, .
в регулярном выражении означает «любой символ», но я думаю, что вам нужна буквальная точка).
grep_cmd=(grep -I -r "$pattern" "$@")
if (( ${#file_types[@]} > 0 )); then
regexp='\.\('
for file_type in "${file_types[@]}"; do
regexp="$regexp$file_type\\|"
done
regexp="${regexp%?}):"
run_grep () {
"${grep_cmd[@]}" | grep "$file_types"
}
else
run_grep () {
"${grep_cmd[@]}"
}
fi
run_grep
¹ В более общем смысле, используйте значение IFS
.
² Только для экспертов: всегда используйте двойные кавычки вокруг подстановок переменных и команд, если только вы не понимаете, почему их отсутствие дает нужный эффект.
³ Только для экспертов: если вам нужно вставить команду оболочки в переменную, будьте очень осторожны с кавычками.
Обратите внимание, что то, что вы делаете, кажется слишком сложным и ненадежным — что делать, если у вас есть файл, содержащий foo.c: 42
? GNU grep имеет --include
опцию для поиска только в определенных файлах в рекурсивном обходе — просто используйте ее.
grep_cmd=(grep -I -r)
for file_type in "${file_types[@]}"; do
grep_cmd+=(--include "*.$file_type")
done
"${grep_cmd[@]}" "$pattern" "$@"
решение3
command="grep $regex1 filelist | grep $regex2"
echo $command | bash