| @@ -36,6 +36,8 @@ public struct BabelJob: Sendable, Equatable { |
| 36 | 36 | var resolved: [String: Babel.VarValue] = [:] |
| 37 | 37 | var caret = 0 |
| 38 | 38 | 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? |
| 39 | 41 | /// An Emacs Lisp block's form, for `Babel.evaluateNatively`. |
| 40 | 42 | public internal(set) var lispForm: String? |
| 41 | 43 | |
| @@ -91,6 +93,8 @@ public struct BabelOutput: Sendable, Equatable { |
| 91 | 93 | |
| 92 | 94 | public enum Babel { |
| 93 | 95 | 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__" |
| 94 | 98 | static let shells: Set<String> = ["sh", "bash", "zsh", "shell", "fish", "ksh", "dash", "ash", "csh", "mksh", "posh"] |
| 95 | 99 | static let generic: [String: (String, [String])] = [ |
| 96 | 100 | "ruby": ("ruby", []), "js": ("node", []), "javascript": ("node", []), "R": ("Rscript", ["-"]), |
| @@ -253,10 +257,11 @@ public enum Babel { |
| 253 | 257 | if eval == "never" || eval == "no" || eval == "never-export" || eval == "no-export" { |
| 254 | 258 | return .failure(.message("Evaluation of this \(language) code block\(block.name.map { " (\($0)) " } ?? " ")is disabled.")) |
| 255 | 259 | } |
| 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.")) |
| 258 | 262 | } |
| 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)) { |
| 260 | 265 | return .failure(.message("\(key) isn't supported yet; nothing was run.")) |
| 261 | 266 | } |
| 262 | 267 | let session = params.single[":session"].flatMap { $0 == "none" ? nil : ($0.isEmpty ? "*\(language)*" : $0) } |
| @@ -285,8 +290,10 @@ public enum Babel { |
| 285 | 290 | file: toFile ? file : nil, fileDescription: params.single[":file-desc"]) |
| 286 | 291 | } |
| 287 | 292 | let vars: [(String, VarValue)] |
| 293 | var stdin: VarValue? |
| 288 | 294 | do { |
| 289 | 295 | 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) } |
| 290 | 297 | } catch let needed as Needed { |
| 291 | 298 | // Asked about once, with every block the references reach. |
| 292 | 299 | 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 { |
| 314 | 321 | } |
| 315 | 322 | lines.append("\(name)='" + s.replacingOccurrences(of: "'", with: "'\"'\"'") + "'") |
| 316 | 323 | } |
| 317 | | let full = (lines + [body]).joined(separator: "\n") + (valueIsExitStatus ? "\necho $?" : "") |
| 318 | 324 | 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 $?" : "") |
| 319 | 343 | return .success(BabelJob( |
| 320 | 344 | language: language, runner: session.map { .session(name: $0, executable: executable, python: false) } ?? .process(executable: executable, arguments: []), |
| 321 | 345 | input: full.trimmingCharacters(in: .whitespacesAndNewlines) + "\n", directory: dir, |
| @@ -756,8 +780,8 @@ public enum Babel { |
| 756 | 780 | static func referencedBlocks(_ params: Params, model: DocumentModel, text: NSString, seen: Set<String>) -> [SrcBlockInfo] { |
| 757 | 781 | var found: [SrcBlockInfo] = [] |
| 758 | 782 | 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 } |
| 761 | 785 | found.append(block) |
| 762 | 786 | let extra = arguments.map { [splitArguments($0).map { (":var", $0) }] } ?? [] |
| 763 | 787 | if let inner = try? Params(block: block, model: model, text: text, extra: extra) { |
| @@ -820,6 +844,22 @@ public enum Babel { |
| 820 | 844 | throw Failure.message("\(value) isn't a table or list.") |
| 821 | 845 | } |
| 822 | 846 | |
| 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 | |
| 823 | 863 | /// `org-babel-python-var-to-python`. |
| 824 | 864 | static func pythonLiteral(_ value: VarValue) -> String { |
| 825 | 865 | func cell(_ c: Cell) -> String { |