Hello World, должно быть, самая часто создаваемая компьютерная программа. Уже десятилетия это первая программа, которую пишут люди, когда начинают изучение нового языка программирования.

Конечно же эта простая программа не должна иметь баги?

В конце концов, hello world программы делают только одну вещь. Как там может быть баг?

Hello world в C

Есть множество различных способов написать hello world в C. Версия Википедии, hello world в книге K&R и даже самая старая из известных hello world программ из 1974.

Вот ещё одна написанная на "ANSI C":

/* Hello World in C, Ansi-style */

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
  puts("Hello World!");
  return EXIT_SUCCESS;
}

Это самая надёжная версия из представленных. Она использует (void) чтобы гарантировать что main вызывается без аргументов. Она использует EXIT_SUCCESS макрос вместо предположения что платформа использует 0 для индикации успеха, что не обязательно согласно стандарту C, но мы не рискуем здесь. Она использует соответствующие заголовки, чтобы избежать неявного объявления puts. Эта версия пытается сделать правильно всё.

И всё равно, тут есть баг.

Во всех версиях представленных выше есть баг.

Баг?

В Linux есть интересный файл под названием "/dev/full", который похож на его более известного собрата "/dev/null", но когда вы пишете в "/dev/full", вместо того чтобы игнорировать данные, он выдаёт ошибку. Он ведёт себя как файл в файловой системе у которой закончилось место.

$ echo "Hello World!" > /dev/full
bash: echo: write error: No space left on device
$ echo $?
1

Это прекрасный инструмент для тестирования того, что программа правильно обрабатывает ошибки ввода-вывода. Неудобно создавать реальную файловую систему без свободного места или эмулировать диск, который сломан, но очень просто направить вывод программы в "/dev/full" и посмотреть что произойдёт.

Так давайте проверим ту программу на C выше:

$ gcc hello.c -o hello
$ ./hello > /dev/full
$ echo $?
0

В отличие от echo в предыдущем примере, программа ничего не вывела и код возврата был 0. Это значит что программа hello сообщила что завершилась успешно. Однако, это не так. Мы можем убедиться, что ошибка была, с помощью strace:

$ strace -etrace=write ./hello > /dev/full
write(1, "Hello World!\n", 13)          = -1 ENOSPC (No space left on device)
+++ exited with 0 +++

Вот и ошибка о нехватке места, но программа просто проигнорировала её и возвратила 0 - код успеха. Это баг!

Насколько серьёзный это баг? Возможно, hello world - это не самая критичная программа. Однако, hello world делает то же, что и другие программы в реальном мире: печать в стандартный вывод, который может быть перенаправлен в файл, а ведь в реальном мире для файла может закончиться место. Если программа игнорирует такую ошибку и не сообщает о ней через код возврата, её родительский процесс не будет знать что она завершилась с ошибкой и продолжит работать как ни в чём не бывало, хотя вывод, который он ожидал получить, был потерян.

Например, рассмотрим программу, которая печатает yaml файл в стандартный поток вывода. Если у стандартного вывода закончится место, то вывод может быть обрезан в какой-то произвольной точке, хотя это всё ещё может быть валидный yaml. Поэтому нам стоит ожидать, что программы обнаруживают такую ситуацию и сообщают о ней.

Что насчёт других языков?

Ранее мы рассмотрели bash и C, но что насчёт Python, который говорит нам что "Ошибки никогда не должны замалчиваться"? Вот Python 2:

$ python2 hello.py > /dev/full
close failed in file object destructor:
sys.excepthook is missing
lost sys.stderr
$ echo $?
0

Он напечатал сообщение в stderr, хотя оно и сбивает с толку. Однако он также вернул 0, что означает что программа завершилась успешно.

К счастью, Python 3 правильно сообщает об ошибке и печатает понятное сообщение о ней:

$ python3 hello.py > /dev/full
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
OSError: [Errno 28] No space left on device
$ echo $?
120

Вот результаты работы hello world программ на различных языках программирования с различных обучающих сайтов, которые я попробовал запустить:

Язык

Есть ли баг

Тестируемая версия

C

Да

(все)

C++

Да

(все)

Python 2

Да

Python 2.7.18

Ruby

Да

ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux-gnu]

Java

Да

openjdk 11.0.11 2021-04-20

Node.js

Да

v12.21.0

Haskell

Да

