fuzzyworld

template / commit

Merge branch 'direct-output' into 'main'

61ec18589dac861c5169ea5a51a4aa72be099eda · Pavel S · 2022-12-10 08:39:56 +0000 · parents 49a90ad9f5bf bbd011d02317

Merge branch 'direct-output' into 'main'

Implemented piping file contents

See merge request fuzzy-rp/template!7
 .vscode/launch.json   |  12 ++++-
 .vscode/settings.json |   3 ++
 README.md             |  43 ++++++++++-----
 config_test.go        |  64 ++++++++++------------
 default_config.go     | 143 ++++++++++++++++++++++++++------------------------
 file.go               |  33 ++++++++++--
 file_test.go          |  41 +++++++++++----
 main.go               |  26 ++++++---
 monkey-patch.go       |  15 ++++++
 profile.go            |  95 +++++++++++++++++++++------------
 profile_test.go       |  70 +++++++++++-------------
 11 files changed, 330 insertions(+), 215 deletions(-)

diff --git a/.vscode/launch.json b/.vscode/launch.json
index 13d3f1a..882bd7c 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -4,7 +4,6 @@
     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
     "configurations": [
-        
         {
             "name": "Launch tests",
             "type": "go",
@@ -72,6 +71,17 @@
                 "daily"
             ]
         },
