159 lines
4.4 KiB
Go
159 lines
4.4 KiB
Go
package cmd
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/rogeecn/renamer/internal/listing"
|
|
"github.com/rogeecn/renamer/internal/sequence"
|
|
)
|
|
|
|
func newSequenceCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "sequence",
|
|
Short: "Append or prepend sequential numbers to filenames",
|
|
Long: `Preview and apply numbered renames across the active scope. Sequence numbers
|
|
respect deterministic traversal order, support configurable start offsets, and record every
|
|
batch in the .renamer ledger for undo.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
scope, err := listing.ScopeFromCmd(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dryRun, err := getBool(cmd, "dry-run")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
autoApply, err := getBool(cmd, "yes")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if dryRun && autoApply {
|
|
return errors.New("--dry-run cannot be combined with --yes; remove one of them")
|
|
}
|
|
|
|
start, err := cmd.Flags().GetInt("start")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
width, err := cmd.Flags().GetInt("width")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
placement, err := cmd.Flags().GetString("placement")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
separator, err := cmd.Flags().GetString("separator")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
numberPrefix, err := cmd.Flags().GetString("number-prefix")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
numberSuffix, err := cmd.Flags().GetString("number-suffix")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts := sequence.DefaultOptions()
|
|
opts.WorkingDir = scope.WorkingDir
|
|
opts.IncludeDirectories = scope.IncludeDirectories
|
|
opts.IncludeHidden = scope.IncludeHidden
|
|
opts.Recursive = scope.Recursive
|
|
opts.Extensions = append([]string(nil), scope.Extensions...)
|
|
opts.DryRun = dryRun
|
|
opts.AutoApply = autoApply
|
|
if start != 0 {
|
|
opts.Start = start
|
|
}
|
|
if cmd.Flags().Changed("width") {
|
|
opts.Width = width
|
|
opts.WidthSet = true
|
|
}
|
|
if placement != "" {
|
|
opts.Placement = sequence.Placement(strings.ToLower(placement))
|
|
}
|
|
if separator != "" {
|
|
opts.Separator = separator
|
|
}
|
|
opts.NumberPrefix = numberPrefix
|
|
opts.NumberSuffix = numberSuffix
|
|
|
|
out := cmd.OutOrStdout()
|
|
|
|
plan, err := sequence.Preview(cmd.Context(), opts, out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, candidate := range plan.Candidates {
|
|
status := "UNCHANGED"
|
|
switch candidate.Status {
|
|
case sequence.CandidatePending:
|
|
status = "CHANGE"
|
|
case sequence.CandidateSkipped:
|
|
status = "SKIP"
|
|
}
|
|
fmt.Fprintf(out, "%s: %s -> %s\n", status, candidate.OriginalPath, candidate.ProposedPath)
|
|
}
|
|
|
|
for _, conflict := range plan.SkippedConflicts {
|
|
fmt.Fprintf(out, "Warning: %s skipped due to %s (target %s)\n", conflict.OriginalPath, conflict.Reason, conflict.ConflictingPath)
|
|
}
|
|
for _, warning := range plan.Summary.Warnings {
|
|
fmt.Fprintf(out, "Warning: %s\n", warning)
|
|
}
|
|
|
|
if plan.Summary.TotalCandidates == 0 {
|
|
fmt.Fprintln(out, "No candidates found.")
|
|
return nil
|
|
}
|
|
|
|
fmt.Fprintf(out, "Preview: %d candidates, %d renames, %d skipped (width %d).\n",
|
|
plan.Summary.TotalCandidates,
|
|
plan.Summary.RenamedCount,
|
|
plan.Summary.SkippedCount,
|
|
plan.Summary.AppliedWidth,
|
|
)
|
|
|
|
if dryRun || !autoApply {
|
|
if !autoApply {
|
|
fmt.Fprintln(out, "Preview complete. Re-run with --yes to apply.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
entry, err := sequence.Apply(cmd.Context(), opts, plan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Fprintf(out, "Applied %d sequence updates. Ledger updated.\n", len(entry.Operations))
|
|
if plan.Summary.SkippedCount > 0 {
|
|
fmt.Fprintf(out, "%d candidates were skipped due to conflicts.\n", plan.Summary.SkippedCount)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Flags().Int("start", 1, "Starting sequence value (>=1)")
|
|
cmd.Flags().Int("width", 0, "Minimum digit width for zero padding (defaults to 3 digits, auto-expands as needed)")
|
|
cmd.Flags().String("placement", string(sequence.PlacementPrefix), "Placement for the sequence number: prefix or suffix")
|
|
cmd.Flags().String("separator", "_", "Separator between the filename and sequence label and the original name")
|
|
cmd.Flags().String("number-prefix", "", "Static text placed immediately before the sequence digits")
|
|
cmd.Flags().String("number-suffix", "", "Static text placed immediately after the sequence digits")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(newSequenceCommand())
|
|
}
|