The Glorious Glasgow Haskell Compilation System, version 8.8.4

Rust

Нет

rustc 1.59.0 (9d1b2106e 2022-02-23)

Python 3

Нет

Python 3.9.5

Perl

Нет

perl 5, version 32, subversion 1 (v5.32.1) built for x86_64-linux-gnu-thread-multi (with 46 registered patches...)

Perl 6

Нет

v2020.12

Bash

Нет

GNU bash, version 5.1.4(1)-release (x86_64-pc-linux-gnu)

Awk

Нет

GNU Awk 5.1.0, API: 3.0 (GNU MPFR 4.1.0, GNU MP 6.2.1)

OCaml

Нет

4.08.1

Tcl

Нет

8.6.11

C#

Нет

Mono JIT compiler version 6.8.0.105

Более полный и актуальный список находится тут.

Комментарии (20)


  1. stanislavshwartsman
    18.03.2022 12:17
    +5

    Программа положила вывод в stdout? Положила. У неё это получилось? Получилось. Перенаправление stdout в файл это не функция программы, а функция оболочки (shell).

    Если у shell не получилось, он и должен поправить код ошибки.


    1. ufm
      18.03.2022 12:28
      +9

      У программы не получилось, она просто проигнорила код ошибки.
      А еще вы не знаете как работает перенаправление.


    1. ris58h Автор
      18.03.2022 12:43

      Начнём с того, что это перевод, поэтому за автора ответить не смогу.

      Как вы смотрите на то, что некоторые программы из таблицы считают это ошибкой? Или на то, что в Python2 это игнорируется, а в Python3 нет? Разработчики Python3 заблуждаются? Пример с обрезанным yaml тоже интересный.

      Поэтому и перевёл, что тема не однозначная.


    1. ris58h Автор
      18.03.2022 13:09
      +5

      У неё это получилось? Получилось.

      Почему вы так решили?

      Смотрим документацию puts для примера на C:

      If successful, non-negative value is returned. On error, the function returns EOF.

      Достаточно просто вернуть это значение вместо того чтобы всегда возвращать 0, чтобы убедиться что не "получилось".

      #include <stdio.h>
      #include <stdlib.h>
      
      int main(void)
      {
        return puts("Hello World!");
      }

      Для /dev/null вернётся 0, а для /dev/full вернётся 10. Т.е. у программиста получилось проигнорировать ошибку, а не у программы получилось что-то сделать.


      1. iig
        18.03.2022 13:25
        -2

        Если бы все ошибки в программах обрабатывались правильно - не было бы багов. Наверное. Ведь в каждом обработчике ошибок могут быть свои ошибки, которые тоже нужно обрабатывать.


        1. ris58h Автор
          18.03.2022 13:33
          +8

          Статья как раз о том что они есть. Даже в hello world. Ну и не стоит её так серьёзно воспринимать, зато есть что подчерпнуть. Я, например, узнал что есть /dev/full. Потом узнал что его нет в macOS.


          1. unsignedchar
            18.03.2022 17:02
            +2

            То есть в macos баг не воспроизводится, тикет можно закрывать? ;)


            1. ris58h Автор
              18.03.2022 18:07

              В macOS приходится приседать https://www.thedroidsonroids.com/blog/dev-full-osx


      1. sshikov
        18.03.2022 17:22
        +3

        >Почему вы так решили?
        Потому что вы не описали требования к программе. Их еще нет — а вы уже заявляете о наличии багов. Есть требования по возврату кодов, отличных от нуля, и со списком ошибок, которые нужно было обрабатывать? Нету? Значит и багов тоже нет.


        1. unsignedchar
          18.03.2022 18:20

          Задокументированный баг становится фичей ;)


        1. ris58h Автор
          18.03.2022 18:22

          Давайте по порядку. Напомню вам ваши же слова:

          Программа положила вывод в stdout? Положила. У неё это получилось? Получилось.

          Нет, у неё это не получилось. Документацию и примеры кода с результатами я привёл выше. Ваше утверждение ложно, к сожалению.

          Потому что вы не описали требования к программе.

          Строго говоря, я вообще ничего не описывал, а только перевёл.

          Есть требования по возврату кодов, отличных от нуля, и со списком ошибок, которые нужно было обрабатывать?

          Есть требование не возвращать 0, если программа не завершилась успехом, а коды ошибок могут быть произвольные. Удостовериться в этом можно, например в Википедии.


          1. sshikov
            18.03.2022 18:29
            +2

            >Напомню вам ваши же слова:
            Вообще-то они не мои… вы перепутали. Ну да ладно.

            >а только перевёл.
            Ну я это заметил.

            Повторю в двух словах: у автора (да, перевод, не вопрос, но общаемся же мы с вами, а не с ним) нет требования, чтобы hello обрабатывала ошибки. Если есть — можете показать, где оно написано? В итоге, нет определения того, что есть успех. Тот факт, что автор (и возможно вы с ним за компанию) считаете, что успехом является вывод Hello, world куда-то, на самом деле не сформулирован. Поэтому скажем я вполне могу считать успехом то, что программа всегда возвращает 0. И чем мой успех хуже вашего, если требования на бумажке не записаны?


            1. ris58h Автор
              18.03.2022 19:39

              Действительно, перепутал. Без аватарок сложно ориентироваться.

              Есть определение hello world, как программы выводящей соответствующий текст. Есть требование к программам возвращать не 0, если выполнение неуспешно. Ни одно из условий не выполнено. Лично у меня язык не поворачивается назвать это не багом. Т.е. баг есть. Об этом автор, вроде как, и пишет.


              1. sshikov
                18.03.2022 20:09

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

                >Есть требование к программам возвращать не 0, если выполнение неуспешно.
                Я думаю, что нет такого требования. Это это называется соглашение. И следовать ему никто не обязан, строго говоря.

                Против той мысли, что баги могут быть даже в таком коротком куске кода — нет никаких возражений (начиная с того, что он может делать совсем не то, что ожидалось человеком).

                Это тема в общем не новая, есть широко известный текст о том, как на интервью человеку предлагаю написать копирование файла — и все точно так же упирается в спецификацию. Там примерно можно понять, какого объема на самом деле может быть спецификация на простой одиночный вызов API.

                >Ни одно из условий не выполнено.
                Весь вопрос в том, что считать успехом. Ну вот представьте себе, что вы выводите этот текст на диск. Достаточно ли проверить код возврата API, или нужно убедиться, что текст сохранен (и тут уже начинается, негибкость API, кеши нескольких уровней, энергонезависимая память, пятое-десятое)? А если вы текст отдали функции API, а потом выключилось электричество, и он не сохранился — это баг? А чей, ваш, или скажем линукса/драйвера диска/файловой системы/микропрограммы где-то в потрохах SSD?


                1. ris58h Автор
                  19.03.2022 13:11
                  +2

                  это называется соглашение

                  Именно так. Если в геттерах/сеттерах творить какую-то дичь, но не get/set, то это говнокод. Ничто не мешает, конечно, упереться в то, что спецификации нет и вообще как угодно методы называть можно, только зачем?

                  Достаточно ли проверить код возврата API

                  Достаточно. На то оно и API.

                  А если вы текст отдали функции API, а потом выключилось электричество, и он не сохранился — это баг?

                  Если API нам вернул статус, что текст сохранился, а на самом деле нет, то это баг API. Мы на это повлиять не можем.


      1. eptr
        19.03.2022 13:13
        +1

        Тогда уже не так:

        #include <stdio.h>
        #include <stdlib.h>
        
        int main(void)
        {
          return puts("Hello World!");
        }

        А так:

        #include <stdio.h>
        #include <stdlib.h>
        
        int main(void)
        {
          return puts("Hello World!") != EOF ? EXIT_SUCCESS : EXIT_FAILURE;
        }


        1. ris58h Автор
          19.03.2022 13:13

          Конечно. Просто суть была в том, чтобы показать что коды возврата разные.


    1. amarao
      19.03.2022 15:11

      Программа положила вывод в stdout? Положила.

      Не положила. Интерфейс "положить в stdout"

      man printf:

         If an output error is encountered, a negative value is returned.

      Игнорировать ошибки программа может, но это деяние, которое может быть ошибочным.


  1. sparhawk
    18.03.2022 16:07
    +4

    Hello World на Си уже исправляли на Хабре 12 лет назад habr.com/ru/post/75971


  1. iig
    19.03.2022 14:45
    +1

    Решил посмотреть, часто ли програмисты проверяют, что им вернул printf.

    Внутри busybox насчитал 2720 строк с [f]printf. Проверяется return value аж в 4 местах. Ушол за валерьянкой.