Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ Options:

[default: md,mkd,mdx,mdown,mdwn,mkdn,mkdown,markdown,html,htm,txt]

--default-extension <EXTENSION>
Default file extension to treat files without extensions as having.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really confused by this sentence 😅

Suggested change
Default file extension to treat files without extensions as having.
This is the default file extension that is applied to files without an extension.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably just me editing the wording over and over again until I broke the grammar.

Copy link
Member

@thomas-zahner thomas-zahner Oct 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mre I noticed now that you merged your version. Did you forget about changing it? I still think this original version is quite confusing 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that was by mistake. I thought I updated the text but apparently not.


This is useful for files without extensions or with unknown extensions. The extension will be used to determine the file type for processing. Examples: --default-extension md, --default-extension html

--cache
Use request cache stored on disk at `.lycheecache`

Expand Down
19 changes: 18 additions & 1 deletion lychee-bin/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,16 @@ impl LycheeOptions {
all_inputs.extend(files_from.inputs);
}

// Convert default extension to FileType if provided
let default_file_type = self
.config
.default_extension
.as_deref()
.and_then(FileType::from_extension);

all_inputs
.iter()
.map(|raw_input| Input::new(raw_input, None, self.config.glob_ignore_case))
.map(|raw_input| Input::new(raw_input, default_file_type, self.config.glob_ignore_case))
.collect::<Result<_, _>>()
.context("Cannot parse inputs from arguments")
}
Expand Down Expand Up @@ -428,6 +435,15 @@ specify both extensions explicitly."
#[serde(default = "FileExtensions::default")]
pub(crate) extensions: FileExtensions,

/// Default file extension to treat files without extensions as having.
///
/// This is useful for files without extensions or with unknown extensions.
/// The extension will be used to determine the file type for processing.
/// Examples: --default-extension md, --default-extension html
#[arg(long, value_name = "EXTENSION")]
#[serde(default)]
pub(crate) default_extension: Option<String>,

#[arg(help = HELP_MSG_CACHE)]
#[arg(long)]
#[serde(default)]
Expand Down Expand Up @@ -886,6 +902,7 @@ impl Config {
cache: false,
cache_exclude_status: StatusCodeExcluder::default(),
cookie_jar: None,
default_extension: None,
dump: false,
dump_inputs: false,
exclude: Vec::<String>::new(),
Expand Down
61 changes: 61 additions & 0 deletions lychee-bin/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2880,4 +2880,65 @@ mod cli {

Ok(())
}

/// Test the --default-extension option for files without extensions
#[test]
fn test_default_extension_option() -> Result<()> {
let mut file_without_ext = NamedTempFile::new()?;
// Create markdown content but with no file extension
writeln!(file_without_ext, "# Test File")?;
writeln!(file_without_ext, "[Example](https://example.com)")?;
writeln!(file_without_ext, "[Local](local.md)")?;

// Test with --default-extension md
main_command()
.arg("--default-extension")
.arg("md")
.arg("--dump")
.arg(file_without_ext.path())
.assert()
.success()
.stdout(contains("https://example.com"));

let mut html_file_without_ext = NamedTempFile::new()?;
// Create HTML content but with no file extension
writeln!(html_file_without_ext, "<html><body>")?;
writeln!(
html_file_without_ext,
"<a href=\"https://html-example.com\">HTML Link</a>"
)?;
writeln!(html_file_without_ext, "</body></html>")?;

// Test with --default-extension html
main_command()
.arg("--default-extension")
.arg("html")
.arg("--dump")
.arg(html_file_without_ext.path())
.assert()
.success()
.stdout(contains("https://html-example.com"));

Ok(())
}

/// Test that unknown --default-extension values are handled gracefully
#[test]
fn test_default_extension_unknown_value() {
let mut file_without_ext = NamedTempFile::new().unwrap();
// Create file content with a link that should be extracted as plaintext
writeln!(file_without_ext, "# Test").unwrap();
writeln!(file_without_ext, "Visit https://example.org for more info").unwrap();

// Unknown extensions should fall back to default behavior (plaintext)
// and still extract links from the content
main_command()
.arg("--default-extension")
.arg("unknown")
.arg("--dump")
.arg(file_without_ext.path())
.assert()
.success()
.stdout(contains("https://example.org")); // Should extract the link as plaintext
}
}
2 changes: 1 addition & 1 deletion lychee-lib/src/retry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl RetryExt for http::Error {
let inner = self.get_ref();
inner
.source()
.and_then(<(dyn std::error::Error + 'static)>::downcast_ref)
.and_then(<dyn std::error::Error + 'static>::downcast_ref)
.is_some_and(should_retry_io)
}
}
Expand Down
25 changes: 23 additions & 2 deletions lychee-lib/src/types/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,9 @@ impl FileType {
}

/// Get the [`FileType`] from an extension string
fn from_extension(ext: &str) -> Option<Self> {
let ext = ext.to_lowercase();
#[must_use]
pub fn from_extension(extension: &str) -> Option<Self> {
let ext = extension.to_lowercase();
if Self::MARKDOWN_EXTENSIONS.contains(&ext.as_str()) {
Some(Self::Markdown)
} else if Self::HTML_EXTENSIONS.contains(&ext.as_str()) {
Expand Down Expand Up @@ -281,4 +282,24 @@ mod tests {
assert!(!is_url(Path::new("file:///foo/bar.txt")));
assert!(!is_url(Path::new("ftp://foo.com")));
}

#[test]
fn test_from_extension() {
// Valid extensions
assert_eq!(FileType::from_extension("html"), Some(FileType::Html));
assert_eq!(FileType::from_extension("HTML"), Some(FileType::Html));
assert_eq!(FileType::from_extension("htm"), Some(FileType::Html));
assert_eq!(
FileType::from_extension("markdown"),
Some(FileType::Markdown)
);
assert_eq!(FileType::from_extension("md"), Some(FileType::Markdown));
assert_eq!(FileType::from_extension("MD"), Some(FileType::Markdown));
assert_eq!(FileType::from_extension("txt"), Some(FileType::Plaintext));
assert_eq!(FileType::from_extension("TXT"), Some(FileType::Plaintext));

// Unknown extension
assert_eq!(FileType::from_extension("unknown"), None);
assert_eq!(FileType::from_extension("xyz"), None);
}
}
Loading