+        {
+            "name": "Run: template log",
+            "type": "go",
+            "request": "launch",
+            "mode": "auto",
+            "program": ".",
+            "args": [
+                "log"
+            ],
+            "console": "integratedTerminal"
+        },
         {
             "name": "Run: template work test-project w/ in-memory defaults",
             "type": "go",
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/README.md b/README.md
index 9cb0997..7f91302 100644
--- a/README.md
+++ b/README.md
@@ -25,29 +25,29 @@ This is one of the profiles that comes with the tool.
       isdefault: false
       isdisabled: false
       type: file
-      path: ~/DailyNotes
-      namepatterns:
-        - '{{$version := getVersion}}{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName | snakecase }}{{end}}{{if (gt $version 1)}}v{{$version}}{{end}}.md'
       runpostscriptonfailure: true
       postscripts:
         - code . {{ if .FullPath }}"{{.FullPath}}"{{else}}(ls -t | head -n1){{ end }}
-      file:
-        template: |-
-            # {{.FileNameWithoutExtension}}
+      namepatterns:
+        - '{{$version := getVersion}}{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName | snakecase }}{{end}}{{if (gt $version 1)}}v{{$version}}{{end}}.md'
+      targetdirectory: ~/DailyNotes
+      filetemplate: |-
+        # {{.FileNameWithoutExtension}}
 
-            Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
+        Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 
-            ## Todo
+        ## Todo
 
-            1. Check email
-            2. Check chat
-            3. Check calendar
-            4. Check yesterday's notes
-            5. Have fun!
-            6. ADD MORE ITEMS HERE
+        1. Check email
+        2. Check chat
+        3. Check calendar
+        4. Check yesterday's notes
+        5. Have fun!
+        6. ADD MORE ITEMS HERE
 ```
 
 Using the above configuration:
+
 ```sh
 > $ template daily "Cool Thought"
 Profile: daily
@@ -64,6 +64,21 @@ This will cause VSCode to open the `~\DailyNotes` folder as a workspace, and the
 The above example also uses the [`snakecase`](https://github.com/Masterminds/sprig/blob/master/docs/strings.md#user-content-snakecase) template function.
 This is one of the [sprig template functions](https://github.com/Masterminds/sprig/blob/master/docs/index.md) that are exposed by template.
 
+File-based profiles (`type: file`) can accept piped input, and this input will be piped to the created file.
+In this instance, the profile's file template field is ignored.
+Here's an example of piping the results of `ps` to a file with an auto-generated name:
+
+```sh
+> $ echo "$(ps)" | go run . log "ps-info"
+Profile: log
+Project name: ps-info
+Loaded config from /Users/pavel/.template.yaml
+Loaded user config from /Users/pavel/.template.user.yaml, combining with global config
+Creating file: 2022-12-10-00-05-21-ps-info.log
+Executing post script: sh -c code . 2022-12-10-00-05-21-ps-info.log
+Post script had no output
+```
+
 Note that user profiles are combined with the default ones.
 This is so you can run `template reset` to reset `~/.template.yaml`, but keep your local profiles/changes as-is.
 
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..fa4e813 100644
--- a/default_config.go
+++ b/default_config.go
@@ -4,10 +4,10 @@ func defaultConfig() Config {
 	config := Config{
 		Profiles: []Profile{
 			{
-				Name:        "daily",
-				Description: "Creates a MD file in ~/DailyNotes folder in the format YYYY-MM-DD[-vX].md",
-				Path:        "~/DailyNotes",
-				Type:        FileType,
+				Name:            "daily",
+				Description:     "Creates a MD file in ~/DailyNotes folder in the format YYYY-MM-DD[-vX].md",
+				TargetDirectory: "~/DailyNotes",
+				Type:            FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
@@ -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,18 +27,17 @@ 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 }}",
 				},
 				RunPostscriptOnFailure: true,
 			},
 			{
-				Name:        "note",
-				Description: "Creates a MD file in ~/Notes in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
-				OS:          "All",
-				Path:        "~/Notes",
-				Type:        FileType,
+				Name:            "note",
+				Description:     "Creates a MD file in ~/Notes in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
+				OS:              "All",
+				TargetDirectory: "~/Notes",
+				Type:            FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
@@ -52,12 +50,12 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 				},
 			},
 			{
-				Name:        "note-here",
-				Description: "Creates a MD file in the current folder in the format YYYY-MM-DD[-HH[-MM[-SS]]][-ProjectName][-vVersion].md",
-				OS:          "All",
-				Type:        FileType,
-				IsDefault:   true,
-				Path:        ".", // current folder
+				Name:            "note-here",
+				Description:     "Creates a MD file in the current folder in the format YYYY-MM-DD[-HH[-MM[-SS]]][-ProjectName][-vVersion].md",
+				OS:              "All",
+				Type:            FileType,
+				IsDefault:       true,
+				TargetDirectory: ".", // current folder
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
@@ -70,7 +68,12 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 						"{{if (gt $version 1)}}n{{$version}}{{end}}" +
 						".md",
 					"{{$version := getVersion}}" +
-						"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Second}}" +
+						"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}" +
+						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
+						"{{if (gt $version 1)}}n{{$version}}{{end}}" +
+						".md",
+					"{{$version := getVersion}}" +
+						"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}" +
 						"{{if .ProjectName}}-{{.ProjectName}}{{end}}" +
 						"{{if (gt $version 1)}}n{{$version}}{{end}}" +
 						".md",
@@ -80,12 +83,11 @@ 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}}
+				Name:            "blog",
+				Description:     "Creates a MD file in the ~/Blog folder in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
+				OS:              "All",
+				TargetDirectory: "~/Blog",
+				FileTemplate: `# {{.ProjectName}}
 
 Created by template, on {{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}.
 
@@ -97,7 +99,6 @@ cspell:words nanite
 
 ## Writing
 `,
-				},
 				Type: FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -111,11 +112,11 @@ 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",
-				Type:        FolderType,
+				Name:            "work-win",
+				Description:     "Creates a folder in the c:/work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
+				OS:              "Windows",
+				TargetDirectory: "C:/work",
+				Type:            FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
 					"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -127,12 +128,11 @@ 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}}
+				Name:            "therapy",
+				Description:     "Creates a MD file in the ~/Therapy folder in the format YYYY-MM-DD[-vVersion].md",
+				OS:              "All",
+				TargetDirectory: "~/Therapy",
+				FileTemplate: `# {{if .ProjectName}}{{.ProjectName}}{{else}}Therapy notes{{end}}
 
 {{if .ProjectName}}Therapy notes on {{.ProjectName}}{{else}}Therapy notes{{end}}
 
@@ -144,7 +144,6 @@ Start writing here...
 
 Sincerely, me
 `,
