Skip to content

Commit bae3a1e

Browse files
committed
feat: add shebang support to scripts
This enables much greater portability!
1 parent 791144d commit bae3a1e

File tree

3 files changed

+63
-1
lines changed

3 files changed

+63
-1
lines changed

agent/agentssh/agentssh.go

+23-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"time"
2020

2121
"github.com/gliderlabs/ssh"
22+
"github.com/kballard/go-shellquote"
2223
"github.com/pkg/sftp"
2324
"github.com/prometheus/client_golang/prometheus"
2425
"github.com/spf13/afero"
@@ -515,8 +516,29 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string)
515516
if runtime.GOOS == "windows" {
516517
caller = "/c"
517518
}
519+
name := shell
518520
args := []string{caller, script}
519521

522+
if strings.HasPrefix(strings.TrimSpace(script), "#!") {
523+
// If the script starts with a shebang, we should
524+
// execute it directly. This is useful for running
525+
// scripts that aren't executable.
526+
shebang := strings.SplitN(script, "\n", 2)[0]
527+
shebang = strings.TrimPrefix(shebang, "#!")
528+
shebang = strings.TrimSpace(shebang)
529+
words, err := shellquote.Split(shebang)
530+
if err != nil {
531+
return nil, xerrors.Errorf("split shebang: %w", err)
532+
}
533+
name = words[0]
534+
if len(words) > 1 {
535+
args = words[1:]
536+
} else {
537+
args = []string{}
538+
}
539+
args = append(args, caller, script)
540+
}
541+
520542
// gliderlabs/ssh returns a command slice of zero
521543
// when a shell is requested.
522544
if len(script) == 0 {
@@ -528,7 +550,7 @@ func (s *Server) CreateCommand(ctx context.Context, script string, env []string)
528550
}
529551
}
530552

531-
cmd := pty.CommandContext(ctx, shell, args...)
553+
cmd := pty.CommandContext(ctx, name, args...)
532554
cmd.Dir = manifest.Directory
533555

534556
// If the metadata directory doesn't exist, we run the command

agent/agentssh/agentssh_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"bytes"
77
"context"
88
"net"
9+
"runtime"
910
"strings"
1011
"sync"
1112
"testing"
@@ -71,6 +72,42 @@ func TestNewServer_ServeClient(t *testing.T) {
7172
<-done
7273
}
7374

75+
func TestNewServer_ExecuteShebang(t *testing.T) {
76+
t.Parallel()
77+
if runtime.GOOS == "windows" {
78+
t.Skip("bash doesn't exist on Windows")
79+
}
80+
81+
ctx := context.Background()
82+
logger := slogtest.Make(t, nil)
83+
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), 0, "")
84+
require.NoError(t, err)
85+
t.Cleanup(func() {
86+
_ = s.Close()
87+
})
88+
s.AgentToken = func() string { return "" }
89+
s.Manifest = atomic.NewPointer(&agentsdk.Manifest{})
90+
91+
t.Run("Basic", func(t *testing.T) {
92+
t.Parallel()
93+
cmd, err := s.CreateCommand(ctx, `#!/usr/bin/bash
94+
echo test`, nil)
95+
require.NoError(t, err)
96+
output, err := cmd.AsExec().CombinedOutput()
97+
require.NoError(t, err)
98+
require.Equal(t, "test\n", string(output))
99+
})
100+
t.Run("Args", func(t *testing.T) {
101+
t.Parallel()
102+
cmd, err := s.CreateCommand(ctx, `#!/usr/bin/env bash
103+
echo test`, nil)
104+
require.NoError(t, err)
105+
output, err := cmd.AsExec().CombinedOutput()
106+
require.NoError(t, err)
107+
require.Equal(t, "test\n", string(output))
108+
})
109+
}
110+
74111
func TestNewServer_CloseActiveConnections(t *testing.T) {
75112
t.Parallel()
76113

dogfood/main.tf

+3
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ resource "coder_agent" "dev" {
172172
display_name = "Swap Usage (Host)"
173173
key = "4_swap_usage_host"
174174
script = <<EOT
175+
#!/bin/bash
175176
echo "$(free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }') GiB"
176177
EOT
177178
interval = 10
@@ -183,6 +184,7 @@ resource "coder_agent" "dev" {
183184
key = "5_load_host"
184185
# get load avg scaled by number of cores
185186
script = <<EOT
187+
#!/bin/bash
186188
echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
187189
EOT
188190
interval = 60
@@ -201,6 +203,7 @@ resource "coder_agent" "dev" {
201203
display_name = "Word of the Day"
202204
key = "7_word"
203205
script = <<EOT
206+
#!/bin/bash
204207
curl -o - --silent https://www.merriam-webster.com/word-of-the-day 2>&1 | awk ' $0 ~ "Word of the Day: [A-z]+" { print $5; exit }'
205208
EOT
206209
interval = 86400

0 commit comments

Comments
 (0)