fuzzyworld

template / commit

Starting to implement direct profile type. This is a profile that will just output the templated text to the terminal.

873846b9edaacf7dd81588efc03e0990e5d0b1f2 · Pavel Safronov · 2022-12-09 01:09:34 -0800 · parents 49a90ad9f5bf

Starting to implement direct profile type. This is a profile that will just output the templated text to the terminal.
 .vscode/settings.json |  3 ++
 config_test.go        | 64 +++++++++++++++--------------------
 default_config.go     | 46 ++++++++++---------------
 file.go               |  6 ++--
 file_test.go          | 12 ++-----
 main.go               | 44 ++++++++++++++++--------
 profile.go            | 94 +++++++++++++++++++++++++++++++++++----------------
 profile_test.go       | 56 +++++++++++++-----------------
 8 files changed, 169 insertions(+), 156 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3e2495f..851e655 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,12 +1,15 @@
 {
   "cSpell.words": [
     "bashdb",
+    "filetemplate",
     "logrus",
     "pwsh",
     "runpostscriptonfailure",
     "scriptshell",
     "sirupsen",
+    "snakecase",
     "stretchr",
+    "targetdirectory",
     "urfave"
   ]
 }
\ No newline at end of file
diff --git a/config_test.go b/config_test.go
index 683694d..c1a4093 100644
--- a/config_test.go
+++ b/config_test.go
@@ -115,53 +115,43 @@ func TestValidateConfig(t *testing.T) {
 			Name:         "daily",
 			IsDefault:    true,
 			NamePatterns: []ParameterizedText{"daily"},
-			File: File{
-				Template: "foo",
-			},
+			FileTemplate: "foo",
 		},
 	}}, "error validating profile")
 	testFailure(Config{Profiles: []Profile{
 		{
-			Name:         "daily",
-			IsDefault:    true,
-			NamePatterns: []ParameterizedText{"daily"},
-			Type:         FileType,
-			Path:         "foo",
-			File: File{
-				Template: "foo",
-			},
+			Name:            "daily",
+			IsDefault:       true,
+			NamePatterns:    []ParameterizedText{"daily"},
+			Type:            FileType,
+			TargetDirectory: "foo",
+			FileTemplate:    "foo",
 		},
 		{
-			Name:         "daily2",
-			IsDefault:    true,
-			NamePatterns: []ParameterizedText{"daily"},
-			Type:         FileType,
-			Path:         "foo",
-			File: File{
-				Template: "foo",
-			},
+			Name:            "daily2",
+			IsDefault:       true,
+			NamePatterns:    []ParameterizedText{"daily"},
+			Type:            FileType,
+			TargetDirectory: "foo",
+			FileTemplate:    "foo",
 		},
 	}}, "multiple default profiles found")
 	testFailure(Config{Profiles: []Profile{
 		{
-			Name:         "daily",
-			IsDefault:    true,
-			NamePatterns: []ParameterizedText{"daily"},
-			Type:         FileType,
-			Path:         "foo",
-			File: File{
-				Template: "foo",
-			},
+			Name:            "daily",
+			IsDefault:       true,
+			NamePatterns:    []ParameterizedText{"daily"},
+			Type:            FileType,
+			TargetDirectory: "foo",
+			FileTemplate:    "foo",
 		},
 		{
-			Name:         "daily",
-			IsDefault:    true,
-			NamePatterns: []ParameterizedText{"daily"},
-			Type:         FileType,
-			Path:         "foo",
-			File: File{
-				Template: "foo",
-			},
+			Name:            "daily",
+			IsDefault:       true,
+			NamePatterns:    []ParameterizedText{"daily"},
+			Type:            FileType,
+			TargetDirectory: "foo",
+			FileTemplate:    "foo",
 		},
 	}}, "duplicate profile name 'daily'")
 }
@@ -178,7 +168,7 @@ profiles:
 	// load config
 	_, err = loadConfig("./invalid-config.yaml")
 	assert.NotNil(t, err, "Expected to fail to load config, but succeeded")
