Как POSIX-ли подсчитать количество строк в строковой переменной?

10

Я знаю, что могу сделать это в Bash:

wc -l <<< "${string_variable}"

В основном все, что я нашел, связано с <<<оператором Bash.

Но в оболочке POSIX <<<она не определена, и я часами не мог найти альтернативный подход. Я совершенно уверен, что есть простое решение, но, к сожалению, я не нашел его до сих пор.

LinuxSecurityFreak
источник

Ответы:

11

Простой ответ - это wc -l <<< "${string_variable}"ярлык для ksh / bash / zsh printf "%s\n" "${string_variable}" | wc -l.

На самом деле существуют различия в способе <<<и работе канала: <<<создается временный файл, который передается в качестве входных данных для команды, тогда как |создается канал. В bash и pdksh / mksh (но не в ksh93 или zsh) команда на правой стороне канала работает в подоболочке. Но эти различия не имеют значения в данном конкретном случае.

Обратите внимание, что с точки зрения подсчета строк это предполагает, что переменная не является пустой и не заканчивается новой строкой. Не конец новой строки - это случай, когда переменная является результатом подстановки команды, поэтому в большинстве случаев вы получите правильный результат, но вы получите 1 для пустой строки.

Существует два различия между var=$(somecommand); wc -l <<<"$var"и somecommand | wc -l: использование подстановки команды и временной переменной удаляет пустые строки в конце, забывает, закончилась ли последняя строка вывода новой строкой или нет (это всегда происходит, если команда выводит допустимый непустой текстовый файл) и пересчитывает на единицу, если выходные данные пусты. Если вы хотите сохранить и результат, и количество строк, вы можете сделать это, добавив известный текст и убрав его в конце:

output=$(somecommand; echo .)
line_count=$(($(printf "%s\n" "$output" | wc -l) - 1))
printf "The exact output is:\n%s" "${output%.}"
Жиль "ТАК - перестань быть злым"
источник
1
@Inian Keeping wc -lв точности соответствует оригиналу: <<<$fooдобавляет новую строку к значению $foo(даже если оно $fooбыло пустым). Я объясняю в своем ответе, почему это, возможно, не то, что хотели, но это то, что спросили
Жиль "ТАК - перестань быть злым"
2

Не соответствует встроенным функциям оболочки, используя внешние утилиты, такие как grepи awkс POSIX-совместимыми параметрами,

string_variable="one
two
three
four"

Делать с, grepчтобы соответствовать началу строк

printf '%s' "${string_variable}" | grep -c '^'
4

И с awk

printf '%s' "${string_variable}" | awk 'BEGIN { count=0 } NF { count++ } END { print count }'

Обратите внимание, что некоторые инструменты GNU, особенно GNU grep, не учитывают POSIXLY_CORRECT=1возможность запуска версии инструмента для POSIX. В grepединственном поведении пострадавших от установки переменного будет разница в обработке заказа флагов командной строки. Из документации ( grepруководство по GNU ) кажется, что

POSIXLY_CORRECT

Если установлено, grep ведет себя так, как требует POSIX; в противном случае grepведет себя больше как другие программы GNU. POSIX требует, чтобы опции, следующие за именами файлов, рассматривались как имена файлов; по умолчанию такие параметры переставляются в начало списка операндов и рассматриваются как параметры.

Смотрите Как использовать POSIXLY_CORRECT в grep?

