Ads
Comments 23
Парни молодцы! Спасибо.
Хоть всё и стандартно, но приятно видеть подробные мануалы
Обратите внимание на то, что параметры командной строки разделяются пробелами.

Нет! Параметры (любые, а не только options/arguments) коммандной строки разделяются всеми символами из «Input field separator» и хранятся в переменной IFS.
По дефолту, ЕМНИП это пробел, таб и перенос строки.

Тут мы передали в цикл while содержимое файла и перебрали все строки этого файла, выводя номер и содержимое каждой из них.

Нет! Вы передали содержимое файла в сабшел, внутри которого запустили цикл while. Принципиальная разница обсужадалсь мной и г-н selivanov_pavel в комментариях к предыдущей статье. Ветка вот этого комментария: https://habrahabr.ru/company/ruvds/blog/325928/#comment_10161776

/bin/bash в шебанге не исправили, ветка вот этого комментария: https://habrahabr.ru/company/ruvds/blog/325928/#comment_10161110

Стилистику кода оставили как есть не смотря на замечания.

И это вещи, которые бросились в глаза быстрым пролистыванием.
Зачем делиться опытом в комментариях, если он не перетекает в другие статьи?

P.S. Я понимаю что это перевод другой работы, но я ещё раз повторяю — работа кишит граблями которые не оговариваются и про которые в комментариях указывается начиная с первой статьи. Пожалуйста перестаньте распространять эти грабли дальше. Вносите исправления тогда уж с учётом комментариев…
Проверка параметров

За пример теста аргументов приведённый в статье надо бить по рукам. Никогда так не делайте!
Если скрипту не передали аргумент, то $1 вообще не “установлена”, мне интересно сколько людей словило фейспалм над явным использованием неинициализированной переменной.
Устанавливается флаг который проверяет все переменные и выдаёт ошибку в случае unset variable
$ cat do_not_test_input_parameters_like_this.bash
#!/usr/bin/env bash
set -u

if [ -n "$1" ]; then
    echo -e "oops"
fi                                                                                                                      
$ bash do_not_test_input_parameters_like_this.bash
do_not_test_input_parameters_like_this.bash: line 4: $1: unbound variable


Правильно проверять на количество входных аргументов используя технику из части Подсчёт параметров или используя default value в случае отсутствия.
Например так:
if [[ $# -eq 0 ]]; then
    echo -e "No parameters found. "
    exit 1
fi
if [[ "${1:-unset}" == "unset" ]]; then
    echo -e "No parameters found. "
    exit 1
fi


Ключи командной строки

Для ключей есть стандартный getopts, который решает кучу вещей сразу. Вот вам пример обработки ключей враппера отправки почты (пример из одного проекта, жизнь такая):
while getopts ":c:f:hl:p:r:s:t:" opt; do
	case $opt in
		c) CC="${OPTARG}" ;;
		f) FROM="${OPTARG}" ;;
		h) mesg_usage; exit 254 ;;
		l) LANGUAGE="${OPTARG}" ;;
		p) FILE_PATH="${OPTARG}" ;;
		r) REPLY_TO="${OPTARG}" ;;
		s) SUBJECT="${OPTARG}" ;;
		t) TO="${OPTARG}" ;;
		\?) logging "critical" "Invalid option: -${OPTARG}." ; exit 253 ;;
		:) logging "critical" "Option -${OPTARG} requires an argument." ; exit 252 ;;
	esac
done
shift $(($OPTIND - 1))

logging — отдельная функция которая принимает два параметра — severity и message.

P.P.S. Обещаю больше не плодить по несколько комментариев за раз. Просто не хочу чтобы несведущие читатели таких статей ломали себе зубы об 1000 раз пройденные вещи.
Будь автор перевода человеком в моей команде, он тут же получит к прочтению «O'Reilly — Classic Shell Scripting» и Bash TLDP. Если же автор перевода все мной описанные грабли и так знает, то какого чёрта эти грабли не исправляются?!
Если скрипту не передали аргумент, то $1 вообще не “установлена”, мне интересно сколько людей словило фейспалм над явным использованием неинициализированной переменной.

В bash не такого понятия, как «неинициализированная переменная». «Не установленная» (unset/not set) — есть, но такие всегда пусты, если вы не используете странные вещи вроде set -u. «Пустая переменная == неустановленная переменная» — это концепция с долгой историей: насколько мне известно, существуют системы, в которых переменная окружения в принципе не может быть удалена, но может быть переписана. Поэтому использование [[ -n "$1" ]] вполне допустимо и идиоматично, а использование set -u вызывает сомнение в вашей адекватности.


