Як POSIX-лі підрахувати кількість рядків у рядковій змінній?


10

Я знаю, що можу це зробити в Bash:

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

По суті, все, що я знайшов, стосувалося <<<оператора Bash.

Але в оболонці POSIX <<<не визначено, і мені не вдалося знайти альтернативний підхід протягом кількох годин. Я впевнений, що для цього є просте рішення, але, на жаль, я цього не знайшов.

Відповіді:


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 у греппі?


2
Зрозуміло wc -l, все ще тут життєздатний?
Майкл Гомер

@MichaelHomer: З того, що я помічав, wc -lпотрібен належний потік з обмеженою лінією нового рядка (маючи кінцевий '\ n' кінець, щоб правильно рахувати). Ніхто не може використовувати простий FIFO для використання з printf, наприклад , printf '%s' "${string_variable}" | wc -lможе не працювати , як очікувалося , але <<<буде з - за відстає \nдоданим в herestring
Inian

1
Це було те printf '%s\n', що ви робили, перш ніж вийняли це ...
Майкл Гомер

1

Тут-рядок <<<є майже однолінійною версією документа тут <<. Перша не є стандартною функцією, але остання є. Ви можете використовувати <<і в цьому випадку. Вони повинні бути еквівалентними:

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%...}розширенням, щоб видалити їх по черзі в циклі ...)


0

У тих дивовижних частих випадках, коли вам потрібно щось обробити непорожні рядки всередині змінної (включаючи їх підрахунок), ви можете встановити IFS просто на новий рядок, а потім використати механізм розбиття слів оболонки, щоб зламати не порожні рядки.

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

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

Тут формуються дужки, а не дужки, для формування складної команди для функції функції. Це змушує функцію виконувати в підрозділі, щоб вона не забруднювала зовнішню світову змінну IFS та параметр розширення імені шляху при кожному виклику.

Якщо ви хочете переглядати непорожні рядки, ви можете зробити це аналогічно:

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

Таким чином, маніпуляція IFS - це часто недооцінена техніка, яка також зручна для того, щоб робити такі речі, як аналіз імен шляхів, які можуть містити пробіли від стовпчикових вхідних даних, обмежених вкладками. Однак вам потрібно пам’ятати, що навмисне видалення символу пробілу, як правило, встановленого за замовчуванням в IFS налаштування пробілу-табуляції-нового рядка, може призвести до відключення розбиття слів у місцях, де ви, як правило, очікуєте його бачити.

Наприклад, якщо ви використовуєте змінні для складання складного командного рядка для чогось подібного 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 в окремий крок, і чому їм доводиться вирішувати питання щодо його збереження.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.