Количество символов в выводе команды оболочки

12

Я пишу сценарий, который должен рассчитать количество символов в выводе команды за один шаг .

Например, использование команды readlink -f /etc/fstabдолжно возвращаться, 10потому что длина этой команды составляет 10 символов.

Это уже возможно с сохраненными переменными, используя следующий код:

variable="somestring";
echo ${#variable};
# 10

К сожалению, использование той же формулы с командной строкой не работает:

${#(readlink -f /etc/fstab)};
# bash: ${#(readlink -f /etc/fstab)}: bad substitution

Я понимаю, что это можно сделать, предварительно сохранив вывод в переменную:

variable=$(readlink -f /etc/fstab);
echo ${#variable};

Но я бы хотел убрать лишний шаг.

Это возможно? Совместимость с оболочкой Almquist (sh) с использованием только встроенных или стандартных утилит является предпочтительной.

user339676
источник
1
Выход readlink -f /etc/fstabсоставляет 11 символов. Не забывайте перевод строки. В противном случае вы увидите, /etc/fstabluser@cern:~$ когда вы запускаете его из оболочки.
Фил Фрост,
@PhilFrost у вас, кажется, смешная подсказка, вы работаете в CERN?
Дмитрий Григорьев

Ответы:

9

С GNU expr :

$ expr length + "$(readlink -f /etc/fstab)"
10

+Есть специальная особенность GNU , exprчтобы убедиться , что следующий аргумент трактуется как строка , даже если это случается, exprоператор , как match, length, +...

Выше будет лишить любой завершающий перевод строки. Чтобы обойти это:

$ expr length + "$(readlink -f /etc/fstab; printf .)" - 2
10

Результат был вычтен до 2, потому что последний перевод строки readlinkи символ .мы добавили.

С Unicode string, exprпохоже, не работает, потому что он возвращает длину строки в байтах вместо количества символов (см. Строку 654 )

$ LC_ALL=C.UTF-8 expr length ăaa
4

Итак, вы можете использовать:

$ printf "ăaa" | LC_ALL=C.UTF-8 wc -m
3

POSIXLY:

$ expr " $(readlink -f /etc/fstab; printf .)" : ".*" - 3
10

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

cuonglm
источник
Благодарность! Кажется, что ваш третий пример работает даже без LC_ALL=C.UTF-8, что значительно упрощает вещи, если кодировка строки не будет известна заранее.
user339676
2
expr length $(echo "*")- нет. По крайней мере , использовать двойные кавычки: expr length "$(…)". Но это убирает завершающие переводы строк из команды, это неизбежная особенность подстановки команд. (Вы можете обойти это, но тогда ответ становится еще более сложным.)
Жиль "ТАК - перестань быть злым"
6

Не уверен, как это сделать с помощью встроенных командных оболочек ( хотя Gnouc есть ), но стандартные инструменты могут помочь:

  1. Вы можете использовать, wc -mкоторый считает символы. К сожалению, он также учитывает окончательный перевод строки, поэтому вам придется сначала избавиться от этого:

    readlink -f /etc/fstab | tr -d '\n' | wc -m
  2. Вы можете, конечно, использовать awk

    readlink -f /etc/fstab | awk '{print length($0)}'
  3. Или Perl

    readlink -f /etc/fstab | perl -lne 'print length'
Тердон
источник
Вы имеете в виду exprвстроенный? В какой оболочке?
mikeserv
5

Я обычно делаю это так:

$ echo -n "$variable" | wc -m
10

Для выполнения команд я бы адаптировал это так:

$ echo -n "$(readlink -f /etc/fstab)" | wc -m
10

Этот подход аналогичен тому, что вы делали в два этапа, за исключением того, что мы объединяем их в один вкладыш.

SLM
источник
2
Вы должны использовать -mвместо -c. С юникод-символами ваш подход будет нарушен.
cuonglm
1
Почему не просто readlink -f /etc/fstab | wc -m?
Фил Фрост
1
Почему вы используете этот ненадежный метод вместо ${#variable}? По крайней мере используйте двойные кавычки echo -n "$variable", но это все равно не сработает, если, например, значение variableравно -e. Когда вы используете его в сочетании с подстановкой команд, имейте в виду, что завершающие символы новой строки удаляются.
Жиль "ТАК - перестань быть злым"
@philfrost b / c то, что я показал, построено на том, о чем уже думала опера. Также это работает для любых cmds, которые он может настроить ранее в vars, и хочет, чтобы их длина была после слов. Также у Тердона уже есть этот пример.
СЛМ
1

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

Zsh

В zsh вы можете написать, ${#$(readlink -f /etc/fstab)}чтобы получить длину подстановки команд. Обратите внимание, что это не длина вывода команды, а длина вывода без завершающей строки.

Если вам нужна точная длина вывода, выведите дополнительный не-символ новой строки в конце и вычтите его.

$((${#$(readlink -f /etc/fstab; echo .)} - 1))

Если вы хотите получить полезную нагрузку в выводе команды, то вам нужно вычесть два здесь, потому что вывод readlink -f- это канонический путь плюс новая строка.

$((${#$(readlink -f /etc/fstab; echo .)} - 2))

Это отличается от ${#$(readlink -f /etc/fstab)}того редкого, но возможного случая, когда сам канонический путь заканчивается новой строкой.

Для этого конкретного примера вам вообще не нужна внешняя утилита, потому что zsh имеет встроенную конструкцию, которая эквивалентна readlink -fмодификатору истории A.

echo /etc/fstab(:A)

Чтобы получить длину, используйте модификатор истории в расширении параметра:

${#${:-/etc/fstab}:A}

Если у вас есть имя файла в переменной filename, это будет ${#filename:A}.

Оболочки в стиле Bourne / POSIX

Ни одна из чистых оболочек Bourne / POSIX (Bourne, ash, mksh, ksh93, bash, yash ...) не имеет аналогичного расширения, о котором я знаю. Если вам нужно применить подстановку параметров к выходным данным подстановки команд или для подстановки подстановок параметров, используйте последовательные этапы.

Вы можете добавить обработку в функцию, если хотите.

command_output_length_sans_trailing_newlines () {
  set -- "$("$@")"
  echo "${#1}"
}

или

command_output_length () {
  set -- "$("$@"; echo .)"
  echo "$((${#1} - 1))"
}

но обычно нет никакой выгоды; кроме как с ksh93, это приводит к тому, что дополнительная вилка может использовать выходные данные функции, так что это замедляет работу вашего скрипта и редко дает какие-либо преимущества для удобства чтения.

Еще раз, выходные данные readlink -f- канонический путь плюс новая строка; если вы хотите длину канонического пути, вычтите 2 вместо 1 в command_output_length. Использование command_output_length_sans_trailing_newlinesдает правильный результат только тогда, когда сам канонический путь не заканчивается новой строкой.

Байт против символов

${#…}должна быть длина в символах, а не в байтах, что имеет значение в многобайтовых локалях. Разумно обновленные версии ksh93, bash и zsh вычисляют длину в символах в соответствии со значением LC_CTYPEв момент раскрытия ${#…}конструкции. Многие другие распространенные оболочки в действительности не поддерживают многобайтовые локали: по состоянию на dash 0.5.7, mksh 46 и posh 0.12.3 ${#…}возвращает длину в байтах. Если вы хотите, чтобы длина в символах надежным образом, используйте wcутилиту:

$(readlink -f /etc/fstab | wc -m)

Пока вы $LC_CTYPEуказываете действительный языковой стандарт, вы можете быть уверены, что это приведет к ошибке (на древней или ограниченной платформе, которая не поддерживает многобайтовые языковые стандарты) или вернет правильную длину в символах. (Для Unicode «длина в символах» означает количество кодовых точек - количество глифов - это еще одна история из-за сложностей, таких как объединение символов.)

Если вы хотите длину в байтах, установите LC_CTYPE=Cвременно или используйте wc -cвместо wc -m.

Подсчет байтов или символов wcвключает любые завершающие символы новой строки из команды. Если вы хотите, чтобы длина канонического пути в байтах, это

$(($(readlink -f /etc/fstab | wc -c) - 1))

Чтобы получить это в символах, вычтите 2.

Жиль "ТАК - перестань быть злым"
источник
@cuonglm Нет, вам нужно вычесть 1. echo .добавляет два символа, но второй символ - это завершающий символ новой строки, который удаляется подстановкой команды.
Жиль "ТАК - перестань быть злым"
Новая строка из readlinkвывода, плюс .по echo. Мы оба согласны, что echo .добавим два символа, но завершающий символ новой строки был удален. Попробуйте printf .или посмотрите мой ответ unix.stackexchange.com/a/160499/38906 .
cuonglm
@cuonglm Вопрос задал количество символов в выводе команды. Выходные данные readlink- цель ссылки плюс новая строка.
Жиль "ТАК - перестань быть злым"
0

Это работает, dashно требует, чтобы целевой var был определенно пуст или не установлен. Вот почему это на самом деле две команды - я явно пустой $lв первой:

l=;printf '%.slen is %d and result is %s\n' \
    "${l:=$(readlink -f /etc/fstab)}" "${#l}" "$l"

ВЫХОД

len is 10 and result is /etc/fstab

Это все встроенные функции оболочки - не считая, readlinkконечно, - но ее оценка в текущей оболочке таким образом подразумевает, что вы должны выполнить присваивание перед получением len, поэтому я %.sвыбираю первый аргумент в printfстроке формата и просто добавляю его снова для буквальное значение в printfконце списка аргументов.

С eval:

l=$(readlink -f /etc/fstab) eval 'l=${#l}:$l'
printf %s\\n "$l"

ВЫХОД

10:/etc/fstab

Вы можете приблизиться к тому же самому, но вместо вывода в переменной в первой команде вы получите его в stdout:

PS4='${#0}:$0' dash -cx '2>&1' "$(readlink -f /etc/fstab)"

... который пишет ...

10:/etc/fstab

... для файлового дескриптора 1 без присвоения какого-либо значения любым переменным в текущей оболочке.

mikeserv
источник
1
Разве это не то, чего ОП хотел избежать? «Я понимаю, что это можно сделать, предварительно сохранив выходные данные в переменной: variable=$(readlink -f /etc/fstab); echo ${#variable};но я бы хотел удалить дополнительный шаг».
Тердон
@terdon, возможно, я неправильно понял, но у меня сложилось впечатление, что проблема с точкой с запятой, а не с переменной. Вот почему они получают len и выводятся одной простой командой, используя только встроенные функции оболочки. Например, оболочка не выполняет readlink, а затем exec expr. Это, вероятно, имеет значение только в том случае, если каким-то образом получение len перекрывает значение, которое, я признаю, мне трудно понять, почему это может быть, но я подозреваю, что может быть случай, в котором это имеет значение.
mikeserv
1
evalСпособ, кстати, является , вероятно , самым чистым здесь - он назначает выход и Лен к тому же имени вара в одном исполнении - очень близок к этому l=length(l):out(l). Делать expr length $(command) ли закупорить значение в пользу Len, кстати.
mikeserv