Repository: twpayne/chezmoi
Pull request #1017 (2021-01-31 00:34:03)

chezmoi2: Use before_ and after_ for scripts instead of first_ and last_
As proposed by @MunifTanjim in #987.


Patch:
diff --git a/chezmoi2/cmd/chattrcmd.go b/chezmoi2/cmd/chattrcmd.go
index b472d538d..4602b718a 100644
--- a/chezmoi2/cmd/chattrcmd.go
+++ b/chezmoi2/cmd/chattrcmd.go
@@ -21,11 +21,11 @@ const (
 type orderModifier int
 
 const (
-	orderModifierSetFirst       orderModifier = -2
-	orderModifierClearFirst     orderModifier = -1
+	orderModifierSetBefore      orderModifier = -2
+	orderModifierClearBefore    orderModifier = -1
 	orderModifierLeaveUnchanged orderModifier = 0
-	orderModifierClearLast      orderModifier = 1
-	orderModifierSetLast        orderModifier = 2
+	orderModifierClearAfter     orderModifier = 1
+	orderModifierSetAfter       orderModifier = 2
 )
 
 type attrModifier struct {
@@ -41,12 +41,12 @@ type attrModifier struct {
 
 func (c *Config) newChattrCmd() *cobra.Command {
 	attrs := []string{
+		"after", "a",
+		"before", "b",
 		"empty", "e",
 		"encrypted",
 		"exact",
 		"executable", "x",
-		"first", "f",
-		"last", "l",
 		"once", "o",
 		"private", "p",
 		"template", "t",
@@ -137,21 +137,21 @@ func (m boolModifier) modify(b bool) bool {
 
 func (m orderModifier) modify(order int) int {
 	switch m {
-	case orderModifierSetFirst:
+	case orderModifierSetBefore:
 		return -1
-	case orderModifierClearFirst:
+	case orderModifierClearBefore:
 		if order < 0 {
 			return 0
 		}
 		return order
 	case orderModifierLeaveUnchanged:
 		return order
-	case orderModifierClearLast:
+	case orderModifierClearAfter:
 		if order > 0 {
 			return 0
 		}
 		return order
-	case orderModifierSetLast:
+	case orderModifierSetAfter:
 		return 1
 	default:
 		panic(fmt.Sprintf("%d: unknown order modifier", m))
@@ -182,32 +182,32 @@ func parseAttrModifier(s string) (*attrModifier, error) {
 			attribute = modifierStr
 		}
 		switch attribute {
-		case "empty", "e":
-			am.empty = bm
-		case "encrypted":
-			am.encrypted = bm
-		case "exact":
-			am.exact = bm
-		case "executable", "x":
-			am.executable = bm
-		case "first", "f":
+		case "after", "a":
 			switch bm {
 			case boolModifierClear:
-				am.order = orderModifierClearFirst
+				am.order = orderModifierClearAfter
 			case boolModifierLeaveUnchanged:
 				am.order = orderModifierLeaveUnchanged
 			case boolModifierSet:
-				am.order = orderModifierSetFirst
+				am.order = orderModifierSetAfter
 			}
-		case "last", "l":
+		case "before", "b":
 			switch bm {
 			case boolModifierClear:
-				am.order = orderModifierClearLast
+				am.order = orderModifierClearBefore
 			case boolModifierLeaveUnchanged:
 				am.order = orderModifierLeaveUnchanged
 			case boolModifierSet:
-				am.order = orderModifierSetLast
+				am.order = orderModifierSetBefore
 			}
+		case "empty", "e":
+			am.empty = bm
+		case "encrypted":
+			am.encrypted = bm
+		case "exact":
+			am.exact = bm
+		case "executable", "x":
+			am.executable = bm
 		case "once", "o":
 			am.once = bm
 		case "private", "p":
diff --git a/chezmoi2/cmd/reference.gen.go b/chezmoi2/cmd/reference.gen.go
index 119fed49a..c84bc644d 100644
--- a/chezmoi2/cmd/reference.gen.go
+++ b/chezmoi2/cmd/reference.gen.go
@@ -340,24 +340,24 @@ func init() {
 		"\n" +
 		"| Prefix       | Effect                                                                         |\n" +
 		"| ------------ | ------------------------------------------------------------------------------ |\n" +
+		"| `after_`     | Run script after updating the destination.                                     |\n" +
+		"| `before_`    | Run script before updating the desintation.                                    |\n" +
+		"| `dot_`       | Rename to use a leading dot, e.g. `dot_foo` becomes `.foo`.                    |\n" +
+		"| `empty_`     | Ensure the file exists, even if is empty. By default, empty files are removed. |\n" +
 		"| `encrypted_` | Encrypt the file in the source state.                                          |\n" +
+		"| `exact_`     | Remove anything not managed by chezmoi.                                        |\n" +
+		"| `executable_`| Add executable permissions to the target file.                                 |\n" +
 		"| `exists_`    | Ensure that the file exists, and initalize its contents if it does not.        |\n" +
 		"| `once_`      | Run script once.                                                               |\n" +
 		"| `private_`   | Remove all group and world permissions from the target file or directory.      |\n" +
-		"| `empty_`     | Ensure the file exists, even if is empty. By default, empty files are removed. |\n" +
-		"| `exact_`     | Remove anything not managed by chezmoi.                                        |\n" +
-		"| `executable_`| Add executable permissions to the target file.                                 |\n" +
-		"| `first_`     | Run script before updating the desintation.                                    |\n" +
-		"| `last_`      | Run script after updating the destination.                                     |\n" +
 		"| `run_`       | Treat the contents as a script to run.                                         |\n" +
 		"| `symlink_`   | Create a symlink instead of a regular file.                                    |\n" +
-		"| `dot_`       | Rename to use a leading dot, e.g. `dot_foo` becomes `.foo`.                    |\n" +
 		"\n" +
 		"| Suffix  | Effect                                               |\n" +
 		"| ------- | ---------------------------------------------------- |\n" +
 		"| `.tmpl` | Treat the contents of the source file as a template. |\n" +
 		"\n" +
-		"The order of prefixes is important, the order is `run_`, `first_`, `last_`,\n" +
+		"The order of prefixes is important, the order is `run_`, `before_`, `after_`,\n" +
 		"`exact_`, `exists_`, `private_`, `empty_`, `executable_`, `symlink_`, `once_`,\n" +
 		"`dot_`.\n" +
 		"\n" +
@@ -368,7 +368,7 @@ func init() {
 		"| Directory     | `exact_`, `private_`, `dot_`                               | *none*           |\n" +
 		"| Present file  | `exists_`, `encrypted_`, `private_`, `executable_`, `dot_` | `.tmpl`          |\n" +
 		"| Regular file  | `encrypted_`, `private_`, `empty_`, `executable_`, `dot_`  | `.tmpl`          |\n" +
-		"| Script        | `run_`, `first_` or `last_`, `once_`                       | `.tmpl`          |\n" +
+		"| Script        | `run_`, `once_`, `before_` or `after_`                     | `.tmpl`          |\n" +
 		"| Symbolic link | `symlink_`, `dot_`,                                        | `.tmpl`          |\n" +
 		"\n" +
 		"## Special files and directories\n" +
diff --git a/chezmoi2/completions/chezmoi2-completion.bash b/chezmoi2/completions/chezmoi2-completion.bash
index f1c40c086..b9ab273e3 100644
--- a/chezmoi2/completions/chezmoi2-completion.bash
+++ b/chezmoi2/completions/chezmoi2-completion.bash
@@ -812,15 +812,15 @@ _chezmoi2_chattr()
 
     must_have_one_flag=()
     must_have_one_noun=()
+    must_have_one_noun+=("+a")
+    must_have_one_noun+=("+after")
+    must_have_one_noun+=("+b")
+    must_have_one_noun+=("+before")
     must_have_one_noun+=("+e")
     must_have_one_noun+=("+empty")
     must_have_one_noun+=("+encrypted")
     must_have_one_noun+=("+exact")
     must_have_one_noun+=("+executable")
-    must_have_one_noun+=("+f")
-    must_have_one_noun+=("+first")
-    must_have_one_noun+=("+l")
-    must_have_one_noun+=("+last")
     must_have_one_noun+=("+o")
     must_have_one_noun+=("+once")
     must_have_one_noun+=("+p")
@@ -828,15 +828,15 @@ _chezmoi2_chattr()
     must_have_one_noun+=("+t")
     must_have_one_noun+=("+template")
     must_have_one_noun+=("+x")
+    must_have_one_noun+=("-a")
+    must_have_one_noun+=("-after")
+    must_have_one_noun+=("-b")
+    must_have_one_noun+=("-before")
     must_have_one_noun+=("-e")
     must_have_one_noun+=("-empty")
     must_have_one_noun+=("-encrypted")
     must_have_one_noun+=("-exact")
     must_have_one_noun+=("-executable")
-    must_have_one_noun+=("-f")
-    must_have_one_noun+=("-first")
-    must_have_one_noun+=("-l")
-    must_have_one_noun+=("-last")
     must_have_one_noun+=("-o")
     must_have_one_noun+=("-once")
     must_have_one_noun+=("-p")
@@ -844,24 +844,24 @@ _chezmoi2_chattr()
     must_have_one_noun+=("-t")
     must_have_one_noun+=("-template")
     must_have_one_noun+=("-x")
+    must_have_one_noun+=("a")
+    must_have_one_noun+=("after")
+    must_have_one_noun+=("b")
+    must_have_one_noun+=("before")
     must_have_one_noun+=("e")
     must_have_one_noun+=("empty")
     must_have_one_noun+=("encrypted")
     must_have_one_noun+=("exact")
     must_have_one_noun+=("executable")
-    must_have_one_noun+=("f")
-    must_have_one_noun+=("first")
-    must_have_one_noun+=("l")
-    must_have_one_noun+=("last")
+    must_have_one_noun+=("noa")
+    must_have_one_noun+=("noafter")
+    must_have_one_noun+=("nob")
+    must_have_one_noun+=("nobefore")
     must_have_one_noun+=("noe")
     must_have_one_noun+=("noempty")
     must_have_one_noun+=("noencrypted")
     must_have_one_noun+=("noexact")
     must_have_one_noun+=("noexecutable")
-    must_have_one_noun+=("nof")
-    must_have_one_noun+=("nofirst")
-    must_have_one_noun+=("nol")
-    must_have_one_noun+=("nolast")
     must_have_one_noun+=("noo")
     must_have_one_noun+=("noonce")
     must_have_one_noun+=("nop")
diff --git a/chezmoi2/docs/CHANGES.md b/chezmoi2/docs/CHANGES.md
index 0e047756b..9790059b3 100644
--- a/chezmoi2/docs/CHANGES.md
+++ b/chezmoi2/docs/CHANGES.md
@@ -31,7 +31,7 @@ General:
 - `chezmoi init --one-shot`
 - new type `--exists`
 - `chezmoi archive --format=zip`
-- `first_` and `last_` script attributes change script order, scripts now run during
+- `before_` and `after_` script attributes change script order, scripts now run during
 - new `fqdnHostname` template var (UNIX only for now)
 - age encryption support
 
diff --git a/chezmoi2/docs/REFERENCE.md b/chezmoi2/docs/REFERENCE.md
index d170ed887..e79a464e0 100644
--- a/chezmoi2/docs/REFERENCE.md
+++ b/chezmoi2/docs/REFERENCE.md
@@ -333,24 +333,24 @@ special, and are collectively referred to as "attributes":
 
 | Prefix       | Effect                                                                         |
 | ------------ | ------------------------------------------------------------------------------ |
+| `after_`     | Run script after updating the destination.                                     |
+| `before_`    | Run script before updating the desintation.                                    |
+| `dot_`       | Rename to use a leading dot, e.g. `dot_foo` becomes `.foo`.                    |
+| `empty_`     | Ensure the file exists, even if is empty. By default, empty files are removed. |
 | `encrypted_` | Encrypt the file in the source state.                                          |
+| `exact_`     | Remove anything not managed by chezmoi.                                        |
+| `executable_`| Add executable permissions to the target file.                                 |
 | `exists_`    | Ensure that the file exists, and initalize its contents if it does not.        |
 | `once_`      | Run script once.                                                               |
 | `private_`   | Remove all group and world permissions from the target file or directory.      |
-| `empty_`     | Ensure the file exists, even if is empty. By default, empty files are removed. |
-| `exact_`     | Remove anything not managed by chezmoi.                                        |
-| `executable_`| Add executable permissions to the target file.                                 |
-| `first_`     | Run script before updating the desintation.                                    |
-| `last_`      | Run script after updating the destination.                                     |
 | `run_`       | Treat the contents as a script to run.                                         |
 | `symlink_`   | Create a symlink instead of a regular file.                                    |
-| `dot_`       | Rename to use a leading dot, e.g. `dot_foo` becomes `.foo`.                    |
 
 | Suffix  | Effect                                               |
 | ------- | ---------------------------------------------------- |
 | `.tmpl` | Treat the contents of the source file as a template. |
 
-The order of prefixes is important, the order is `run_`, `first_`, `last_`,
+The order of prefixes is important, the order is `run_`, `before_`, `after_`,
 `exact_`, `exists_`, `private_`, `empty_`, `executable_`, `symlink_`, `once_`,
 `dot_`.
 
@@ -361,7 +361,7 @@ Different target types allow different prefixes and suffixes:
 | Directory     | `exact_`, `private_`, `dot_`                               | *none*           |
 | Present file  | `exists_`, `encrypted_`, `private_`, `executable_`, `dot_` | `.tmpl`          |
 | Regular file  | `encrypted_`, `private_`, `empty_`, `executable_`, `dot_`  | `.tmpl`          |
-| Script        | `run_`, `first_` or `last_`, `once_`                       | `.tmpl`          |
+| Script        | `run_`, `once_`, `before_` or `after_`                     | `.tmpl`          |
 | Symbolic link | `symlink_`, `dot_`,                                        | `.tmpl`          |
 
 ## Special files and directories
diff --git a/chezmoi2/internal/chezmoi/attr.go b/chezmoi2/internal/chezmoi/attr.go
index 3ad31cc00..812d3edb4 100644
--- a/chezmoi2/internal/chezmoi/attr.go
+++ b/chezmoi2/internal/chezmoi/attr.go
@@ -121,18 +121,18 @@ func parseFileAttr(sourceName string) FileAttr {
 	case strings.HasPrefix(name, runPrefix):
 		sourceFileType = SourceFileTypeScript
 		name = mustTrimPrefix(name, runPrefix)
-		switch {
-		case strings.HasPrefix(name, firstPrefix):
-			name = mustTrimPrefix(name, firstPrefix)
-			order = -1
-		case strings.HasPrefix(name, lastPrefix):
-			name = mustTrimPrefix(name, lastPrefix)
-			order = 1
-		}
 		if strings.HasPrefix(name, oncePrefix) {
 			name = mustTrimPrefix(name, oncePrefix)
 			once = true
 		}
+		switch {
+		case strings.HasPrefix(name, beforePrefix):
+			name = mustTrimPrefix(name, beforePrefix)
+			order = -1
+		case strings.HasPrefix(name, afterPrefix):
+			name = mustTrimPrefix(name, afterPrefix)
+			order = 1
+		}
 	case strings.HasPrefix(name, symlinkPrefix):
 		sourceFileType = SourceFileTypeSymlink
 		name = mustTrimPrefix(name, symlinkPrefix)
@@ -204,14 +204,14 @@ func (fa FileAttr) SourceName() string {
 		}
 	case SourceFileTypeScript:
 		sourceName = runPrefix
+		if fa.Once {
+			sourceName += oncePrefix
+		}
 		switch fa.Order {
 		case -1:
-			sourceName += firstPrefix
+			sourceName += beforePrefix
 		case 1:
-			sourceName += lastPrefix
-		}
-		if fa.Once {
-			sourceName += oncePrefix
+			sourceName += afterPrefix
 		}
 	case SourceFileTypeSymlink:
 		sourceName = symlinkPrefix
diff --git a/chezmoi2/internal/chezmoi/chezmoi.go b/chezmoi2/internal/chezmoi/chezmoi.go
index 4076ded72..5879c44f1 100644
--- a/chezmoi2/internal/chezmoi/chezmoi.go
+++ b/chezmoi2/internal/chezmoi/chezmoi.go
@@ -19,14 +19,14 @@ var (
 // Suffixes and prefixes.
 const (
 	ignorePrefix     = "."
+	afterPrefix      = "after_"
+	beforePrefix     = "before_"
 	dotPrefix        = "dot_"
 	emptyPrefix      = "empty_"
 	encryptedPrefix  = "encrypted_"
 	exactPrefix      = "exact_"
 	executablePrefix = "executable_"
 	existsPrefix     = "exists_"
-	firstPrefix      = "first_"
-	lastPrefix       = "last_"
 	oncePrefix       = "once_"
 	privatePrefix    = "private_"
 	runPrefix        = "run_"


Test patch:
diff --git a/chezmoi2/cmd/chattrcmd_test.go b/chezmoi2/cmd/chattrcmd_test.go
index a30cf67e3..23b52dbd0 100644
--- a/chezmoi2/cmd/chattrcmd_test.go
+++ b/chezmoi2/cmd/chattrcmd_test.go
@@ -62,27 +62,27 @@ func TestParseAttrModifier(t *testing.T) {
 			},
 		},
 		{
-			s: "f",
+			s: "b",
 			expected: &attrModifier{
-				order: orderModifierSetFirst,
+				order: orderModifierSetBefore,
 			},
 		},
 		{
-			s: "-f",
+			s: "-b",
 			expected: &attrModifier{
-				order: orderModifierClearFirst,
+				order: orderModifierClearBefore,
 			},
 		},
 		{
-			s: "last",
+			s: "after",
 			expected: &attrModifier{
-				order: orderModifierSetLast,
+				order: orderModifierSetAfter,
 			},
 		},
 		{
-			s: "nolast",
+			s: "noafter",
 			expected: &attrModifier{
-				order: orderModifierClearLast,
+				order: orderModifierClearAfter,
 			},
 		},
 		{
diff --git a/chezmoi2/internal/chezmoi/sourcestate_test.go b/chezmoi2/internal/chezmoi/sourcestate_test.go
index a8b030045..b05875236 100644
--- a/chezmoi2/internal/chezmoi/sourcestate_test.go
+++ b/chezmoi2/internal/chezmoi/sourcestate_test.go
@@ -1196,27 +1196,27 @@ func TestSourceStateTargetRelPaths(t *testing.T) {
 			name: "scripts",
 			root: map[string]interface{}{
 				"/home/user/.local/share/chezmoi": map[string]interface{}{
-					"run_first_1first": "",
-					"run_first_2first": "",
-					"run_first_3first": "",
-					"run_1":            "",
-					"run_2":            "",
-					"run_3":            "",
-					"run_last_1last":   "",
-					"run_last_2last":   "",
-					"run_last_3last":   "",
+					"run_before_1before": "",
+					"run_before_2before": "",
+					"run_before_3before": "",
+					"run_1":              "",
+					"run_2":              "",
+					"run_3":              "",
+					"run_after_1after":   "",
+					"run_after_2after":   "",
+					"run_after_3after":   "",
 				},
 			},
 			expectedTargetRelPaths: RelPaths{
-				"1first",
-				"2first",
-				"3first",
+				"1before",
+				"2before",
+				"3before",
 				"1",
 				"2",
 				"3",
-				"1last",
-				"2last",
-				"3last",
+				"1after",
+				"2after",
+				"3after",
 			},
 		},
 	} {
diff --git a/chezmoi2/testdata/scripts/chattr.txt b/chezmoi2/testdata/scripts/chattr.txt
index 4d7a2c0f7..cceb157bc 100644
--- a/chezmoi2/testdata/scripts/chattr.txt
+++ b/chezmoi2/testdata/scripts/chattr.txt
@@ -44,24 +44,24 @@ chezmoi chattr -- -t $HOME${/}.symlink
 ! exists $CHEZMOISOURCEDIR/symlink_dot_symlink.tmpl
 exists $CHEZMOISOURCEDIR/symlink_dot_symlink
 
-chezmoi chattr -- first $HOME/script
+chezmoi chattr -- before $HOME/script
 ! exists $CHEZMOISOURCEDIR/run_script
-exists $CHEZMOISOURCEDIR/run_first_script
+exists $CHEZMOISOURCEDIR/run_before_script
 
 chezmoi chattr -- once $HOME/script
-! exists $CHEZMOISOURCEDIR/run_first_script
-exists $CHEZMOISOURCEDIR/run_first_once_script
+! exists $CHEZMOISOURCEDIR/run_before_script
+exists $CHEZMOISOURCEDIR/run_once_before_script
 
-chezmoi chattr -- last $HOME/script
-! exists $CHEZMOISOURCEDIR/run_first_once_script
-exists $CHEZMOISOURCEDIR/run_last_once_script
+chezmoi chattr -- after $HOME/script
+! exists $CHEZMOISOURCEDIR/run_once_before_script
+exists $CHEZMOISOURCEDIR/run_once_after_script
 
 chezmoi chattr -- -o $HOME/script
-! exists $CHEZMOISOURCEDIR/run_last_once_script
-exists $CHEZMOISOURCEDIR/run_last_script
+! exists $CHEZMOISOURCEDIR/run_once_after_script
+exists $CHEZMOISOURCEDIR/run_after_script
 
-chezmoi chattr -- -l $HOME/script
-! exists $CHEZMOISOURCEDIR/run_last_script
+chezmoi chattr -- -a $HOME/script
+! exists $CHEZMOISOURCEDIR/run_after_script
 exists $CHEZMOISOURCEDIR/run_script
 
 -- home/user/.local/share/chezmoi/run_script --
diff --git a/chezmoi2/testdata/scripts/scriptorder_unix.txt b/chezmoi2/testdata/scripts/scriptorder_unix.txt
index cee8c2072..e0e1a04e4 100644
--- a/chezmoi2/testdata/scripts/scriptorder_unix.txt
+++ b/chezmoi2/testdata/scripts/scriptorder_unix.txt
@@ -1,22 +1,22 @@
 [windows] skip 'UNIX only'
 
 # test that chezmoi apply runs scripts in the correct order
-symlink home/user/.local/share/chezmoi/run_first_00-first -> .script
-symlink home/user/.local/share/chezmoi/run_first_99-first -> .script
+symlink home/user/.local/share/chezmoi/run_before_00-before -> .script
+symlink home/user/.local/share/chezmoi/run_before_99-before -> .script
 symlink home/user/.local/share/chezmoi/run_00 -> .script
 symlink home/user/.local/share/chezmoi/run_99 -> .script
-symlink home/user/.local/share/chezmoi/run_last_00-last -> .script
-symlink home/user/.local/share/chezmoi/run_last_99-last -> .script
+symlink home/user/.local/share/chezmoi/run_after_00-after -> .script
+symlink home/user/.local/share/chezmoi/run_after_99-after -> .script
 chezmoi apply --force
 cmp stdout golden/apply
 
 -- golden/apply --
-00-first
-99-first
+00-before
+99-before
 00
 99
-00-last
-99-last
+00-after
+99-after
 -- home/user/.local/share/chezmoi/.script --
 #!/bin/sh
 
diff --git a/chezmoi2/testdata/scripts/scriptorder_windows.txt b/chezmoi2/testdata/scripts/scriptorder_windows.txt
index 7c1daeb17..9f85f22fe 100644
--- a/chezmoi2/testdata/scripts/scriptorder_windows.txt
+++ b/chezmoi2/testdata/scripts/scriptorder_windows.txt
@@ -3,22 +3,22 @@
 unix2dos golden/apply
 
 # test that chezmoi apply runs scripts in the correct order
-symlink home/user/.local/share/chezmoi/run_first_00-first.cmd -> .script.cmd
-symlink home/user/.local/share/chezmoi/run_first_99-first.cmd -> .script.cmd
+symlink home/user/.local/share/chezmoi/run_before_00-before.cmd -> .script.cmd
+symlink home/user/.local/share/chezmoi/run_before_99-before.cmd -> .script.cmd
 symlink home/user/.local/share/chezmoi/run_00.cmd -> .script.cmd
 symlink home/user/.local/share/chezmoi/run_99.cmd -> .script.cmd
-symlink home/user/.local/share/chezmoi/run_last_00-last.cmd -> .script.cmd
-symlink home/user/.local/share/chezmoi/run_last_99-last.cmd -> .script.cmd
+symlink home/user/.local/share/chezmoi/run_after_00-after.cmd -> .script.cmd
+symlink home/user/.local/share/chezmoi/run_after_99-after.cmd -> .script.cmd
 chezmoi apply --force
 cmp stdout golden/apply
 
 -- golden/apply --
-00-first
-99-first
+00-before
+99-before
 00
 99
-00-last
-99-last
+00-after
+99-after
 -- home/user/.local/share/chezmoi/.script.cmd --
 @echo off
 set base=%~n0