fuzzyworld

template / commit

Merge branch 'files-in-folder' into 'main'

a99ff077685c15fb49db5a490a6d6529ea7eb32b · Pavel S · 2022-10-21 22:38:13 +0000 · parents ad5348d57ac9 5e7795dc9a8e

Merge branch 'files-in-folder' into 'main'

Support creating files inside a created folder

See merge request fuzzy-rp/template!4
 config.go            |  10 +++--
 config_test.go       |  22 +++++++----
 default_config.go    |  85 ++++++++++++++++++++++++---------------
 extras_test.go       |   5 ++-
 file.go              |   6 +--
 name_pattern.go      |   8 ++--
 name_pattern_test.go |  10 ++---
 postscript.go        |   2 +-
 postscript_test.go   |   8 ++--
 profile.go           | 109 ++++++++++++++++++++++++++++++++++-----------------
 profile_test.go      |  58 +++++++++++++++------------
 11 files changed, 200 insertions(+), 123 deletions(-)

diff --git a/config.go b/config.go
index 8058844..7438b09 100644
--- a/config.go
+++ b/config.go
@@ -12,23 +12,25 @@ type Config struct {
 	Profiles []Profile
 }
 
-func validateConfig(config Config) error {
+func validateConfig(config *Config) error {
 	if config.Profiles == nil || len(config.Profiles) == 0 {
 		return fmt.Errorf("no profiles found")
 	}
 
 	foundDefault := false
-	for _, profile := range config.Profiles {
+	for index, profile := range config.Profiles {
+		profile := profile
 		if profile.IsDefault {
 			if foundDefault {
 				return fmt.Errorf("multiple default profiles found")
 			}
 			foundDefault = true
 		}
-		error := validateProfile(profile)
+		error := validateProfile(&profile)
 		if error != nil {
 			return fmt.Errorf("error validating profile %s: %s", profile.Name, error)
 		}
+		config.Profiles[index] = profile
 	}
 	return nil
 }
@@ -90,7 +92,7 @@ func getOrCreateConfig(configFilePath string, usr user.User) (Config, error) {
 	}
 
 	// validate config
-	err = validateConfig(config)
+	err = validateConfig(&config)
 	if err != nil {
 		return emptyConfig, fmt.Errorf("error validating config: %v", err)
 	}
diff --git a/config_test.go b/config_test.go
index 59a30ff..6d30a6f 100644
--- a/config_test.go
+++ b/config_test.go
@@ -86,7 +86,7 @@ func TestGetOrCreateConfig(t *testing.T) {
 
 func TestValidateDefaultConfig(t *testing.T) {
 	config := defaultConfig()
-	err := validateConfig(config)
+	err := validateConfig(&config)
 	if err != nil {
 		t.Errorf("Expected to validate config, but failed: %v", err)
 	}
@@ -94,7 +94,7 @@ func TestValidateDefaultConfig(t *testing.T) {
 
 func TestValidateConfig(t *testing.T) {
 	testFailure := func(config Config, errorText string) {
-		err := validateConfig(config)
+		err := validateConfig(&config)
 		if err == nil {
 			t.Errorf("Expected to fail to validate config, but succeeded")
 		}
@@ -107,24 +107,30 @@ func TestValidateConfig(t *testing.T) {
 		{
 			Name:         "daily",
 			IsDefault:    true,
-			NamePatterns: []string{"daily"},
-			FileTemplate: "foo",
+			NamePatterns: []ParameterizedText{"daily"},
+			File: File{
+				Template: "foo",
+			},
 		},
 	}}, "error validating profile")
 	testFailure(Config{Profiles: []Profile{
 		{
 			Name:         "daily",
 			IsDefault:    true,
-			NamePatterns: []string{"daily"},
+			NamePatterns: []ParameterizedText{"daily"},
 			Type:         FileType,
-			FileTemplate: "foo",
+			File: File{
+				Template: "foo",
+			},
 		},
 		{
 			Name:         "daily2",
 			IsDefault:    true,
-			NamePatterns: []string{"daily"},
+			NamePatterns: []ParameterizedText{"daily"},
 			Type:         FileType,
-			FileTemplate: "foo",
+			File: File{
+				Template: "foo",
+			},
 		},
 	}}, "multiple default profiles found")
 }
diff --git a/default_config.go b/default_config.go
index c6ab10d..698d955 100644
--- a/default_config.go
+++ b/default_config.go
@@ -1,27 +1,30 @@
 package main
 
 func defaultConfig() Config {
-	return Config{
+	config := Config{
 		Profiles: []Profile{
 			{
 				Name:         "daily",
 				Description:  "Creates a MD file in ~/Dropbox/DailyNotes folder in the format YYYY-MM-DD[-vX].md",
 				Path:         "~/Dropbox/DailyNotes",
 				Type:         FileType,
-				NamePatterns: []string{"{{.Year}}-{{.Month}}-{{.Day}}.md"},
-				FileTemplate: `# {{.Year}}-{{.Month}}-{{.Day}}
+				NamePatterns: []ParameterizedText{"{{.Year}}-{{.Month}}-{{.Day}}.md"},
+				File: File{
+					Template: `# {{.Year}}-{{.Month}}-{{.Day}}
 
 Today's notes
 
 ## Todo
 
-1. [ ] Check email
-2. [ ] Check chat
-3. [ ] Check calendar
-4. [ ] Check yesterday's notes
-5. [ ] Have fun!`,
+1. Check email
+2. Check chat
+3. Check calendar
+4. Check yesterday's notes
+5. Have fun!
+6. ADD MORE ITEMS HERE`,
+				},
 				ScriptShell: "pwsh",
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"code . \"{{.Path}}\"",
 				},
 			},
@@ -32,7 +35,7 @@ Today's notes
 				Path:        "~/Dropbox/Notes",
 				IsDefault:   true,
 				Type:        FileType,
-				NamePatterns: []string{
+				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
 						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
@@ -40,18 +43,20 @@ Today's notes
 						".md",
 				},
 				ScriptShell: "pwsh",
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"code \"{{.Path}}\"",
 				},
 			},
 			{
-				Name:             "blog",
-				Description:      "Creates a MD file in the ~/Dropbox/Blog folder in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
-				OS:               "All",
-				Path:             "~/Dropbox/Blog",
-				FileTemplatePath: "~/.templates/blog-template.md",
-				Type:             FileType,
-				NamePatterns: []string{
+				Name:        "blog",
+				Description: "Creates a MD file in the ~/Dropbox/Blog folder in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
+				OS:          "All",
+				Path:        "~/Dropbox/Blog",
+				File: File{
+					TemplatePath: "~/.templates/blog-template.md",
+				},
+				Type: FileType,
+				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
 						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
@@ -59,7 +64,7 @@ Today's notes
 						".md",
 				},
 				ScriptShell: "pwsh",
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"code . \"{{.Name}}\"",
 				},
 			},
@@ -85,7 +90,8 @@ Today's notes
 				Description: "Creates a MD file in the ~/Dropbox/Therapy folder in the format YYYY-MM-DD[-vVersion].md",
 				OS:          "All",
 				Path:        "~/Dropbox/Therapy",
-				FileTemplate: `# {{.Year}}-{{.Month}}-{{.Day}}
+				File: File{
+					Template: `# {{.Year}}-{{.Month}}-{{.Day}}
 
 {{if .ProjectName}}Therapy notes on {{.ProjectName}}{{else}}Therapy notes{{end}}
 
@@ -97,15 +103,16 @@ Start writing here...
 
 Sincerely, me
 `,
+				},
 				Type: FileType,
-				NamePatterns: []string{
+				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
 						"{{if (gt $version 1)}}v{{$version}}{{end}}" +
 						".md",
 				},
 				ScriptShell: "pwsh",
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"code . \"{{.Name}}\"",
 				},
 			},
@@ -114,24 +121,35 @@ Sincerely, me
 				Description: "Creates a folder in the ~/Work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
 				Path:        "~/Work",
 				Type:        FolderType,
-				NamePatterns: []string{
+				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: []string{
+				PostScripts: []ParameterizedText{
 					"code \"{{.Path}}\"",
 				},
+				Files: []FileInFolder{
+					FileInFolder{
+						File: File{
+							Template: `# {{.ProjectName}}
+
+Work notes on {{.ProjectName}}
+`,
+						},
+						Name: "README.md",
+					},
+				},
 			},
 			{
 				Name:        "folder",
 				Description: "Creates a folder in the current folder in the format YYYY-MM-DD[-ProjectName][-vVersion]",
 				Type:        FolderType,
 				ScriptShell: "pwsh",
-				PostScripts: []string{},
-				NamePatterns: []string{
+				PostScripts: []ParameterizedText{},
+				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
 						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
@@ -143,13 +161,13 @@ Sincerely, me
 				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",
 				ScriptShell: "pwsh",
-				NamePatterns: []string{
+				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"create-react-app" +
 						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
 						"{{if (gt $version 1)}}-n{{$version}}{{end}}",
 				},
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"npx create-react-app .",
 					"code .",
 				},
@@ -160,14 +178,14 @@ Sincerely, me
 				OS:          "Windows",
 				Path:        "C:/code",
 				Type:        FolderType,
-				NamePatterns: []string{
+				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: []string{
+				PostScripts: []ParameterizedText{
 					"git init",
 					"code \"{{.Path}}\"",
 				},
@@ -176,11 +194,11 @@ Sincerely, me
 				Name:        "test",
 				Description: "Echoes the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
 				Type:        ScriptType,
-				NamePatterns: []string{
+				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
 				},
 				ScriptShell: "pwsh",
-				PostScripts: []string{
+				PostScripts: []ParameterizedText{
 					"echo \"Name={{.Name}}\"",
 					"echo \"Path={{.Path}}\"",
 					"echo \"Value1={{.Extras.value1}}\"",
@@ -189,4 +207,7 @@ Sincerely, me
 			},
 		},
 	}
+
+	validateConfig(&config)
+	return config
 }
diff --git a/extras_test.go b/extras_test.go
index 5719ab0..b252313 100644
--- a/extras_test.go
+++ b/extras_test.go
@@ -51,8 +51,9 @@ func TestExtras(t *testing.T) {
 	assert.Equal(t, extras, loadedExtras)
 
 	// test path errors
-	_, error = loadExtras("non-existent-file.yaml")
-	assert.NotNil(t, error, "Expected to fail to load extras, but succeeded")
+	extras, error = loadExtras("non-existent-file.yaml")
+	assert.Nil(t, error, "Expected to load extras")
+	assert.Empty(t, extras, "Expected to load empty extras")
 
 	os.WriteFile(file.Name(), []byte("invalid yaml"), 0644)
 
diff --git a/file.go b/file.go
index e364f77..5574051 100644
--- a/file.go
+++ b/file.go
@@ -30,7 +30,7 @@ func createFilePatternData(now time.Time, projectName string, name string, extra
 	}
 }
 
-func createTemplateFile(srcPath string, destPath string, projectName string, name string, extras map[string]string, usr user.User) error {
+func createFileFromTemplateFile(srcPath string, destPath string, projectName string, name string, extras map[string]string, usr user.User) error {
 	now := time.Now()
 	filePatternData := createFilePatternData(now, projectName, name, extras)
 
@@ -60,12 +60,12 @@ func createTemplateFile(srcPath string, destPath string, projectName string, nam
 	return nil
 }
 
-func createTemplateString(templateText string, projectName string, name string, extras map[string]string) (string, error) {
+func createTemplateString(templateText ParameterizedText, projectName string, name string, extras map[string]string) (string, error) {
 	now := time.Now()
 	filePatternData := createFilePatternData(now, projectName, name, extras)
 
 	// parse template
-	template, err := template.New("text-template").Parse(templateText)
+	template, err := template.New("text-template").Parse(string(templateText))
 	if err != nil {
 		return "", fmt.Errorf("error parsing template text: %v", err)
 	}
diff --git a/name_pattern.go b/name_pattern.go
index 32fb4d3..0ef34b3 100644
--- a/name_pattern.go
+++ b/name_pattern.go
@@ -16,8 +16,8 @@ type NamePatternData struct {
 	Extras                                 map[string]string
 }
 
-func createNewNameForProfile(now time.Time, projectName string, profile Profile, extras map[string]string, existingEntities []string) (string, error) {
-	for index, namePattern := range profile.NamePatterns {
+func createNewNameForPatterns(now time.Time, projectName string, namePatterns []ParameterizedText, extras map[string]string, existingEntities []string) (string, error) {
+	for index, namePattern := range namePatterns {
 
 		startingVersion := 1
 		invokeCount := 0
@@ -41,7 +41,7 @@ func createNewNameForProfile(now time.Time, projectName string, profile Profile,
 
 		for {
 
-			template, err := template.Parse(namePattern)
+			template, err := template.Parse(string(namePattern))
 			if err != nil {
 				return "", fmt.Errorf("error parsing name pattern %d: %w", index, err)
 			}
@@ -55,7 +55,7 @@ func createNewNameForProfile(now time.Time, projectName string, profile Profile,
 			newName := sb.String()
 			alreadyExists := slices.Contains(existingEntities, newName)
 			useIndex := invokeCount > 0
-			isLastPattern := index == len(profile.NamePatterns)-1
+			isLastPattern := index == len(namePatterns)-1
 
 			// if no conflict, use this name
 			if !alreadyExists {
diff --git a/name_pattern_test.go b/name_pattern_test.go
index f0e4dc9..6e53ae6 100644
--- a/name_pattern_test.go
+++ b/name_pattern_test.go
@@ -16,7 +16,7 @@ func TestCreateNewName(t *testing.T) {
 	profile := Profile{
 		IsDefault: true,
 		Type:      FileType,
-		NamePatterns: []string{
+		NamePatterns: []ParameterizedText{
 			"{{.Extras.test1}}-{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}.txt",
 			"{{.Extras.test1}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}.txt",
 			"{{.Extras.test1}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.ProjectName}}.txt",
@@ -29,7 +29,7 @@ func TestCreateNewName(t *testing.T) {
 
 	testHappyPath := func(projectName string, expectedName string, existingEntities []string) func(t *testing.T) {
 		return func(t *testing.T) {
-			newName, err := createNewNameForProfile(now, projectName, profile, extras, existingEntities)
+			newName, err := createNewNameForPatterns(now, projectName, profile.NamePatterns, extras, existingEntities)
 			if err != nil {
 				t.Error("Error creating new name: ", err)
 			}
@@ -40,7 +40,7 @@ func TestCreateNewName(t *testing.T) {
 	}
 	testErrorPath := func(projectName string, existingEntities []string) func(t *testing.T) {
 		return func(t *testing.T) {
-			_, err := createNewNameForProfile(now, projectName, profile, extras, existingEntities)
+			_, err := createNewNameForPatterns(now, projectName, profile.NamePatterns, extras, existingEntities)
 			if err == nil {
 				t.Error("Expected error but got none")
 			}
@@ -68,7 +68,7 @@ func TestCreateNewName_Index(t *testing.T) {
 	profile := Profile{
 		IsDefault: true,
 		Type:      FileType,
-		NamePatterns: []string{
+		NamePatterns: []ParameterizedText{
 			"{{.Extras.test1}}-{{$version := getVersion}}{{.Year}}-{{.Month}}-{{.Day}}v{{$version}}.txt",
 		},
 	}
@@ -78,7 +78,7 @@ func TestCreateNewName_Index(t *testing.T) {
 
 	testHappyPath := func(expectedName string, existingEntities []string) func(t *testing.T) {
 		return func(t *testing.T) {
-			newName, err := createNewNameForProfile(now, "test-project", profile, extras, existingEntities)
+			newName, err := createNewNameForPatterns(now, "test-project", profile.NamePatterns, extras, existingEntities)
 			if err != nil {
 				t.Error("Error creating new name: ", err)
 			}
diff --git a/postscript.go b/postscript.go
index e8cfcce..d9967f5 100644
--- a/postscript.go
+++ b/postscript.go
@@ -26,7 +26,7 @@ func createPostScriptArrays(profile Profile, newName string, fullPath string, ex
 	}
 
 	for _, postScriptPattern := range profile.PostScripts {
-		template, err := template.New("postscript").Parse(postScriptPattern)
+		template, err := template.New("postscript").Parse(string(postScriptPattern))
 		if err != nil {
 			return nil, err
 		}
diff --git a/postscript_test.go b/postscript_test.go
index 8ae03f6..722cb62 100644
--- a/postscript_test.go
+++ b/postscript_test.go
@@ -22,7 +22,7 @@ func TestCreatePostScriptArrays(t *testing.T) {
 	}
 
 	testSuccess(Profile{
-		PostScripts: []string{
+		PostScripts: []ParameterizedText{
 			"echo \"{{.Name}}\"",
 		},
 	})
@@ -30,15 +30,15 @@ func TestCreatePostScriptArrays(t *testing.T) {
 		PostScripts: nil,
 	})
 	testFailure(Profile{
-		PostScripts: []string{},
+		PostScripts: []ParameterizedText{},
 	})
 	testFailure(Profile{
-		PostScripts: []string{
+		PostScripts: []ParameterizedText{
 			"echo \"{{.Name\"",
 		},
 	})
 	testFailure(Profile{
-		PostScripts: []string{
+		PostScripts: []ParameterizedText{
 			"echo \"{{.NameUnknown}}\"",
 		},
 	})
diff --git a/profile.go b/profile.go
index 888ee70..7d9d95a 100644
--- a/profile.go
+++ b/profile.go
@@ -31,20 +31,30 @@ const (
 	All     OSType = "All"
 )
 
+type ParameterizedText string
+
+type File struct {
+	TemplatePath string
+	Template     ParameterizedText
+}
+
+type FileInFolder struct {
+	File
+	Name ParameterizedText
+}
+
 type Profile struct {
-	IsDefault        bool
-	Name             string
-	Description      string
-	Path             string
-	Type             ProfileType
-	NamePatterns     []string
-	PostScripts      []string
-	ScriptShell      string
-	OS               OSType
-	FileTemplatePath string
-	FileTemplate     string
-
-	// FileTemplatePath string
+	File
+	IsDefault    bool
+	Name         string
+	Description  string
+	Path         string
+	Type         ProfileType
+	NamePatterns []ParameterizedText
+	PostScripts  []ParameterizedText
+	ScriptShell  string
+	OS           OSType
+	Files        []FileInFolder
 }
 
 var forbiddenProfileNames = []string{
@@ -118,7 +128,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 
 	// generate a new name based on time, profile, and existing entities
 	now := time.Now()
-	newName, err := createNewNameForProfile(now, projectName, profile, extras, entities)
+	newName, err := createNewNameForPatterns(now, projectName, profile.NamePatterns, extras, entities)
 	if err != nil {
 		return err
 	}
@@ -140,25 +150,26 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 		if err != nil {
 			return fmt.Errorf("unable to change directory to %v: %v", fullPath, err)
 		}
-	} else if profileType == FileType {
-		useTemplateFile := profile.FileTemplatePath != ""
-		useTemplate := profile.FileTemplate != ""
-		if useTemplateFile {
-			fmt.Printf("Creating file '%v' from template: %v\n", fullPath, profile.FileTemplatePath)
-			err := createTemplateFile(profile.FileTemplatePath, fullPath, projectName, newName, extras, usr)
-			if err != nil {
-				return fmt.Errorf("error creating template file: %v", err)
-			}
-		} else if useTemplate {
-			fmt.Printf("Creating file '%v' from template string\n", fullPath)
-			templateString, err := createTemplateString(profile.FileTemplate, projectName, newName, extras)
-			if err != nil {
-				return fmt.Errorf("error creating template: %v", err)
+
+		// create files in the new folder
+		if len(profile.Files) > 0 {
+			fmt.Printf("Creating files in directory: %v\n", fullPath)
+			for index, file := range profile.Files {
+				fileName, err := createNewNameForPatterns(now, projectName, []ParameterizedText{file.Name}, extras, []string{})
+				if err != nil {
+					return fmt.Errorf("error creating file name for file index %v : %v", index, err)
+				}
+				subFilePath := filepath.Join(fullPath, fileName)
+				err = createFile(file.File, subFilePath, projectName, fileName, extras, usr)
+				if err != nil {
+					return err
+				}
 			}
-			ioutil.WriteFile(fullPath, []byte(templateString), os.ModePerm)
-		} else {
-			fmt.Printf("Creating empty file: %v\n", fullPath)
-			os.WriteFile(fullPath, []byte{}, os.ModePerm)
+		}
+	} else if profileType == FileType {
+		err := createFile(profile.File, fullPath, projectName, newName, extras, usr)
+		if err != nil {
+			return err
 		}
 	} else {
 		fmt.Printf("Profile type is script, doing nothing during main phase\n")
@@ -185,7 +196,30 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 	return nil
 }
 
-func validateProfile(profile Profile) error {
+func createFile(file File, fullPath string, projectName string, newName string, extras map[string]string, usr user.User) error {
+	useTemplateFile := file.TemplatePath != ""
+	useTemplate := file.Template != ""
+	if useTemplateFile {
+		fmt.Printf("Creating file '%v' from template: %v\n", fullPath, file.TemplatePath)
+		err := createFileFromTemplateFile(file.TemplatePath, fullPath, projectName, newName, extras, usr)
+		if err != nil {
+			return fmt.Errorf("error creating template file: %v", err)
+		}
+	} else if useTemplate {
+		fmt.Printf("Creating file '%v' from template string\n", fullPath)
+		templateString, err := createTemplateString(file.Template, projectName, newName, extras)
+		if err != nil {
+			return fmt.Errorf("error creating template: %v", err)
+		}
+		ioutil.WriteFile(fullPath, []byte(templateString), os.ModePerm)
+	} else {
+		fmt.Printf("Creating empty file: %v\n", fullPath)
+		os.WriteFile(fullPath, []byte{}, os.ModePerm)
+	}
+	return nil
+}
+
+func validateProfile(profile *Profile) error {
 	if profile.Name == "" {
 		return fmt.Errorf("Profile name is required")
 	}
@@ -198,13 +232,13 @@ func validateProfile(profile Profile) error {
 	if profile.Type == "" || (profile.Type != FileType && profile.Type != FolderType && profile.Type != ScriptType) {
 		return fmt.Errorf("invalid profile type %s for profile %s", profile.Type, profile.Name)
 	}
-	if profile.Type != FileType && profile.FileTemplatePath != "" {
+	if profile.Type != FileType && profile.TemplatePath != "" {
 		return fmt.Errorf("file template path is not supported for profile %s, which has Type=%s", profile.Name, profile.Type)
 	}
-	if profile.Type != FileType && profile.FileTemplate != "" {
+	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)
 	}
-	if profile.Type == FileType && profile.FileTemplatePath != "" && profile.FileTemplate != "" {
+	if profile.Type == FileType && profile.TemplatePath != "" && profile.Template != "" {
 		return fmt.Errorf("both file template path and file template are specified for profile %s", profile.Name)
 	}
 	if profile.Type == ScriptType && profile.NamePatterns != nil && len(profile.NamePatterns) > 1 {
@@ -213,5 +247,8 @@ func validateProfile(profile Profile) error {
 	if profile.ScriptShell == "" && profile.PostScripts != nil && len(profile.PostScripts) > 0 {
 		return fmt.Errorf("script shell is required for profile %s", profile.Name)
 	}
+	if profile.Files == nil {
+		profile.Files = []FileInFolder{}
+	}
 	return nil
 }
diff --git a/profile_test.go b/profile_test.go
index f2c747f..47fe19e 100644
--- a/profile_test.go
+++ b/profile_test.go
@@ -6,13 +6,13 @@ import (
 
 func TestValidateProfile(t *testing.T) {
 	testSuccess := func(profile Profile) {
-		err := validateProfile(profile)
+		err := validateProfile(&profile)
 		if err != nil {
 			t.Errorf("Expected to validate profile '%v', but failed: %v", profile.Name, err)
 		}
 	}
 	testFailure := func(profile Profile) {
-		err := validateProfile(profile)
+		err := validateProfile(&profile)
 		if err == nil {
 			t.Errorf("Expected to fail to validate profile '%v', but succeeded", profile.Name)
 		}
@@ -21,19 +21,23 @@ func TestValidateProfile(t *testing.T) {
 	testSuccess(Profile{
 		Type:         FileType,
 		Name:         "daily",
-		NamePatterns: []string{"{{.Date}}"},
+		NamePatterns: []ParameterizedText{"{{.Date}}"},
 	})
 	testSuccess(Profile{
 		Type:         FileType,
 		Name:         "daily",
-		NamePatterns: []string{"{{.Date}}"},
-		FileTemplate: "foo",
+		NamePatterns: []ParameterizedText{"{{.Date}}"},
+		File: File{
+			Template: "foo",
+		},
 	})
 	testSuccess(Profile{
-		Type:             FileType,
-		Name:             "daily",
-		NamePatterns:     []string{"{{.Date}}"},
-		FileTemplatePath: "foo",
+		Type:         FileType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"{{.Date}}"},
+		File: File{
+			TemplatePath: "foo",
+		},
 	})
 
 	// empty name
@@ -61,42 +65,48 @@ func TestValidateProfile(t *testing.T) {
 	testFailure(Profile{
 		Type:         FileType,
 		Name:         "daily",
-		NamePatterns: []string{},
+		NamePatterns: []ParameterizedText{},
 	})
 	// file template and file template path are set
 	testFailure(Profile{
-		Type:             FileType,
-		Name:             "daily",
-		NamePatterns:     []string{"test"},
-		FileTemplatePath: "foo",
-		FileTemplate:     "bar",
+		Type:         FileType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"test"},
+		File: File{
+			TemplatePath: "foo",
+			Template:     "bar",
+		},
 	})
 	// file template set with wrong type
 	testFailure(Profile{
 		Type:         FolderType,
 		Name:         "daily",
-		NamePatterns: []string{"test"},
-		FileTemplate: "bar",
+		NamePatterns: []ParameterizedText{"test"},
+		File: File{
+			Template: "bar",
+		},
 	})
 	// file template path set with wrong type
 	testFailure(Profile{
-		Type:             FolderType,
-		Name:             "daily",
-		NamePatterns:     []string{"test"},
-		FileTemplatePath: "bar",
+		Type:         FolderType,
+		Name:         "daily",
+		NamePatterns: []ParameterizedText{"test"},
+		File: File{
+			TemplatePath: "bar",
+		},
 	})
 	// script type with multiple name patterns
 	testFailure(Profile{
 		Type:         ScriptType,
 		Name:         "daily",
-		NamePatterns: []string{"test", "test2"},
+		NamePatterns: []ParameterizedText{"test", "test2"},
 	})
 	// postscripts with no shell
 	testFailure(Profile{
 		Type:         ScriptType,
 		Name:         "daily",
-		NamePatterns: []string{"test"},
-		PostScripts:  []string{"test"},
+		NamePatterns: []ParameterizedText{"test"},
+		PostScripts:  []ParameterizedText{"test"},
 		ScriptShell:  "",
 	})