file.go

 1// Copyright (c) 2016, Daniel MartΓ­ <mvdan@mvdan.cc>
 2// See LICENSE for licensing information
 3
 4// Package fileutil allows inspecting shell files, such as detecting whether a
 5// file may be shell or extracting its shebang.
 6package fileutil
 7
 8import (
 9	"io/fs"
10	"os"
11	"regexp"
12	"strings"
13)
14
15var (
16	shebangRe = regexp.MustCompile(`^#![ \t]*/(usr/)?bin/(env[ \t]+)?(sh|bash|mksh|bats|zsh)(\s|$)`)
17	extRe     = regexp.MustCompile(`\.(sh|bash|mksh|bats|zsh)$`)
18)
19
20// TODO: consider removing HasShebang in favor of Shebang in v4
21
22// HasShebang reports whether bs begins with a valid shell shebang.
23// It supports variations with /usr and env.
24func HasShebang(bs []byte) bool {
25	return Shebang(bs) != ""
26}
27
28// Shebang parses a "#!" sequence from the beginning of the input bytes,
29// and returns the shell that it points to.
30//
31// For instance, it returns "sh" for "#!/bin/sh",
32// and "bash" for "#!/usr/bin/env bash".
33func Shebang(bs []byte) string {
34	m := shebangRe.FindSubmatch(bs)
35	if m == nil {
36		return ""
37	}
38	return string(m[3])
39}
40
41// ScriptConfidence defines how likely a file is to be a shell script,
42// from complete certainty that it is not one to complete certainty that
43// it is one.
44type ScriptConfidence int
45
46const (
47	// ConfNotScript describes files which are definitely not shell scripts,
48	// such as non-regular files or files with a non-shell extension.
49	ConfNotScript ScriptConfidence = iota
50
51	// ConfIfShebang describes files which might be shell scripts, depending
52	// on the shebang line in the file's contents. Since [CouldBeScript] only
53	// works on [fs.FileInfo], the answer in this case can't be final.
54	ConfIfShebang
55
56	// ConfIsScript describes files which are definitely shell scripts,
57	// which are regular files with a valid shell extension.
58	ConfIsScript
59)
60
61// CouldBeScript is a shortcut for CouldBeScript2(fs.FileInfoToDirEntry(info)).
62//
63// Deprecated: prefer [CouldBeScript2], which usually requires fewer syscalls.
64func CouldBeScript(info fs.FileInfo) ScriptConfidence {
65	return CouldBeScript2(fs.FileInfoToDirEntry(info))
66}
67
68// CouldBeScript2 reports how likely a directory entry is to be a shell script.
69// It discards directories, symlinks, hidden files and files with non-shell
70// extensions.
71func CouldBeScript2(entry fs.DirEntry) ScriptConfidence {
72	name := entry.Name()
73	switch {
74	case entry.IsDir(), name[0] == '.':
75		return ConfNotScript
76	case entry.Type()&os.ModeSymlink != 0:
77		return ConfNotScript
78	case extRe.MatchString(name):
79		return ConfIsScript
80	case strings.IndexByte(name, '.') > 0:
81		return ConfNotScript // different extension
82	default:
83		return ConfIfShebang
84	}
85}