Skip to content
Draft
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
66 changes: 66 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) {
cmd.PrintErrln("Error:", err)
return err
}
if lexer == nil {
lexer = detectShebang(data)
}
if lexer == nil {
lexer = lexers.Analyse(string(data))
}
Expand All @@ -91,6 +94,10 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) {
}
if language == "" {
lexer = lexers.Match(filename)
if lexer == nil {
lexer = lexers.Analyse(string(data))
// lexer = detectShebang((data))
}
}
printData(&data, cmd, lexer)
}
Expand All @@ -101,6 +108,65 @@ func cmdMain(cmd *cobra.Command, args []string) (err error) {
return
}

func detectShebang(data []byte) chroma.Lexer {
if len(data) < 2 || data[0] != '#' || data[1] != '!' {
return nil
}

// Find the end of the first line
endIdx := bytes.IndexByte(data, '\n')
if endIdx == -1 {
endIdx = len(data)
}

shebangLine := bytes.TrimSpace(data[2:endIdx]) // Skip "#!"

// Parse interpreter from shebang
var interpreter string
if bytes.Contains(shebangLine, []byte("/env ")) {
// Handle "/usr/bin/env python3" format
parts := bytes.Fields(shebangLine)
if len(parts) >= 2 {
interpreter = string(parts[1])
}
} else {
// Handle "/bin/bash" format
parts := bytes.Split(shebangLine, []byte("/"))
if len(parts) > 0 {
lastPart := parts[len(parts)-1]
// Remove any arguments after space
if idx := bytes.IndexByte(lastPart, ' '); idx != -1 {
lastPart = lastPart[:idx]
}
interpreter = string(lastPart)
}
}

// Map interpreter to lexer
interpreterMap := map[string]string{
"bash": "bash",
"sh": "bash",
"zsh": "bash",
"ksh": "bash",
"python": "python",
"python3": "python",
"python2": "python",
"ruby": "ruby",
"perl": "perl",
"node": "javascript",
"nodejs": "javascript",
"php": "php",
"lua": "lua",
"awk": "awk",
}

if lexerName, ok := interpreterMap[interpreter]; ok {
return lexers.Get(lexerName)
}

return nil
}

func printData(data *[]byte, cmd *cobra.Command, lexer chroma.Lexer) {
out := cmd.OutOrStdout()
if number {
Expand Down
129 changes: 129 additions & 0 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,132 @@ func _unhighlightedGoCode() string {
}
return unhighlightedGoCode
}

func TestDetectShebang(t *testing.T) {
t.Run("Bash shebang", func(t *testing.T) {
data := []byte("#!/bin/bash\necho 'hello'")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Bash")
})

t.Run("Python3 with env", func(t *testing.T) {
data := []byte("#!/usr/bin/env python3\nprint('hello')")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Python")
})

t.Run("Ruby shebang", func(t *testing.T) {
data := []byte("#!/usr/bin/ruby\nputs 'hello'")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Ruby")
})

t.Run("Node with env", func(t *testing.T) {
data := []byte("#!/usr/bin/env node\nconsole.log('hello')")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "JavaScript")
})

t.Run("Sh shebang", func(t *testing.T) {
data := []byte("#!/bin/sh\necho 'hello'")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Bash")
})

t.Run("Perl shebang", func(t *testing.T) {
data := []byte("#!/usr/bin/perl\nprint 'hello'")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Perl")
})

t.Run("No shebang", func(t *testing.T) {
data := []byte("echo 'hello'")
lexer := detectShebang(data)
assert.Nil(t, lexer)
})

t.Run("Comment but not shebang", func(t *testing.T) {
data := []byte("# This is a comment\necho 'hello'")
lexer := detectShebang(data)
assert.Nil(t, lexer)
})

t.Run("Unknown interpreter", func(t *testing.T) {
data := []byte("#!/bin/unknowninterpreter\necho 'hello'")
lexer := detectShebang(data)
assert.Nil(t, lexer)
})

t.Run("Shebang with arguments", func(t *testing.T) {
data := []byte("#!/bin/bash -e\necho 'hello'")
lexer := detectShebang(data)
assert.NotNil(t, lexer)
assert.Contains(t, lexer.Config().Name, "Bash")
})

t.Run("Empty file", func(t *testing.T) {
data := []byte("")
lexer := detectShebang(data)
assert.Nil(t, lexer)
})
}

func TestShebangFileDetection(t *testing.T) {
setupTerminalMock(t)

t.Run("Bash script without extension", func(t *testing.T) {
var o, e bytes.Buffer
rootCmd.SetArgs([]string{"testdata/bashscript"})
rootCmd.SetOut(&o)
rootCmd.SetErr(&e)
err := rootCmd.Execute()

assert.NoError(t, err)
assert.Empty(t, e.String())
assert.NotEmpty(t, o.String())
// Should be syntax highlighted as bash
assert.Contains(t, o.String(), "[38;5;")
})

t.Run("Python script without extension", func(t *testing.T) {
t.Cleanup(resetStrings)
var o, e bytes.Buffer
rootCmd.SetArgs([]string{"testdata/pythonscript"})
rootCmd.SetOut(&o)
rootCmd.SetErr(&e)
err := rootCmd.Execute()

assert.NoError(t, err)
assert.Empty(t, e.String())
assert.NotEmpty(t, o.String())
// Should be syntax highlighted as python
assert.Contains(t, o.String(), "[38;5;")
})
}

func TestShebangFromStdin(t *testing.T) {
setupTerminalMock(t)

t.Run("Bash script from stdin", func(t *testing.T) {
t.Cleanup(resetStrings)
i := bytes.NewBufferString("#!/bin/bash\necho 'hello'")
var o, e bytes.Buffer
rootCmd.SetArgs([]string{})
rootCmd.SetIn(i)
rootCmd.SetOut(&o)
rootCmd.SetErr(&e)
err := rootCmd.Execute()

assert.NoError(t, err)
assert.Empty(t, e.String())
assert.NotEmpty(t, o.String())
// Should be syntax highlighted
assert.Contains(t, o.String(), "[38;5;")
})
}
2 changes: 2 additions & 0 deletions cmd/testdata/bashscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
echo "hello from bash"
2 changes: 2 additions & 0 deletions cmd/testdata/pythonscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
print("hello from python")
2 changes: 2 additions & 0 deletions testscript/bashscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/bash
echo "hello from bash"
2 changes: 2 additions & 0 deletions testscript/pythonscript
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
print("hello from python")
Loading