I/O Operations in Crystal
Learn console output, reading from standard input, file reading and writing, formatted output, and I/O error handling in Crystal with Docker-ready examples
Input and output are how a program talks to the outside world - the terminal, the keyboard, and the filesystem. Crystal inherits Ruby’s friendly I/O vocabulary (puts, print, gets) but layers its static type system and nil safety on top. The result is I/O code that reads like a script yet compiles to a native binary.
Crystal models everything as an IO object. STDOUT, STDERR, STDIN, and open files all share the same IO interface, so the methods you learn here work uniformly across every stream. Because Crystal is nil-safe, reading input returns a String? (a String or Nil), and the compiler forces you to handle the “no more input” case - a class of bug that silently slips through in many dynamic languages.
In this tutorial you will move beyond the single puts from Hello World: writing to standard output and standard error, reading typed input from the keyboard, reading and writing files, formatting output with printf, and handling I/O errors gracefully.
Console Output
Crystal offers several ways to write to the terminal, each suited to a different purpose. puts appends a newline, print does not, and p/pp show the inspected form of a value - invaluable for debugging.
Create a file named console_output.cr:
| |
Use puts/print when you want human-readable text, and p/pp when you want to see exactly what a value contains, including its type structure.
Reading Input from the Keyboard
gets reads one line from standard input. Because the stream can end at any time, gets returns String? - the compiler will not let you use the result until you have handled the possible nil.
Create a file named read_input.cr:
| |
The if name check narrows the type from String? to String inside the block, so calling .chomp is safe. This compile-time narrowing is one of Crystal’s signature features.
Writing Files
File.write is the quickest way to create a file. For more control - such as appending - open the file with a block, which guarantees the file is closed automatically even if an error occurs.
Create a file named file_write.cr:
| |
After running this, greeting.txt contains three lines. The block form of File.open is idiomatic: you never have to remember to call close.
Reading Files
Crystal gives you several reading strategies: slurp the whole file into a String, stream it line by line, or collect every line into an array.
Create a file named file_read.cr:
| |
File.each_line is memory-efficient for large files because it never holds the entire contents in memory at once, while File.read and File.read_lines are convenient when the file is small.
Formatted Output and I/O Errors
For aligned columns, fixed decimal places, or number bases, use printf (prints directly) or sprintf (returns a string). File operations can fail - a missing file raises File::NotFoundError, which you catch with begin/rescue.
Create a file named formatted_output.cr:
| |
The specifiers mirror C’s printf: %-10s left-justifies a string in a 10-character field, %3d right-justifies an integer in a 3-character field, %.4f fixes four decimals, and %% prints a literal percent sign.
Running with Docker
Run the examples with the official Crystal image - no local install required.
| |
The read_input.cr example needs data on standard input. Add -i and pipe the answers in:
| |
Expected Output
Running console_output.cr:
puts appends a newline automatically
print does not append a newline - so this continues the same line
Language: Crystal, version: 1.14
"a string with \"quotes\""
[1, 2, 3]
{"a" => 1, "b" => 2}
Running read_input.cr with the piped input above (prompts and answers share a line because piped input is not echoed):
What is your name? Hello, Ada!
Enter your age: Next year you will be 31.
Running file_write.cr:
Finished writing greeting.txt
Running file_read.cr:
Whole file:
line 1
line 2
line 3
Line by line:
-> line 1
-> line 2
-> line 3
The file has 3 lines.
Running formatted_output.cr:
Name: Ada Age: 30
Pi is approximately 3.1416
Hex: ff, Octal: 377, Binary: 11111111
Item #007
Progress: 75%
Could not read file: it does not exist.
Key Concepts
- Everything is an
IO-STDOUT,STDERR,STDIN, and open files all share the same interface, so methods likeputsandgetswork uniformly across streams. getsreturnsString?- Crystal’s nil safety forces you to handle the end-of-input case, and anifcheck narrows the type fromString?toStringfor safe use.putsavoids double newlines - when the string already ends in a newline,putsdoes not add a second one, which is why reading and re-printing a file preserves its exact line breaks.- Block form of
File.openauto-closes - passing a block guarantees the file handle is released even if an exception is raised mid-write. - Choose your read strategy -
File.readfor small files,File.each_linefor streaming large files without loading them fully into memory, andFile.read_linesfor an array of lines. printf/sprintffor formatting - C-style specifiers handle alignment, decimal precision, and number bases; the%operator on aStringis a compact shorthand forsprintf.- I/O errors are exceptions - missing files raise
File::NotFoundError, caught withbegin/rescue, keeping error handling explicit and typed.
Running Today
All examples can be run using Docker:
docker pull crystallang/crystal:1.14.0
Comments
Loading comments...
Leave a Comment