Комментарии 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 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, остальные же будут понимать такое как ./~/…
.
С 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
?
Только 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. Если они ищутся в текущем каталоге, то задача — искать их в том числе в текущем каталоге. Вы не ищете — задача не решена.
Снимаю «некорректность» до ответа на https://habrahabr.ru/company/ruvds/blog/327896/#comment_10203768.
Меня сбивает с толку слово «раскрытие». Нету там никакого раскрытия, происходит замена знака «тильда» содержимым определённой переменной. Почему вы это раскрытием называете?
Потому что это называется «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, не то что не совместимо с остальными оболочками.
В моём конкретном примере, где используется find, так как $path внутри цикла обёрнут двойными кавычками во избежание проблем с пробелами и другими белыми знаками, внимание, раскрытия тильды НЕ произойдёт (я уже вам предлагал это попробовать), поэтому и конструкция которая это раскрытие заменяет.
Я уже несколько раз повторил что мы говорим об одном и том же, вы просто сфокусировались на конкретной вещи, так и не вникнув в суть мной предложенного решения. Я не спорю с вами что тильду класть себе в PATH не следует.
Задача буквально звучит как «Напишем bash-скрипт, который подсчитывает файлы, находящиеся в директориях, которые записаны в переменную окружения PATH». Я интерпретирую её как «Написать bash‐скрипт, который подсчитывает файлы, находящиеся в каталогах, в которых POSIX‐совместимая оболочка будет искать исполняемый файлы и скрипты», потому что это то, что первое приходит на ум (если бы был пользователем bash, писал бы про то, где будет искать сам bash). Хотелось бы услышать вашу точную интерпретацию, а то я могу сказать, что если в $PATH находится /usr/local/bin
, то там записаны следующие каталоги:
- /
- /usr
- /usr/local
- /usr/local/bin
- /bin
Они же ведь там действительно записаны!
Я не против, это отличное решение.
Мы находимся в цикле статей про 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.
Во‐первых, пустой каталог.
Я наконец-таки понял что вы имели ввиду вот этой фразой:
И ещё вы забыли одну вещь: на случай, если в $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
В моём варианте это обрабатывается также правильно. А если вы просто берёте и заменяете тильду — то нет.
Подумайте чего только стоит упоминание о регексах, если читателей заранее не проинформировали что в баше есть понятие 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 направить своё время и силы на перевод указанных вещей, вместо этого материала.
Интересны были бы практические примеры с командой grep для разбора файлов логов веб сервера на предмет поиска попыток эксплойтов и т.п., например.
Регулярные выражения в bash проверяются через:
if [[ $string =~ /regex/ ]]; then
…
fi
Bash-скрипты, часть 9: регулярные выражения