Inian
источник
2
Конечно wc -l, все еще жизнеспособен здесь?
Майкл Гомер
@MichaelHomer: Из того, что я наблюдал, wc -lнужен правильный поток с разделителями новой строки (с последующим символом '\ n` в конце для правильного подсчета). Невозможно использовать простой FIFO для использования printf, например, он printf '%s' "${string_variable}" | wc -lможет работать не так, как ожидалось, но <<<будет работать из-за трейлинга, \nдобавленного в следующей строке
Inian
1
Это было то, что printf '%s\n'делали, прежде чем вы вынули это ...
Майкл Гомер
1

Строка here - <<<это в значительной степени однострочная версия документа here <<. Первое не является стандартной функцией, но второе есть. Вы можете использовать <<тоже в этом случае. Они должны быть эквивалентны:

wc -l <<< "$somevar"

wc -l << EOF
$somevar
EOF

Хотя обратите внимание, что оба добавляют дополнительный символ новой строки в конце $somevar, например, это печатает 6, хотя переменная имеет только пять строк:

s=$'foo\n\n\nbar\n\n'
wc -l <<< "$s"

С помощью printfвы можете решить, хотите ли вы дополнительный перевод строки:

printf "%s\n" "$s" | wc -l         # 6
printf "%s"   "$s" | wc -l         # 5

Но затем, обратите внимание, что wcподсчитывает только полные строки (или количество символов новой строки в строке). grep -c ^Также следует посчитать последний фрагмент строки.

s='foo'
printf "%s" "$s" | wc -l           # 0 !

printf "%s" "$s" | grep -c ^       # 1

(Конечно, вы также можете полностью посчитать строки в оболочке, используя ${var%...}расширение для удаления их по одной в цикле ...)

ilkkachu
источник
0

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

Например, вот небольшая функция оболочки, которая суммирует непустые строки во всех предоставленных аргументах:

lines() (
IFS='
'
set -f #disable pathname expansion
set -- $*
echo $#
)

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

Если вы хотите перебрать непустые строки, вы можете сделать это аналогично:

IFS='
'
set -f
for line in $lines
do
    printf '[%s]\n' $line
done

Таким образом, манипулирование IFS является часто пропускаемым методом, который также удобен для выполнения таких операций, как разбор путей, которые могут содержать пробелы в столбцовом вводе с разделителями табуляции. Однако вы должны знать, что преднамеренное удаление пробела, обычно включаемого в стандартную настройку IFS для space-tab-newline, может привести к отключению разделения слов в тех местах, где вы обычно ожидаете его увидеть.

Например, если вы используете переменные для создания сложной командной строки для чего-то подобного ffmpeg, вы можете включить их, -vf scale=$scaleтолько если для переменной scaleзадано непустое значение. Обычно этого можно достичь с помощью, ${scale:+-vf scale=$scale}но если IFS не включает свой обычный пробел во время выполнения расширения этого параметра, пробел между -vfи scale=не будет использоваться в качестве разделителя слов и ffmpegбудет передаваться -vf scale=$scaleкак один аргумент, чего он не поймет.

Чтобы исправить это, вы либо должны убедиться , что IFS был установлен более обычно , прежде чем делать ${scale}расширение, или сделать два разложения: ${scale:+-vf} ${scale:+scale=$scale}. Разделение слов, которое оболочка выполняет в процессе первоначального разбора командных строк, в отличие от разбиения, которое происходит на этапе расширения обработки этих командных строк, не зависит от IFS.

Еще кое-что, что может стоить вашего времени, если вы собираетесь делать такие вещи, - это создание двух глобальных переменных оболочки, содержащих только табуляцию и только новую строку:

t=' '
n='
'

Таким образом, вы можете просто включить $tи $nв расширения, где вам нужны вкладки и переводы строк, вместо того, чтобы засорять весь код пробелами в кавычках. Если вы предпочитаете вообще избегать заключенных в кавычки пробелов в оболочке POSIX, у которой нет другого механизма, это printfможет помочь, хотя вам нужно немного поработать, чтобы обойти удаление завершающих строк в расширениях команд:

nt=$(printf '\n\t')
n=${nt%?}
t=${nt#?}

Иногда установка IFS, как будто это переменная среды для каждой команды, работает хорошо. Например, вот цикл, который считывает имя пути, которое может содержать пробелы и коэффициент масштабирования из каждой строки входного файла с разделителями табуляции:

while IFS=$t read -r path scale
do
    ffmpeg -i "$path" ${scale:+-vf scale=$scale} "${path%.*}.out.mkv"
done <recode-queue.txt

В этом случае readвстроенная функция видит, что IFS установлен только на вкладку, поэтому он не разбивает строку ввода, которую он читает, также на пробелы. Но IFS=$t set -- $lines не работает: оболочка расширяется $linesпри построении setаргументов встроенной функции перед выполнением команды, поэтому временная настройка IFS таким образом, который применяется только во время выполнения самой встроенной функции, приходит слишком поздно. Вот почему приведенные выше фрагменты кода устанавливают IFS на отдельном этапе, и поэтому им приходится иметь дело с проблемой его сохранения.

flabdablet
источник