Babel tables and lists in shell :var !153

merged merged by cmc on 2026-10-08 01:04 UTC · krz/orgstar:babel-shell-tables into main

3 files changed, +59 −26

Layout: unified · split

Sources/OrgCore/Compute/Babel.swift +43 −26
@@ -326,7 +326,7 @@ public enum Babel {
326 if useCache, !inline, params.single[":cache"] == "yes" { 326 if useCache, !inline, params.single[":cache"] == "yes" {
327 do { 327 do {
328 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name, 328 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: location, name: name,
329 call: location != block.range, text: ns) 329 call: location != block.range, text: ns, options: options)
330 if state.current { return .failure(.message(cachedMessage(state.value))) } 330 if state.current { return .failure(.message(cachedMessage(state.value))) }
331 // Appended or prepended results accumulate, so they carry no hash. 331 // Appended or prepended results accumulate, so they carry no hash.
332 if params.results.contains("replace") { hash = state.hash } 332 if params.results.contains("replace") { hash = state.hash }
@@ -342,16 +342,8 @@ public enum Babel {
342 342
343 if shells.contains(language) { 343 if shells.contains(language) {
344 let valueIsExitStatus = params.results.contains("value") 344 let valueIsExitStatus = params.results.contains("value")
345 var lines: [String] = []
346 for (name, value) in vars {
347 let s: String
348 switch value {
349 case .scalar(let v), .number(let v): s = v
350 default: return .failure(.message("Tables and lists in :var aren't supported for \(language) yet."))
351 }
352 lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'")
353 }
354 let executable = language == "shell" ? options.shell : language 345 let executable = language == "shell" ? options.shell : language
346 let lines = shellAssignments(vars, shell: executable, params: params)
355 let cmdline = params.single[":cmdline"] 347 let cmdline = params.single[":cmdline"]
356 let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil } 348 let shebang = params.single[":shebang"].flatMap { $0.contains(where: { !$0.isWhitespace }) ? $0 : nil }
357 if session == nil, stdin != nil || cmdline != nil || shebang != nil { 349 if session == nil, stdin != nil || cmdline != nil || shebang != nil {
@@ -363,7 +355,7 @@ public enum Babel {
363 let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "") 355 let command = Babel.scriptFileToken + (stdin != nil || cmdline != nil ? " " + (cmdline ?? "") : "")
364 var job = BabelJob( 356 var job = BabelJob(
365 language: language, runner: .process(executable: executable, arguments: ["-c", command]), 357 language: language, runner: .process(executable: executable, arguments: ["-c", command]),
366 input: stdin.map(shellString) ?? "", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk, 358 input: stdin.map { shellString($0) } ?? "", directory: dir, block: location, blockText: blockText, alwaysAsk: alwaysAsk,
367 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus)) 359 finish: finishBase(.shell(valueIsExitStatus: valueIsExitStatus))
368 ) 360 )
369 job.script = script 361 job.script = script
@@ -682,9 +674,11 @@ public enum Babel {
682 /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results 674 /// `:cache yes` (`org-babel-execute-src-block`): the block's hash, and whether its results
683 /// already carry it, with their value when it can be read. 675 /// already carry it, with their value when it can be read.
684 static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?, 676 static func cacheState(language: String, params: Params, tables: Tables, body: String, location: Range<Int>, name: String?,
685 call: Bool, text: NSString) throws -> (hash: String, current: Bool, value: VarValue?) { 677 call: Bool, text: NSString, options: Options) throws -> (hash: String, current: Bool, value: VarValue?) {
678 // The hash is taken outside the shell's own binding of `shell-file-name`, so its
679 // variables are written for the user's shell whatever the block's language.
686 let input = try cacheInput(language: language, params: params, tables: tables, 680 let input = try cacheInput(language: language, params: params, tables: tables,
687 expanded: expandedBody(language: language, vars: tables.vars, body: body), call: call) 681 expanded: expandedBody(language: language, vars: tables.vars, body: body, shell: options.shell, params: params), call: call)
688 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined() 682 let hash = Insecure.SHA1.hash(data: Data(input.utf8)).map { String(format: "%02x", $0) }.joined()
689 guard let line = existingResults(name: name, block: location, text: text), resultsHash(at: line, text: text) == hash else { 683 guard let line = existingResults(name: name, block: location, text: text), resultsHash(at: line, text: text) == hash else {
690 return (hash, false, nil) 684 return (hash, false, nil)
@@ -697,10 +691,9 @@ public enum Babel {
697 } 691 }
698 692
699 /// The body with its variables, as the language's `org-babel-expand-body:` writes it. 693 /// The body with its variables, as the language's `org-babel-expand-body:` writes it.
700 static func expandedBody(language: String, vars: [(String, VarValue)], body: String) -> String { 694 static func expandedBody(language: String, vars: [(String, VarValue)], body: String, shell: String, params: Params) -> String {
701 if shells.contains(language) { 695 if shells.contains(language) {
702 let lines = vars.map { "\($0.0)='" + shellString($0.1).replacingOccurrences(of: "'", with: "'\"'\"'") + "'" } 696 return (shellAssignments(vars, shell: shell, params: params) + [body]).joined(separator: "\n")
703 return (lines + [body]).joined(separator: "\n")
704 } 697 }
705 if language == "python" { 698 if language == "python" {
706 return (vars.map { "\($0.0)=\(pythonLiteral($0.1))" } + [body]).joined(separator: "\n") 699 return (vars.map { "\($0.0)=\(pythonLiteral($0.1))" } + [body]).joined(separator: "\n")
@@ -1073,7 +1066,7 @@ public enum Babel {
1073 if params.single[":cache"] == "yes", let language = block.language { 1066 if params.single[":cache"] == "yes", let language = block.language {
1074 let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params) 1067 let tables = try disassemble(params.vars.map { ($0.name, try resolve($0.value, model: model, text: text, refs: inner)) }, params: params)
1075 let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block) 1068 let body = nowebEvaluates(params.single[":noweb"]) ? try expandNoweb(block, params: params, model: model, text: text) : normalizedBody(block)
1076 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text) 1069 let state = try cacheState(language: language, params: params, tables: tables, body: body, location: block.range, name: name, call: false, text: text, options: refs.options)
1077 if state.current { 1070 if state.current {
1078 if let value = state.value { return value } 1071 if let value = state.value { return value }
1079 useCache = false 1072 useCache = false
@@ -1127,17 +1120,41 @@ public enum Babel {
1127 1120
1128 /// `org-babel-sh-var-to-string`: a table's rows as tab-separated lines, a list's items as 1121 /// `org-babel-sh-var-to-string`: a table's rows as tab-separated lines, a list's items as
1129 /// lines. 1122 /// lines.
1130 static func shellString(_ value: VarValue) -> String { 1123 static func shellString(_ value: VarValue, separator: String? = nil, hline: String? = nil) -> String {
1131 func cell(_ c: Cell) -> String {
1132 switch c {
1133 case .number(let n): n
1134 case .text(let s): s
1135 }
1136 }
1137 switch value { 1124 switch value {
1138 case .scalar(let s), .number(let s): return s 1125 case .scalar(let s), .number(let s): return s
1139 case .list(let items): return items.map(cell).joined(separator: "\n") 1126 case .list(let items): return items.map(cellText).joined(separator: "\n")
1140 case .table(let rows): return rows.compactMap { $0?.map(cell).joined(separator: "\t") }.joined(separator: "\n") 1127 case .table(let rows): return rows.compactMap { $0?.map(cellText).joined(separator: separator ?? "\t") ?? hline }.joined(separator: "\n")
1128 }
1129 }
1130
1131 /// `org-babel-variable-assignments:shell`, by the shell's name: for bash, an array for a
1132 /// list and an associative array for a table of two or more columns; `set` for fish; a
1133 /// quoted string otherwise.
1134 static func shellAssignments(_ vars: [(String, VarValue)], shell: String, params: Params) -> [String] {
1135 let separator = params.single[":separator"]
1136 let hline = params.single[":hlines"] == "yes" ? params.single[":hline-string"] ?? "hline" : nil
1137 func quoted(_ value: VarValue) -> String {
1138 "'" + shellString(value, separator: separator, hline: hline).replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
1139 }
1140 return vars.map { name, value in
1141 if shell.hasSuffix("bash") {
1142 switch value {
1143 case .table(let rows) where (rows.first ?? nil).map({ $0.count >= 2 }) ?? false:
1144 let entries = rows.compactMap { $0 }.map { row in
1145 "\(name)[\(quoted(.scalar(cellText(row[0]))))]=\(quoted(.list(Array(row.dropFirst()))))"
1146 }
1147 return (["unset \(name)", "declare -A \(name)"] + entries).joined(separator: "\n")
1148 case .table(let rows):
1149 let items = rows.map { $0.map { quoted(.list($0)) } ?? quoted(.scalar("hline")) }
1150 return "unset \(name)\ndeclare -a \(name)=( \(items.joined(separator: " ")) )"
1151 case .list(let items):
1152 return "unset \(name)\ndeclare -a \(name)=( \(items.map { quoted(.scalar(cellText($0))) }.joined(separator: " ")) )"
1153 case .scalar, .number:
1154 break
1155 }
1156 }
1157 return (shell.hasSuffix("fish") ? "set \(name) " : "\(name)=") + quoted(value)
1141 } 1158 }
1142 } 1159 }
1143 1160
Tests/OrgCoreTests/BabelCacheTests.swift +4
@@ -29,6 +29,10 @@ struct BabelCacheTests {
29 Case(text: "#+PROPERTY: header-args :cache yes\n#+begin_src sh :session none\necho prop\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]), 29 Case(text: "#+PROPERTY: header-args :cache yes\n#+begin_src sh :session none\necho prop\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"]),
30 Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames yes :rownames yes\nreturn t\n#+end_src\n", 30 Case(text: "#+NAME: tbl\n| a | b |\n|---+---|\n| 1 | x |\n\n#+begin_src python :cache yes :var t=tbl :colnames yes :rownames yes\nreturn t\n#+end_src\n",
31 runs: ["#+begin_src", "#+begin_src"]), 31 runs: ["#+begin_src", "#+begin_src"]),
32 Case(text: "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src bash :cache yes :var t=kv :var l='(1 2)\necho ${t[a]} ${l[1]}\n#+end_src\n",
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",
35 runs: ["#+begin_src", "#+begin_src"]),
32 // Edited after the first run, so it runs again. 36 // Edited after the first run, so it runs again.
33 Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]), 37 Case(text: "#+begin_src sh :cache yes\necho one\n#+end_src\n", runs: ["#+begin_src", "#+begin_src"], edits: [("echo one", "echo two")]),
34 // A reference to a cached block takes its result without running it. 38 // A reference to a cached block takes its result without running it.
Tests/OrgCoreTests/BabelTests.swift +12
@@ -177,6 +177,18 @@ struct BabelTests {
177 "MULTI#+begin_src emacs-lisp :var tb=multi :hlines yes\ntb\n#+end_src\n", 177 "MULTI#+begin_src emacs-lisp :var tb=multi :hlines yes\ntb\n#+end_src\n",
178 "MULTI#+begin_src sh :stdin multi :results output\ncat\n#+end_src\n", 178 "MULTI#+begin_src sh :stdin multi :results output\ncat\n#+end_src\n",
179 "#+begin_src python :var t=tb2 :colnames yes\nreturn t\n#+end_src\n\n#+NAME: tb2\n#+begin_src python\nreturn [['h1', 'h2'], None, [1, 2]]\n#+end_src\n", 179 "#+begin_src python :var t=tb2 :colnames yes\nreturn t\n#+end_src\n\n#+NAME: tb2\n#+begin_src python\nreturn [['h1', 'h2'], None, [1, 2]]\n#+end_src\n",
180 // Tables and lists in shell variables.
181 "TBL#+begin_src sh :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
182 "MULTI#+begin_src sh :var t=multi :hlines yes :separator , :results output\necho \"$t\"\n#+end_src\n",
183 "MULTI#+begin_src sh :var t=multi :hlines yes :hline-string -- :results output\necho \"$t\"\n#+end_src\n",
184 "#+NAME: items\n- one\n- it's\n\n#+begin_src sh :var l=items :results output\necho \"$l\"\n#+end_src\n",
185 "#+NAME: items\n- one\n- it's\n\n#+begin_src bash :var l=items :results output\necho ${#l[@]} \"${l[1]}\"\n#+end_src\n",
186 "#+NAME: kv\n| a | 1 |\n| b | 2 |\n\n#+begin_src bash :var t=kv :results output\necho ${t[a]} ${t[b]} ${#t[@]}\n#+end_src\n",
187 "#+NAME: kv3\n| a | 1 | x |\n| b | 2 | y |\n\n#+begin_src bash :var t=kv3 :results output\necho \"${t[b]}\"\n#+end_src\n",
188 "#+NAME: col\n| 5 |\n| 6 |\n\n#+begin_src bash :var t=col :results output\necho ${t[@]}\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",
191 "TBL#+begin_src shell :var t=tbl :results output\necho \"$t\"\n#+end_src\n",
180 // Lisp values. 192 // Lisp values.
181 "#+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", 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",
182 "#+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", 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",