# К домашним заданиям ### Частые проблемы в сдаваемых шелл-скриптах 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]`, и так по кругу -- алгоритм зацикливается).