OCaml Module 8 – Errors, input/output and command-line programs
Files, channels and processes
Read and write files with In_channel and Out_channel, handle missing files, run external programs safely without a shell, and work with paths and directories.
What you will learn
- Read and write files with In_channel and Out_channel
- Handle Sys_error for missing or unreadable files
- Run external programs without a shell and avoid command injection
- Work with paths, directories and environment variables
Before you start
What you’ll learn · Before you start
What you will learn
- Read and write files with In_channel and Out_channel
- Handle Sys_error for missing or unreadable files
- Run external programs without a shell and avoid command injection
- Work with paths, directories and environment variables
Before you start
On this page
A log file of six lines holds three INFO, one WARN and two ERROR entries. One function, In_channel.fold_lines, reads it a line at a time and counts (3, 1, 2) without ever holding the whole file in memory. That matters when the log is gigabytes and holding every line would exhaust memory, or when a second read would repeat work that a single pass could finish.
OCaml files are accessed through channels, abstract handles that the In_channel and Out_channel modules provide. A channel can read from or write to a file, a pipe or the standard streams. OCaml files opened in text mode translate line endings on some platforms, while binary mode reads and writes bytes exactly as they are. You will read and write files with channel: An abstract type that reads from or writes to a stream of bytes, such as a file, a pipe or standard input/output., handle the Sys_error that a missing file raises, run external programs safely without letting user input inject commands, and work with paths, directories and environment variables.
Channels read and write streams of bytes or text
An In_channel.t reads from a file, a pipe or standard streams: The three channels a process inherits: standard input (stdin) for reading, standard output (stdout) and standard error (stderr) for writing.; an Out_channel.t writes to them. The with_open_text and with_open_bin functions open a channel, pass it to a function and close it when that function returns, even if it raises an exception. Step through the lifecycle:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Open the file. with_open_text opens the file in text mode and passes an In_channel to f. If the file does not exist, it raises Sys_error.
- Read lines. input_lines reads every line into a list. For a large file, fold_lines processes lines one at a time without holding them all in memory.
- Your function runs. The function f receives the channel and uses the lines. If an exception escapes f, with_open_text still closes the channel.
- Automatic close. Whether f returned normally or raised, the channel is closed. No try-finally needed in your code.
- Error path: file missing. If the file does not exist, Sys_error is raised before f runs, and there is no channel to close.
- Error path: exception in f. If f raises, the channel is closed and the exception is re-raised to the caller.
The session below creates a small log file, reads it three ways and writes a summary:
"""Channels, reading and writing files with In_channel and Out_channel."""
from session import toplevel, run, files, write
# Create a small log file
write("server.log", """[INFO] Starting server on port 8080
[ERROR] Connection refused: database
[INFO] Retry attempt 1
[WARN] Slow query: 2.3 seconds
[ERROR] Timeout waiting for response
[INFO] Shutting down gracefully
""")
run('''
cat server.log
''')
# Show In_channel.with_open_text, input_all, input_lines and fold_lines in one session
toplevel('''
(* Read the whole file at once *)
let contents = In_channel.with_open_text "server.log" In_channel.input_all;;
String.length contents;;
(* Read as a list of lines *)
let lines = In_channel.with_open_text "server.log" In_channel.input_lines;;
List.length lines;;
List.hd lines;;
(* Count lines by level, one at a time *)
let count_levels path =
In_channel.with_open_text path (fun ic ->
In_channel.fold_lines (fun (info, warn, err) line ->
if String.length line >= 6 then
match String.sub line 1 4 with
| "INFO" -> (info + 1, warn, err)
| "WARN" -> (info, warn + 1, err)
| "ERRO" -> (info, warn, err + 1)
| _ -> (info, warn, err)
else (info, warn, err)
) (0, 0, 0) ic);;
count_levels "server.log";;
(* Write a summary to a file *)
Out_channel.with_open_text "summary.txt" (fun oc ->
let (info, warn, err) = count_levels "server.log" in
Printf.fprintf oc "INFO: %d\\nWARN: %d\\nERROR: %d\\n" info warn err);;
''')
run('''
cat summary.txt
''') Output
$ cat server.log
[INFO] Starting server on port 8080
[ERROR] Connection refused: database
[INFO] Retry attempt 1
[WARN] Slow query: 2.3 seconds
[ERROR] Timeout waiting for response
[INFO] Shutting down gracefully
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# (* Read the whole file at once *)
let contents = In_channel.with_open_text "server.log" In_channel.input_all;;
val contents : string =
"[INFO] Starting server on port 8080\n[ERROR] Connection refused: database\n[INFO] Retry attempt 1\n[WARN] Slow query: 2.3 seconds\n[ERROR] Timeout waiting for response\n[INFO] Shutting down gracefully\n"
#
String.length contents;;
- : int = 196
#
(* Read as a list of lines *)
let lines = In_channel.with_open_text "server.log" In_channel.input_lines;;
val lines : string list =
["[INFO] Starting server on port 8080";
"[ERROR] Connection refused: database"; "[INFO] Retry attempt 1";
"[WARN] Slow query: 2.3 seconds"; "[ERROR] Timeout waiting for response";
"[INFO] Shutting down gracefully"]
#
List.length lines;;
- : int = 6
#
List.hd lines;;
- : string = "[INFO] Starting server on port 8080"
#
(* Count lines by level, one at a time *)
let count_levels path =
In_channel.with_open_text path (fun ic ->
In_channel.fold_lines (fun (info, warn, err) line ->
if String.length line >= 6 then
match String.sub line 1 4 with
| "INFO" -> (info + 1, warn, err)
| "WARN" -> (info, warn + 1, err)
| "ERRO" -> (info, warn, err + 1)
| _ -> (info, warn, err)
else (info, warn, err)
) (0, 0, 0) ic);;
val count_levels : string -> int * int * int = <fun>
#
count_levels "server.log";;
- : int * int * int = (3, 1, 2)
#
(* Write a summary to a file *)
Out_channel.with_open_text "summary.txt" (fun oc ->
let (info, warn, err) = count_levels "server.log" in
Printf.fprintf oc "INFO: %d\nWARN: %d\nERROR: %d\n" info warn err);;
- : unit = ()
$ cat summary.txt
INFO: 3
WARN: 1
ERROR: 2
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 channels_session.py
In_channel.with_open_text path fopens the file in text mode and callsfwith the channel. Whenfreturns, the channel is closed. Text mode translates line endings on Windows; usewith_open_binfor binary files like images or archives.In_channel.input_allreturns the whole file as one string. For small configuration files or templates this is the simplest choice.In_channel.input_lines(since 5.1) returns every line as a list. The session shows six lines and 196 characters. Lines do not include the newline character.In_channel.fold_lines f init ic(since 5.1) processes lines one at a time, callingfwith the accumulator and each line. For a large file, this uses constant memory because only one line is in memory at a time. The accumulator works likeList.fold_left: the function receives the current value and returns the next.Out_channel.with_open_text path fopens a file for writing and truncates any existing content. UsePrintf.fprintf oc "..."oroutput_string oc sto write.
Key idea
Use input_all or input_lines for small files, and fold_lines for files large enough that holding every line would cost memory or time.
Checkpoint
Check: fold_lines versus input_lines · Channels and with_open functions
A missing file raises Sys-error with the path and the reason
When with_open_text cannot open a file, it raises Sys_error with a message that names the path and the reason. The program below tries to read a file that does not exist, catches the error and returns None instead of crashing:
"""Handling a missing file without crashing."""
from session import toplevel
# Show the Sys_error
toplevel('''
In_channel.with_open_text "missing.txt" In_channel.input_all;;
''')
# Show how to handle it
toplevel('''
let read_or_empty path =
match In_channel.with_open_text path In_channel.input_all with
| contents -> Some contents
| exception Sys_error msg ->
Printf.eprintf "Could not read %s: %s\\n%!" path msg;
None;;
read_or_empty "missing.txt";;
''') Output
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# In_channel.with_open_text "missing.txt" In_channel.input_all;;
Exception: Sys_error "missing.txt: No such file or directory".
$ ocaml
OCaml version 5.5.1
Enter #help;; for help.
# let read_or_empty path =
match In_channel.with_open_text path In_channel.input_all with
| contents -> Some contents
| exception Sys_error msg ->
Printf.eprintf "Could not read %s: %s\n%!" path msg;
None;;
val read_or_empty : string -> string option = <fun>
#
read_or_empty "missing.txt";;
Could not read missing.txt: missing.txt: No such file or directory
- : string option = None
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_file_session.py
The message "missing.txt: No such file or directory" comes from the operating system. On different systems the exact wording may vary, but the path is always at the start. The pattern | exception Sys_error msg catches the error inside the match, which is cleaner than wrapping the whole expression in try ... with. The function read_or_empty returns Some contents when the file exists and None with a log line when it does not, so the caller can decide what to do without the program stopping.
Other file operations raise the same Sys_error: writing to a read-only file, opening a directory as a file, or removing a file that does not exist. Catching Sys_error in one place handles all of them. The locked parts below cover running external programs safely without a shell, working with paths and directories, and walking a directory tree while skipping symbolic links.
Checkpoint
Check: Handling Sys_error for missing files
Practise reading and handling files
Practice that adapts to you
Practise this lesson
4 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Running programs without a shell: the risk and the fix Pro: opens with a Pro, Premium or Ultimate pass
Sys.command passes its argument to a shell, so a user who knows shell syntax can inject commands. This part shows the weakness on your own program with a harmless payload, fixes it with Unix.open_process_args_in, explains the OWASP recommendations for input validation, and demonstrates the output ordering problem that buffering causes.
In this part:
- Shell injection shell injection: A vulnerability where user input reaches a shell and is interpreted as commands, allowing arbitrary code execution. with Sys.command
- The fix: an argument vector argument vector: An array of strings passed to a program as its command-line arguments, without shell interpretation., no shell
- Buffering: flush before you spawn
2 runnable examples (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Paths, directories, the environment and symbolic links Pro: opens with a Pro, Premium or Ultimate pass
File operations beyond reading and writing need Filename, Sys and the unix library. This part covers path manipulation, directory listing, environment variables, temporary files with proper cleanup, and a recursive walk that skips symbolic links to avoid loops and double counting.
In this part:
- Paths with Filename
- Temporary files with cleanup
- A du-like walk that skips symbolic links
- Key takeaways from the two parts
3 runnable examples (Python) · about 4 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid
A service processes a 50 GB log file to count errors. The first version used In_channel.input_lines and ran out of memory. Explain why, and rewrite it with fold_lines so it uses constant memory.
-
Mid to senior
A tool runs Sys.command ("grep '" ^ pattern ^ "' access.log") with pattern taken from an HTTP request; the author added the quotes to keep the pattern whole. A request sends x' access.log; touch injected.txt; echo ' as the pattern. Explain what runs, and fix the code so no request can run a command.
-
Fresher to mid
A program prints "Starting backup..." then calls Sys.command "rsync ..." to run a backup. In the terminal, the rsync output appears before "Starting backup..." does. Explain why, and fix it.
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
How does an OCaml program read and write files?
Through channels, abstract handles from the In_channel and Out_channel modules, which can read from or write to a file, a pipe or the standard streams.
-
What does In_channel.with_open_text do for you?
It opens the file, passes the channel to your function and closes the channel afterwards, even if the function raises.
-
When do you read with input_all or input_lines, and when with fold_lines?
The first two for small files; fold_lines for files large enough that holding every line would cost memory or time, because it processes one line at a time in constant memory.
-
What did fold_lines count in the six-line log, and what did it never do?
(3, 1, 2): three INFO, one WARN and two ERROR lines. It never held the whole file in memory.
-
What happens when a program opens a file that does not exist?
It raises Sys_error carrying the path and the reason; catching it lets the program report the problem instead of crashing.
Key takeaways
In_channel.with_open_textopens, passes to a function and closes the channel automatically, even if the function raises.fold_linesprocesses a large file a line at a time, using constant memory;input_linesloads every line into a list.- A missing file raises
Sys_errorwith the path and the reason; catching it lets the program report the problem instead of crashing.
Terms in this lesson
- argument vector
- An array of strings passed to a program as its command-line arguments, without shell interpretation.
- channel
- An abstract type that reads from or writes to a stream of bytes, such as a file, a pipe or standard input/output.
- shell injection
- A vulnerability where user input reaches a shell and is interpreted as commands, allowing arbitrary code execution.
- standard streams
- The three channels a process inherits: standard input (stdin) for reading, standard output (stdout) and standard error (stderr) for writing.
References
- The OCaml Manual 5.5: module In_channel (Inria and the OCaml developers)
- The OCaml Manual 5.5: module Out_channel (Inria and the OCaml developers)
- The OCaml Manual 5.5: module Sys (Inria and the OCaml developers)
- The OCaml Manual 5.5: module Filename (Inria and the OCaml developers)
- The OCaml Manual 5.5: The unix library (Inria and the OCaml developers)
- OCaml.org: File manipulation (OCaml.org)
- OWASP: OS Command Injection Defense Cheat Sheet (OWASP Foundation)
- OWASP: Input Validation Cheat Sheet (OWASP Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress