can1357/oh-my-pi · error · CatError

Too many levels of symbolic links

Error message

Too many levels of symbolic links

What it means

This io::Error (ELOOP, 'Too many levels of symbolic links') is surfaced through CatError::TooManySymlinks in the cat builtin. The OS throws it when resolving a path requires following more symlinks than the kernel allows (typically 40), which almost always means a symlink loop. The builtin wraps OS open/read failures so callers get a typed, displayable error per file.

Source

Thrown at crates/pi-builtins/src/cat.rs:83

}

#[derive(Error, Debug)]
enum CatError {
	/// Wrapper around `io::Error`.
	#[error("{}", strip_errno(.0))]
	Io(io::Error),
	/// The downstream reader closed its pipe; this ends the copy quietly.
	#[error("broken pipe")]
	BrokenPipe,
	/// Unknown file type; it is not a regular file, socket, or known device.
	#[error("unknown filetype: {ft_debug}")]
	UnknownFiletype { ft_debug: String },
	#[error("Is a directory")]
	IsDirectory,
	#[cfg(unix)]
	#[error("No such device or address")]
	NoSuchDeviceOrAddress,
	#[error("Too many levels of symbolic links")]
	TooManySymlinks,
}

impl From<io::Error> for CatError {
	fn from(error: io::Error) -> Self {
		if error.kind() == ErrorKind::BrokenPipe {
			Self::BrokenPipe
		} else {
			Self::Io(error)
		}
	}
}

fn strip_errno(error: &io::Error) -> String {
	let mut message = error.to_string();
	if let Some(position) = message.find(" (os error ") {
		message.truncate(position);
	}

View on GitHub (pinned to 9690622007)

Solutions

  1. Inspect the path with `ls -la` or `readlink -f <path>` to find the symlink cycle and remove/repoint the offending link
  2. Use `find -L <dir> -type l` to detect loops under a directory tree before batching cat over it
  3. If the loop is intentional (rare), read the target file directly rather than through the symlink
  4. Handle CatError::TooManySymlinks per-file so one bad link does not abort concatenating the remaining files

Example fix

// before
ln -s config config  # self-referential symlink
cat config            # ELOOP
// after
rm config
ln -s real-config config
cat config
Defensive patterns

Strategy: try-catch

Validate before calling

import std::fs;import std::os::unix::fs::MetadataExt;fn has_symlink_loop(path: &str) -> bool { fs::metadata(path).is_err() && fs::symlink_metadata(path).is_ok() }

Type guard

fn is_eloop(err: &CatError) -> bool { matches!(err, CatError::TooManySymlinks) || err.to_string().contains("Too many levels of symbolic links") }

Try / catch

match cat_file(path) { Err(CatError::TooManySymlinks) => eprintln!("skipping {}: symlink loop", path), Err(e) => return Err(e), Ok(_) => {} }

Prevention

When it happens

Trigger: Running the cat builtin on a path whose resolution traverses a symlink cycle, e.g. `ln -s loop loop; cat loop`, or a chain of symlinks pointing back at an ancestor (a -> b -> a).

Common situations: Broken symlink setups in dotfiles repos, build systems or package symlinks that accidentally self-reference, restoring from backups with `cp -a` misconfigurations, or symlinking a directory into itself.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/3e6ef3e3898b75e6. Report an issue: GitHub.