template / commit
Lots of small-ish changes:
Makefile | 2 ++ config.go | 106 ++++++++++++++++++++++++++++++++++-------------------- config_test.go | 22 ++++-------- default_config.go | 37 +++++++++---------- extras.go | 14 ++++---- extras_test.go | 32 ++++++++--------- main.go | 85 ++++++++++++++++++++++++++++--------------- profile.go | 30 +++++----------- profile_test.go | 17 ++++++--- 9 files changed, 198 insertions(+), 147 deletions(-) diff --git a/Makefile b/Makefile index c5736b3..7163fb6 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ name = "" .PHONY: test +all: build test install + build: go build . diff --git a/config.go b/config.go index e514851..afd393b 100644 --- a/config.go +++ b/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "golang.org/x/exp/slices" "gopkg.in/yaml.v3" ) @@ -16,18 +17,29 @@ func validateConfig(config *Config) error { return fmt.Errorf("no profiles found") } + names := []string{} foundDefault := false for index, profile := range config.Profiles { profile := profile + + // check for duplicate names + if slices.Contains(names, profile.Name) { + return fmt.Errorf("duplicate profile name '%v'", profile.Name) + } + names = append(names, profile.Name) + + // check for multiple default profiles if profile.IsDefault { if foundDefault { return fmt.Errorf("multiple default profiles found") } foundDefault = true } - error := validateProfile(&profile) - if error != nil { - return fmt.Errorf("error validating profile %s: %s", profile.Name, error) + + // validate and possibly fix profile + err := validateProfile(&profile) + if err != nil { + return fmt.Errorf("error validating profile %s: %s", profile.Name, err) } config.Profiles[index] = profile } @@ -35,64 +47,82 @@ func validateConfig(config *Config) error { } func loadConfig(path string) (Config, error) { - file, error := os.ReadFile(path) - if error != nil { - return Config{}, error + file, err := os.ReadFile(path) + if err != nil { + return Config{}, err } var config Config - error = yaml.Unmarshal(file, &config) - if error != nil { - return Config{}, error + err = yaml.Unmarshal(file, &config) + if err != nil { + return Config{}, err + } + + err = validateConfig(&config) + if err != nil { + return Config{}, err } return config, nil } func saveConfig(path string, config Config) error { - file, error := os.Create(path) - if error != nil { - return error + err := validateConfig(&config) + if err != nil { + return err + } + file, err := os.Create(path) + if err != nil { + return err } defer file.Close() encoder := yaml.NewEncoder(file) - error = encoder.Encode(config) - if error != nil { - return error + err = encoder.Encode(config) + if err != nil { + return err } return nil } -func getOrCreateConfig(configFilePath string) (Config, error) { - emptyConfig := Config{} - - // try and load config - config, err := loadConfig(configFilePath) +func mergeConfigs(a Config, b Config) (Config, error) { + err := validateConfig(&a) + if err != nil { + return Config{}, err + } + err = validateConfig(&b) if err != nil { - fmt.Printf("No config present at '%v', creating and using sample config\n", configFilePath) + return Config{}, err + } - // create config and save - err := saveConfig(configFilePath, defaultConfig()) - if err != nil { - return emptyConfig, fmt.Errorf("error creating config: %v", err) + merged := Config{ + Profiles: a.Profiles, + } + for _, profile := range b.Profiles { + index, err := findProfileIndex(merged.Profiles, profile.Name) + if err == nil { + merged.Profiles[index] = profile + } else { + merged.Profiles = append(merged.Profiles, profile) } + } + return merged, nil +} - // load config - config, err = loadConfig(configFilePath) - if err != nil { - return emptyConfig, fmt.Errorf("error loading config: %v", err) +func findDefaultProfile(profiles []Profile) (Profile, error) { + for _, p := range profiles { + if p.IsDefault { + return p, nil } - } else { - // log successful load - fmt.Printf("Config loaded from '%v'\n", configFilePath) } + return Profile{}, fmt.Errorf("unable to find default profile") +} - // validate config - err = validateConfig(&config) - if err != nil { - return emptyConfig, fmt.Errorf("error validating config: %v", err) +func findProfileIndex(profiles []Profile, profileName string) (int, error) { + for index, p := range profiles { + if p.Name == profileName { + return index, nil + } } - - return config, nil + return 0, fmt.Errorf("unable to find profile '%v'", profileName) } diff --git a/config_test.go b/config_test.go index b7b7577..01a7528 100644 --- a/config_test.go +++ b/config_test.go @@ -44,9 +44,12 @@ func TestSaveLoadConfig(t *testing.T) { _, err = os.Stat(configPath) assert.True(t, errors.Is(err, os.ErrNotExist), "Expected config file to not exist, but it does") - // get and create config - loadedConfig, err = getOrCreateConfig(configPath) - assert.Nil(t, err, "Error loading config") + // fail to get config + _, err = loadConfig(configPath) + assert.NotNil(t, err, "Expected to fail to load config, but succeeded") + + // create default config + err = saveConfig(configPath, defaultConfig()) // test config assert.Equal(t, loadedConfig, config) @@ -56,7 +59,7 @@ func TestSaveLoadConfig(t *testing.T) { assert.Nil(t, err, "Expected config file to exist, but it does not") // get and create config - loadedConfig, err = getOrCreateConfig(configPath) + loadedConfig, err = loadConfig(configPath) assert.Nil(t, err, "Error loading config") // test config @@ -68,17 +71,6 @@ func TestSaveLoadConfig(t *testing.T) { assert.Equal(t, stat1.ModTime(), stat2.ModTime(), "Expected config file to not be modified, but it was") } -func TestGetOrCreateConfig(t *testing.T) { - testFailure := func(configPath string) { - _, err := getOrCreateConfig(configPath) - if err == nil { - t.Errorf("Expected to fail to get or create config, but succeeded") - } - } - - testFailure("invalid/path/invalid-config.yaml") -} - func TestValidateDefaultConfig(t *testing.T) { config := defaultConfig() err := validateConfig(&config) diff --git a/default_config.go b/default_config.go index 35981c4..aa6d0de 100644 --- a/default_config.go +++ b/default_config.go @@ -107,23 +107,23 @@ cspell:words nanite "code . \"{{.FileName}}\"", }, }, - // { - // Name: "work", - // Description: "Creates a folder in the c:/work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]", - // OS: "Windows", - // Path: "C:/work", - // Type: FolderType, - // NamePatterns: []string{ - // "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - // "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - // "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - // "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", - // }, - // ScriptShell: "pwsh", - // PostScripts: []string{ - // "explorer \"{{.Path}}\"", - // }, - // }, + { + Name: "work-win", + Description: "Creates a folder in the c:/work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]", + OS: "Windows", + Path: "C:/work", + Type: FolderType, + NamePatterns: []ParameterizedText{ + "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + "{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", + }, + ScriptShell: "pwsh", + PostScripts: []ParameterizedText{ + "explorer \"{{.Path}}\"", + }, + }, { Name: "therapy", Description: "Creates a MD file in the ~/Therapy folder in the format YYYY-MM-DD[-vVersion].md", @@ -240,13 +240,14 @@ echo "Let's test {{.ProjectName}}!"`, }, { Name: "test", - Description: "Echoes the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]", + Description: "Echoes formatted date and time, and project name if it is provided", Type: ScriptType, NamePatterns: []ParameterizedText{ "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}", }, ScriptShell: "pwsh", PostScripts: []ParameterizedText{ + "echo \"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}\"", "echo \"Name={{.FileName}}\"", "echo \"Path={{.FullPath}}\"", "echo \"Value1={{.Extras.value1}}\"", diff --git a/extras.go b/extras.go index 2ecd856..087ae1e 100644 --- a/extras.go +++ b/extras.go @@ -10,19 +10,19 @@ import ( func loadExtras(path string) (map[string]string, error) { emptyMap := make(map[string]string) - file, error := os.ReadFile(path) - if error != nil { - if os.IsNotExist(error) { + file, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { fmt.Printf("extras file %s does not exist, extras not being used\n", path) return emptyMap, nil } - return emptyMap, error + return emptyMap, err } result := make(map[string]string) - error = yaml.Unmarshal(file, &result) - if error != nil { - return emptyMap, error + err = yaml.Unmarshal(file, &result) + if err != nil { + return emptyMap, err } return result, nil diff --git a/extras_test.go b/extras_test.go index b252313..5fe946c 100644 --- a/extras_test.go +++ b/extras_test.go @@ -16,9 +16,9 @@ func TestExtras(t *testing.T) { "customer": "abc", } - file, error := os.CreateTemp("", "extras.yaml") - if error != nil { - t.Error("Error creating temp file: ", error) + file, err := os.CreateTemp("", "extras.yaml") + if err != nil { + t.Error("Error creating temp file: ", err) } // cleanup code @@ -29,34 +29,34 @@ func TestExtras(t *testing.T) { // write extras to file encoder := yaml.NewEncoder(file) - error = encoder.Encode(extras) - if error != nil { - t.Error("Error encoding extras: ", error) + err = encoder.Encode(extras) + if err != nil { + t.Error("Error encoding extras: ", err) } // close file - error = file.Close() - if error != nil { - t.Error("Error closing file: ", error) + err = file.Close() + if err != nil { + t.Error("Error closing file: ", err) } // file name fileName := file.Name() // happy path - loadedExtras, error := loadExtras(fileName) - if error != nil { - t.Error("Error loading extras: ", error) + loadedExtras, err := loadExtras(fileName) + if err != nil { + t.Error("Error loading extras: ", err) } assert.Equal(t, extras, loadedExtras) // test path errors - extras, error = loadExtras("non-existent-file.yaml") - assert.Nil(t, error, "Expected to load extras") + extras, err = loadExtras("non-existent-file.yaml") + assert.Nil(t, err, "Expected to load extras") assert.Empty(t, extras, "Expected to load empty extras") os.WriteFile(file.Name(), []byte("invalid yaml"), 0644) - _, error = loadExtras(fileName) - assert.NotNil(t, error, "Expected to fail to parse extras, but succeeded") + _, err = loadExtras(fileName) + assert.NotNil(t, err, "Expected to fail to parse extras, but succeeded") } diff --git a/main.go b/main.go index b6aedbd..2a9468a 100644 --- a/main.go +++ b/main.go @@ -11,12 +11,15 @@ import ( "gopkg.in/yaml.v3" ) +const defaultUserConfigPath = "~/.template.user.yaml" +const defaultConfigPath = "~/.template.yaml" + type ArgumentValues struct { - profile string - projectName string - configFilePath string - extrasFilePath string - useDefaultConfig bool + profile string + projectName string + configFilePath string + userConfigPath string + extrasFilePath string } type Settings struct { ArgumentValues @@ -55,9 +58,16 @@ func main() { Name: "config-yaml", Aliases: []string{"c"}, Usage: "path to the config yaml file", - Value: "~/.template.yaml", + Value: defaultConfigPath, Destination: &argumentValues.configFilePath, }, + &cli.PathFlag{ + Name: "user-config-yaml", + Aliases: []string{"u"}, + Usage: "path to the user config yaml file", + Value: defaultUserConfigPath, + Destination: &argumentValues.userConfigPath, + }, &cli.PathFlag{ Name: "extras-yaml", Aliases: []string{"e"}, @@ -65,13 +75,6 @@ func main() { Value: "~/.template-extras.yaml", Destination: &argumentValues.extrasFilePath, }, - &cli.BoolFlag{ - Name: "default-config", - Aliases: []string{"d"}, - Usage: "use the default hard-coded config (testing only)", - Value: false, - Destination: &argumentValues.useDefaultConfig, - }, }, Action: func(context *cli.Context) error { settings, err := parseArguments(argumentValues, context) @@ -117,12 +120,15 @@ func parseArguments( } fmt.Println("Project name:", settings.projectName) - // extra features - extras, err := loadExtras(settings.extrasFilePath) - if err != nil { - return settings, fmt.Errorf("error loading extras from %v: %w", settings.extrasFilePath, err) + // extras strings + _, err := os.Stat(settings.extrasFilePath) + if err == nil { + extras, err := loadExtras(settings.extrasFilePath) + if err != nil { + return settings, fmt.Errorf("error loading extras from %v: %w", settings.extrasFilePath, err) + } + settings.extras = extras } - settings.extras = extras return settings, nil } @@ -133,15 +139,28 @@ func mainLogic(settings Settings) error { log.Fatal(err) } - // get config + // get config from default path fullConfigPath := getFullPath(settings.configFilePath, *usr) - var config Config - if settings.useDefaultConfig { - config = defaultConfig() + config, err := loadConfig(fullConfigPath) + if err != nil { + return fmt.Errorf("error loading config from %v: %w", fullConfigPath, err) } else { - config, err = getOrCreateConfig(fullConfigPath) + fmt.Printf("Loaded config from %v\n", fullConfigPath) + } + + // load user config, if present + fullUserConfigPath := getFullPath(settings.userConfigPath, *usr) + _, err = os.Stat(fullUserConfigPath) + if err == nil { + userConfig, err := loadConfig(fullUserConfigPath) if err != nil { - return err + fmt.Printf("error loading user config from %v: %v\n", fullUserConfigPath, err) + } else { + fmt.Printf("loaded user config from %v, combining with global config\n", fullUserConfigPath) + config, err = mergeConfigs(config, userConfig) + if err != nil { + return fmt.Errorf("error merging configs: %w", err) + } } } @@ -157,10 +176,11 @@ func mainLogic(settings Settings) error { // get profile information if settings.profile == "get" { fmt.Printf("Profile '%v':\n", settings.projectName) - profile, err := findProfile(config, settings.projectName) + index, err := findProfileIndex(config.Profiles, settings.projectName) if err != nil { return fmt.Errorf("profile '%v' not found", settings.projectName) } + profile := config.Profiles[index] writer := strings.Builder{} encoder := yaml.NewEncoder(&writer) @@ -185,9 +205,18 @@ func mainLogic(settings Settings) error { } // find profile - profile, err := findProfile(config, settings.profile) - if err != nil { - return fmt.Errorf("error finding profile: %v", err) + var profile Profile + if settings.profile == "" { + profile, err = findDefaultProfile(config.Profiles) + if err != nil { + return fmt.Errorf("no profile specified and no default profile found") + } + } else { + index, err := findProfileIndex(config.Profiles, settings.profile) + if err != nil { + return fmt.Errorf("error finding profile: %v", err) + } + profile = config.Profiles[index] } // process profile diff --git a/profile.go b/profile.go index 00dc2c3..1fff7f4 100644 --- a/profile.go +++ b/profile.go @@ -1,7 +1,7 @@ package main import ( - "errors" + "bytes" "fmt" "os" "os/user" @@ -60,25 +60,6 @@ var forbiddenProfileNames = []string{ "get", } -func findProfile(config Config, profileName string) (Profile, error) { - if profileName == "" { - // find default profile - for _, p := range config.Profiles { - if p.IsDefault { - return p, nil - } - } - return Profile{}, errors.New("no default profile found") - } - // find profile by name - for _, p := range config.Profiles { - if p.Name == profileName { - return p, nil - } - } - return Profile{}, fmt.Errorf("unable to find profile '%v'", profileName) -} - func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User) []error { // decide if the profile should run on this OS profileOs := profile.OS @@ -89,6 +70,9 @@ func processProfile(profile Profile, projectName string, extras map[string]strin (profileOs == "Mac" && !isWindows) if !shouldRun { fmt.Println("This profile is not supported by this OS") + fmt.Println("Supported OS values: Windows, Mac, All") + fmt.Println("Current OS:", runtime.GOOS) + fmt.Println("Profile OS:", profileOs) return nil } @@ -167,7 +151,11 @@ func processProfile(profile Profile, projectName string, extras map[string]strin shell := profile.ScriptShell fmt.Printf("Executing post script: %v %v\n", shell, strings.Join(allArgs, " ")) cmd := monkey.ExecCommand(shell, allArgs...) - if err := cmd.Run(); err != nil { + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + fmt.Printf("Post script output: %v", out.String()) + if err != nil { // post scripts tend to fail (TODO: investigate why), so for now we'll just log the error fmt.Printf("Error executing post script '%v': %v\n", diff --git a/profile_test.go b/profile_test.go index 683aac4..c70ed53 100644 --- a/profile_test.go +++ b/profile_test.go @@ -95,7 +95,7 @@ func TestValidateProfile(t *testing.T) { } } -func TestFindProfile(t *testing.T) { +func TestFindProfileIndex(t *testing.T) { config := Config{ Profiles: []Profile{ { @@ -108,25 +108,26 @@ func TestFindProfile(t *testing.T) { }, } testSuccess := func(profileName string, expectedProfileName string) { - profile, err := findProfile(config, profileName) + index, err := findProfileIndex(config.Profiles, profileName) if err != nil { t.Errorf("Failed to find profile '%v': %v", profileName, err) } + profile := config.Profiles[index] if profile.Name != expectedProfileName { t.Errorf("Asked for profile '%v', expected '%v', got '%v'", profileName, expectedProfileName, profile.Name) } } testFailure := func(profileName string) { - _, err := findProfile(config, profileName) + _, err := findProfileIndex(config.Profiles, profileName) if err == nil { t.Errorf("Expected to fail to find profile '%v', but succeeded", profileName) } } - testSuccess("", "notes") testSuccess("daily", "daily") testSuccess("notes", "notes") + testFailure("") testFailure("non-existent") config.Profiles[1].IsDefault = false @@ -134,6 +135,14 @@ func TestFindProfile(t *testing.T) { testFailure("non-default") } +func TestFindDefaultProfile(t *testing.T) { + _, err := findDefaultProfile(defaultConfig().Profiles) + assert.Nil(t, err, "Expected to find default profile, but failed") + + _, err = findDefaultProfile([]Profile{}) + assert.NotNil(t, err, "Expected to fail to find default profile, but succeeded") +} + func TestCreateEntity(t *testing.T) { // setup now := testNow()