Run shell blocks with :cmdline, :stdin and :shebang !136

merged merged by cmc on 2026-10-07 07:59 UTC · krz/orgstar:shell-cmdline into main

3 files changed, +69 −9

Layout: unified · split

Sources/OrgApp/BabelRunner.swift +7 −1
@@ -34,7 +34,13 @@ public enum BabelRunner {
3434 process.environment = environment
3535 let stdin = Pipe()
3636 switch job.runner {
37 case .process(let executable, let arguments):
37 case .process(let executable, var arguments):
38 if let script = job.script {
39 let file = folder.appendingPathComponent("script")
40 try script.write(to: file, atomically: true, encoding: .utf8)
41 try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: file.path)
42 arguments = arguments.map { $0.replacingOccurrences(of: Babel.scriptFileToken, with: file.path) }
43 }
3844 process.arguments = [executable] + arguments
3945 case .emacs:
4046 guard let emacs = EmacsRunner.executable() else { throw Failure(description: "Emacs isn't installed, so Emacs Lisp blocks can't run.") }
Sources/OrgCore/Compute/Babel.swift +46 −6
@@ -36,6 +36,8 @@ public struct BabelJob: Sendable, Equatable {
3636 var resolved: [String: Babel.VarValue] = [:]
3737 var caret = 0
3838 var options = Babel.Options()
39 /// A script for the runner to write to an executable file, for `Babel.scriptFileToken`.
40 public internal(set) var script: String?
3941 /// An Emacs Lisp block's form, for `Babel.evaluateNatively`.
4042 public internal(set) var lispForm: String?
4143
@@ -91,6 +93,8 @@ public struct BabelOutput: Sendable, Equatable {
9193
9294public enum Babel {
9395 public static let resultFileToken = "__ORGSTAR_RESULT_FILE__"
96 /// Stands, in a job's arguments, for the executable file the runner writes `job.script` to.
97 public static let scriptFileToken = "__ORGSTAR_SCRIPT_FILE__"
9498 static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"]
9599 static let generic: [String: (String, [String])] = [
96100 "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]),
@@ -253,10 +257,11 @@ public enum Babel {
253257 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
254258 return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled."))
255259 }
256 if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language) {
257 return .failure(.message(":cmdline isn't supported yet; nothing was run."))
260 if params.single[":cmdline"] != nil, !["dot", "plantuml", "mermaid"].contains(language), !shells.contains(language) {
261 return .failure(.message(":cmdline isn't supported for \(language) yet; nothing was run."))
258262 }
259 for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil {
263 for key in [":prologue", ":epilogue", ":post", ":stdin", ":shebang"] where params.single[key] != nil
264 && !(shells.contains(language) && [":stdin", ":shebang"].contains(key)) {
260265 return .failure(.message("\(key) isn't supported yet; nothing was run."))
261266 }
262267 let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) }
@@ -285,8 +290,10 @@ public enum Babel {
285290 file: toFile ? file : nil, fileDescription: params.single[":file-desc"])
286291 }
287292 let vars: [(String, VarValue)]
293 var stdin: VarValue?
288294 do {
289295 vars = try params.vars.map { ($0.name, try resolve($0.value, model: model, text: ns, refs: refs)) }
296 stdin = try params.single[":stdin"].map { try resolve($0, model: model, text: ns, refs: refs) }
290297 } catch let needed as Needed {
291298 // Asked about once, with every block the references reach.
292299 let texts = [ns.substring(with: NSRange(location))] + referencedBlocks(params, model: model, text: ns, seen: []).map { ns.substring(with: NSRange($0.range)) }
@@ -314,8 +321,25 @@ public enum Babel {
314321 }
315322 lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'")
316323 }
317 let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
318324 let executable = language == "shell" ? options.shell : language
325 let cmdline = params.single[":cmdline"]
326 let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil }
327 if session == nil, stdin != nil || cmdline != nil || shebang != nil {
328 // `org-babel-sh-evaluate`: an executable script, run by the shell with the
329 // command line after it and `:stdin` as its input; the value is its last line.
330 var script = (shebang ?? "#!/usr/bin/env \(executable)") + "\n"
331 if params.single[":padline"] != "no" { script += "\n" }
332 script += (lines + [body]).joined(separator: "\n")
333 let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "")
334 var job = BabelJob(
335 language: language, runner: .process(executable: executable, arguments: ["-c", command]),
336 input: stdin.map(shellString) ?? "", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk,
337 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
338 )
339 job.script = script
340 return .success(job)
341 }
342 let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "")
319343 return .success(BabelJob(
320344 language: language, runner: session.map { .session(name: $0, executable: executable, python: false) } ?? .process(executable: executable, arguments: []),
321345 input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir,
@@ -756,8 +780,8 @@ public enum Babel {
756780 static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] {
757781 var found: [SrcBlockInfo] = []
758782 var seen = seen
759 for variable in params.vars {
760 guard let (block, arguments) = referencedBlock(variable.value, model: model), let name = block.name, seen.insert(name).inserted else { continue }
783 for value in params.vars.map(\.value) + [params.single[":stdin"]].compactMap({ $0 }) {
784 guard let (block, arguments) = referencedBlock(value, model: model), let name = block.name, seen.insert(name).inserted else { continue }
761785 found.append(block)
762786 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? []
763787 if let inner = try? Params(block: block, model: model, text: text, extra: extra) {
@@ -820,6 +844,22 @@ public enum Babel {
820844 throw Failure.message("\(value) isn't a table or list.")
821845 }
822846
847 /// `org-babel-sh-var-to-string`: a table's rows as tab-separated lines, a list's items as
848 /// lines.
849 static func shellString(_ value: VarValue) -> String {
850 func cell(_ c: Cell) -> String {
851 switch c {
852 case .number(let n): n
853 case .text(let s): s
854 }
855 }
856 switch value {
857 case .scalar(let s), .number(let s): return s
858 case .list(let items): return items.map(cell).joined(separator: "\n")
859 case .table(let rows): return rows.map { $0.map(cell).joined(separator: "\t") }.joined(separator: "\n")
860 }
861 }
862
823863 /// `org-babel-python-var-to-python`.
824864 static func pythonLiteral(_ value: VarValue) -> String {
825865 func cell(_ c: Cell) -> String {
Tests/OrgCoreTests/BabelTests.swift +16 −2
@@ -15,7 +15,13 @@ enum TestBabelRunner {
1515 let process = Process()
1616 process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
1717 switch job.runner {
18 case .process(let executable, let arguments):
18 case .process(let executable, var arguments):
19 if let script = job.script {
20 let file = folder.appendingPathComponent("script")
21 try script.write(to: file, atomically: true, encoding: .utf8)
22 try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: file.path)
23 arguments = arguments.map { $0.replacingOccurrences(of: Babel.scriptFileToken, with: file.path) }
24 }
1925 process.arguments = [executable] + arguments
2026 case .emacs:
2127 let script = folder.appendingPathComponent("babel.el")
@@ -132,6 +138,14 @@ struct BabelTests {
132138 "#+PROPERTY: header-args :results output\n#+begin_src sh\nprintf 'a b\\nc d\\n'\n#+end_src\n",
133139 "* Heading\n:PROPERTIES:\n:header-args:sh: :results verbatim\n:END:\n#+begin_src sh\necho 1 2\n#+end_src\n",
134140 "#+HEADER: :results output\n#+begin_src sh\nprintf 'x y\\nz w\\n'\n#+end_src\n",
141 // `:cmdline`, `:stdin` and `:shebang`.
142 "#+begin_src sh :cmdline one \"two words\" :results output\nfor a in \"$@\"; do echo \"[$a]\"; done\n#+end_src\n",
143 "#+begin_src sh :cmdline 3\necho $(( $1 * 2 ))\n#+end_src\n",
144 "#+NAME: rows\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :stdin rows :results output\nwhile IFS=$(printf '\\t') read -r k v; do echo \"$k=$v\"; done\n#+end_src\n",
145 "#+NAME: items\n- x\n- y\n\n#+begin_src sh :stdin items\nwc -l | tr -d ' '\n#+end_src\n",
146 "#+begin_src bash :shebang #!/bin/bash :results output\necho ${BASH_VERSION:+bash}\n#+end_src\n",
147 "#+begin_src sh :cmdline a b :padline no :var x=5 :results output\necho $x $#\n#+end_src\n",
148 "#+begin_src sh :stdin src :results output\ntr a-z A-Z\n#+end_src\n\n#+NAME: src\n#+begin_src sh\necho hello\n#+end_src\n",
135149 // `:var` from other blocks' results.
136150 "#+begin_src sh :var x=five\necho $((x+1))\n#+end_src\n\n#+NAME: five\n#+begin_src sh\necho 5\n#+end_src\n",
137151 "#+begin_src python :var t=tbl\nreturn [len(t), t[1][0]]\n#+end_src\n\n#+NAME: tbl\n#+begin_src python\nreturn [[1, 2], [3, 4]]\n#+end_src\n",
@@ -231,7 +245,7 @@ struct BabelTests {
231245 ("#+begin_src sh :eval never\necho\n#+end_src\n", "Evaluation of this sh code block is disabled."),
232246 ("#+begin_src ruby :session s\nputs 1\n#+end_src\n", ":session isn't supported for ruby yet; nothing was run."),
233247 ("#+begin_src sh :noweb yes\n<<x()>>\n#+end_src\n", "Noweb references that run a block (x()) aren't supported yet; nothing was run."),
234 ("#+begin_src sh :cmdline -x\necho\n#+end_src\n", ":cmdline isn't supported yet; nothing was run."),
248 ("#+begin_src python :cmdline -x\nreturn 1\n#+end_src\n", ":cmdline isn't supported for python yet; nothing was run."),
235249 ("#+begin_src cobol\nDISPLAY\n#+end_src\n", "No way to run cobol blocks yet."),
236250 ("#+begin_src sh :var x=loop\necho\n#+end_src\n#+NAME: loop\n#+begin_src sh :var y=loop\necho\n#+end_src\n", "loop refers to itself through :var"),
237251 ] {