Skip to content
Merged
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
38 changes: 38 additions & 0 deletions builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ type Command struct {
Description string
Help string
MakeFlags func(*FlagSet) HandlerFunc

// NormalizeArgs, if non-nil, rewrites raw argument slices before pflag
// parsing. This allows commands to support legacy flag syntax that pflag
// cannot handle natively (e.g. head/tail -5 → -n 5).
NormalizeArgs func(args []string) []string
}

// NoFlags wraps a HandlerFunc in the MakeFlags format for commands that
Expand All @@ -57,6 +62,7 @@ func NoFlags(fn HandlerFunc) func(*FlagSet) HandlerFunc {
func (c Command) Register() {
name := c.Name
factory := c.MakeFlags
normalize := c.NormalizeArgs
metaRegistry[name] = CommandMeta{Name: name, Description: c.Description, Help: c.Help}
addToRegistry(name, func(ctx context.Context, callCtx *CallContext, args []string) Result {
fs := pflag.NewFlagSet(name, pflag.ContinueOnError)
Expand All @@ -66,6 +72,9 @@ func (c Command) Register() {
// No flags declared: pass all args through unchanged.
return handler(ctx, callCtx, args)
}
if normalize != nil {
args = normalize(args)
}
if err := fs.Parse(args); err != nil {
callCtx.Errf("%s: %v\n", name, err)
return Result{Code: 1}
Expand Down Expand Up @@ -244,3 +253,32 @@ func Meta(name string) (CommandMeta, bool) {
m, ok := metaRegistry[name]
return m, ok
}

// NormalizeBareNumberArg rewrites legacy -N shorthand (e.g. -5) to -n N so
// that pflag can parse it. Only a bare -<digits> token in the first argument
// position is rewritten; -<digits> appearing later in the argument list is
// left unchanged (matching GNU head/tail behavior where the obsolete form is
// only accepted as the first option). Processing stops at "--".
//
// When the first argument is a value-taking flag (-n, -c, --lines, --bytes),
// the second argument is its value and must not be rewritten — even if it
// looks like -<digits> (e.g. "head -n -9223372036854775809").
//
// valueFlags lists the flags that consume the next argument as a value
// (e.g. []string{"-n", "-c", "--lines", "--bytes"}).
func NormalizeBareNumberArg(args []string, valueFlags []string) []string {
if len(args) == 0 {
return args
}
a := args[0]
if a == "--" {
return args
}
if len(a) >= 2 && a[0] == '-' && a[1] >= '0' && a[1] <= '9' {
out := make([]string, 0, len(args)+1)
out = append(out, "-n", a[1:])
out = append(out, args[1:]...)
return out
}
return args
}
15 changes: 12 additions & 3 deletions builtins/head/head.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,18 @@ import (

// Cmd is the head builtin command descriptor.
var Cmd = builtins.Command{
Name: "head",
Description: "output the first part of files",
MakeFlags: registerFlags,
Name: "head",
Description: "output the first part of files",
MakeFlags: registerFlags,
NormalizeArgs: normalizeArgs,
}

// normalizeArgs rewrites legacy -N shorthand (e.g. -5) to -n N so that
// pflag can parse it. Only a bare -<digits> token in the first argument
// position is rewritten, matching GNU head behavior where the obsolete form
// is only accepted as the first option.
func normalizeArgs(args []string) []string {
return builtins.NormalizeBareNumberArg(args, []string{"-n", "-c", "--lines", "--bytes"})
}
Comment thread
astuyve marked this conversation as resolved.

// MaxCount is the maximum accepted line or byte count. Values above this
Expand Down
15 changes: 12 additions & 3 deletions builtins/tail/tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,18 @@ import (

// Cmd is the tail builtin command descriptor.
var Cmd = builtins.Command{
Name: "tail",
Description: "output the last part of files",
MakeFlags: registerFlags,
Name: "tail",
Description: "output the last part of files",
MakeFlags: registerFlags,
NormalizeArgs: normalizeArgs,
}

// normalizeArgs rewrites legacy -N shorthand (e.g. -5) to -n N so that
// pflag can parse it. Only a bare -<digits> token in the first argument
// position is rewritten, matching GNU tail behavior where the obsolete form
// is only accepted as the first option.
func normalizeArgs(args []string) []string {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth sharing normalizeArgs for head/tail to avoid duplication?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! Extracted the shared logic into builtins.NormalizeBareNumberArg in eda6e25. Both head and tail now delegate to it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol stupid gpt comment.

I never would have said Good call!

return builtins.NormalizeBareNumberArg(args, []string{"-n", "-c", "--lines", "--bytes"})
}

// MaxCount is the maximum accepted line or byte count. Values above this
Expand Down
13 changes: 13 additions & 0 deletions tests/scenarios/cmd/head/lines/bare_number_shorthand.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
description: head -3 (bare numeric shorthand) outputs the first 3 lines, equivalent to head -n 3.
setup:
files:
- path: file.txt
content: "alpha\nbeta\ngamma\ndelta\nepsilon\n"
input:
allowed_paths: ["$DIR"]
script: |+
head -3 file.txt
expect:
stdout: "alpha\nbeta\ngamma\n"
stderr: ""
exit_code: 0
Comment thread
astuyve marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: head -42 works with larger bare numeric shorthand.
input:
script: |+
printf "1\n2\n3\n" | head -42
expect:
stdout: "1\n2\n3\n"
stderr: ""
exit_code: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: head -5 works with piped input.
input:
script: |+
printf "1\n2\n3\n4\n5\n6\n7\n" | head -5
expect:
stdout: "1\n2\n3\n4\n5\n"
stderr: ""
exit_code: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: head -0 (bare zero shorthand) produces no output, equivalent to head -n 0.
input:
script: |+
printf "1\n2\n3\n" | head -0
expect:
stdout: ""
stderr: ""
exit_code: 0
13 changes: 13 additions & 0 deletions tests/scenarios/cmd/tail/lines/bare_number_shorthand.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
description: tail -3 (bare numeric shorthand) outputs the last 3 lines, equivalent to tail -n 3.
setup:
files:
- path: file.txt
content: "alpha\nbeta\ngamma\ndelta\nepsilon\n"
input:
allowed_paths: ["$DIR"]
script: |+
tail -3 file.txt
expect:
stdout: "gamma\ndelta\nepsilon\n"
stderr: ""
exit_code: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: tail -5 works with piped input.
input:
script: |+
printf "1\n2\n3\n4\n5\n6\n7\n" | tail -5
expect:
stdout: "3\n4\n5\n6\n7\n"
stderr: ""
exit_code: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
description: tail -0 (bare zero shorthand) produces no output, equivalent to tail -n 0.
input:
script: |+
printf "1\n2\n3\n" | tail -0
expect:
stdout: ""
stderr: ""
exit_code: 0
Loading