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;
+ }
+}