Low-Level File I/O and the File System

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Written against MATLAB R2026b. The sessions below were run in GNU Octave 11.3.0 (MATLAB code that Octave also accepts) and show Octave’s output; MATLAB’s display layout differs a little. Functions that Octave 11.3.0 does not have (readlines, writelines) are checked against the R2026b documentation and labelled "not run here".

Most files should be read with the format-specific functions on Reading and Writing Tabular Data (readtable, readmatrix, …​), and MAT-files with load and save (see Workspace Variables and MAT-Files). The documentation itself describes low-level reading as a workflow that is not commonly used: it gives the most control, but you must know the exact structure of the data. This page covers the cases where you do need that control (unusual text formats, binary files, log files written line by line) and the functions that manage files and folders around them.

The open, use, close cycle

Low-level I/O works with a file identifier: an integer returned by fopen that you pass to every other function. The cycle is always the same.

  1. fid = fopen(filename, permission) opens the file. If it cannot, fid is -1 (no error is thrown), so test it.

  2. Read or write with fid.

  3. fclose(fid) closes it.

Identifiers 0, 1 and 2 are reserved for standard input, standard output and standard error; a successfully opened file gets an identifier of 3 or more. fprintf(1, ...) therefore prints to the screen, and fprintf(2, ...) to standard error.

Permissions

The default permission is read-only, in binary mode. The documented values are:

Permission Meaning

r

Open for reading (default).

w

Open or create for writing, discarding existing contents.

a

Open or create for writing, appending to the end.

r+

Open for reading and writing.

w+

Open or create for reading and writing, discarding existing contents.

a+

Open or create for reading and writing, appending.

A, W

Append or write without automatic flushing of the output buffer.

Attach a t (rt, wt+) to open in text mode. On UNIX-like systems both modes behave the same; on Windows, text mode removes the carriage return before a newline when reading and inserts one when writing. The documentation notes that in most cases text mode is not needed, because MATLAB’s import functions and most editors recognise a bare newline, and that binary mode is better for performance. When you open a file for reading and writing (a permission with +), call fseek or frewind between a read and a following write.

Byte order and encoding

The third argument of fopen is the machine format (n native, the default; b or ieee-be big-endian; l or ieee-le little-endian). The fourth is the character encoding. If you open a file for reading without naming an encoding, fopen detects it automatically; for writing, it defaults to UTF-8. Name the encoding explicitly whenever the file comes from an old system, for example fopen(name, "w", "n", "Shift_JIS") in the documentation. A name that is not on the supported list produces a warning.

[fid, errmsg] = fopen(...) also returns a system-dependent message when the open fails, which is better than a bare -1 in an error message. fopen(fid) returns the name behind an identifier, openedFiles lists the identifiers still open, and since R2024b fopen accepts http:// and https:// URLs for reading, but not URLs that require authentication.

Writing and reading text

fprintf(fid, formatSpec, A1, ..., An) writes formatted text; with no fid it writes to the screen. The format is reused for all the elements of the arguments, column by column, which is why a loop over rows is the clearest way to write a table. fgetl(fid) returns the next line without its newline, and fgets(fid) keeps the newline. Both return the number -1 at the end of the file, so the test ischar(tline) ends a loop. The example writes the sensors.csv of the heatflow scenario (columns Time, SensorID, Temperature, as in Tables) and reads it back line by line.

fname = fullfile("io_tmp", "sensors.csv");
fid = fopen(fname, "w");
if fid < 0
    error("Cannot open %s for writing", fname);
end
fprintf(fid, "Time,SensorID,Temperature\n");
rows = {"2026-01-01 00:00:00","S1",21.5; "2026-01-01 00:10:00","S1",21.9; "2026-01-01 00:00:00","S2",19.8};
for k = 1:size(rows, 1)
    fprintf(fid, "%s,%s,%.1f\n", rows{k, :});
end
fclose(fid);

fid = fopen(fname, "r");
header = fgetl(fid)
while true
    tline = fgetl(fid);
    if ~ischar(tline)
        break
    end
    parts = strsplit(tline, ",");
    fprintf("%s | %s | %5.1f\n", parts{1}, parts{2}, str2double(parts{3}));
end
fclose(fid);
[fid, msg] = fopen(fullfile("io_tmp", "missing.csv"))

Octave 11.3.0 output (run in a folder io_tmp created beforehand):

header = Time,SensorID,Temperature
2026-01-01 00:00:00 | S1 |  21.5
2026-01-01 00:10:00 | S1 |  21.9
2026-01-01 00:00:00 | S2 |  19.8
fid = -1
msg = No such file or directory

MATLAB shows header = 'Time,SensorID,Temperature' with quotes, and the error message text of the failed open is system-dependent. Opening a file in the current folder for reading also searches the MATLAB path, while opening for writing or appending creates the file in the current folder when it is not found; that is another reason to build full paths (see Paths and the file system).

Scanning with fscanf and textscan

