fix: strip trailing newlines from shell stdout by default#51
Conversation
Shell commands like echo, date, hostname add trailing newlines for terminal
formatting. When using ${node.stdout} in file paths, this creates corrupted
filenames with embedded newlines (e.g., "myfile\n.txt").
Changes:
- Add strip_newline parameter to shell node (default: true)
- Strip trailing newlines from text stdout (matches bash $() convention)
- Extract _store_output() helper to reduce post() complexity
- Update 3 tests expecting trailing newlines
- Add 4 high-value tests for strip_newline behavior
- Update agent instructions with opt-out documentation
Users can disable stripping with strip_newline: false for edge cases
where raw output is needed.
Fixes #48
Code review fixes: - Remove default from _store_output() method signature (warning #4) - Add explicit strip_newline=False for stderr call - Fix type hints: str | bytes with isinstance narrowing (warning #5) - Update docstrings to clarify stdout-only behavior (warning #3) - Add test for binary data + strip_newline interaction (critical #1) - Add test for empty stdout edge case (critical #2) - Update agent instructions to clarify stderr is never modified All code review items addressed except Windows CRLF (documented as Unix-first).
Code Review - PR #51: Strip trailing newlines from shell stdoutSummaryThis PR addresses a real bug where shell commands add trailing newlines that corrupt filenames when using ✅ Strengths
🔴 Critical — must fix before merge1. Missing test for binary data + strip_newline interactionIssue: The Why critical: If someone refactors this code without understanding the constraint, they could introduce a bug where binary data gets corrupted by string operations. Suggested fix: Add a test to def test_strip_newline_ignored_for_binary_stdout(self):
"""strip_newline parameter has no effect on binary data.
Binary data should never be processed as text,
even if strip_newline=true (the default).
"""
shared = {}
# Binary data ending with 0x0a (newline byte)
run_shell_node(
shared,
command="printf '\\x89PNG\\r\\n\\x1a\\n'", # PNG header
strip_newline=True # Explicit true, should have no effect
)
# Binary data unchanged (base64 encoded)
assert shared["stdout_is_binary"] is True
decoded = base64.b64decode(shared["stdout"])
assert decoded.endswith(b'\n'), "Binary newline byte was stripped"2. Empty stdout edge case not testedIssue: When Why critical: Empty output is common (e.g., Suggested fix: Add to def test_empty_stdout_unchanged(self):
"""Empty stdout remains empty (edge case for rstrip).
Common scenario: commands with conditional output.
"""
shared = {}
run_shell_node(shared, command="echo -n ''") # Empty output
assert shared["stdout"] == ""
assert shared["stdout_is_binary"] is False
|
Code Review ResponseThanks for the thorough review! All critical and warning items have been addressed: ✅ Critical Issues Fixed#1 Binary data + strip_newline test: Added #2 Empty stdout test: Added ✅ Warnings Addressed#3 stderr asymmetry documented:
#4 Default parameter inconsistency: Removed default from #5 Type hint specificity: Changed ⏭️ Deferred#6 Windows CRLF: This is Unix-first following bash convention. Documented as known behavior. Test Results
|
Code Review: Strip Trailing Newlines from Shell StdoutSummaryThis PR addresses a legitimate and well-documented bug where trailing newlines from shell commands corrupt filenames in template variable substitution. The implementation is clean, well-tested, and follows the project's quality standards. ✅ Approval - Ready to MergeThis is high-quality work with excellent documentation and testing. No critical issues found. Warnings — Should be addressed1. Stderr Stripping InconsistencyLocation: The PR correctly never strips stderr, but the behavior could be made more consistent by offering Current behavior:
Consideration: What if a user wants to strip stderr newlines? Currently impossible. Suggested improvement: # In prep():
strip_newline_stdout = self.params.get("strip_newline", True)
strip_newline_stderr = self.params.get("strip_newline_stderr", False)
# In post():
self._store_output(
shared, "stderr", exec_res["stderr"],
exec_res.get("stderr_is_binary", False),
strip_newline=prep_res.get("strip_newline_stderr", False)
)This maintains backward compatibility (stderr behavior unchanged) while offering future flexibility. Priority: Low - can be addressed in a follow-up if users request it. 2. Documentation Could Be More ProminentLocation: The documentation update is accurate but buried in a bullet list. Consider adding a dedicated callout or example showing the file path use case, since that's the primary motivation. Priority: Medium - helps users discover this feature. Suggestions — Optional improvements1. Test Coverage: Edge Case for Windows Line EndingsLocation: The tests cover Unix newlines ( Note: Priority: Very Low - Unix shells almost never produce 2. Helper Method Could Be StaticLocation: The Priority: Very Low - cosmetic improvement. 3. Scratchpad Files Could Be GitignoredLocation: The scratchpad directory contains excellent debugging artifacts. Current choice to keep them is fine - this is valuable historical context. Consider documenting the scratchpad retention policy in Priority: Very Low - documentation clarification. Strengths of This PR✅ Excellent Problem DocumentationThe ✅ Thoughtful Default ChoiceMaking ✅ Comprehensive Test CoverageThe tests cover default behavior, opt-out, multiline output (internal newlines preserved), multiple trailing newlines, empty output, and binary data. This is high-quality testing focused on behavior, not just coverage metrics. ✅ Cyclomatic Complexity ReductionExtracting ✅ Backward CompatibilityExisting tests that expected Security & Safety✅ No security concerns - the change only affects output processing, not command execution Performance✅ Negligible impact - Architecture Alignment✅ Follows project patterns:
✅ Matches
Final RecommendationAPPROVE ✅ - This PR should be merged as-is. The warnings and suggestions are optional enhancements that can be addressed in follow-up PRs if desired. The core fix is solid, well-tested, and solves a real user pain point. Excellent work on:
This is the kind of quality the project needs. 🎯 |
Potential edge case considerationThe current implementation uses if strip_newline:
text_value = text_value.rstrip("\n")This means Scenarios where this might matter:
Current mitigation: Users can set Alternative consideration: Strip only ONE trailing newline (the shell-added artifact) instead of all: if strip_newline and text_value.endswith("\n"):
text_value = text_value[:-1]This is probably not worth changing since:
Just documenting this for future reference in case someone reports unexpected behavior with multi-line outputs. |
Summary
Shell commands like
echo,date,hostnameadd trailing newlines for terminal formatting. When using${node.stdout}in file paths, this creates silently corrupted filenames with embedded newlines (e.g.,myfile\n.txt).This fix adds a
strip_newlineparameter to the shell node (default:true) that strips trailing newlines from text stdout, matching bash$()convention.Changes
strip_newlineparameter to shell node (default:true)post()_store_output()helper to reduce cyclomatic complexitystrip_newlinebehaviorBehavior
echo hello→${node.stdout}hello\nhello${node.stdout}.txthello\n.txt(corrupted)hello.txtstrip_newline: falsehello\n(opt-out)\npreservedTesting
make test- All 3857 tests passmake check- All linting/type checks passFixes #48