Skip to content

Commit c5c6f02

Browse files
isaac-fletchermeta-codesync[bot]
authored andcommitted
Fix bug with path arg type and add env var expansion (#571)
Summary: Pull Request resolved: #571 The path arg type was not being evaluated based on the path of the TTP YAML. This caused relative paths such as `default: <FILE>` or `default: ./<FILE>` to resolve based on the path you executed ttpforge from as opposed to the path of the YAML. This has been updated to resolve relative to the YAML when provided as a default value and resolve relative to the current working directory when provided as a command-line argument. Additionally added handling for variable expansion within the shell. This should be helpful for TTPs referencing variables like $HOME or alike. Reviewed By: RoboticPrism Differential Revision: D85156301 fbshipit-source-id: 69d8db00c37576684a03ef30d0d55ba078ec2e61
1 parent d85758b commit c5c6f02

File tree

12 files changed

+440
-48
lines changed

12 files changed

+440
-48
lines changed

docs/foundations/args.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -55,20 +55,27 @@ TTPForge supports the following argument types (which you can specify with the
5555

5656
## The `path` Argument Type
5757

58-
If your TTP will accept arguments that refer to file paths on disk, you should
59-
**almost always** use `type: path` when declaring those arguments, as shown in
60-
the example below (just focus on the `args:` section for now, though you can
61-
check out the [edit_file documentation](actions/edit_file.md) if you are
62-
curious):
63-
64-
https://github.com/facebookincubator/TTPForge/blob/7634dc65879ec43a108a4b2d44d7eb2105a2a4b1/example-ttps/actions/edit-file/append-delete.yaml#L1-L35
65-
66-
You must use `type: path` because when you execute `ttpforge run [ttp]`,
67-
**TTPForge changes its working directory to the folder containing the TTP.**
68-
This means that relative paths such as `foo/bar` won't retain their original
69-
meaning by default - however, when you declare your argument using `type: path`,
70-
TTPForge knows to expand its value to an absolute path prior to changing
71-
directories, ensuring that everything will work as intended.
58+
Use `type: path` for file path arguments. Path arguments are automatically:
59+
60+
- **Resolved to absolute paths** with symlinks resolved
61+
- **Expanded for variables** like `$HOME`, `${USER}`, `~`, etc.
62+
63+
Relative paths resolve differently depending on where they're defined:
64+
65+
- **Default values** (in YAML): Resolved relative to the TTP's directory
66+
- **CLI arguments**: Resolved relative to where you run `ttpforge`
67+
68+
**Example:**
69+
70+
```yaml
71+
args:
72+
- name: config_file
73+
type: path
74+
default: ./config.yaml # Relative to TTP directory
75+
- name: output_dir
76+
type: path
77+
default: $HOME/output # Variable expansion
78+
```
7279
7380
## Predefined Choices for Argument Values
7481

example-ttps/args/path.yaml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
api_version: 2.0
3+
uuid: 8f3c9d2a-1b5e-4c7a-9f8e-2d3a4b5c6d7e
4+
name: Path Argument Demo
5+
description: |
6+
This TTP demonstrates how path arguments with default values work.
7+
The default path is relative to the YAML file's directory, NOT the
8+
directory where ttpforge was executed.
9+
10+
Example Usage:
11+
# Create a test file in the same directory as this YAML
12+
echo "Hello from YAML directory" > example-ttps/args/test-file.txt
13+
14+
# Run with default (will read the file relative to YAML location)
15+
ttpforge run example-ttps/args/path-default-relative.yaml
16+
17+
# Or override with your own file (relative to where you run ttpforge)
18+
echo "Hello from execution directory" > my-file.txt
19+
ttpforge run example-ttps/args/path-default-relative.yaml --arg file_path=my-file.txt
20+
21+
args:
22+
- name: new_dir
23+
type: path
24+
description: Path to change directory to
25+
default: $HOME
26+
- name: file_path
27+
type: path
28+
description: Path to a file to read
29+
default: ./test-file.txt
30+
31+
steps:
32+
- name: show_current_directory
33+
description: |
34+
Show the current directory
35+
inline: |
36+
echo "Current Directory: $(pwd)"
37+
- name: change_directory
38+
description: |
39+
Change directory to demonstrate how path arguments are resolved
40+
cd: {{ .Args.new_dir }}
41+
- name: print_file_contents
42+
description: |
43+
Reads the contents of the file provided as an argument
44+
inline: |
45+
echo Reading file: {{ .Args.file_path }}
46+
cat {{.Args.file_path}}

example-ttps/args/test-file.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Hello from the YAML directory!
2+
This file is in the same directory as path-default-relative.yaml.
3+
When you run the TTP with the default argument, this file will be read.

pkg/args/spec.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package args
2222
import (
2323
"errors"
2424
"fmt"
25+
"path/filepath"
2526
"regexp"
2627
"strconv"
2728
"strings"
@@ -47,13 +48,14 @@ type Spec struct {
4748
//
4849
// specs: slice of argument Spec values loaded from the TTP yaml
4950
// argKvStrs: slice of arguments in "ARG_NAME=ARG_VALUE" format
51+
// cliBaseDir: the directory to resolve CLI path arguments relative to
52+
// defaultBaseDir: the directory to resolve default path values relative to
5053
//
5154
// **Returns:**
5255
//
53-
// map[string]string: the parsed and validated argument key-value pairs
56+
// map[string]any: the parsed and validated argument key-value pairs
5457
// error: an error if there is a problem
55-
func ParseAndValidate(specs []Spec, argsKvStrs []string) (map[string]any, error) {
56-
58+
func ParseAndValidate(specs []Spec, argsKvStrs []string, cliBaseDir string, defaultBaseDir string) (map[string]any, error) {
5759
// validate the specs
5860
processedArgs := make(map[string]any)
5961
specsByName := make(map[string]Spec)
@@ -68,12 +70,20 @@ func ParseAndValidate(specs []Spec, argsKvStrs []string) (map[string]any, error)
6870
}
6971

7072
// set the default value, will be overwritten by passed value
73+
// Path defaults are resolved relative to defaultBaseDir (typically the YAML directory)
7174
if spec.Default != "" {
7275
if !spec.isValidChoice(spec.Default) {
7376
return nil, fmt.Errorf("invalid default value: %v, allowed values: %v ", spec.Default, strings.Join(spec.Choices, ", "))
7477
}
7578

76-
defaultVal, err := spec.convertArgToType(spec.Default)
79+
// For path types, resolve relative paths relative to defaultBaseDir
80+
// Absolute paths and paths with shell variables are left as-is
81+
defaultValue := spec.Default
82+
if spec.Type == "path" && !filepath.IsAbs(spec.Default) && !fileutils.ContainsShellVariable(spec.Default) {
83+
defaultValue = filepath.Join(defaultBaseDir, spec.Default)
84+
}
85+
86+
defaultVal, err := spec.convertArgToType(defaultValue)
7787
if err != nil {
7888
return nil, fmt.Errorf("default value type does not match spec: %w", err)
7989
}
@@ -123,7 +133,14 @@ func ParseAndValidate(specs []Spec, argsKvStrs []string) (map[string]any, error)
123133
return nil, fmt.Errorf("invalid value format: %v, expected regex format: %v ", argVal, spec.Format)
124134
}
125135

126-
typedVal, err := spec.convertArgToType(argVal)
136+
// For path types, resolve relative paths relative to cliBaseDir
137+
// Absolute paths and paths with shell variables are left as-is
138+
argValue := argVal
139+
if spec.Type == "path" && !filepath.IsAbs(argVal) && !fileutils.ContainsShellVariable(argVal) {
140+
argValue = filepath.Join(cliBaseDir, argVal)
141+
}
142+
143+
typedVal, err := spec.convertArgToType(argValue)
127144
if err != nil {
128145
return nil, fmt.Errorf(
129146
"failed to process value '%v' specified for argument '%v': %v",
@@ -164,6 +181,8 @@ func (spec Spec) convertArgToType(val string) (any, error) {
164181
}
165182
return asBool, nil
166183
case "path":
184+
// Path has already been resolved relative to appropriate baseDir
185+
// Just convert to absolute and resolve symlinks
167186
absPath, err := fileutils.AbsPath(val)
168187
if err != nil {
169188
return nil, fmt.Errorf("failed to process argument of type `path`: %w", err)

0 commit comments

Comments
 (0)