Последнее потому, что вы можете написать MY_OPTION_PASSED_THROUGH_ENVIRONMENT= my_script, чтобы временно «удалить» некую настройку и ожидать, что скрипт будет работать как будто она не установлена. Но эквивалента для временного удаления просто нет (только subshell и unset, не слишком удобно), а скрипт с set -u просто провоцирует нарушать конвенцию «пустая == неустановленная».


И заметьте, что тот же ${1:-foo} сделает подстановку foo, если 1 не установлена, либо пуста. А чего‐то, что делает подстановку, если переменная не установлена нет. В zsh вот зачем‐то есть (${1-foo}), а в bash и POSIX оболочках нет.

Хотя я не снимаю свою позицию по поводу set -u, вообще‐то я слишком быстро пошёл писать негативный комментарий: относительно входных параметров вы полностью правы, концепция в гораздо меньшей степени касается их: точно так же как бо́льшинство скриптов и программ обработают пустые переменные окружения как отсутствующие, ожидается, что пустой входной параметр будет обработан как пустой входной параметр. Подозреваю, что причиной тому является то, как легче всего писать код обработки на C.


Тем не менее, код [[ "${1:-unset}" == "unset" ]] ещё хуже кода [[ -n "${1}" ]]: мало того, что вы обрабатываете пустую переменную как неопределённую вопреки тому, против чего предостерегали, так ещё и лишаете пользователя возможности использовать строку unset в качестве параметра. Никогда не пишите такой код.

Николай, попрошу впредь воздержаться от переходов на личности, мы всё же на хабре а не в дота чатике. nounset опция в моём коллективе просто маст-хэв. Она одновременно учит людей думать над использованием переменных и помогает отлавливать баги ещё до того как они чем-нить плохим закончатся.
${1:-foo} сделает подстановку foo, если 1 не установлена, либо пуста. А чего‐то, что делает подстановку, если переменная не установлена нет

Есть — ${parameter-word}; Вот вам ссыль на мой любимый TLDP: http://www.tldp.org/LDP/abs/html/parameter-substitution.html
«Пустая переменная == неустановленная переменная»

Сам факт наличия вышеупомянутых конструкций, разделяющих empty и unset опровергает это утверждение. Найдёте ссылку на источник? Интересно будет почитать.
мало того, что вы обрабатываете пустую переменную как неопределённую вопреки тому, против чего предостерегали, так ещё и лишаете пользователя возможности использовать строку unset в качестве параметра

Если вдуматься, то данная конструкция именно что обрабатывает оба случая. Если ничего не прилетит (unset) — дефолт, если прилетит что-то пустое (empty/null) — например скрипт вызывают с переменной, которая оказалась без значения — дефолт. Про слово «unset» в качестве default word — с вашей стороны просто придирка. Это был пример, текст можно использовать другой по желанию + я ещё не видел скрипта которому надо было передавать именно такой параметр (вы видели хоть раз в жизни?) + это сделано для повышения читабельности, где слово unset явно выражает что эта конструкция проверяет и зачем. Всё равно хотите делать тест -n? Напишите вот так:
[[ -n "${1:-}" ]]
результат будет таким же.
Есть — ${parameter-word}; Вот вам ссыль на мой любимый TLDP: http://www.tldp.org/LDP/abs/html/parameter-substitution.html

Гм, в man zsh эта конструкция явно прописана. А в man bash это спрятано в параграфе перед конструкциями («отсутствие двоеточия приводит к тесту на только неустановленные переменные»), в стандарте так же, только написано после перечисления различных ${parameter:*} штук.


Сам факт наличия вышеупомянутых конструкций, разделяющих empty и unset опровергает это утверждение. Найдёте ссылку на источник? Интересно будет почитать.

Я в основном по опыту говорю: в tutorial’ах обычно именно так, C’шный код того же Vim явным образом превращает пустые в неустановленные (использует NULL, когда p != NULL && *p == NUL с однозначным комментарием «empty is the same as not set»).


Если вдуматься, то данная конструкция именно что обрабатывает оба случая. Если ничего не прилетит (unset) — дефолт, если прилетит что-то пустое (empty/null) — например скрипт вызывают с переменной, которая оказалась без значения — дефолт. Про слово «unset» в качестве default word — с вашей стороны просто придирка. Это был пример, текст можно использовать другой по желанию + я ещё не видел скрипта которому надо было передавать именно такой параметр (вы видели хоть раз в жизни?) + это сделано для повышения читабельности, где слово unset явно выражает что эта конструкция проверяет и зачем. Всё равно хотите делать тест -n? Напишите вот так:

Если у вас по $1 имя команды, то это нормально. А если там имя файла, то нужно предполагать, что туда запихнут любую дичь, включая $'./my\nmultiline\nstring', даже если её туда ни в жизнь в реальности не запихнут. unset — это корректное имя файла, которое даже не начинается с дефисоминуса, значит оно должно обрабатываться как имя файла.


