День 07 · Ввод и первые ошибки · страница 2 из 6

Ввод из конвейера и файла

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


Конвейер: |

Ситуация. Нужно запустить программу приёмки на числе 40, не набирая его руками.

В scratch/main.go — программа «Сколько штук пришло?» с первой страницы.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ echo 40 | go run .
Сколько штук пришло? Вы ввели: 40

Как это читать. echo 40 печатает 40 и перевод строки. Знак | — конвейер: то, что левая команда напечатала бы на экран, становится вводом правой. Программе всё равно, набрал строку человек или её прислал echo: scanner.Scan() в обоих случаях читает символы до перевода строки.

Почему теперь всё в одной строке? С клавиатуры вы нажимали Enter, и терминал показывал переход на новую строку — это было эхо вашего набора, а не вывод программы. Из конвейера ничего не набирается и эха нет. Сама программа напечатала только Сколько штук пришло? и Вы ввели: 40.

Две строки ввода — через printf, в котором \n означает перевод строки. С программой «Товар / Склад»:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ printf 'Болт М6\nСевер\n' | go run .
Товар: Склад: Принято: Болт М6 на склад Север

echo 3 5 — это одна строка 3 5, а не две. Каждое значение — на своей строке: printf '3\n5\n'.


Файл: < и >

Ввод, который нужен часто, удобно держать в файле. Файл можно создать в редакторе, а можно одной командой:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\n' > input.txt

Знак > отправляет в файл то, что команда напечатала бы на экран. Файла не было — он появится; был — перезапишется без предупреждения.

Знак < делает обратное: подаёт содержимое файла программе на ввод.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run . < input.txt
Сколько штук пришло? Вы ввели: 40

Их можно совместить: ввод из файла, вывод в файл. На экране пусто, весь вывод — в out.txt:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ go run . < input.txt > out.txt
stagiaire@lab:~/gocourse/day07/scratch$ cat out.txt
Сколько штук пришло? Вы ввели: 40

Сводка:

<, > и | — это не Go. Их понимает bash, ещё до запуска программы: он открывает файлы и соединяет команды трубой. Программа об этом не знает. Для неё os.Stdin — всегда просто «откуда читать», а fmt.Println — «куда печатать». Поэтому одна и та же программа, без правок, работает и с клавиатурой, и с файлом, и в конвейере. Проверка курса пользуется тем же: подаёт вашей программе заранее заготовленные вводы.


Попробуйте сейчас: конвейер и файлы.

Цель: подать программе ввод через | и < и отправить её вывод в файл через >.

1. Наберите в scratch/main.go программу «Товар / Склад» — ту же, что на первой странице:

✎ Наберите в файл scratch/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	fmt.Print("Товар: ")
	scanner.Scan()
	item := scanner.Text()
	fmt.Print("Склад: ")
	scanner.Scan()
	store := scanner.Text()
	fmt.Println("Принято:", item, "на склад", store)
}

2. В папке scratch выполните по очереди:

▶ Выполните
cd ~/gocourse/day07/scratch
printf 'Болт М6\nСевер\n' | go run .
printf 'Гайка М8\nЮг\n' > input.txt
go run . < input.txt
go run . < input.txt > out.txt
cat out.txt
echo Шайба | go run .

3. В последнем запуске одна переменная осталась пустой. Какая и почему? Ответьте себе до того, как читать дальше: разбор — в первых строках следующего раздела, «Scan() отвечает, прочитал ли строку».

Готово, когда: первая команда напечатала Товар: Склад: Принято: Болт М6 на склад Север, cat out.txt показал то же, что go run . < input.txt печатал на экран, а ваш ответ про пустую переменную сверен со следующим разделом (пункты проверки t_pipe, t_infile, t_outfile).


Scan() отвечает, прочитал ли строку

Ситуация. В последнем опыте склад остался пустым, а программа молча напечатала «на склад» и пустое место. Нужно отличать «строку прочитали» от «ввод кончился».

scanner.Scan() не только читает, но и возвращает ответ типа bool: true, если строка прочитана, и false, если читать больше нечего. До сих пор этот ответ пропадал. Посмотрим на него:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	ok := scanner.Scan()
	fmt.Println(ok, "["+scanner.Text()+"]")
	ok = scanner.Scan()
	fmt.Println(ok, "["+scanner.Text()+"]")
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ echo 7 | go run .
true [7]
false []

echo 7 дал одну строку. Первый Scan() её прочитал — true. Второму читать нечего — false, и Text() отдаёт пустую строку.

