Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Intermediate
  • 10 minutes
  • Examples run with ocaml 5.5.1, opam 2.6.0, dune 3.24.2 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 4 of 10 minutes free
Free: Channels read and write streams of bytes or text · A missing file raises Sys-error with the path and the reason · Practise reading and handling files. With a pass: 2 in-depth parts and 3 interview answers, about 6 minutes.

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:

Interactive · stage by stage A channel's lifecycle from open to close Step through opening a file, reading from it and closing it safely
A channel's lifecycle from open to close, step 6 of 6. Showing Open, Read, Process, Close. In focus: exception.Sys_errorany exceptionOpenwith_open_text path fReadinput_lines or fold_linesProcessf receives the channelCloseautomatic when f returns

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. Automatic close. Whether f returned normally or raised, the channel is closed. No try-finally needed in your code.
  5. Error path: file missing. If the file does not exist, Sys_error is raised before f runs, and there is no channel to close.
  6. 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:

Reading a log file and writing a summary Python · channels_session.py
"""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 f opens the file in text mode and calls f with the channel. When f returns, the channel is closed. Text mode translates line endings on Windows; use with_open_bin for binary files like images or archives.
  • In_channel.input_all returns 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, calling f with 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 like List.fold_left: the function receives the current value and returns the next.
  • Out_channel.with_open_text path f opens a file for writing and truncates any existing content. Use Printf.fprintf oc "..." or output_string oc s to 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

  1. Question 1 of 2 A program reads a 10 GB log file to count error lines. Which function uses constant memory?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: In_channel.fold_lines

    fold_lines calls the accumulator function on each line as it is read, so only one line is in memory at a time. input_lines and input_all build a list or string that holds everything.

  2. Question 2 of 2 When does with_open_text close the channel?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Whether the function returns or raises

    with_open_text uses Fun.protect internally, so the channel is closed whether the function returns a value or raises an exception.

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 Python · missing_file_session.py
"""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

  1. One question What does Sys_error carry when a file does not exist?

    Choose one answer.

    Show the answer: One question

    Answer: The path and the reason as a string

    Opening "missing.txt" raises Sys_error "missing.txt: No such file or directory". The message names the path and the reason, so a catch can log or report it.

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.

  1. Question 1 of 4 Sys.readdir returns an array of names. What is their order?

    Choose one answer.

    Show the answer: Question 1 of 4

    Answer: Unspecified by the manual

    Sys.readdir's order is unspecified. If your program needs a predictable order, sort the array with Array.sort or pipe through List.sort.

  2. Question 2 of 4 How does Sys.getenv_opt differ from Sys.getenv?

    Choose one answer.

    Show the answer: Question 2 of 4

    Answer: getenv_opt returns None when the variable is missing; getenv raises Not_found

    Sys.getenv_opt HOME returns Some value or None. Sys.getenv HOME returns the value or raises Not_found. Prefer getenv_opt to avoid an unexpected exception.

  3. Question 3 of 4 When should you use with_open_bin instead of with_open_text?

    Choose one answer.

    Show the answer: Question 3 of 4

    Answer: When the file contains binary data like images or compressed archives

    with_open_bin opens in binary mode, which reads and writes bytes exactly as they are. Use it for images, archives or any file that is not plain text.

  4. Question 4 of 4 fold_lines (fun acc line -> ...) init ic calls the function with which arguments on the first line?

    Choose one answer.

    Show the answer: Question 4 of 4

    Answer: init and the first line

    fold_lines calls f init first_line, then f (result of that) second_line, and so on. The accumulator comes first, as with List.fold_left.

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.

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).

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.

  • 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…

  • 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.

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

  1. 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.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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.

    Model answer, Pro. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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_text opens, passes to a function and closes the channel automatically, even if the function raises.
  • fold_lines processes a large file a line at a time, using constant memory; input_lines loads every line into a list.
  • A missing file raises Sys_error with 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

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.