A = fscanf(fid, formatSpec, sizeA) reads numbers into an array, filling it in column order; sizeA is a positive integer or [m n], with Inf allowed for the last dimension. [A, count] = fscanf(...) also returns how many values were read. If the data stops matching the format, fscanf reads only the part that matches and stops.

C = textscan(fid, formatSpec) is the tool for a mix of text and numbers: it returns a cell array with one cell per conversion specifier and reapplies the format to the whole file. Useful name-value arguments from the reference include Delimiter, HeaderLines, CollectOutput, CommentStyle, TreatAsEmpty and MultipleDelimsAsOne. It also reads from a character vector instead of a file. When you finish with a file, close it.

d = "io_tmp";
fid = fopen(fullfile(d, "sensors.csv"), "r");
C = textscan(fid, "%s %s %f", "Delimiter", ",", "HeaderLines", 1);
fclose(fid);
C{1}, C{2}, C{3}

fid = fopen(fullfile(d, "nums.txt"), "w"); fprintf(fid, "%d %d %d\n", magic(3)); fclose(fid);
fid = fopen(fullfile(d, "nums.txt"), "r");
[A, count] = fscanf(fid, "%d", [3 Inf])
fclose(fid);

Octave 11.3.0 output (shortened):

ans =
{
  [1,1] = 2026-01-01 00:00:00
  [2,1] = 2026-01-01 00:10:00
  [3,1] = 2026-01-01 00:00:00
}
ans =
{
  [1,1] = S1
  [2,1] = S1
  [3,1] = S2
}
ans =
   21.500
   21.900
   19.800
A =
   8   1   6
   3   5   7
   4   9   2
count = 9

The second half shows the column-order rule in both directions: fprintf consumed magic(3) column by column, writing one column per line, and fscanf with [3 Inf] filled a 3-by-3 array column by column, so the original matrix comes back. In MATLAB, textscan returns text columns as a cell array of character vectors by default; the TextType option can return strings instead.

Binary files with fread and fwrite

n = fwrite(fid, A, precision) writes the elements of A as the given type (for example double, int16, uint8) and returns the number written. A = fread(fid, sizeA, precision, skip, machinefmt) reads them back into a column vector or an array filled in column order. A precision of the form uint8=>uint8 keeps the output class equal to the stored class; without the => part the result is a double. Use skip to jump over bytes after each value and the machine format (ieee-le or ieee-be) when the file came from another architecture.

v = [1.5 2.5 3.5];
fid = fopen(fullfile(d, "v.bin"), "w", "ieee-le"); n = fwrite(fid, v, "double"); fclose(fid);
info = dir(fullfile(d, "v.bin")); disp(info.bytes)
fid = fopen(fullfile(d, "v.bin"), "r", "ieee-le"); w = fread(fid, [1 Inf], "double"); fclose(fid);
n, w

Octave 11.3.0 output:

24
n = 3
w =
   1.5000   2.5000   3.5000

Three doubles occupy 24 bytes. Reading the first eight of those bytes as uint8 in the same session gave 0 0 0 0 0 0 248 63, the little-endian IEEE 754 representation of 1.5. For very large binary files, the documentation points to memory-mapping, which lets you index file data like an array.

Whole-file text: readlines and writelines

Since R2022a, writelines(lines, filename) writes a string array, character vector or cell array of character vectors to a plain-text file, one element per line, and overwrites an existing file. Name-value arguments include WriteMode="append", LineEnding, Encoding and TrailingLineEndingRule. Its counterpart S = readlines(filename) returns an N-by-1 string array with one string per line, and accepts options such as EmptyLineRule="skip", Whitespace, LineEnding and Encoding. Both accept S3 and Azure locations given as HTTPS URLs, and readlines also reads compressed and archived files.

writelines(["Time,SensorID,Temperature"; "2026-01-01 00:00:00,S1,21.5"], "log.csv");
writelines("2026-01-01 00:10:00,S1,21.9", "log.csv", WriteMode="append");
lines = readlines("log.csv", EmptyLineRule="skip")
numLines = numel(lines)

Not run here: readlines and writelines are not available in Octave 11.3.0. The file then holds the header and two data lines, so numLines should be 3 (illustrative; the exact count depends on the trailing line ending, which TrailingLineEndingRule controls). For small text files this pair replaces the fopen, fgetl loop, fclose pattern entirely: there is no identifier to leak.

Closing files safely with onCleanup

If an error occurs between fopen and fclose, the file stays open. onCleanup creates an object that runs a function when it is destroyed. A local variable is destroyed when its function ends, whether normally, by an error or by Ctrl+C, so assigning the object to a local variable guarantees the cleanup.

function readHeader(fname)
    fid = fopen(fname, "r");
    if fid < 0
        error("heatflow:io:cannotOpen", "Cannot open %s", fname);
    end
    cleaner = onCleanup(@() fclose(fid));
    header = fgetl(fid);
    disp(header)
end

