Как стать автором
Обновить

Комментарии 45

Для того, чтобы это сделать, понадобится, для начала, сформировать список путей к директориям. Сделаем это с помощью sed, заменив двоеточия на пробелы
$ echo $PATH | sed 's/:/ /g'


Я чуть не расплакался со смеху, сабшел, да ещё и sed для этого… Не удержался, простите. Невероятно что себе некоторые люди позволяют публиковать.
echo ${PATH//:/ } #всё

Они тут ещё пути по пробелам делят. Вспомнил сколько в моём $PATH из cygwin пробелов… По‐хорошему нужно устраивать while цикл с ${PATH%%:*}, раз уж до typeset -T bash не доросла (возможность zsh, позволяет связать скалярный (читай строковый) параметр, содержащий разделённый некоторым символом список, с параметром‐списком).

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


saved_ifs="$IFS"
IFS=:
path=( $PATH )
IFS=$saved_ifs
for p in "${path[@]}" ; do
    …
done

И никаких регулярных выражений вообще.

Я интереса ради прикинул несколько вариантов решений на которые меня натолкнуло ваше предложение использовать while, вот так безопаснее всего для данного конкретного случая (и заодно работает на Linux/BSD), но читаемость — ад… это мне и самому понятно:
while IFS= read -r -d: path; do
    i=0;
    while read -d $'\0' ; do
        let i=i+1;
    done < <(find "${path/#\~/$HOME}" -maxdepth 1 -type f -print0 2>/dev/null);
    printf "%5d - %s\n" $i "${path}";
done < <(echo "${PATH}")

Неплохой квест для начинающих получился в итоге — понять что вообще тут происходит.

Если хотите на чистом POSIX shell, то лучше именно вариант с ${PATH%}:


list_paths() {
    local paths="$PATH"
    while test -n "$paths" ; do
        local next_path="${paths%%:*}"
        printf '%s\0' "$next_path"
        paths="${paths#*:}"
        if test "$next_path" = "$paths" ; then
            break
        fi
    done
}

list_paths | xargs -0 printf 'path item: %s\n'

Если вы знаете, что такое ${var#}, ${var%%} и что такое * внутри, то всё просто и понятно. Единственное но: мне долгое время не удавалось запомнить, что из # и % делает что, пока я не додумался до мнемоники: «авторы стандарта обманывают: # комментирует до конца строки, но удаляет с начала» (короткий вариант: «# противоположно комментарию»).

Моя мнемоника # и % связана с положением на клавиатуре при стандартной английской раскладке.
Шарп на тройке (раньше), процент на пятёрке (позже) => Шарп трёт с начала, процент с конца.

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

Точнее, у меня programming dvorak. Тут процент на единице, а решётка на =, до максимального удаления друг от друга не хватает одной клавиши, а относительный порядок обратный. Можно было бы запомнить как «процент оставляет начало, решётка оставляет конец» также привязав к клавиатуре, но эти символы на разных руках, это не воспринимается как что‐то находящееся в одной последовательности. Ну и «оставляет» сложно использовать как мнемонику, если после решётки/процента у вас написано не что оставить, а что удалить.

Кстати, вы куда‐то потеряли -e, если PATH=-e. И новую строку, если PATH=$'/bin:/hacky/path\n'.


Если строка может содержать любые данные, то echo использовать нельзя. А куда делась новая строка из PATH=$'/hacky/path\n' я сам не знаю; если засунуть её внутрь, то всё нормально.


Хотя я сейчас заметил: если $PATH состоит только из одного каталога, то ничего у вас не печатается, не только если этот каталог -e. А, в отличие от $'/hacky/path\n' в конце $PATH и PATH=-e данный сценарий вполне реален: я себе так в тестах powerline относительно чистую среду для тестирования организую: создаю свой каталог, пихаю туда символические ссылки на нужные программы и делаю его единственным каталогом в $PATH.

Уточнение: вы теряете не единственный каталог в $PATH, когда он единственный. Вы всегда теряете последний каталог. Хотя этот вариант просто поправить: просто используйте "$PATH:" вместо "$PATH".

Т.е. поправленный ваш вариант будет просто


while IFS= read -r -d: path; do
    i=0;
    while read -d $'\0' ; do
        let i=i+1;
    done < <(find "$path" -maxdepth 1 -type f -print0 2>/dev/null);
    printf "%5d - %s\n" $i "${path}";
done < <(printf '%s' "${PATH}:")

Тут ещё одно отличие: ~/ в $PATH раскрывает bash. Другие оболочки (проверено в zsh, posh, dash, busybox ash, ksh, mksh) этим не отличаются. Libc (execlp, execvp, execvpe) этим не отличается также. Поэтому я раскрытие ~ удалил, если у вас есть такие пути, то вы оказываете себе медвежью услугу: только bash будет понимать, что там $HOME, остальные же будут понимать такое как ./~/….

"${PATH}:" — не досмотрел, вы правы, поскольку -d флаг у read является «data-end».

С echo -e я вас не понял. По дефолту применяется -E «disable interpretation of backslash escapes», чего я и добивался чтобы табы, переносы строк, нульбайты и так далее не интерпретировались, соответственно while IFS= read -r -d: будет резать исключительно по двоеточиям.

А вот про "${path/#\~/$HOME}" сдаётся мне не поняли вы. Это замена тильды в начале на $HOME (parameter expansion — search and replace) как раз на тот случай, если кто-то умудрился себе в PATH тильду засунуть. Проверьте ваш вариант убрав 2>/dev/null — словите ошибку в find на тильде потому что она не раскроется.
С echo -e я вас не понял. По дефолту применяется -E «disable interpretation of backslash escapes», чего я и добивался чтобы табы, переносы строк, нульбайты и так далее не интерпретировались, соответственно while IFS= read -r -d: будет резать исключительно по двоеточиям.

Во‐первых, не везде. К примеру, создаёте вы «кроссплатформенный» bash/zsh скрипт. А в zsh по‐умолчанию -e, но есть настройка BSD_ECHO. Во‐вторых, я имел ввиду, когда PATH равен -e у вас будет echo -e, что не выведет ничего.


Проверьте ваш вариант убрав 2>/dev/null — словите ошибку в find на тильде потому что она не раскроется.

Словлю, но не ошибку. Только bash раскрывает тильду там. Для остальных это ./~. Так что «ошибка» — полностью корректное и ожидаемое поведение. И, кстати, вы написали ${path/#\~/$HOME}. А что если в $PATH не ~/, а ~foo/? В любом случае, не копируйте ошибки bash.

Чтобы было понятнее: напишите скрипт


dir="$(mktemp -d)"
cd "$dir"
mkdir -p home/bin
mkdir -p \~/bin
HOME="$dir/home"
printf '#!/bin/sh\necho script'     > home/bin/script
printf '#!/bin/sh\necho script2'    > home/bin/script2
printf '#!/bin/sh\necho vulnerable' > \~/bin/script
chmod a+x home/bin/script
chmod a+x home/bin/script2
chmod a+x \~/bin/script
PATH='~/bin'
if test $# -gt 0 ; then
    "$@"
else
    script
fi
cd /
/bin/rm -r "$dir"

Теперь запустите его (далее он зовётся script.sh) в bash и в другой оболочке (если нужно что‐то вроде fish, то можно писать sh script.sh =fish -c script, а так просто {shell} script.sh). Результаты будут такие:


% bash script.sh
script
% dash script.sh
vulnerable
% posh script.sh
vulnerable
% sh script.sh =fish -c script
vulnerable
% zsh script.sh
vulnerable
% mksh script.sh
vulnerable
% ksh script.sh
vulnerable
% echo 'main() { execlp("script", "script", "script"); }' > prog.c
% sh script.sh =tcc -run $PWD/prog.c
vulnerable
% sh script.sh =vim --cmd 'echo executable("script2")' --cmd qa
0
% sh script.sh =nvim --cmd 'echo system(["script"])' --cmd qa
vulnerable
% sh script.sh =python -c 'from subprocess import check_call ; check_call(["script"])'
vulnerable

В вашем примере find найдёт скрипт с echo script. Но с какой радости, если все остальные запустят не его, а скрипт с echo vulnerable?

Уточнение: =fish — это возможность zsh. В остальных оболочках нужно писать "$(which fish)".

Только bash раскрывает тильду там.

bash-4.4$ export PATH="~/Desktop"
bash-4.4$ /usr/bin/find "${PATH}" -maxdepth 1 -type f
find: ~/Desktop: No such file or directory

Даже баш не раскроет (кавычки виноваты), вот что я имел ввиду. Поэтому тильда заменяется на $HOME
А что если в $PATH не ~/, а ~foo/?

Из ~foo/ после замены получится /home/usernamefoo (что вполне ожидаемо) и при раскрытии башем — получится тожесамое всёравно.
Я, если вы не поняли, не утверждаю что тильду в PATH иметь нормально, как раз наоборот. Этот пример замены — полученные на практике синяки поскольку тильду мне туда подсовывали.

К примеру, создаёте вы «кроссплатформенный» bash/zsh скрипт.

Я всё же ограничился на комментариях к Башу, цикл статей ведь о нём :)
Формально вы правы конечно.

Пока писали комментарий, я написал свой. Я к тому, что вариант без замены работает корректно, в моём примере он найдёт "$dir/~/bin/script", а не "$dir/home/bin/script", хотя bash запустит "$dir/home/bin/script". Нельзя там писать раскрытие ~, вы найдёте не то.


И ещё вы забыли одну вещь: на случай, если в $PATH встретится пустой каталог (к примеру, /bin::/sbin), то вам нужно написать : ${path:=.}. Потому что пустое значение — это текущий каталог, но find '' пишет find: ‘’: Нет такого файла или каталога.

Меня сбивает с толку слово «раскрытие». Нету там никакого раскрытия, происходит замена знака «тильда» содержимым определённой переменной. Почему вы это раскрытием называете?
bash-4.4$ homedir="/home/username"
bash-4.4$ var1="dir/~/bin/script"
bash-4.4$ echo ${var1/#\~/$homedir}
dir/~/bin/script
bash-4.4$ var2="~/bin/script"
bash-4.4$ echo ${var2/#\~/$homedir}
/home/username/bin/script

Возвращаясь к тому что можно создать директорию с названием "~". Да можно. Да вы правы что из-за того что другие шелы не делают то самое раскрытие (tilde -> $HOME -> actual value) — поведение будет разное. Я с самого начала с этим не спорю.
Как я уже писал выше — цикл статей о баше, я ограничился им. Если мы ещё и про шелл-кроссплатформенность будем комментировать… собственно пример тому — сегодняшние полотнища.

то вам нужно написать: ${path:=.}

Категорически не согласен, в моей версии :: выдаст ошибку и будет отловлен редиректом:
bash-4.4$ cat pathfinder.bash
p="/bin::/sbin"
while IFS= read -r -d: path; do
    i=0;
    while read -d $'\0' ; do
        let i=i+1;
    done < <(find "${path/#\~/$HOME}" -maxdepth 1 -type f -print0 2>/dev/null);
    printf "%5d - %s\n" $i "${path}";
done < <(echo "${p}:")
bash-4.4$ bash pathfinder.bash
   37 - /bin
    0 -
   40 - /sbin

Единственная некрасивость — ноль у директории без имени.
Вами предложенный default value добавит дополнительную строку с кол-вом файлов в ".", что противоречит исходной задаче извлечения кол-ва файлов в директориях из PATH
Вами предложенный default value добавит дополнительную строку с кол-вом файлов в ".", что противоречит исходной задаче извлечения кол-ва файлов в директориях из PATH

Не противоречит:


% (cd /bin ; PATH='/xxx:' /bin/bash -c 'sh --version')
GNU bash, version 4.3.48(1)-release (x86_64-pc-linux-gnu)
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Вопрос на засыпку: откуда bash внезапно взял исполняемый файл sh? Каталога /xxx у меня нет. Кстати, работает и с пустым $PATH.

А если сделать так, то не сработает:
(cd / ; PATH= /bin/bash -c 'sh --version')
/bin/bash: sh: No such file or directory

И ответ очевиден, вы в вашем примере сначала перешли в директорию /bin, откуда баш прочитал sh напрямую. Честно говоря вообще не понял к чему именно этот пример.
Я говорил о том, что вами предложенная замена на default value выпишет так же кол-во файлов в той директории, где вы находитесь на момент запуска скрипта — это противоречит условиям исходной задачи.

Bash не берёт файлы из текущего каталога, если текущего каталога в $PATH нет. Уберите из моего примера единственное находящееся там двоеточие, получите ту же ошибку. С пустым $PATH то же самое: не хотите читать бинарники из текущего каталога, не допускайте пустых путей в $PATH. Задача не решена, замена пустой строки на default value нужно.


Я не понимаю, зачем вы настаиваете на откровенно некорректном ответе. Когда в $PATH есть пустой путь, bash ищет исполняемые файлы и скрипты (я про команду .) в текущем каталоге. Задача — искать файлы в $PATH. Если они ищутся в текущем каталоге, то задача — искать их в том числе в текущем каталоге. Вы не ищете — задача не решена.

Меня сбивает с толку слово «раскрытие». Нету там никакого раскрытия, происходит замена знака «тильда» содержимым определённой переменной. Почему вы это раскрытием называете?

Потому что это называется «tilde expansion». К примеру, из CHANGES самого bash:


Bash no longer expands tildes in $PATH elements while in Posix mode.

«Expands» часто переводится как «раскрывает». Так что это раскрытие и есть.

И, кстати, говорите про bash? Где код, который предотвращает раскрытие ~ в $PATH, если bash 4.4 и выше и находится в POSIX режиме? Я его у вас не видел. И про ~foo/ сейчас проверил: bash -c 'PATH='~zyx/bin' ; srun' вполне себе вызывает мой скрипт из $HOME/bin, а ваш код этот скрипт в этом случае не найдёт. Так что не нужно писать там ${path/#\~/$HOME}, это не полностью соответствует даже самому bash, не то что не совместимо с остальными оболочками.

Вы сами и ответили, именно так и есть, я это прекрасно знаю, поэтому, именно по этому ваша формулировка меня и сбивает с толку. В мной приведённом примере раскрытия не происходит, происходит контролируемая замена знака «тильда» на содержимое переменной $HOME и лишь только в том случае, если тильда является первым знаком в данной секции, потому что ожидается (предполагается) что кто-то умудрился себе домашнюю директорию обозначить тильдой в PATH.
В моём конкретном примере, где используется find, так как $path внутри цикла обёрнут двойными кавычками во избежание проблем с пробелами и другими белыми знаками, внимание, раскрытия тильды НЕ произойдёт (я уже вам предлагал это попробовать), поэтому и конструкция которая это раскрытие заменяет.
Я уже несколько раз повторил что мы говорим об одном и том же, вы просто сфокусировались на конкретной вещи, так и не вникнув в суть мной предложенного решения. Я не спорю с вами что тильду класть себе в PATH не следует.

Задача буквально звучит как «Напишем bash-скрипт, который подсчитывает файлы, находящиеся в директориях, которые записаны в переменную окружения PATH». Я интерпретирую её как «Написать bash‐скрипт, который подсчитывает файлы, находящиеся в каталогах, в которых POSIX‐совместимая оболочка будет искать исполняемый файлы и скрипты», потому что это то, что первое приходит на ум (если бы был пользователем bash, писал бы про то, где будет искать сам bash). Хотелось бы услышать вашу точную интерпретацию, а то я могу сказать, что если в $PATH находится /usr/local/bin, то там записаны следующие каталоги:


  • /
  • /usr
  • /usr/local
  • /usr/local/bin
  • /bin

Они же ведь там действительно записаны!

Вы просто на ровном месте взяли и от себя добавили про POSIX-совместимую оболочку в вашей интерпретации.
Я не против, это отличное решение.
Мы находимся в цикле статей про bash и задача «bash-скрипт» а не «POSIX-compatible-скрипт». Почему же вас тогда не устраивает решение для баша на баше?

Потому что оно некорректно. Во‐первых, пустой каталог. Во‐вторых, ~user. В‐третьих, режим POSIX‐совместимости и определённая версия bash (в нём, кстати, не запустится process substitution). Если вы настаиваете, что ваш скрипт должен «подсчитывать файлы, находящиеся в каталогах, в которых bash будет искать исполняемые файлы и скрипты», то исправьте все три проблемы.

Кстати, моё решение именно этой задачи с bash:


set -e
set -u

path_iter() {
    local paths="$PATH"
    while true ; do
        local next_path="${paths%%:*}"
        local real_next_path="${next_path:-.}"
        "${@:-echo}" "$real_next_path"
        paths="${paths#*:}"
        if test "$next_path" = "$paths" ; then
            break
        fi
    done
}

count_files() {
    local path="$1" ; shift
    test -d "$path" || return
    local filenum="$(find "$path" -maxdepth 1 -type f -exec echo \; | wc -c)"
    printf '%5u - %s\n' "$filenum" "$path"
}

if ( ( test ${BASH_VERSION%%.*} -gt 4 \
       || ( test ${BASH_VERSION%%.*} -eq 4 \
            && eval '(( ${BASH_VERSINFO[1]} >= 4 ))' ) ) \
     && ( set +o | grep -q -x 'set -o posix' ) )
then
    bash_count_files() {
        count_files "$@"
    }
else
    bash_count_files() {
        local path="$1" ; shift
        # Warning: assumes specific kind of escaping bash does. /usr/bin/printf
        # uses another way.
        path="$(printf "%qx" "$path")"
        path="${path%x}"
        path="${path/#\\~/\~}"
        eval "path=$path"
        count_files "$path"
    }
fi

path_iter bash_count_files

Если убрать ветвление с bash_count_files, то оно запустится даже в posh.

UPDATE: пока писал ответ, вижу вы обновлённый скрипт с «точкой» добавили.
Во‐первых, пустой каталог.

Я наконец-таки понял что вы имели ввиду вот этой фразой:
И ещё вы забыли одну вещь: на случай, если в $PATH встретится пустой каталог (к примеру, /bin::/sbin), то вам нужно написать: ${path:=.}. Потому что пустое значение — это текущий каталог, но find '' пишет find: ‘’: Нет такого файла или каталога.

Я думал, что вас не устроило, что если в PATH встретится «пустышка» то find выдаст ошибку (из-за наличия кавычек)
cd /bin && find "" -maxdepth 1 -type f
find: cannot search `': No such file or directory

вместо того, чтобы использовать текущий каталог, как он это делает по дефолту в линуксе:
cd /bin && find -maxdepth 1 -type f
# here be files

Вы же говорили об этом (из man bash):
A zero-length (null) directory name in the value of PATH indicates the current directory. A null directory name may appear as two adjacent colons, or as an initial or trailing colon.

Но чёрт возьми, исправьте тогда и ваш собственный скрипт, поскольку на «adjanced colons» и «initial colon» он выдаст пустой путь (а не вами предложенную точку), а «trailing colon» не распознает вообще.
Я сидел и в упор не мог понять, что же вас так сильно в этом конкретном случае не устраивало, особенно с учётом того, что поведение вашего скрипта аналогично.
Смеюсь в голос (с себя). Да конечно, добавляем туда точку.

Во‐вторых, ~user.

Вот мой пример:
bash-4.4$ mkdir ~/bin && ln -s /bin/sh ~/bin/sh
bash-4.4$ ls bin/
sh
bash-4.4$ PATH=~bin
bash-4.4$ /usr/bin/which sh
bash-4.4$ PATH=~/bin
bash-4.4$ /usr/bin/which sh
/home/tester/bin/sh

Мной предложенный вариант замены тильды приведёт к аналогичному результату.

P.S. Предлагаю перевести диалог в личные сообщения чтобы дальше не плодить полотнища в комментариях.

Нет, когда я начал писать свою альтернативу в список тестируемых $PATH попал в том числе :/bin:/usr/bin::~zyx/bin. Это полностью эквивалентно :/bin:/usr/bin::~/bin, т.к. в системе я zyx. Под ~user я имею ввиду именно это.


И, кстати:


V=~xxx_nonexistent_user/bin
echo $V
# echoes `~xxx_nonexistent_user/bin`
mkdir -p $V
ln -s /bin/sh $V/s
(PATH=$V:$PATH ; s --version)
# Prints bash version

В моём варианте это обрабатывается также правильно. А если вы просто берёте и заменяете тильду — то нет.

Да понятное дело что пути с пробелами в указаном примере тут же сломаются и что вообще весь этот пример можно переписать в однострочник через find. Я просто выдернул из статьи уже просто режущее глаза выражение.

Подумайте чего только стоит упоминание о регексах, если читателей заранее не проинформировали что в баше есть понятие glob. Скольким кол-вом отстреленых конечностей закончится попытка применения регексов в баше напрямую — я думать просто не хочу.

P.S. Если кому-то не понравился тон предыдущего сообщения, прошу понять что я (и не только) уже неоднократно критиковал неточности и неверную информацию в статьях этого цикла. Но вот это, ровно как и регекс для мейла на который обратил внимание redfs, уже настоящий антипаттерн учащий читателей плохому. Такое на хабре, ИМХО, вообще появляться не должно.

Кстати, в linux используют два основных движка регулярных выражений — glibc и libpcre. ERE и BRE — это диалекты, предоставляемые одним движком glibc, если только авторы утилиты не решили реализовать регулярные выражения сами. Многие программы (тот же grep) могут использовать, на выбор, ERE, BRE и PCRE (хотя последнее обычно можно отключить при компиляции).

НЛО прилетело и опубликовало эту надпись здесь

С таким количеством ошибок и неточностей что в коде, что в тексте, использовать эту «отличную шпору» будет попросту опасно.

НЛО прилетело и опубликовало эту надпись здесь

Значительную часть неточностей можно было бы исправить, чтобы статья была действительно нормальной. С такой статьёй вы будете «затыкать дырку», не видя проблема, привыкните, а потом выйдете из «зоны комфорта» и напишете тот же код с итерацией по $PATH в cygwin. И получите странные ошибки, некорректно работающий код, а если не повезёт ещё и какой‐нибудь rm -rf /. А если это было вашим «введением» в программирование на bash, то проблемы вам ещё будет трудно исправить.

Блин, ну хоть к другим частям прочтите комментарии.
В четвёртый раз: Advanced Bash-Scripting Guide [single-page, html, 2.3Mb]
Во второй раз: Bash Hackers Wiki [вагон и маленькая тележка примеров, описания, ссылки на кучу других полезных ресурсов вроде bash pitfalls]

Проблема в том что оно не на русском? Тогда лучше поддержите меня в том что я уже предлагал переводчикам из ruvds направить своё время и силы на перевод указанных вещей, вместо этого материала.
Это отличные примеры доков по башу.
ABSG же на русском есть: http://www.opennet.ru/docs/RUS/bash_scripting_guide/
Регулярные выражения странный предмет… Во первых, в bash их нет. Во вторых, очень не часто встречаются задачи, которые хорошо решаются этими самыми выражениями. Примером тому оба примера из статьи: для проверки email указанное заклинание не годится (и достаточно сложно проверить почему; наглядности в регэкспах 0). А второй пример — классическая сова, натянутая на глобус. Без регэкспов решается гораздо проще.
Символ точка в регулярном выражении это не любой символ, это любой единичный символ за исключением перевода строки т.е. [^\n], а для единичного необязательного символа есть оператор?.. Например colou?r. В приведенном примере регулярного выражения для email адресов допустимым будет адрес состоящий из одних точек .....@.....ru, что неверно, такой адрес недопустим.

Интересны были бы практические примеры с командой grep для разбора файлов логов веб сервера на предмет поиска попыток эксплойтов и т.п., например.
НЛО прилетело и опубликовало эту надпись здесь
Статья вообще не о 'регулярках' в bash, как минимум про awk и sed.
Регулярные выражения в bash проверяются через:
if [[ $string =~ /regex/ ]]; then
    …
fi
Зарегистрируйтесь на Хабре, чтобы оставить комментарий