# К домашним заданиям
### Частые проблемы в сдаваемых шелл-скриптах
0. Не игнорируйте, пожалуйста, общую страницу домашнего задания на вики. В ней описано многое из перечисленного здесь. В частности, проверяйте свои скрипты в [ShellCheck](https://shellcheck.net), чтобы сократить цикл проверки задания.
1. `#!/bin/bash` в качестве shebang использовать не стоит, т.к. место расположения `bash` не специфицируется FHS (в принципе, он может лежать в `/usr/bin/bash`, например -- так обстоят дела по крайней мере в некоторых версиях FreeBSD -- или вообще по непредсказуемому пути, как в NixOS). Вместо `#!/bin/bash` следует использовать `#!/usr/bin/env bash` (однако имейте в виду для скриптов, которые предназначены для запуска от имени суперпользователя, [риск для безопасности](https://github.com/koalaman/shellcheck/issues/880#issuecomment-298032886)). Если вы ничего bash-специфичного не используете, рекомендуется использовать `#!/bin/sh` (на некоторых системах `#!/bin/sh` ссылается на `bash`, но на других это могут быть более простые и быстрые оболочки, а `bash` в принципе может быть не установлен). *Напомню, что за `sh`-совместимые скрипты с соответствующим shebang начисляются дополнительные баллы.*
2. В случае, когда пользователь сам просит о помощи (`-h`/`--help`), её вывод -- ожидаемый эффект, стоит выводить сообщение на `stdout` и завершаться с кодом 0. Когда же справка об использовании программы выводится в ответ на некорректное использование, следует осуществлять вывод на `stderr` и завершаться с кодом, отличным от 0 (представьте, что пользователь осуществляет перенаправление в файл, который дальше обрабатывает другими командами, или в конвейер -- ваша команда успешно завершится и запишет в этот файл "мусор" в виде справки об опциях). Для этих целей удобно завести функцию, выполняющую вывод на `stdout` и при необходимости перенаправлять её вывод в `stderr`.
3. По вышеуказанным причинам вообще любые сообщения об ошибках и отладочные сообщения стоит выводить в `stderr` (условно считайте, что `stdlog = stderr`; например, в С++ `std::clog` и `std::cerr` оба используют файловый дескриптор 2).
4. **Архиважно (выработайте привычку):** Практически всегда рекомендуется обращаться к переменным и другим expansions (вроде `"$(...)"`), окружая их использование двойными кавычками. В противном случае практически всегда возникают [разные проблемы](http://mywiki.wooledge.org/BashPitfalls) с переменными, содержащими пробелы или glob-символы. Также ознакомьтесь с конструкцией `"$@"`/`"${array[@]}"`.
5. Вместо конструкций вроде `ls *` или `for file in *; do ...` из-за возможного [переполнения допустимого числа аргументов](https://www.in-ulm.de/~mascheck/various/argmax/) стоит использовать другие схемы, например `find ... -exec ...` или `find ... | while IFS= read -r filename; do ...` или, в зависимости от ситуации, `ls | while ...`.
6. Вместо `cat FILENAME | command` (что называется useless use of cat) можно использовать `command < FILENAME`, а зачастую и `command FILENAME`.
7. Аргументом `if` может являться абсолютно любая команда (и `[` -- просто частный случай). Если она завершается с кодом 0 (`EXIT_SUCCESS`), тело `if` выполняется (обратите внимание, что, в противоположность C, здесь 0 -- truthy, а всё остальное -- falsey). Например, вместо
```(sh)
some_command
if [ $? = 0 ]; then
echo 'Er, dull...'
fi
```
стоит делать так:
```(sh)
if some_command; then
echo 'Simple, yay!'
fi
```
а можно и так:
```(sh)
some_command && echo 'Cool, eh?'
```
В последнем случае, однако, стоит учитывать, что если левый аргумент `&&` возвращает ненулевой код возврата, он становится кодом возврата всей составной команды. Во-первых, если эта строка была последней в вашем скрипте или функции, она определит код возврата всего скрипта или всей функции, соответственно; во-вторых, в режиме `set -e` в `bash` и `sh` такой случай обрабатывается по-разному. В случае же `if` код возврата при невыполненном условии равен 0.
8. Тонкость работы с локализацией: если вы ищете определённые подстроки в выводе команд (что не рекомендуется, т.к. они зачастую могут меняться от версии к версии, если не гарантируется иное, как в `git --porcelain`) или сортируете текстовые строки, предварительно поставьте `LC_ALL=C`, иначе результаты могут зависеть от локали пользователя (например, порядок строк в выводе `sort` будет отличным от ожидаемого или подстрока `Circular` не найдётся в выводе `make: Цыклiчная залежнасць`).
9. Зачастую код поиска слова `word` через `grep word` страдает от проблемы ложных срабатываний на строках вроде `overworded`. Избавляет от проблемы, например, опция `-w`/`--word-regexp`.
10. Неочевидная проблема с `trap ... DEBUG`: [дополнительные срабатывания](https://seasonofcode.com/posts/debug-trap-and-prompt_command-in-bash.html).
11. Как корректно обрабатывать потенциальные опасности в именах файлов: https://dwheeler.com/essays/filenames-in-shell.html
12. Если ваш скрипт предназначен для включения (sourcing) с помощью команды `. имя_скрипта` (в Bash также возможен синоним `source имя_скрипта`; типично это условие выполнено, если скрипт содержит функции и переменные среды, которые меняют окружение текущей оболочки пользователя --- в частности, всякие настройки вроде задач showtime или группы функций в задаче where/wtf, хотя в последнем случае можно сделать и 4 отдельных скрипта вместо этого), то shebang вида `#!/bin/sh` или `#!/usr/bin/env bash` ему не нужен (этот скрипт не планируется выполнять как отдельный исполняемый файл, ибо такое его выполнение будет влиять на новую командную оболочку, которая запустится и завершится). В качестве подсказки пользователю в таком случае я нередко использую трюк вида (см. https://github.com/koalaman/shellcheck/issues/2110):
```(sh)
#!/usr/bin/echo This script is supposed to be sourced like this: .
# shellcheck disable=2096
```
### Дополнительные замечания
0. Если посылка в Review Board состоит из нескольких файлов, стоит перепосылать _все_ при каждом изменении хотя бы одного из них, иначе оставшиеся в diff выглядят удалёнными.
1. Когда версия с использованием `bash`-специфики не получается сильно короче или выразительнее, не стоит посылать её параллельно с `sh`-версией -- достаточно лишь последней.
2. Способ избежать useless use of cat: `while IFS= read -r line; do ...; done < "$input_file"` (да, так можно!).
3. Несколько нетрадиционным является показывать usage в случае I/O (permission) ошибки, обычно его показывают, когда формат вызова не соблюдён; иначе показывают сообщение о самой невозможности прочитать файл. (Если показать usage, пользователь будет искать ошибку в формате вызова команды.)
4. Попытки поместить команду в переменную и затем её выполнить [очень сложно заставить работать во всех случаях](http://mywiki.wooledge.org/BashFAQ/050) *(при проверке я всё-таки зачастую закрываю на это глаза)*. Используйте альтернативы, описанные по ссылке.
### К другим заданиям
1. Мало кто уделяет этому внимание, но системные вызовы зачастую могут завершаться с ошибкой `EINTR`, и в этом случае выходить с `perror` далеко не всегда правильно (зачастую правильнее повторить вызов, но это зависит от ситуации). См., например, [EINTR and what is it good for](http://250bpm.com/blog:12). _Обычно я закрываю на это глаза при проверке, но имейте в виду на всякий случай._
2. При работе с сокетами как `recv`, так и `send` могут послать не все данные, которые им предоставлены (они возвращают число байт, над которыми действие произведено успешно). Это значит, что в приложениях (при посылке и чтении более чем одного байта) зачастую приходится реализовывать обёртки вроде `sendall` и `recvall` (например, как это [сделано в стандартной библиотеке Python](https://stackoverflow.com/a/34252690)).
### И пожелания на будущее
0. Иногда я засчитываю задание, оставляя при этом ещё какие-то комментарии. Я надеюсь, что ознакомление с ними принесёт вам пользу (не пренебрегайте этим, пожалуйста), но перепосылать решение ради следования им не требуется. То же, в принципе, касается комментариев в Review Board, которые не помечаются как проблемы (issues) -- зачастую я их также выделяю курсивом.
1. Когда вы пишете код программы или скрипта, нужно позаботиться о тех, кто будет её читать -- скорее всего, это будете вы сами (см. [Master Foo and the Programming Prodigy](https://habrahabr.ru/post/273023/)). Не пренебрегайте соблюдением консистентных отступов, понятными идентификаторами, комментариями в неочевидных местах. Вместо комментирования "отмерших" участков кода используйте системы контроля версий.
2. В случае с shell-сценариями это ещё можно выразить в использовании `--длинных-понятных-названий-опций` вместо `-s` `-h` `-o` `-rt`, смысл которых приходится смотреть в документации (правило буравчика: короткие опции для скорости набора в интерактивном режиме, длинные -- для понятности в скриптах и другом публикуемом материале, например, инструкциях). К сожалению, не все команды предоставляют длинные варианты для коротких опций; в этом случае для редких опций имеет смысл пояснить их значение комментариями.
3. При реализации задач вроде Guess -- где есть ортогональные подзадачи (взаимодействие с сервером и собственно отгадывание в данном случае) -- имеет смысл спроектировать архитектуру так, чтобы сделать это разделение явным (например, через [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection)), а компоненты независимыми. В частности, в задаче Guess у многих первая реализация дихотомии была некорректной, но тестировать её было неудобно -- нужно было запускать сервер etc. Чтобы найти вход, на котором алгоритм отрабатывал некорректно, приходилось комментировать куски кода, взаимодействующие с сервером, и заменять их на перебор. В идеале же -- при независимой реализации алгоритма угадывания -- тест на корректность на большом числе входов мог бы сосуществовать с самой программой.
4. Раз уж речь зашла о дихотомии, корректность реализации проще всего доказывать с помощью инвариантов (условий, которые выполняются на входе в цикл, не нарушаются после исполнения тела цикла и потому -- по индукции -- верны после каждой итерации, в том числе на выходе из цикла). Для дихотомии инвариантом выступает обычно условие "искомое число находится в закрытом интервале `[left, right]`" или условие "искомое число находится в полуоткрытом интервале `[left, right)`". См., например, [эти слайды](https://www.eecs.yorku.ca/course_archive/2013-14/W/2011/lectures/09%20Loop%20Invariants%20and%20Binary%20Search.pdf). Кроме того, чтобы доказать, что алгоритм завершится, можно показать, что он движется в направлении завершения -- например, что длина (полу)интервала после каждой итерации гарантированно сокращается после итерации цикла (например, очень частая проблема: `left` и `right` сходятся до `right = left + 1`, а затем происходит последовательность `mid = (left + right) / 2 [ = left]`, `left = mid [ = left]`, и так по кругу -- алгоритм зацикливается).