krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 3ed8024d96

3ed8024d96095786cc5d67f2e876ba5e08b1c94f

parent: 2f73c9b694

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 01:54 UTC

Babel Lisp header values and the user's shell

Header values starting with ( ' ` or [ are evaluated with the Elisp
interpreter, as org-babel-read does: strings and numbers replace the form,
lists serve :colnames and :rownames, and all of them go into the :cache
hash. Babel.Options.shell comes from SHELL, as shell-file-name does.

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +55 −15
@@ -113,8 +113,9 @@ public enum Babel {
113 113
114 public struct Options: Sendable, Equatable { 114 public struct Options: Sendable, Equatable {
115 public var python = "python3" 115 public var python = "python3"
116 /// `shell-file-name`, for blocks in the `shell` language. 116 /// `shell-file-name`, from `SHELL` as Emacs sets it: runs blocks in the `shell` language
117 public var shell = "/bin/sh" 117 /// and chooses how variables are written in the `:cache` hash.
118 public var shell = ProcessInfo.processInfo.environment["SHELL"] ?? "/bin/sh"
118 public init() {} 119 public init() {}
119 } 120 }
120 121
@@ -262,6 +263,8 @@ public enum Babel {
262 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options, 263 static func plan(_ block: SrcBlockInfo, params: Params, model: DocumentModel, text ns: NSString, options: Options,
263 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> { 264 location: Range<Int>, name: String?, inline: Bool = false, refs: References? = nil, useCache: Bool = true) -> Result<BabelJob, Failure> {
264 let refs = refs ?? References(resolved: [:], options: options) 265 let refs = refs ?? References(resolved: [:], options: options)
266 var params = params
267 params.evaluateLisp()
265 guard let language = block.language else { return .failure(.message("This block has no language")) } 268 guard let language = block.language else { return .failure(.message("This block has no language")) }
266 let eval = params.single[":eval"]?.lowercased() 269 let eval = params.single[":eval"]?.lowercased()
267 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { 270 if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" {
@@ -298,7 +301,7 @@ public enum Babel {
298 let finishBase = { (kind: BabelJob.Finish.Kind) in 301 let finishBase = { (kind: BabelJob.Finish.Kind) in
299 BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline, 302 BabelJob.Finish(kind: kind, language: language, params: params.results, output: output, wrap: params.wrap, name: name, inline: inline,
300 file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash, 303 file: toFile ? file : nil, fileDescription: params.single[":file-desc"], hash: hash,
301 colnames: tables.picked(tables.colnames, params.single[":colnames"]), rownames: tables.picked(tables.rownames, params.single[":rownames"])) 304 colnames: tables.picked(tables.colnames, ":colnames", params), rownames: tables.picked(tables.rownames, ":rownames", params))
302 } 305 }
303 let vars: [(String, VarValue)] 306 let vars: [(String, VarValue)]
304 var stdin: VarValue? 307 var stdin: VarValue?
@@ -581,14 +584,26 @@ public enum Babel {
581 var colnames: [(name: String, names: [Cell])] = [] 584 var colnames: [(name: String, names: [Cell])] = []
582 var rownames: [(name: String, names: [Cell])] = [] 585 var rownames: [(name: String, names: [Cell])] = []
583 586
584 /// `org-babel-pick-name`: the last variable's names, unless the header argument is 587 /// `org-babel-pick-name`: a Lisp list given as the header argument, or the last
585 /// absent, whose `nil` selects none. 588 /// variable's names, unless the header argument is absent, whose `nil` selects none.
586 func picked(_ names: [(name: String, names: [Cell])], _ selector: String?) -> [String]? { 589 func picked(_ names: [(name: String, names: [Cell])], _ key: String, _ params: Params) -> [String]? {
587 guard selector != nil, let last = names.last else { return nil } 590 if let list = params.evaluated[key].flatMap(Babel.lispNames) { return list }
591 guard params.single[key] != nil, let last = names.last else { return nil }
588 return last.names.map(Babel.cellText) 592 return last.names.map(Babel.cellText)
589 } 593 }
590 } 594 }
591 595
596 /// A Lisp list of strings and numbers, as names.
597 static func lispNames(_ value: Sexp) -> [String]? {
598 guard case .list(let items) = value else { return nil }
599 return try? items.map { item in
600 if let s = item.string { return s }
601 let printed = Elisp.printed(item)
602 guard number(printed) == printed else { throw Failure.message(printed) }
603 return printed
604 }
605 }
606
592 static func cellText(_ c: Cell) -> String { 607 static func cellText(_ c: Cell) -> String {
593 switch c { 608 switch c {
594 case .number(let n): n 609 case .number(let n): n
@@ -600,8 +615,8 @@ public enum Babel {
600 /// by default when only its second row is a rule; its first column with `:rownames`; its 615 /// by default when only its second row is a rule; its first column with `:rownames`; its
601 /// rules unless `:hlines yes`. 616 /// rules unless `:hlines yes`.
602 static func disassemble(_ vars: [(String, VarValue)], params: Params) throws -> Tables { 617 static func disassemble(_ vars: [(String, VarValue)], params: Params) throws -> Tables {
603 for key in [":colnames", ":rownames"] where params.lisp.contains(key) { 618 for key in [":colnames", ":rownames"] where params.lisp.contains(key) && params.evaluated[key].flatMap(lispNames) == nil {
604 throw Failure.message("\(key) with a Lisp value isn't supported yet; nothing was run.") 619 throw Failure.message("\(key) \(params.single[key] ?? "") is Lisp that only Emacs can evaluate; nothing was run.")
605 } 620 }
606 let colnames = params.single[":colnames"] 621 let colnames = params.single[":colnames"]
607 let rownames = params.single[":rownames"] 622 let rownames = params.single[":rownames"]
@@ -708,8 +723,8 @@ public enum Babel {
708 /// What `org-babel-sha1-hash` hashes: the header arguments after `org-babel-process-params`, 723 /// What `org-babel-sha1-hash` hashes: the header arguments after `org-babel-process-params`,
709 /// sorted by key and printed with `%S`, then the expanded body. 724 /// sorted by key and printed with `%S`, then the expanded body.
710 static func cacheInput(language: String, params: Params, tables: Tables, expanded: String, call: Bool) throws -> String { 725 static func cacheInput(language: String, params: Params, tables: Tables, expanded: String, call: Bool) throws -> String {
711 if !params.lisp.isEmpty { 726 if let key = params.lisp.first(where: { params.evaluated[$0] == nil }) {
712 throw Failure.message(":cache with Lisp header values isn't supported yet; nothing was run.") 727 throw Failure.message(":cache with \(key) \(params.single[key] ?? "") needs Emacs to evaluate the Lisp; nothing was run.")
713 } 728 }
714 let ignored: Set<String> = ["replace", "silent", "none", "discard", "append", "prepend"] 729 let ignored: Set<String> = ["replace", "silent", "none", "discard", "append", "prepend"]
715 let drawsFile = language == "dot" || language == "plantuml" 730 let drawsFile = language == "dot" || language == "plantuml"
@@ -719,7 +734,9 @@ public enum Babel {
719 var entries: [(key: String, printed: String)] = [] 734 var entries: [(key: String, printed: String)] = []
720 for (key, value) in defaults.merging(params.single, uniquingKeysWith: { $1 }) { 735 for (key, value) in defaults.merging(params.single, uniquingKeysWith: { $1 }) {
721 let printed: String 736 let printed: String
722 if value.isEmpty { 737 if let evaluated = params.evaluated[key] {
738 printed = Elisp.printed(evaluated, escape: true)
739 } else if value.isEmpty {
723 continue 740 continue
724 } else if key == ":exports" { 741 } else if key == ":exports" {
725 printed = lispString(value.split(whereSeparator: \.isWhitespace).map(String.init).filter { !ignored.contains($0) }.sorted().joined(separator: " ")) 742 printed = lispString(value.split(whereSeparator: \.isWhitespace).map(String.init).filter { !ignored.contains($0) }.sorted().joined(separator: " "))
@@ -803,10 +820,32 @@ public enum Babel {
803 var single: [String: String] = [:] 820 var single: [String: String] = [:]
804 var vars: [(name: String, value: String)] = [] 821 var vars: [(name: String, value: String)] = []
805 var wrap: String? 822 var wrap: String?
806 /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`. 823 /// Keys whose value is Lisp to evaluate (`org-babel-read`): unquoted, starting with `(`, `'`, `` ` `` or `[`.
807 var lisp: Set<String> = [] 824 var lisp: Set<String> = []
808 /// Keys whose value was a quoted string, which `org-babel-read` doesn't take for a number. 825 /// Keys whose value was a quoted string, which `org-babel-read` doesn't take for a number.
809 var quoted: Set<String> = [] 826 var quoted: Set<String> = []
827 /// Lisp values evaluated to something other than a string or number, such as a list of
828 /// column names.
829 var evaluated: [String: Sexp] = [:]
830
831 /// `org-babel-read` on the Lisp values the interpreter can evaluate: a string or number
832 /// takes the form's place; other values are kept in `evaluated`.
833 mutating func evaluateLisp() {
834 for key in lisp {
835 guard let form = try? LispReader.readFirst(single[key] ?? "").sexp, let value = try? Elisp().eval(form) else { continue }
836 let printed = Elisp.printed(value)
837 if let s = value.string {
838 single[key] = s
839 quoted.insert(key)
840 lisp.remove(key)
841 } else if Babel.number(printed) == printed {
842 single[key] = printed
843 lisp.remove(key)
844 } else {
845 evaluated[key] = value
846 }
847 }
848 }
810 849
811 static let resultGroups: [Set<String>] = [ 850 static let resultGroups: [Set<String>] = [
812 ["file", "list", "vector", "table", "scalar", "verbatim"], 851 ["file", "list", "vector", "table", "scalar", "verbatim"],
@@ -859,7 +898,7 @@ public enum Babel {
859 let quoted = key != ":var" && raw.count >= 2 && raw.hasPrefix("\"") && raw.hasSuffix("\"") 898 let quoted = key != ":var" && raw.count >= 2 && raw.hasPrefix("\"") && raw.hasSuffix("\"")
860 let value = quoted ? Params.unescape(String(raw.dropFirst().dropLast())) : raw 899 let value = quoted ? Params.unescape(String(raw.dropFirst().dropLast())) : raw
861 if key != ":var" { 900 if key != ":var" {
862 if !quoted, value.hasPrefix("(") { lisp.insert(key) } else { lisp.remove(key) } 901 if !quoted, let first = value.first, "('`[".contains(first) { lisp.insert(key) } else { lisp.remove(key) }
863 if quoted { self.quoted.insert(key) } else { self.quoted.remove(key) } 902 if quoted { self.quoted.insert(key) } else { self.quoted.remove(key) }
864 } 903 }
865 switch key { 904 switch key {
@@ -1060,7 +1099,8 @@ public enum Babel {
1060 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] 1099 let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? []
1061 var inner = refs 1100 var inner = refs
1062 inner.visiting.insert(name) 1101 inner.visiting.insert(name)
1063 let params = try Params(block: block, model: model, text: text, extra: extra) 1102 var params = try Params(block: block, model: model, text: text, extra: extra)
1103 params.evaluateLisp()
1064 // A cached block gives its result without running, as `org-babel-execute-src-block` does. 1104 // A cached block gives its result without running, as `org-babel-execute-src-block` does.
1065 var useCache = true 1105 var useCache = true
1066 if params.single[":cache"] == "yes", let language = block.language { 1106 if params.single[":cache"] == "yes", let language = block.language {
Tests/OrgCoreTests/BabelCacheTests.swift +6 −2
@@ -33,6 +33,10 @@ struct BabelCacheTests {
33 runs: ["#+begin_src", "#+begin_src"]), 33 runs: ["#+begin_src", "#+begin_src"]),
34 Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :cache yes :var t=kv :separator ,\necho $t\n#+end_src\n", 34 Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src sh :cache yes :var t=kv :separator ,\necho $t\n#+end_src\n",
35 runs: ["#+begin_src", "#+begin_src"]), 35 runs: ["#+begin_src", "#+begin_src"]),
36 Case(text: "#+begin_src sh :cache yes :dir (concat \"/\" \"usr\") :foo (list 1 \"a\") :bar (+ 1 2)\npwd\n#+end_src\n",
37 runs: ["#+begin_src", "#+begin_src"]),
38 Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
39 runs: ["#+begin_src", "#+begin_src"]),
36 // Edited after the first run, so it runs again. 40 // Edited after the first run, so it runs again.
37 Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]), 41 Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]),
38 // A reference to a cached block takes its result without running it. 42 // A reference to a cached block takes its result without running it.
@@ -93,12 +97,12 @@ struct BabelCacheTests {
93 } 97 }
94 98
95 @Test func lispHeadersAreRefused() { 99 @Test func lispHeadersAreRefused() {
96 let text = "#+begin_src sh :cache yes :dir (concat \"/\" \"tmp\")\necho hi\n#+end_src\n" 100 let text = "#+begin_src sh :cache yes :dir (expand-file-name \"x\")\necho hi\n#+end_src\n"
97 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2]) 101 let context = EditContext(revision: 0, text: text, tree: OrgParser.parse(text), selection: [2..<2])
98 guard case .failure(.message(let message)) = Babel.plan(context) else { 102 guard case .failure(.message(let message)) = Babel.plan(context) else {
99 Issue.record("planned") 103 Issue.record("planned")
100 return 104 return
101 } 105 }
102 #expect(message.hasPrefix(":cache with Lisp header values")) 106 #expect(message.hasPrefix(":cache with :dir (expand-file-name"))
103 } 107 }
104} 108}
Tests/OrgCoreTests/BabelTests.swift +4
@@ -189,6 +189,10 @@ struct BabelTests {
189 "#+begin_src bash :var l='(1 \"two\" 3) :results output\necho ${l[2]} ${l[1]}\n#+end_src\n", 189 "#+begin_src bash :var l='(1 \"two\" 3) :results output\necho ${l[2]} ${l[1]}\n#+end_src\n",
190 "#+NAME: items\n- one\n- two\n\n#+begin_src zsh :var l=items :results output\necho \"$l\"\n#+end_src\n", 190 "#+NAME: items\n- one\n- two\n\n#+begin_src zsh :var l=items :results output\necho \"$l\"\n#+end_src\n",
191 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n", 191 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
192 "TBL#+begin_src python :var t=tbl :colnames '(\"x\" \"y\")\nreturn t\n#+end_src\n",
193 "TBL#+begin_src python :var t=tbl :rownames '(\"r1\" \"r2\")\nreturn t\n#+end_src\n",
194 "#+begin_src sh :dir (concat \"/\" \"usr\") :results output\npwd\n#+end_src\n",
195 "#+NAME: items\n- one\n- two\n\n#+begin_src shell :var l=items :results output\necho \"$l\"\n#+end_src\n",
192 // Lisp values. 196 // Lisp values.
193 "#+begin_src python :var l='(1 2) m='((1 2) (3 4)) n=(+ 1 2) s='(\"a\" \"b\")\nreturn [l, m, n, s]\n#+end_src\n", 197 "#+begin_src python :var l='(1 2) m='((1 2) (3 4)) n=(+ 1 2) s='(\"a\" \"b\")\nreturn [l, m, n, s]\n#+end_src\n",
194 "#+begin_src emacs-lisp :var l='(1 2) h='((1 2) hline (3 4)) s=(concat \"a\" \"b\")\n(list l h s)\n#+end_src\n", 198 "#+begin_src emacs-lisp :var l='(1 2) h='((1 2) hline (3 4)) s=(concat \"a\" \"b\")\n(list l h s)\n#+end_src\n",