diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/FormatDetector.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/FormatDetector.cs index a5c224a..abaee80 100644 --- a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/FormatDetector.cs +++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/FormatDetector.cs @@ -31,6 +31,7 @@ public class FormatDetector new MakefileDetector(), new HostsDetector(), new DockerfileDetector(), + new ShellScriptDetector(), new KrcDetector(), ]; diff --git a/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/ShellScriptDetector.cs b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/ShellScriptDetector.cs new file mode 100644 index 0000000..e15f43d --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.TextViewer/Detectors/ShellScriptDetector.cs @@ -0,0 +1,85 @@ +// Copyright © 2017-2026 QL-Win Contributors +// +// This file is part of QuickLook program. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +using System; +using System.IO; + +namespace QuickLook.Plugin.TextViewer.Detectors; + +/// +/// Detect whether a text file without suffix is ​​a shell script file +/// +public sealed class ShellScriptDetector : IFormatDetector +{ + public string Name => "Shell Script"; + + public string Extension => ".sh"; + + public bool Detect(string path, string text) + { + if (string.IsNullOrEmpty(text)) return false; + + // Only handle files without extension + if (Path.GetExtension(path) != string.Empty) + return false; + + ReadOnlySpan span = text.AsSpan(); + + int i = 0; + + // Skip UTF-8 BOM (\uFEFF) if present + if (span.Length > 0 && span[0] == '\uFEFF') + i = 1; + + // Must start with shebang "#!" + if (span.Length < i + 2 || span[i] != '#' || span[i + 1] != '!') + return false; + + i += 2; + + // Skip whitespace after shebang + while (i < span.Length && (span[i] == ' ' || span[i] == '\t')) + i++; + + // Read the first line only + int start = i; + while (i < span.Length && span[i] != '\n' && span[i] != '\r') + i++; + + var line = span.Slice(start, i - start).Trim(); + + // Case 1: direct interpreter path (e.g. /bin/bash, /bin/sh) + if (line.Length >= 2 && + line[line.Length - 2] == 's' && + line[line.Length - 1] == 'h') + return true; + + // Case 2: env style (e.g. /usr/bin/env bash) + int lastSpace = line.LastIndexOf(' '); + if (lastSpace >= 0) + { + var lastToken = line.Slice(lastSpace + 1); + + if (lastToken.Length >= 2 && + lastToken[lastToken.Length - 2] == 's' && + lastToken[lastToken.Length - 1] == 'h') + return true; + } + + return false; + } +}