-				},
 				Type: FileType,
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
@@ -157,10 +156,10 @@ Sincerely, me
 				},
 			},
 			{
-				Name:        "work",
-				Description: "Creates a folder in the ~/Work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
-				Path:        "~/Work",
-				Type:        FolderType,
+				Name:            "work",
+				Description:     "Creates a folder in the ~/Work folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
+				TargetDirectory: "~/Work",
+				Type:            FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
 					"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -173,25 +172,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,17 +199,16 @@ echo "Let's test {{.ProjectName}}!"`,
     ]
 }
 							`,
-						},
 						Name: ".vscode/launch.json",
 					},
 				},
 			},
 			{
-				Name:        "folder",
-				Description: "Creates a folder in the current folder in the format YYYY-MM-DD[-ProjectName][-vVersion]",
-				Type:        FolderType,
-				Path:        ".", // current folder
-				PostScripts: []ParameterizedText{},
+				Name:            "folder",
+				Description:     "Creates a folder in the current folder in the format YYYY-MM-DD[-ProjectName][-vVersion]",
+				Type:            FolderType,
+				TargetDirectory: ".", // current folder
+				PostScripts:     []ParameterizedText{},
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"{{.Year}}-{{.Month}}-{{.Day}}" +
@@ -224,10 +217,10 @@ 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
+				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",
+				TargetDirectory: ".", // current folder
 				NamePatterns: []ParameterizedText{
 					"{{$version := getVersion}}" +
 						"create-react-app" +
@@ -240,10 +233,10 @@ 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",
-				Type:        FolderType,
+				Name:            "git",
+				Description:     "Creates a  folder in the ~/code folder in the format YYYY-MM-DD[-hh][-mm][-ss][-ProjectName]",
+				TargetDirectory: "~/code",
+				Type:            FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
 					"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -256,11 +249,11 @@ 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",
-				Type:        FolderType,
+				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,
+				TargetDirectory: "~/code",
+				Type:            FolderType,
 				NamePatterns: []ParameterizedText{
 					"{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
 					"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}{{if .ProjectName}}-{{.ProjectName}}{{end}}",
@@ -272,6 +265,18 @@ echo "Let's test {{.ProjectName}}!"`,
 					"code .",
 				},
 			},
+			{
+				Name:        "log",
+				Description: "Creates a log file in the current folder",
+				Type:        FileType,
+				NamePatterns: []ParameterizedText{
+					"{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}{{if .ProjectName}}-{{.ProjectName}}{{end}}.log",
+				},
+				TargetDirectory: ".",
+				PostScripts: []ParameterizedText{
+					"code . {{.FullPath}}",
+				},
+			},
 		},
 	}
 
diff --git a/file.go b/file.go
index bc1c208..2b451fe 100644
--- a/file.go
+++ b/file.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"os"
 	"strings"