The object must be kept in a variable. The reference page states that if you pass the cleanup object outside the function, the cleanup does not run when the function ends, but when MATLAB destroys the object. In Octave 11.3.0 a function that opened a file, created such an object and then threw error("demo:fail", ...) was caught by the caller with the identifier demo:fail; that run checked only the error propagation, and the cleanup itself is as documented for MATLAB. For the rest of error handling see Errors, Warnings and Exceptions.

Paths and the file system

Building and splitting names

fullfile(part1, ..., partN) joins parts with the platform’s separator and does not add a trailing separator. It collapses repeated inner separators and . folders, but not ... On Windows it also turns forward slashes into backslashes, so code written with fullfile runs on every platform. [folder, name, ext] = fileparts(path) splits a path into the three parts. Never build paths with [folder "/" name] strings.

d = "io_tmp";
p = fullfile(d, "runs", "run01", "out.txt")
[folder, name, ext] = fileparts(p)
fullfile("a", "b", "..", "c.m")

Octave 11.3.0 output (on a UNIX-like system):

p = io_tmp/runs/run01/out.txt
folder = io_tmp/runs/run01
name = out
ext = .txt
ans = a/b/../c.m

Testing, listing, creating, copying, moving and deleting

  • isfile(name) and isfolder(name) return true when the name is an existing file or folder. They are clearer than exist with its numeric codes.

  • dir name lists a folder; dir(name) returns an n-by-1 structure array with fields name, folder, date, bytes, isdir and datenum. Wildcards * and ** are allowed (dir ** includes subfolders), and SortOrder="natural" sorts numbered names such as run2 before run10. The listing always includes the entries . and ...

  • mkdir parent child creates a folder, including a missing parent. With an output (status = mkdir(...)) it returns 1 if the folder was created or already exists and does not throw; without an output, an existing folder gives a warning.

  • copyfile source destination and movefile source destination copy or move files and folders. If source and destination are in the same place, movefile renames. Both accept f to overwrite a read-only destination and can return [status, msg, msgID].

  • delete filename removes a file without asking. By default it is permanent; the recycle behaviour is a MATLAB setting, or call recycle. rmdir folder removes an empty folder, and rmdir folder s also removes its contents.

  • tempname returns a unique path for a temporary file in the system’s temporary folder (tempdir), without creating the file; tempname(folder) uses another folder.

mkdir(fullfile(d, "runs", "run01"));
fid = fopen(p, "w"); fprintf(fid, "hello\n"); fclose(fid);
copyfile(p, fullfile(d, "runs", "copy.txt"));
[ok, msg] = movefile(fullfile(d, "runs", "copy.txt"), fullfile(d, "runs", "renamed.txt"));
ok
isfile(p), isfolder(fullfile(d, "runs")), isfolder(p)
L = dir(fullfile(d, "runs"));
disp({L.name})
delete(fullfile(d, "runs", "renamed.txt"));
[ok, msg] = rmdir(fullfile(d, "runs"), "s")

Octave 11.3.0 output (shortened; ordering of dir entries follows the operating system):

ok = 1
ans = 1
ans = 1
ans = 0
{
  [1,1] = .
  [1,2] = ..
  [1,3] = renamed.txt
  [1,4] = run01
}
ok = 1
msg =

The deletion at the end is permanent: rmdir with s removes everything below the folder. Always build the path from a variable you control, never from user input.

Choosing the right tool

Task Function

A table with headers, many columns

readtable (see Reading and Writing Tabular Data)

Numbers only, delimited

readmatrix

Small text file, line by line

readlines and writelines

Irregular text with a known pattern

textscan or fscanf

A log written as events happen

fopen with a, then fprintf

Binary data with a known layout

fread and fwrite

Workspace variables

save and load

JSON, XML or web content

See JSON, XML and Web Access

Pitfalls

  • Forgetting fclose. Open files accumulate and, on Windows, stay locked. Close in the same function that opened, or use onCleanup. fclose all closes every open file, including those of other code, so prefer closing the identifier you own.

  • Not checking for -1. A failed fopen does not stop the program; the following fgetl(-1) fails with a confusing message.

  • Looping until feof. Testing feof before reading can run one extra iteration for a file that ends with a newline; testing ischar on the result of fgetl is more robust.

  • Mixing read and write on one file without fseek or frewind between them.

  • Assuming the encoding or the byte order. Name them when the file comes from another system.

  • Building paths with string concatenation (separators differ by platform). Use fullfile.

  • Hard-coded absolute paths. They break on every other machine; use paths relative to the project or tempname for scratch files.

Where older books differ

Books for MATLAB 7 use fopen, fgetl and fscanf loops for all text input, because readtable, readmatrix, readlines, writelines, isfile and isfolder did not exist (they arrived in later releases; writelines is documented as since R2022a, and the release notes give the others). Older code often tests files with exist(name, "file"), which also finds a file on the search path and returns a numeric code, and passes "all" to fopen; the R2025a version history says fopen no longer supports the all option. Some older texts concatenate paths with [dir "\" file], which is not portable, and assume the system’s default character encoding where today’s fopen detects the encoding when reading and writes UTF-8 by default. fprintf, fscanf, fread and fwrite themselves have been stable for decades.