Теперь то же с условием из дня 06:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		fmt.Println("нет данных")
		return
	}
	fmt.Println("строка:", scanner.Text())
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ echo 7 | go run .
строка: 7
stagiaire@lab:~/gocourse/day07/scratch$ echo | go run .
строка: 
stagiaire@lab:~/gocourse/day07/scratch$ go run . < /dev/null
нет данных

Как это читать. if !scanner.Scan() — «если строку прочитать не удалось». Scan() выполняется прямо внутри условия: строка читается здесь, второй вызов перед Text() не нужен. return внутри main завершает программу — строки ниже не выполняются.

echo без аргументов печатает пустую строку. Это всё-таки строка: Scan() вернул true, Text() — "". Пустая строка и отсутствие ввода — два разных случая. /dev/null — системный «пустой файл»: чтение из него заканчивается сразу. С клавиатуры то же самое даёт Ctrl+D, нажатый на пустой строке.


Пробелы по краям: strings.TrimSpace

Ситуация. Человек поставил пробел перед числом, файл пришёл с табуляцией в конце строки. Глазами этого не видно, а в строке эти символы есть.

Напечатаем строку в квадратных скобках и её длину:

Код для чтения · разбираем, набирать не нужно
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	line := scanner.Text()
	fmt.Printf("[%s] %d\n", line, len(line))
	clean := strings.TrimSpace(line)
	fmt.Printf("[%s] %d\n", clean, len(clean))
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ printf '  40 \t\n' | go run .
[  40 	] 6
[40] 2

Как это читать. В строке шесть байт: два пробела, 4, 0, пробел и табуляция. strings.TrimSpace возвращает новую строку без пробелов, табуляций и переводов строк по краям; то, что внутри, не трогает: из 1 000 получится 1 000. Скобки в Printf нужны только затем, чтобы края стали видны.

Файлы, сохранённые в Windows, заканчивают строки двумя символами — \r\n. scanner сам убирает \r вместе с переводом строки:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day07/scratch$ printf '40\r\n' | go run .
[40] 2
[40] 2

Правило на весь курс: строку, прочитанную с ввода, сначала пропускайте через strings.TrimSpace, потом работайте с ней. На следующей странице будет видно, что без этого ломается.


Что может пойти не так

Что видите Что это значит Что делать
bash: input.txt: No such file or directory, программа не запустилась файла для < нет в текущей папке ls — где файл; создать его или указать путь
go run . input.txt — программа ждёт имя файла после точки программа не читает, она ждёт клавиатуру Ctrl+C, потом go run . < input.txt
после echo 3 5 вторая переменная пустая 3 5 — одна строка printf '3\n5\n'
запустили go run . > out.txt, вопроса нет, программа молчит приглашение ушло в файл вместе с остальным выводом, а программа ждёт клавиатуру набрать ввод и Enter вслепую или подать ввод через <
из out.txt пропало старое содержимое > перезаписывает файл нужный файл — под другим именем
[ 40 ] — в строке лишние пробелы пробелы пришли с вводом strings.TrimSpace
второй scanner ничего не прочитал, хотя строк две создано два scanner из os.Stdin: первый забрал ввод с запасом один scanner на всю программу

Попробуйте сейчас: пробелы по краям и пустой ввод.

Цель: увидеть, что strings.TrimSpace убирает только края, и отличить ввод, которого нет, от пустой строки.

1. Наберите в scratch/main.go программу с strings.TrimSpace:

✎ Наберите в файл scratch/main.go
package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	scanner.Scan()
	line := scanner.Text()
	fmt.Printf("[%s] %d\n", line, len(line))
	clean := strings.TrimSpace(line)
	fmt.Printf("[%s] %d\n", clean, len(clean))
}

Проверьте её на разных краях:

▶ Выполните
cd ~/gocourse/day07/scratch
printf '  40 \t\n' | go run .
printf '1 000\n' | go run .
go run . < nosuch.txt

2. Потом добавьте в неё проверку if !scanner.Scan() из программы выше и запустите с < /dev/null:

▶ Выполните
go run . < /dev/null

Готово, когда: на первый ввод программа напечатала длину 6, а потом [40] 2; у 1 000 пробел внутри остался; на nosuch.txt bash ответил No such file or directory, и программа не запустилась; с < /dev/null сработала ветка if !scanner.Scan().


Дальше: строка в число — strconv.Atoi — course next