@@ -11,15 +12,37 @@ import (
 
 func createFile(
 	prePatternData PrePatternData,
-	file File,
-	fullPath string) error {
-	useTemplate := file.Template != ""
-	if useTemplate {
+	fileTemplate ParameterizedText,
+	fullPath string,
+	pipeInputScanner *bufio.Scanner) error {
+
+	useTemplate := fileTemplate != ""
+	useInputPipe := pipeInputScanner != nil
+
+	if useInputPipe {
+		log.WithFields(log.Fields{"fullPath": fullPath}).Debug("Creating file from input pipe content", fullPath)
+
+		file, err := monkey.OSCreate(fullPath)
+		if err != nil {
+			return fmt.Errorf("error creating file: %v", err)
+		}
+		defer file.Close()
+
+		writer := bufio.NewWriter(file)
+		for pipeInputScanner.Scan() {
+			line := pipeInputScanner.Text()
+			_, err := writer.WriteString(line + "\n")
+			if err != nil {
+				return fmt.Errorf("error writing file: %v", err)
+			}
+		}
+		writer.Flush()
+	} else 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..7a75ed5 100644
--- a/file_test.go
+++ b/file_test.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"bufio"
 	"os"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -12,9 +14,18 @@ func TestCreateFile(t *testing.T) {
 	path := "~/test/out/test.txt"
 	prePatternData := testPrePatternData()
 	expectedText := "test-project-2022-04-20-16-20-69-value1"
+	var createdFileName = ""
 
 	// monkey patch
 	applyTestMonkeyPatch(t, monkeyPatch{
+		OSCreate: func(filename string) (*os.File, error) {
+			assert.Equal(t, path, filename)
+			file, err := os.CreateTemp("./test", "test")
+			assert.Nil(t, err, "error creating temp file")
+			assert.True(t, createdFileName == "", "createFileName should be empty")
+			createdFileName = file.Name()
+			return file, nil
+		},
 		OSWriteFile: func(filename string, data []byte, perm os.FileMode) error {
 			assert.Equal(t, path, filename)
 			assert.Equal(t, perm, os.ModePerm)
@@ -24,21 +35,31 @@ func TestCreateFile(t *testing.T) {
 	})
 
 	// test error path
-	err := createFile(prePatternData, File{
-		Template: "{{.ProjectName",
-	}, path)
+	err := createFile(prePatternData, "{{.ProjectName", path, nil)
 	assert.NotNil(t, err)
 
-	// test happy path
-	err = createFile(prePatternData, File{
-		Template: "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}",
-	}, path)
+	// test happy path with file template
+	err = createFile(prePatternData, "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}", path, nil)
+	assert.Nil(t, err)
+
+	// test happy path with pipe input
+	inputText := `line1 
+line2
+line3
+`
+	expectedText = inputText
+	reader := strings.NewReader(inputText)
+	pipeInputScanner := bufio.NewScanner(reader)
+	err = createFile(prePatternData, "{{.ProjectName}}-{{.Year}}-{{.Month}}-{{.Day}}-{{.Hour}}-{{.Minute}}-{{.Second}}-{{.Extras.key1}}", path, pipeInputScanner)
 	assert.Nil(t, err)
+	assert.NotNil(t, createdFileName)
+	bytes, err := os.ReadFile(createdFileName)
+	assert.Nil(t, err, "error reading file "+createdFileName)
+	outputText := string(bytes)
+	assert.Equal(t, inputText, outputText)
 
 	// test empty template
 	expectedText = ""
-	err = createFile(prePatternData, File{
-		Template: "",
-	}, path)
+	err = createFile(prePatternData, "", path, nil)
 	assert.Nil(t, err)
 }
diff --git a/main.go b/main.go
index 852cf0a..2a8abd0 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"os"
 	"os/user"
@@ -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,6 +188,7 @@ func createSettings(
 	if err != nil {
 		return Settings{}, fmt.Errorf("error getting env var values: %w", err)
 	}
+
 	settings := Settings{
 		ArgumentValues: argumentValues,
 		EnvVarValues:   envVarValues,
@@ -210,8 +212,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{})
@@ -260,9 +261,9 @@ func createSettings(
 		config, err = loadConfig(fullConfigPath)
 		if err != nil {
 			return settings, fmt.Errorf("error loading config from %v: %w", fullConfigPath, err)
-		} else {
-			fmt.Printf("Loaded config from %v\n", fullConfigPath)
 		}
+		fmt.Printf("Loaded config from %v\n", fullConfigPath)
+
 		// load user config, if present
 		fullUserConfigPath := getFullPath(settings.userConfigPath, *usr)
 		_, err = os.Stat(fullUserConfigPath)
@@ -325,8 +326,17 @@ func mainLogic(settings Settings) error {
 		return fmt.Errorf("error finding profile: %v", err)
 	}
 
+	var pipeInputScanner *bufio.Scanner = nil
+	stat, err := os.Stdin.Stat()
+	if err != nil {
+		return fmt.Errorf("error getting stdin stat: %w", err)
+	}
+	if stat.Size() > 0 {
+		pipeInputScanner = bufio.NewScanner(os.Stdin)
+	}
+
 	// process profile
-	errors := processProfile(profile, settings.projectName, settings.extras, *settings.usr)
+	errors := processProfile(profile, settings.projectName, settings.extras, *settings.usr, pipeInputScanner)
 	if len(errors) > 0 {
 		return fmt.Errorf("error processing profile: %v", errors)
 	}
diff --git a/monkey-patch.go b/monkey-patch.go
index 26a6ec9..d0b0796 100644
--- a/monkey-patch.go
+++ b/monkey-patch.go
@@ -10,6 +10,7 @@ import (
 
 type monkeyPatch struct {
 	ExecCommand func(name string, arg ...string) *exec.Cmd
+	OSCreate    func(name string) (*os.File, error)
 	OSWriteFile func(filename string, data []byte, perm os.FileMode) error
 	OSGetwd     func() (string, error)
 	OSChdir     func(dir string) error
@@ -22,6 +23,9 @@ type monkeyPatchCalls struct {
 		name string
 		arg  []string
 	}
+	OSCreateCalls []struct {
+		name string
+	}
 	OSWriteFileCalls []struct {
 		filename string
 		data     []byte
@@ -45,6 +49,9 @@ func applyTestMonkeyPatch(t *testing.T, monkeyImpl monkeyPatch) *monkeyPatchCall
 			name string
 			arg  []string
 		}{},
+		OSCreateCalls: []struct {
+			name string
+		}{},
 		OSWriteFileCalls: []struct {
 			filename string
 			data     []byte
@@ -67,6 +74,13 @@ func applyTestMonkeyPatch(t *testing.T, monkeyImpl monkeyPatch) *monkeyPatchCall
 			}{name, arg})
 			return monkeyImpl.ExecCommand(name, arg...)
 		},
+		OSCreate: func(name string) (*os.File, error) {
+			assert.NotNil(t, monkeyImpl.OSCreate, "OSCreate is not implemented")
+			results.OSCreateCalls = append(results.OSCreateCalls, struct {
+				name string
+			}{name})
+			return monkeyImpl.OSCreate(name)
+		},
 		OSWriteFile: func(filename string, data []byte, perm os.FileMode) error {
 			assert.NotNil(t, monkeyImpl.OSWriteFile, "OSWriteFile is not implemented")
 			results.OSWriteFileCalls = append(results.OSWriteFileCalls, struct {
@@ -106,6 +120,7 @@ func applyTestMonkeyPatch(t *testing.T, monkeyImpl monkeyPatch) *monkeyPatchCall
 func defaultMonkeyPatch() monkeyPatch {
 	return monkeyPatch{
 		ExecCommand: exec.Command,
+		OSCreate:    os.Create,
 		OSWriteFile: os.WriteFile,
 		OSGetwd:     os.Getwd,
 		OSChdir:     os.Chdir,
diff --git a/profile.go b/profile.go
index 705c66e..5f727a0 100644
--- a/profile.go
+++ b/profile.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
 	"os"
@@ -21,6 +22,12 @@ const (
 	ScriptType ProfileType = "script"
 )
 
+var allProfileTypes = []ProfileType{
+	FileType,
+	FolderType,
+	ScriptType,
+}
+
 type OSType string
 
 const (
@@ -31,13 +38,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 +49,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{
@@ -60,7 +65,7 @@ var forbiddenProfileNames = []string{
 	"get",
 }
 
-func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User) []error {
+func processProfile(profile Profile, projectName string, extras map[string]string, usr user.User, pipeInputScanner *bufio.Scanner) []error {
 	if profile.IsDisabled {
 		return []error{fmt.Errorf("Profile is disabled, doing nothing")}
 	}
@@ -74,17 +79,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 {
 		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,14 +100,17 @@ 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 {
+		if pipeInputScanner != nil && profileType != FileType {
+			errors = append(errors, fmt.Errorf("cannot use pipe input with non-file type profile"))
+			return errors
+		}
+
 		// get all entities in current directory
 		files, err := monkey.OSReadDir(cwd)
 		if err != nil {
@@ -131,7 +142,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 		}
 
 		if err == nil && newName != "" {
-			fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData)
+			fullPath, err = createEntity(now, profile, cwd, newName, projectName, extras, prePatternData, pipeInputScanner)
 			if err != nil {
 				if swallowPrePostScriptErrors {
 					fmt.Printf("Error creating entity, but will attempt to run post scripts: %v", err)
@@ -210,7 +221,8 @@ func createEntity(
 	newName string,
 	projectName string,
 	extras map[string]string,
-	prePatternData PrePatternData) (string, error) {
+	prePatternData PrePatternData,
+	pipeInputScanner *bufio.Scanner) (string, error) {
 
 	profileType := profile.Type
 	fullPath := filepath.Join(cwd, newName)
@@ -248,7 +260,7 @@ func createEntity(
 					}
 				}
 				fmt.Printf("Creating sub file: %v\n", subFilePath)
-				err = createFile(filePrePatternData, file.File, subFilePath)
+				err = createFile(filePrePatternData, file.FileTemplate, subFilePath, nil)
 				if err != nil {
 					return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
 				}
@@ -256,7 +268,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, pipeInputScanner)
 		if err != nil {
 			return "", err
 		}
@@ -267,12 +279,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 +296,37 @@ 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)
-	}
-	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 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 == FolderType && profile.Path == "" {
-		return fmt.Errorf("path is required 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 == 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 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)
+		}
 	}
+
+	// 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..c34f84d 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,23 +159,19 @@ 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",
 				},
 			},
 		}
 
 		newName := "new-name"
 		expectedFullPath := filepath.Clean("~/test/dir/" + newName)
-		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData)
+		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData, nil)
 		assert.Nil(t, err)
 		assert.Equal(t, expectedFullPath, fullPath)
 		assert.Equal(t, 1, len(monkeyPatchCalls.OSChdirCalls))
@@ -199,14 +191,12 @@ 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)
-		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData)
+		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData, nil)
 		assert.Nil(t, err)
 		assert.Equal(t, expectedFullPath, fullPath)
 		assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls))
@@ -223,7 +213,7 @@ func TestCreateEntity(t *testing.T) {
 		}
 		newName := "new-name.md"
 		expectedFullPath := filepath.Clean("~/test/dir/" + newName)
-		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData)
+		fullPath, err := createEntity(now, profile, cwd, newName, projectName, extras, patternData, nil)
 		assert.Nil(t, err)
 		assert.Equal(t, expectedFullPath, fullPath)
 	}
@@ -258,6 +248,10 @@ func TestProcessProfile(t *testing.T) {
 				shell, shellArgs := shellCommandAndArgs("echo")
 				return exec.Command(shell, shellArgs...)
 			},
+			OSCreate: func(name string) (file *os.File, err error) {
+				dir := os.TempDir()
+				return os.CreateTemp(dir, "test")
+			},
 			OSChdir: func(dir string) error { return nil },
 			OSGetwd: func() (dir string, err error) { return cwd, nil },
 			OSReadDir: func(dirname string) ([]os.DirEntry, error) {
@@ -285,7 +279,7 @@ func TestProcessProfile(t *testing.T) {
 
 	testSuccess := func(profile Profile) {
 		configureMonkeyPatching()
-		errors := processProfile(profile, projectName, extras, *usr)
+		errors := processProfile(profile, projectName, extras, *usr, nil)
 		assert.Equal(t, 0, len(errors))
 		for _, error := range errors {
 			assert.Nil(t, error)
@@ -293,7 +287,7 @@ func TestProcessProfile(t *testing.T) {
 	}
 	testFailure := func(profile Profile, errorCount int, errorStr string) {
 		configureMonkeyPatching()
-		errors := processProfile(profile, projectName, extras, *usr)
+		errors := processProfile(profile, projectName, extras, *usr, nil)
 		assert.Equal(t, errorCount, len(errors))
 		allErrorsText := ""
 		for _, err := range errors {
@@ -313,7 +307,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 +321,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")