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