fuzzyworld

template / commit

Got piping working, added example to the readme, and added piping test.

bbd011d02317e879e291c99b6f8dade7feb3b0a8 · Pavel Safronov · 2022-12-10 00:31:23 -0800 · parents 873846b9edaa

Got piping working, added example to the readme, and added piping test.
 .vscode/launch.json |  12 +++++-
 README.md           |  43 ++++++++++++-------
 default_config.go   | 119 ++++++++++++++++++++++++++++++----------------------
 file.go             |  27 +++++++++++-
 file_test.go        |  35 ++++++++++++++--
 main.go             |  48 ++++++++++-----------
 monkey-patch.go     |  15 +++++++
 profile.go          |  31 ++++++--------
 profile_test.go     |  14 ++++---
 9 files changed, 223 insertions(+), 121 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/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/default_config.go b/default_config.go
index d800add..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",
-				TargetDirectory:        "~/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}}" +
@@ -33,11 +33,11 @@ Today's notes for {{.Year}}-{{.Month}}-{{.Day}}.
 				RunPostscriptOnFailure: true,
 			},
 			{
-				Name:        "note",
-				Description: "Creates a MD file in ~/Notes in the format YYYY-MM-DD[-ProjectName][-vVersion].md",
-				OS:          "All",
-				TargetDirectory:        "~/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}}" +
@@ -50,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,
-				TargetDirectory:        ".", // 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}}" +
@@ -68,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",
@@ -78,10 +83,10 @@ 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",
-				TargetDirectory:        "~/Blog",
+				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}}.
@@ -107,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",
-				TargetDirectory:        "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}}",
@@ -123,10 +128,10 @@ cspell:words nanite
 				},
 			},
 			{
-				Name:        "therapy",
-				Description: "Creates a MD file in the ~/Therapy folder in the format YYYY-MM-DD[-vVersion].md",
-				OS:          "All",
-				TargetDirectory:        "~/Therapy",
+				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}}
@@ -151,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]",
-				TargetDirectory:        "~/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}}",
@@ -199,11 +204,11 @@ 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,
-				TargetDirectory:        ".", // 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}}" +
@@ -212,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",
-				TargetDirectory:        ".", // 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" +
@@ -228,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]",
-				TargetDirectory:        "~/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}}",
@@ -244,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,
-				TargetDirectory:        "~/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}}",
@@ -260,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 2be8d22..2b451fe 100644
--- a/file.go
+++ b/file.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"os"
 	"strings"
@@ -12,9 +13,31 @@ import (
 func createFile(
 	prePatternData PrePatternData,
 	fileTemplate ParameterizedText,
-	fullPath string) error {
+	fullPath string,
+	pipeInputScanner *bufio.Scanner) error {
+
 	useTemplate := fileTemplate != ""
-	if useTemplate {
+	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)
 
diff --git a/file_test.go b/file_test.go
index a53896c..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,15 +35,31 @@ func TestCreateFile(t *testing.T) {
 	})
 
 	// test error path
-	err := createFile(prePatternData, "{{.ProjectName", path)
+	err := createFile(prePatternData, "{{.ProjectName", path, nil)
 	assert.NotNil(t, err)
 
-	// test happy path
-	err = createFile(prePatternData, "{{.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, "", path)
+	err = createFile(prePatternData, "", path, nil)
 	assert.Nil(t, err)
 }
diff --git a/main.go b/main.go
index ab3e976..2a8abd0 100644
--- a/main.go
+++ b/main.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"fmt"
 	"os"
 	"os/user"
@@ -30,11 +31,10 @@ type EnvVarValues struct {
 type Settings struct {
 	ArgumentValues
 	EnvVarValues
-	extras           map[string]string
-	fullConfigPath   string
-	config           Config
-	usr              *user.User
-	outputToTerminal bool
+	extras         map[string]string
+	fullConfigPath string
+	config         Config
+	usr            *user.User
 }
 
 func main() {
@@ -189,16 +189,9 @@ func createSettings(
 		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,
-		outputToTerminal: outputToTerminal,
+		ArgumentValues: argumentValues,
+		EnvVarValues:   envVarValues,
 	}
 
 	// read args
@@ -231,18 +224,14 @@ func createSettings(
 	if settings.profile == "" && args.Len() > 0 {
 		settings.profile = args.Get(0)
 	}
-	if !outputToTerminal {
-		fmt.Println("Profile:", settings.profile)
-	}
+	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)
 	}
-	if !outputToTerminal {
-		fmt.Println("Project name:", settings.projectName)
-	}
+	fmt.Println("Project name:", settings.projectName)
 
 	// extras strings
 	_, err = os.Stat(settings.extrasFilePath)
@@ -272,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 if !outputToTerminal {
-			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)
@@ -283,9 +272,7 @@ func createSettings(
 			if err != nil {
 				return settings, fmt.Errorf("error loading user config from %v: %w", fullUserConfigPath, err)
 			} else {
-				if !outputToTerminal {
-					fmt.Printf("Loaded user config from %v, combining with global config\n", fullUserConfigPath)
-				}
+				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)
@@ -339,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 ca7d90d..5f727a0 100644
--- a/profile.go
+++ b/profile.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bufio"
 	"bytes"
 	"fmt"
 	"os"
@@ -19,14 +20,12 @@ const (
 	FileType   ProfileType = "file"
 	FolderType ProfileType = "folder"
 	ScriptType ProfileType = "script"
-	DirectType ProfileType = "direct"
 )
 
 var allProfileTypes = []ProfileType{
 	FileType,
 	FolderType,
 	ScriptType,
-	DirectType,
 }
 
 type OSType string
@@ -66,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")}
 	}
@@ -88,7 +87,7 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 	profileType := profile.Type
 
 	cwd, err := monkey.OSGetwd()
-	if err != nil && profileType != DirectType {
+	if err != nil {
 		return []error{fmt.Errorf("unable to get current working directory: %v", err)}
 	}
 	if profile.TargetDirectory != "" {
@@ -107,6 +106,11 @@ func processProfile(profile Profile, projectName string, extras map[string]strin
 	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 {
@@ -138,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)
@@ -217,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)
@@ -255,7 +260,7 @@ func createEntity(
 					}
 				}
 				fmt.Printf("Creating sub file: %v\n", subFilePath)
-				err = createFile(filePrePatternData, file.FileTemplate, subFilePath)
+				err = createFile(filePrePatternData, file.FileTemplate, subFilePath, nil)
 				if err != nil {
 					return "", fmt.Errorf("error creating file %v: %v", subFilePath, err)
 				}
@@ -263,7 +268,7 @@ func createEntity(
 		}
 	} else if profileType == FileType {
 		fmt.Printf("Creating file: %v\n", fullPath)
-		err := createFile(prePatternData, profile.FileTemplate, fullPath)
+		err := createFile(prePatternData, profile.FileTemplate, fullPath, pipeInputScanner)
 		if err != nil {
 			return "", err
 		}
@@ -318,16 +323,6 @@ func validateProfile(profile *Profile) error {
 		}
 	}
 
-	// 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{}
diff --git a/profile_test.go b/profile_test.go
index ee435fc..c34f84d 100644
--- a/profile_test.go
+++ b/profile_test.go
@@ -171,7 +171,7 @@ func TestCreateEntity(t *testing.T) {
 
 		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))
@@ -196,7 +196,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)
 		assert.Equal(t, 1, len(monkeyPatchCalls.OSWriteFileCalls))
@@ -213,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)
 	}
@@ -248,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) {
@@ -275,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)
@@ -283,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 {