[[ -n "${1:-}" ]] — это тот же [[ -n "$1" ]] для set -u, чище использовать вариант с $#, но не использовать set -u из‐за проблем с «unset = empty».

В препоследний абзац вы что-то напихали всего сразу докучи.
Как проверка [[ -n "$1" ]] спасёт от той самой «дичи»? Правильно, никак, собственно и мной приведённые вещи. Это уже должны быть дополнительные проверки после того как выянится что входной параметр вообще есть.
Я же вроде уже написал для чего и почему в примере используется слово unset, не говоря о том, что слово настолько редкое, что что-бы получить файл с таким именем, это должно быть что-то очень специфическое и в таком случае можно написать unsetvar или один из массы других вариантов. Пожалуйста, перестаньте пытаться притянуть это за уши.

[[ -n "${1:-}" ]] — это тот же [[ -n "$1" ]] для set -u

Таки да, я разьве не это написал? Абзацем выше явно сказано что вариант с заменой-дефолта для улучшения читаемости.

чище использовать вариант с $#

Увы, нет. Автор скрипта должен знать когда и как правильно использовать подсчёт длины аргументов. В нашем обсуждаемом случае с «пустым» аргументом оно не сработает:
bash-4.4$ cat input-test-checklen.bash
if [[ $# -eq 0 ]]; then
    echo -e "Empty input"
else
    echo -e "Taking in!"
fi
bash-4.4$ bash -x input-test-checklen.bash ""
+ [[ 1 -eq 0 ]]
+ echo -e 'Taking in!'
Taking in!
Я же вроде уже написал для чего и почему в примере используется слово unset, не говоря о том, что слово настолько редкое, что что-бы получить файл с таким именем, это должно быть что-то очень специфическое и в таком случае можно написать unsetvar или один из массы других вариантов. Пожалуйста, перестаньте пытаться притянуть это за уши.

Это «не что‐то специфическое», это «ожидаемое поведение для всех аргументов соответствующих одному классу». "unset" вполне вписывается в типичный шаблон для имён файлов¹. Я не вижу никакой легальной причины сделать так, чтобы любое из легальных имён файлов не могло стоять на этой позиции. Даже если оно маловероятно.


И зачем вы зациклились на именах файлов, пусть я и привёл их в качестве примера? Может вы так забаните регулярное выражение для поиска по кодовой базе, оно и здесь также «настолько редкое»? Просто не нужно на пустом месте увеличивать количество специальных значений в полтора раза: пустое, неустановленное и "unset"; вы не знаете заранее, не потребуется ли оно.


¹ В моём понимании для них ожидается, что всё подпадающее под ^(?!-)(?=.)(\.{0,2}/+)?([^/]+/(/+(?!:))?)*[^/+]$ будет именем файла, возможно и что‐то сверх: file, ./file, ../path/to/file, /absolute/path/to/file, /////path//////to/////file.


Увы, нет. Автор скрипта должен знать когда и как правильно использовать подсчёт длины аргументов. В нашем обсуждаемом случае с «пустым» аргументом оно не сработает:

Я не понимаю, что здесь обсуждается. Моё мнение, основанное на том, как работает большинство утилит (особенно, написанных на C) — пустой параметр, переданный в скрипт — это пустое значение того, что должно быть на этом месте, а не отсутствие параметра. Пустая переменная окружения, содержащая настройки для программы — это то же, что неустановленная. В таком случае в вашем примере «Taking in!» — это ожидаемый вывод, но с set -u неудобно работать в режиме «пустая переменная окружения = неустановленной переменной окружения», потому что придётся писать везде :-.


Непозиционные (-p, --long-param) сюда не относятся, но такое обрабатывается всегда отдельно. Правда, в моих коротких скриптах, часто через if [[ "$1" = "--param" ]] ; then shift ; do_something ; fi, что не совсем корректно.


Хотя, если ваш стиль кодирования предполагает, что все переменные с настройками должны получить значение по‐умолчанию, пусть даже оно и пустое, в одном месте, где‐то наверху, то с set -u будет легче писать: в отличие от переменных‐настроек, для локальных переменных функций unset = empty будет маскировать ошибки. Мне начинает казаться, что это может быть лучше моей сложившейся практики и уже не выглядит дичью. Но почему‐то скриптов с set -u я просто ни разу не видел.

Кстати, вот что с поддержкой C’шных функций работы с окружениям по стандартам, согласно их manам:


(un)setenv: 4.3BSD, POSIX.1-2001
putenv: POSIX.1-2001, POSIX.1-2008, SVr4, 4.3BSD
getenv: SVr4, POSIX.1-2001, 4.3BSD, C89, C99


При этом если у вас только putenv и getenv то ничего из окружения удалить штатными средствами вы не можете. «Нештатными» (т.е. поддерживать свой environ и писать туда/читать оттуда, его же отдавать в execve) сможете, но это сильно усложнит код.


Как пример системы без unsetenv нашёл Solaris 8.

В догонку про Command-Line Processing и IFS:
Each line that the shell reads from the standard input or a script is called a pipeline; it contains one or more commands separated by zero or more pipe characters (|). For each pipeline it reads, the shell breaks it up into commands, sets up the I/O for the pipeline, then does the following for each command:

1. Splits the command into tokens that are separated by the fixed set of metacharacters: SPACE, TAB, NEWLINE, ;, (, ), <, >, |, and &. Types of tokens include words, keywords, I/O redirectors, and semicolons.

9. Takes the parts of the line that resulted from parameter, command, and arithmetic substitution and splits them into words again. This time it uses the characters in $IFS as delimiters instead of the set of metacharacters in Step 1.
Не описан такой вариант read:

while read x; do
echo $x
done << theend
A
B
C
theend
Ключ -s команды read предотвращает отображение на экране данных, вводимых с клавиатуры. На самом деле, данные выводятся, но команда read делает цвет текста таким же, как цвет фона.

Наверное, все-таки вообще не выводятся. Иначе они занимали бы место на экране и во многих терминалах их можно было бы выделить мышкой.
Как вариант еще можно использовать аргументы формата key=value

for argument in ${@}; do
    case $argument in
        -k=* | --key=* )
            VALUE=${argument##*=} # Запишет значение после знака равно в переменную VALUE
            ;;
    esac
done
Тогда уже закидывать аргументы в массив

delare -a params
for argument in ${@}; do
# key=value1 key2=value2 ...
    key=${argument//=*/}
    val="${argument:$((${#key}+1))}"
    params[$key]="$val"
done
Для чего их записывать в массив?

Я использую указанный мной подход когда есть четко заданные аргументы которые можно использовать. Они отлавливаются через case и записываются в соответственную переменную. Удобно когда есть переменные с задаными значениями по умолчанию.

Пример из реального скрипта:
RESTORE=0
ENCRYPT=1

ARGPAD=1 # Если в коде где-то требуются оставшиеся аргументы (например список файлов) их можно получить прибавив этот 'отступ'
for argument in ${@}; do
    case $argument in
        -r | --restore )
            RESTORE=1
            ARGPAD=$(($ARGPAD + 1))
            ;;

        -e=* | --encrypt=* )
            encrypt=${argument##*=}

            if [ "$encrypt" == "non-encrypt" -o $encrypt == 0 ]; then
                ENCRYPT=0
            fi

            ARGPAD=$(($ARGPAD + 1))
            ;;

        -* )
            ARGPAD=$(($ARGPAD + 1))
            ;;
    esac
done

Вы забыли кавычки: for argument in "$@". В zsh вы в такой ситуации только потеряете пустые аргументы (да, zsh тоже нужны кавычки и [@], хотя я долгое время считал иначе). В bash получите переразбитый массив (к примеру, bash -c 'for arg in $@ ; do echo $arg ; done' - 'a b c' выдаст три строки: a, b, c). Хотя можно их вообще не писать, как и in: for argument ; do command ; done.

Реального скрипта говорите? Я скопирастил ваш код из сообщения и запустил вот так:
bash Sergei95ZH.bash -e=
Sergei95ZH.bash: line 17: [: too many arguments

Это пример типичных граблей — переменная $encrypt вне кавычек в односкобочном [...] тесте.

Я предполагаю что кавычек в части сравнения с нулём нет, потому что вы хотели сравнивать с числом? А какого лешего тогда используется оператор сравнения строк "=="? Оператор сравнения чисел: "-eq".
Я понимаю что баш позволяет такие конструкции, и за это я его очень не люблю. Это вредная привычка которая с лёгкостью может перерасти в «говнокодописание».

Самое простое решение вышеуказанной ошибки — просто засунуть всё в двойные кавычки:
[ "$encrypt" == "non-encrypt" -o "$encrypt" == "0" ]

Правильное решение сделать тест с двойными скобками [[...]]
[[ $encrypt == non-encrypt || $encrypt == 0 ]]

все кавычки опущены специально чтобы показать что оно и так сработает, это одно из отличий [...] от [[...]]
кроме того в этом случае работает принцип short-cirquit, а в первом — нет

Мой совет — поголовно использовать extended test command [[...]] вместо [...] и все строковые покрывать двойными кавычками.
Большое спасибо за замечание, исправил. Писать shell скрипты я начал всего неделю назад так что увы еще много пробелов)
Элементы массива можно брать и использовать; длииинный case не нужен. Не нравится мне когда языковая конструкция не помещается в экран ;)
Only those users with full accounts are able to leave comments. Log in, please.