-	assert.Contains(t, err.Error(), "error validating profile : Profile name is required")
+	assert.Contains(t, err.Error(), "error validating profile : profile name is required")
 }
 
 func TestSaveConfig(t *testing.T) {
@@ -202,7 +192,7 @@ func TestMergeConfigs(t *testing.T) {
 		assert.Contains(t, err.Error(), errorText)
 	}
 	testFailure(Config{}, Config{}, "no profiles found")
-	testFailure(Config{Profiles: []Profile{{}}}, Config{}, "error validating config a: error validating profile : Profile name is required")
+	testFailure(Config{Profiles: []Profile{{}}}, Config{}, "error validating config a: error validating profile : profile name is required")
 	testFailure(Config{Profiles: []Profile{defaultConfig.Profiles[0]}}, Config{}, "error validating config b: no profiles found")
 
 	profile1 := defaultConfig.Profiles[0]
diff --git a/default_config.go b/default_config.go
index c93437f..d800add 100644
--- a/default_config.go
+++ b/default_config.go
@@ -6,7 +6,7 @@ func defaultConfig() Config {
 			{
 				Name:        "daily",
 				Description: "Creates a MD file in ~/DailyNotes folder in the format YYYY-MM-DD[-vX].md",
-				Path:        "~/DailyNotes",
+				TargetDirectory:        "~/DailyNotes",
 				Type:        FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -15,8 +15,7 @@ func defaultConfig() Config {
 						"{{if (gt $version 1)}}v{{$version}}{{end}}" +
 						".md",
 				},
-				File: File{
-					Template: `# {{.FileNameWithoutExtension}}
+				FileTemplate: `# {{.FileNameWithoutExtension}}
 
 Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 
@@ -28,7 +27,6 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 4. Check yesterday's notes
 5. Have fun!
 6. ADD MORE ITEMS HERE`,
-				},
 				PostScripts: []ParameterizedText{
 					"code . {{ if .FullPath }}\"{{.FullPath}}\"{{else}}(ls -t | head -n1){{ end }}",
 				},
@@ -38,7 +36,7 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 				Name:        "note",
 				Description: "Creates a MD file in ~/Notes in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
 				OS:          "All",
-				Path:        "~/Notes",
+				TargetDirectory:        "~/Notes",
 				Type:        FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -57,7 +55,7 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 				OS:          "All",
 				Type:        FileType,
 				IsDefault:   true,
-				Path:        ".", // current folder
+				TargetDirectory:        ".", // current folder
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
@@ -83,9 +81,8 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 				Name:        "blog",
 				Description: "Creates a MD file in the ~/Blog folder in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
 				OS:          "All",
-				Path:        "~/Blog",
-				File: File{
-					Template: `# {{.ProjectName}}
+				TargetDirectory:        "~/Blog",
+				FileTemplate: `# {{.ProjectName}}
 
 Created by template, on {{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}.
 
@@ -97,7 +94,6 @@ cspell:words nanite
 
 ## Writing
 `,
-				},
 				Type: FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -114,7 +110,7 @@ cspell:words nanite
 				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",
+				TargetDirectory:        "C:/work",
 				Type:        FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -130,9 +126,8 @@ cspell:words nanite
 				Name:        "therapy",
 				Description: "Creates a MD file in the ~/Therapy folder in the format YYYY-MM-DD[-vVersion].md",
 				OS:          "All",
-				Path:        "~/Therapy",
-				File: File{
-					Template: `# {{if .ProjectName}}{{.ProjectName}}{{else}}Therapy notes{{end}}
+				TargetDirectory:        "~/Therapy",
+				FileTemplate: `# {{if .ProjectName}}{{.ProjectName}}{{else}}Therapy notes{{end}}
 
 {{if .ProjectName}}Therapy notes on {{.ProjectName}}{{else}}Therapy notes{{end}}
 
@@ -144,7 +139,6 @@ Start writing here...
 
 Sincerely, me
 `,
-				},
 				Type: FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -159,7 +153,7 @@ Sincerely, me
 			{
 				Name:        "work",
 				Description: "Creates a folder in the ~/Work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
-				Path:        "~/Work",
+				TargetDirectory:        "~/Work",
 				Type:        FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -173,25 +167,20 @@ Sincerely, me
 				},
 				Files: []FileInFolder{
 					{
-						File: File{
-							Template: `# {{.ProjectName}}
+						FileTemplate: `# {{.ProjectName}}
 
 Work notes on {{.ProjectName}}
 `,
-						},
 						Name: "README.md",
 					},
 					{
-						File: File{
-							Template: `clear
+						FileTemplate: `clear
 
 echo "Let's test {{.ProjectName}}!"`,
-						},
 						Name: "repro.sh",
 					},
 					{
-						File: File{
-							Template: `{
+						FileTemplate: `{
     "version": "0.2.0",
     "configurations": [
         {
@@ -205,7 +194,6 @@ echo "Let's test {{.ProjectName}}!"`,
     ]
 }
 							`,
-						},
 						Name: ".vscode/launch.json",
 					},
 				},
@@ -214,7 +202,7 @@ echo "Let's test {{.ProjectName}}!"`,
 				Name:        "folder",
 				Description: "Creates a folder in the current folder in the format YYYY-MM-DD[-ProjectName][-vVersion]",
 				Type:        FolderType,
-				Path:        ".", // current folder
+				TargetDirectory:        ".", // current folder
 				PostScripts: []ParameterizedText{},
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -227,7 +215,7 @@ echo "Let's test {{.ProjectName}}!"`,
 				Name:        "create-react-app",
 				Type:        FolderType,
 				Description: "Creates a folder in the current folder in the format create-react-app[-ProjectName][-vVersion], runs `npx create-react-app` to initialize the folder, and opens the folder in VSCode",
-				Path:        ".", // current folder
+				TargetDirectory:        ".", // current folder
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"create-react-app" +
@@ -242,7 +230,7 @@ echo "Let's test {{.ProjectName}}!"`,
 			{
 				Name:        "git",
 				Description: "Creates a  folder in the ~/code folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
-				Path:        "~/code",
+				TargetDirectory:        "~/code",
 				Type:        FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -259,7 +247,7 @@ echo "Let's test {{.ProjectName}}!"`,
 				Name:        "git-clone",
 				Description: "DISABLED - Clones a repository into a new folder under ~/code in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
 				IsDisabled:  true,
-				Path:        "~/code",
+				TargetDirectory:        "~/code",
 				Type:        FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
diff --git a/file.go b/file.go
index bc1c208..2be8d22 100644
--- a/file.go
+++ b/file.go
@@ -11,15 +11,15 @@ import (
 
 func createFile(
 	prePatternData PrePatternData,
-	file File,
+	fileTemplate ParameterizedText,
 	fullPath string) error {
-	useTemplate := file.Template != ""
+	useTemplate := fileTemplate != ""
 	if useTemplate {
 		postPatternData := createPostPatternData(fullPath, prePatternData)
 		log.WithFields(log.Fields{"fullPath": fullPath}).Debug("Creating file from template string\n", fullPath)
 
 		// parse template
-		template, err := template.New("text-template").Parse(string(file.Template))
+		template, err := template.New("text-template").Parse(string(fileTemplate))
 		if err != nil {
 			return fmt.Errorf("error parsing template text: %v", err)
 		}
diff --git a/file_test.go b/file_test.go
index afda356..a53896c 100644
--- a/file_test.go
+++ b/file_test.go
@@ -24,21 +24,15 @@ func TestCreateFile(t *testing.T) {
 	})
 
 	// test error path
-	err := createFile(prePatternData, File{
-		Template: "{{.ProjectName",
-	}, path)
+	err := createFile(prePatternData, "{{.ProjectName", path)
 	assert.NotNil(t, err)
 
 	// test happy path
-	err = createFile(prePatternData, File{
-		Template: "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}",
-	}, path)
+	err = createFile(prePatternData, "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}",path)
 	assert.Nil(t, err)
 
 	// test empty template
 	expectedText = ""
-	err = createFile(prePatternData, File{
-		Template: "",
-	}, path)
+	err = createFile(prePatternData, "", path)
 	assert.Nil(t, err)
 }
diff --git a/main.go b/main.go
index 852cf0a..ab3e976 100644
--- a/main.go
+++ b/main.go
@@ -30,10 +30,11 @@ type EnvVarValues struct {
 type Settings struct {
 	ArgumentValues
 	EnvVarValues
-	extras         map[string]string
-	fullConfigPath string
-	config         Config
-	usr            *user.User
+	extras           map[string]string
+	fullConfigPath   string
+	config           Config
+	usr              *user.User
+	outputToTerminal bool
 }
 
 func main() {
@@ -146,11 +147,11 @@ func main() {
 				Name:  "reset",
 				Usage: "reset the global template config to default profiles",
 				Action: func(c *cli.Context) error {
-					settings, err := createSettings(argumentValues, c)
+					usr, err := user.Current()
 					if err != nil {
-						return fmt.Errorf("error creating settings: %v", err)
+						log.Fatal(err)
 					}
-					fullConfigPath := settings.fullConfigPath
+					fullConfigPath := getFullPath(argumentValues.configFilePath, *usr)
 
 					fmt.Printf("Resetting config at '%v' to default\n", fullConfigPath)
 					err = saveConfig(fullConfigPath, defaultConfig())
@@ -187,9 +188,17 @@ func createSettings(
 	if err != nil {
 		return Settings{}, fmt.Errorf("error getting env var values: %w", err)
 	}
+
+	stat, err := os.Stdout.Stat()
+	if err != nil {
+		return Settings{}, fmt.Errorf("error getting stdout stat: %w", err)
+	}
+	outputToTerminal := (stat.Mode() & os.ModeCharDevice) == 0
+
 	settings := Settings{
-		ArgumentValues: argumentValues,
-		EnvVarValues:   envVarValues,
+		ArgumentValues:   argumentValues,
+		EnvVarValues:     envVarValues,
+		outputToTerminal: outputToTerminal,
 	}
 
 	// read args
@@ -210,8 +219,7 @@ func createSettings(
 	case "panic":
 		log.SetLevel(log.PanicLevel)
 	default:
-		fmt.Printf("invalid log level specified %v, using info", settings.logLevel)
-		log.SetLevel(log.InfoLevel)
+		return Settings{}, fmt.Errorf("invalid log level specified %v, using info", settings.logLevel)
 	}
 	log.SetOutput(os.Stdout)
 	// log.SetFormatter(&log.JSONFormatter{})
@@ -223,14 +231,18 @@ func createSettings(
 	if settings.profile == "" && args.Len() > 0 {
 		settings.profile = args.Get(0)
 	}
-	fmt.Println("Profile:", settings.profile)
+	if !outputToTerminal {
+		fmt.Println("Profile:", settings.profile)
+	}
 
 	// get project name from args
 	// if not set explicitly, use second arg, if present
 	if settings.projectName == "" && args.Len() > 1 {
 		settings.projectName = args.Get(1)
 	}
-	fmt.Println("Project name:", settings.projectName)
+	if !outputToTerminal {
+		fmt.Println("Project name:", settings.projectName)
+	}
 
 	// extras strings
 	_, err = os.Stat(settings.extrasFilePath)
@@ -260,7 +272,7 @@ func createSettings(
 		config, err = loadConfig(fullConfigPath)
 		if err != nil {
 			return settings, fmt.Errorf("error loading config from %v: %w", fullConfigPath, err)
-		} else {
+		} else if !outputToTerminal {
 			fmt.Printf("Loaded config from %v\n", fullConfigPath)
 		}
 		// load user config, if present
@@ -271,7 +283,9 @@ func createSettings(
 			if err != nil {
 				return settings, fmt.Errorf("error loading user config from %v: %w", fullUserConfigPath, err)
 			} else {
-				fmt.Printf("Loaded user config from %v, combining with global config\n", fullUserConfigPath)
+				if !outputToTerminal {
+					fmt.Printf("Loaded user config from %v, combining with global config\n", fullUserConfigPath)
+				}
 				config, err = mergeConfigs(config, userConfig)
 				if err != nil {
 					return settings, fmt.Errorf("error merging configs: %w", err)
diff --git a/profile.go b/profile.go
index 705c66e..ca7d90d 100644
--- a/profile.go
+++ b/profile.go
@@ -19,8 +19,16 @@ const (
 	FileType   ProfileType = "file"
 	FolderType ProfileType = "folder"
 	ScriptType ProfileType = "script"
+	DirectType ProfileType = "direct"
 )
 
+var allProfileTypes = []ProfileType{
+	FileType,
+	FolderType,
+	ScriptType,
+	DirectType,
+}
+
 type OSType string
 
 const (
@@ -31,13 +39,9 @@ const (
 
 type ParameterizedText string
 
-type File struct {
-	Template ParameterizedText `yaml:"template,omitempty"`
-}
-
 type FileInFolder struct {
-	File `yaml:"file,omitempty"`
-	Name ParameterizedText
+	Name         ParameterizedText
+	FileTemplate ParameterizedText `yaml:"file,omitempty"`
 }
 
 type Profile struct {
@@ -46,13 +50,15 @@ type Profile struct {
 	IsDefault              bool
 	IsDisabled             bool
 	Type                   ProfileType
-	Path                   string `yaml:"path,omitempty"`
-	OS                     OSType `yaml:"os,omitempty"`
-	NamePatterns           []ParameterizedText
-	Files                  []FileInFolder      `yaml:"files,omitempty"`
+	OS                     OSType              `yaml:"os,omitempty"`
 	RunPostscriptOnFailure bool                `yaml:"runpostscriptonfailure,omitempty"`
 	PostScripts            []ParameterizedText `yaml:"postscripts,omitempty"`
-	File                   `yaml:"file,omitempty"`
+
+	NamePatterns []ParameterizedText
+
+	TargetDirectory string            `yaml:"targetdirectory,omitempty"`
+	FileTemplate    ParameterizedText `yaml:"filetemplate,omitempty"`
+	Files           []FileInFolder    `yaml:"files,omitempty"`
 }
 
 var forbiddenProfileNames = []string{
@@ -74,17 +80,20 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 		(profileOs == "Mac" && !isWindows)
 	if !shouldRun {
 		return []error{
-			fmt.Errorf("This profile is not supported on this OS, doing nothing. Profile OS: %s, system OS: %s. Supported OS values: Windows, Mac, All", profileOs, runtime.GOOS),
+			fmt.Errorf("this profile is not supported on this OS, doing nothing. Profile OS: %s, system OS: %s. Supported OS values: Windows, Mac, All", profileOs, runtime.GOOS),
 		}
 	}
 
+	// determine if we're dealing with files or directories
+	profileType := profile.Type
+
 	cwd, err := monkey.OSGetwd()
-	if err != nil {
+	if err != nil && profileType != DirectType {
 		return []error{fmt.Errorf("unable to get current working directory: %v", err)}
 	}
-	if profile.Path != "" {
+	if profile.TargetDirectory != "" {
 		// change cwd to the profile's path, if one is specified
-		path := getFullPath(profile.Path, usr)
+		path := getFullPath(profile.TargetDirectory, usr)
 		err = monkey.OSChdir(path)
 		if err != nil {
 			return []error{fmt.Errorf("unable to change directory (1) to %v: %v", path, err)}
@@ -92,13 +101,11 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 		cwd = path
 	}
 
-	// determine if we're dealing with files or directories
-	profileType := profile.Type
-
 	errors := []error{}
 	fullPath := ""
 	now := time.Now()
 	prePatternData := createPrePatternData(now, projectName, extras)
+
 	if profileType == FileType || profileType == FolderType {
 		// get all entities in current directory
 		files, err := monkey.OSReadDir(cwd)
@@ -248,7 +255,7 @@ func createEntity(
 					}
 				}
 				fmt.Printf("Creating sub file: %v\n", subFilePath)
-				err = createFile(filePrePatternData, file.File, subFilePath)
+				err = createFile(filePrePatternData, file.FileTemplate, subFilePath)
 				if err != nil {
 					return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
 				}
@@ -256,7 +263,7 @@ func createEntity(
 		}
 	} else if profileType == FileType {
 		fmt.Printf("Creating file: %v\n", fullPath)
-		err := createFile(prePatternData, profile.File, fullPath)
+		err := createFile(prePatternData, profile.FileTemplate, fullPath)
 		if err != nil {
 			return "", err
 		}
@@ -267,12 +274,15 @@ func createEntity(
 }
 
 func validateProfile(profile *Profile) error {
+	// check name
 	if profile.Name == "" {
-		return fmt.Errorf("Profile name is required")
+		return fmt.Errorf("profile name is required")
 	}
 	if slices.Contains(forbiddenProfileNames, profile.Name) {
 		return fmt.Errorf("forbidden profile name '%s'", profile.Name)
 	}
+
+	// check name patterns
 	if profile.NamePatterns == nil || len(profile.NamePatterns) == 0 {
 		return fmt.Errorf("namePatterns is required")
 	}
@@ -281,23 +291,47 @@ func validateProfile(profile *Profile) error {
 			return fmt.Errorf("name pattern is required for pattern index %v of profile %v", index, profile.Name)
 		}
 	}
-	if profile.Type == "" || (profile.Type != FileType && profile.Type != FolderType && profile.Type != ScriptType) {
+
+	// check type
+	if profile.Type == "" || !slices.Contains(allProfileTypes, profile.Type) {
 		return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name)
 	}
-	if profile.Type != FileType && profile.Template != "" {
-		return fmt.Errorf("file template is not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
+
+	// check FileType profile
+	if profile.Type == FileType {
+		if profile.TargetDirectory == "" {
+			return fmt.Errorf("targetDirectory is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
+		}
 	}
-	if profile.Type == FileType && profile.Path == "" {
-		return fmt.Errorf("path is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
+
+	// check FolderType profile
+	if profile.Type == FolderType {
+		if profile.TargetDirectory == "" {
+			return fmt.Errorf("targetDirectory is not specified for profile %s, which has Type=%s", profile.Name, profile.Type)
+		}
 	}
-	if profile.Type == FolderType && profile.Path == "" {
-		return fmt.Errorf("path is required for profile %s, which has Type=%s", profile.Name, profile.Type)
+
+	// check ScriptType profile
+	if profile.Type == ScriptType {
+		if profile.NamePatterns != nil && len(profile.NamePatterns) > 1 {
+			return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
+		}
 	}
-	if profile.Type == ScriptType && profile.NamePatterns != nil && len(profile.NamePatterns) > 1 {
-		return fmt.Errorf("multiple name patterns are not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
+
+	// check DirectType profile
+	if profile.Type == DirectType {
+		if profile.TargetDirectory != "" {
+			return fmt.Errorf("targetDirectory is not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
+		}
+		if profile.FileTemplate != "" {
+			return fmt.Errorf("fileTemplate is not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
+		}
 	}
+
+	// set files to empty array if nil
 	if profile.Files == nil {
 		profile.Files = []FileInFolder{}
 	}
+
 	return nil
 }
diff --git a/profile_test.go b/profile_test.go
index e661e64..ee435fc 100644
--- a/profile_test.go
+++ b/profile_test.go
@@ -26,24 +26,22 @@ func TestValidateProfile(t *testing.T) {
 	}
 
 	testSuccess(Profile{
-		Type:         FileType,
-		Name:         "daily",
-		NamePatterns: []ParameterizedText{"{{.Date}}"},
-		Path:         "~/test/dir",
+		Type:            FileType,
+		Name:            "daily",
+		NamePatterns:    []ParameterizedText{"{{.Date}}"},
+		TargetDirectory: "~/test/dir",
 	})
 	testSuccess(Profile{
-		Type:         FileType,
-		Name:         "daily",
-		NamePatterns: []ParameterizedText{"{{.Date}}"},
-		File: File{
-			Template: "foo",
-		},
-		Path: "~/test/dir",
+		Type:            FileType,
+		Name:            "daily",
+		NamePatterns:    []ParameterizedText{"{{.Date}}"},
+		FileTemplate:    "foo",
+		TargetDirectory: "~/test/dir",
 	})
 
 	testFailure(Profile{
 		Name: "",
-	}, "Profile name is required")
+	}, "profile name is required")
 	testFailure(Profile{
 		Name: "list",
 	}, "forbidden profile name 'list'")
@@ -58,21 +56,19 @@ func TestValidateProfile(t *testing.T) {
 		Type:         FileType,
 		Name:         "daily",
 		NamePatterns: []ParameterizedText{"blah"},
-	}, "path is not specified for profile daily, which has Type=file")
+	}, "targetDirectory is not specified for profile daily, which has Type=file")
 
 	testFailure(Profile{
 		Type:         FolderType,
 		Name:         "daily",
 		NamePatterns: []ParameterizedText{"test"},
-	}, "path is required for profile daily, which has Type=folder")
+	}, "targetDirectory is not specified for profile daily, which has Type=folder")
 	testFailure(Profile{
 		Type:         FolderType,
 		Name:         "daily",
 		NamePatterns: []ParameterizedText{"test"},
-		File: File{
-			Template: "bar",
-		},
-	}, "file template is not supported for profile daily, which has Type=folder")
+		FileTemplate: "bar",
+	}, "targetDirectory is not specified for profile daily, which has Type=folder")
 	testFailure(Profile{
 		Type:         ScriptType,
 		Name:         "daily",
@@ -82,7 +78,7 @@ func TestValidateProfile(t *testing.T) {
 		Type:         FileType,
 		Name:         "daily",
 		NamePatterns: []ParameterizedText{"test", "test2"},
-	}, "path is not specified for profile daily, which has Type=file")
+	}, "targetDirectory is not specified for profile daily, which has Type=file")
 	testFailure(Profile{
 		Type:         FileType,
 		Name:         "daily",
@@ -163,16 +159,12 @@ func TestCreateEntity(t *testing.T) {
 			Type: FolderType,
 			Files: []FileInFolder{
 				{
-					Name: "readme.md",
-					File: File{
-						Template: "blah",
-					},
+					Name:         "readme.md",
+					FileTemplate: "blah",
 				},
 				{
-					Name: "test.txt",
-					File: File{
-						Template: "foo",
-					},
+					Name:         "test.txt",
+					FileTemplate: "foo",
 				},
 			},
 		}
@@ -199,10 +191,8 @@ func TestCreateEntity(t *testing.T) {
 		})
 
 		profile := Profile{
-			Type: FileType,
-			File: File{
-				Template: "blah",
-			},
+			Type:         FileType,
+			FileTemplate: "blah",
 		}
 		newName := "new-name.md"
 		expectedFullPath := filepath.Clean("~/test/dir/" + newName)
@@ -313,7 +303,7 @@ func TestProcessProfile(t *testing.T) {
 	testSingleFailure(profile, "Profile is disabled, doing nothing")
 	profile.IsDisabled = false
 	profile.OS = getOtherOs(currentOS)
-	testSingleFailure(profile, "This profile is not supported on this OS, doing nothing.")
+	testSingleFailure(profile, "this profile is not supported on this OS, doing nothing.")
 	profile.OS = currentOS
 	// testSingleFailure(profile, "unable to create post scripts: no post scripts specified")
 
@@ -327,7 +317,7 @@ func TestProcessProfile(t *testing.T) {
 
 	profile.NamePatterns = []ParameterizedText{"file3"}
 	profile.PostScripts = []ParameterizedText{"echo {{.ProjectName}}-{{.Year}}"}
-	profile.Path = "~/test/dir"
+	profile.TargetDirectory = "~/test/dir"
 	testSuccess(profile)
 	assert.Equal(t, 1, len(monkeyPatchCalls.ExecCommandCalls), "calls to ExecCommand")
 	assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls), "calls to OSChdir")