1 //===-- REPL.cpp ----------------------------------------------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8
9 #include "lldb/Expression/REPL.h"
10 #include "lldb/Core/Debugger.h"
11 #include "lldb/Core/PluginManager.h"
12 #include "lldb/Core/StreamFile.h"
13 #include "lldb/Expression/ExpressionVariable.h"
14 #include "lldb/Expression/UserExpression.h"
15 #include "lldb/Host/HostInfo.h"
16 #include "lldb/Interpreter/CommandInterpreter.h"
17 #include "lldb/Interpreter/CommandReturnObject.h"
18 #include "lldb/Target/Thread.h"
19 #include "lldb/Utility/AnsiTerminal.h"
20
21 #include <memory>
22
23 using namespace lldb_private;
24
REPL(LLVMCastKind kind,Target & target)25 REPL::REPL(LLVMCastKind kind, Target &target) : m_target(target), m_kind(kind) {
26 // Make sure all option values have sane defaults
27 Debugger &debugger = m_target.GetDebugger();
28 debugger.SetShowProgress(false);
29 auto exe_ctx = debugger.GetCommandInterpreter().GetExecutionContext();
30 m_format_options.OptionParsingStarting(&exe_ctx);
31 m_varobj_options.OptionParsingStarting(&exe_ctx);
32 }
33
34 REPL::~REPL() = default;
35
Create(Status & err,lldb::LanguageType language,Debugger * debugger,Target * target,const char * repl_options)36 lldb::REPLSP REPL::Create(Status &err, lldb::LanguageType language,
37 Debugger *debugger, Target *target,
38 const char *repl_options) {
39 uint32_t idx = 0;
40 lldb::REPLSP ret;
41
42 while (REPLCreateInstance create_instance =
43 PluginManager::GetREPLCreateCallbackAtIndex(idx)) {
44 LanguageSet supported_languages =
45 PluginManager::GetREPLSupportedLanguagesAtIndex(idx++);
46 if (!supported_languages[language])
47 continue;
48 ret = (*create_instance)(err, language, debugger, target, repl_options);
49 if (ret) {
50 break;
51 }
52 }
53
54 return ret;
55 }
56
GetSourcePath()57 std::string REPL::GetSourcePath() {
58 ConstString file_basename = GetSourceFileBasename();
59 FileSpec tmpdir_file_spec = HostInfo::GetProcessTempDir();
60 if (tmpdir_file_spec) {
61 tmpdir_file_spec.SetFilename(file_basename);
62 m_repl_source_path = tmpdir_file_spec.GetPath();
63 } else {
64 tmpdir_file_spec = FileSpec("/tmp");
65 tmpdir_file_spec.AppendPathComponent(file_basename.GetStringRef());
66 }
67
68 return tmpdir_file_spec.GetPath();
69 }
70
GetIOHandler()71 lldb::IOHandlerSP REPL::GetIOHandler() {
72 if (!m_io_handler_sp) {
73 Debugger &debugger = m_target.GetDebugger();
74 m_io_handler_sp = std::make_shared<IOHandlerEditline>(
75 debugger, IOHandler::Type::REPL,
76 "lldb-repl", // Name of input reader for history
77 llvm::StringRef("> "), // prompt
78 llvm::StringRef(". "), // Continuation prompt
79 true, // Multi-line
80 true, // The REPL prompt is always colored
81 1, // Line number
82 *this);
83
84 // Don't exit if CTRL+C is pressed
85 static_cast<IOHandlerEditline *>(m_io_handler_sp.get())
86 ->SetInterruptExits(false);
87
88 if (m_io_handler_sp->GetIsInteractive() &&
89 m_io_handler_sp->GetIsRealTerminal()) {
90 m_indent_str.assign(debugger.GetTabSize(), ' ');
91 m_enable_auto_indent = debugger.GetAutoIndent();
92 } else {
93 m_indent_str.clear();
94 m_enable_auto_indent = false;
95 }
96 }
97 return m_io_handler_sp;
98 }
99
IOHandlerActivated(IOHandler & io_handler,bool interactive)100 void REPL::IOHandlerActivated(IOHandler &io_handler, bool interactive) {
101 lldb::ProcessSP process_sp = m_target.GetProcessSP();
102 if (process_sp && process_sp->IsAlive())
103 return;
104 lldb::StreamFileSP error_sp(io_handler.GetErrorStreamFileSP());
105 error_sp->Printf("REPL requires a running target process.\n");
106 io_handler.SetIsDone(true);
107 }
108
IOHandlerInterrupt(IOHandler & io_handler)109 bool REPL::IOHandlerInterrupt(IOHandler &io_handler) { return false; }
110
IOHandlerInputInterrupted(IOHandler & io_handler,std::string & line)111 void REPL::IOHandlerInputInterrupted(IOHandler &io_handler, std::string &line) {
112 }
113
IOHandlerGetFixIndentationCharacters()114 const char *REPL::IOHandlerGetFixIndentationCharacters() {
115 return (m_enable_auto_indent ? GetAutoIndentCharacters() : nullptr);
116 }
117
IOHandlerGetControlSequence(char ch)118 ConstString REPL::IOHandlerGetControlSequence(char ch) {
119 if (ch == 'd')
120 return ConstString(":quit\n");
121 return ConstString();
122 }
123
IOHandlerGetCommandPrefix()124 const char *REPL::IOHandlerGetCommandPrefix() { return ":"; }
125
IOHandlerGetHelpPrologue()126 const char *REPL::IOHandlerGetHelpPrologue() {
127 return "\nThe REPL (Read-Eval-Print-Loop) acts like an interpreter. "
128 "Valid statements, expressions, and declarations are immediately "
129 "compiled and executed.\n\n"
130 "The complete set of LLDB debugging commands are also available as "
131 "described below.\n\nCommands "
132 "must be prefixed with a colon at the REPL prompt (:quit for "
133 "example.) Typing just a colon "
134 "followed by return will switch to the LLDB prompt.\n\n"
135 "Type “< path” to read in code from a text file “path”.\n\n";
136 }
137
IOHandlerIsInputComplete(IOHandler & io_handler,StringList & lines)138 bool REPL::IOHandlerIsInputComplete(IOHandler &io_handler, StringList &lines) {
139 // Check for meta command
140 const size_t num_lines = lines.GetSize();
141 if (num_lines == 1) {
142 const char *first_line = lines.GetStringAtIndex(0);
143 if (first_line[0] == ':')
144 return true; // Meta command is a single line where that starts with ':'
145 }
146
147 // Check if REPL input is done
148 std::string source_string(lines.CopyList());
149 return SourceIsComplete(source_string);
150 }
151
CalculateActualIndentation(const StringList & lines)152 int REPL::CalculateActualIndentation(const StringList &lines) {
153 std::string last_line = lines[lines.GetSize() - 1];
154
155 int actual_indent = 0;
156 for (char &ch : last_line) {
157 if (ch != ' ')
158 break;
159 ++actual_indent;
160 }
161
162 return actual_indent;
163 }
164
IOHandlerFixIndentation(IOHandler & io_handler,const StringList & lines,int cursor_position)165 int REPL::IOHandlerFixIndentation(IOHandler &io_handler,
166 const StringList &lines,
167 int cursor_position) {
168 if (!m_enable_auto_indent)
169 return 0;
170
171 if (!lines.GetSize()) {
172 return 0;
173 }
174
175 int tab_size = io_handler.GetDebugger().GetTabSize();
176
177 lldb::offset_t desired_indent =
178 GetDesiredIndentation(lines, cursor_position, tab_size);
179
180 int actual_indent = REPL::CalculateActualIndentation(lines);
181
182 if (desired_indent == LLDB_INVALID_OFFSET)
183 return 0;
184
185 return (int)desired_indent - actual_indent;
186 }
187
ReadCode(const std::string & path,std::string & code,lldb::StreamFileSP & error_sp)188 static bool ReadCode(const std::string &path, std::string &code,
189 lldb::StreamFileSP &error_sp) {
190 auto &fs = FileSystem::Instance();
191 llvm::Twine pathTwine(path);
192 if (!fs.Exists(pathTwine)) {
193 error_sp->Printf("no such file at path '%s'\n", path.c_str());
194 return false;
195 }
196 if (!fs.Readable(pathTwine)) {
197 error_sp->Printf("could not read file at path '%s'\n", path.c_str());
198 return false;
199 }
200 const size_t file_size = fs.GetByteSize(pathTwine);
201 const size_t max_size = code.max_size();
202 if (file_size > max_size) {
203 error_sp->Printf("file at path '%s' too large: "
204 "file_size = %zu, max_size = %zu\n",
205 path.c_str(), file_size, max_size);
206 return false;
207 }
208 auto data_sp = fs.CreateDataBuffer(pathTwine);
209 if (data_sp == nullptr) {
210 error_sp->Printf("could not create buffer for file at path '%s'\n",
211 path.c_str());
212 return false;
213 }
214 code.assign((const char *)data_sp->GetBytes(), data_sp->GetByteSize());
215 return true;
216 }
217
IOHandlerInputComplete(IOHandler & io_handler,std::string & code)218 void REPL::IOHandlerInputComplete(IOHandler &io_handler, std::string &code) {
219 lldb::StreamFileSP output_sp(io_handler.GetOutputStreamFileSP());
220 lldb::StreamFileSP error_sp(io_handler.GetErrorStreamFileSP());
221 bool extra_line = false;
222 bool did_quit = false;
223
224 if (code.empty()) {
225 m_code.AppendString("");
226 static_cast<IOHandlerEditline &>(io_handler)
227 .SetBaseLineNumber(m_code.GetSize() + 1);
228 } else {
229 Debugger &debugger = m_target.GetDebugger();
230 CommandInterpreter &ci = debugger.GetCommandInterpreter();
231 extra_line = ci.GetSpaceReplPrompts();
232
233 ExecutionContext exe_ctx(m_target.GetProcessSP()
234 ->GetThreadList()
235 .GetSelectedThread()
236 ->GetSelectedFrame()
237 .get());
238
239 lldb::ProcessSP process_sp(exe_ctx.GetProcessSP());
240
241 if (code[0] == ':') {
242 // Meta command
243 // Strip the ':'
244 code.erase(0, 1);
245 if (!llvm::StringRef(code).trim().empty()) {
246 // "lldb" was followed by arguments, so just execute the command dump
247 // the results
248
249 // Turn off prompt on quit in case the user types ":quit"
250 const bool saved_prompt_on_quit = ci.GetPromptOnQuit();
251 if (saved_prompt_on_quit)
252 ci.SetPromptOnQuit(false);
253
254 // Execute the command
255 CommandReturnObject result(debugger.GetUseColor());
256 result.SetImmediateOutputStream(output_sp);
257 result.SetImmediateErrorStream(error_sp);
258 ci.HandleCommand(code.c_str(), eLazyBoolNo, result);
259
260 if (saved_prompt_on_quit)
261 ci.SetPromptOnQuit(true);
262
263 if (result.GetStatus() == lldb::eReturnStatusQuit) {
264 did_quit = true;
265 io_handler.SetIsDone(true);
266 if (debugger.CheckTopIOHandlerTypes(
267 IOHandler::Type::REPL, IOHandler::Type::CommandInterpreter)) {
268 // We typed "quit" or an alias to quit so we need to check if the
269 // command interpreter is above us and tell it that it is done as
270 // well so we don't drop back into the command interpreter if we
271 // have already quit
272 lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
273 if (io_handler_sp)
274 io_handler_sp->SetIsDone(true);
275 }
276 }
277 } else {
278 // ":" was followed by no arguments, so push the LLDB command prompt
279 if (debugger.CheckTopIOHandlerTypes(
280 IOHandler::Type::REPL, IOHandler::Type::CommandInterpreter)) {
281 // If the user wants to get back to the command interpreter and the
282 // command interpreter is what launched the REPL, then just let the
283 // REPL exit and fall back to the command interpreter.
284 io_handler.SetIsDone(true);
285 } else {
286 // The REPL wasn't launched the by the command interpreter, it is the
287 // base IOHandler, so we need to get the command interpreter and
288 lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
289 if (io_handler_sp) {
290 io_handler_sp->SetIsDone(false);
291 debugger.RunIOHandlerAsync(ci.GetIOHandler());
292 }
293 }
294 }
295 } else {
296 if (code[0] == '<') {
297 // User wants to read code from a file.
298 // Interpret rest of line as a literal path.
299 auto path = llvm::StringRef(code.substr(1)).trim().str();
300 if (!ReadCode(path, code, error_sp)) {
301 return;
302 }
303 }
304
305 // Unwind any expression we might have been running in case our REPL
306 // expression crashed and the user was looking around
307 if (m_dedicated_repl_mode) {
308 Thread *thread = exe_ctx.GetThreadPtr();
309 if (thread && thread->UnwindInnermostExpression().Success()) {
310 thread->SetSelectedFrameByIndex(0, false);
311 exe_ctx.SetFrameSP(thread->GetSelectedFrame());
312 }
313 }
314
315 const bool colorize_err = error_sp->GetFile().GetIsTerminalWithColors();
316
317 EvaluateExpressionOptions expr_options = m_expr_options;
318 expr_options.SetCoerceToId(m_varobj_options.use_objc);
319 expr_options.SetKeepInMemory(true);
320 expr_options.SetUseDynamic(m_varobj_options.use_dynamic);
321 expr_options.SetGenerateDebugInfo(true);
322 expr_options.SetREPLEnabled(true);
323 expr_options.SetColorizeErrors(colorize_err);
324 expr_options.SetPoundLine(m_repl_source_path.c_str(),
325 m_code.GetSize() + 1);
326
327 expr_options.SetLanguage(GetLanguage());
328
329 PersistentExpressionState *persistent_state =
330 m_target.GetPersistentExpressionStateForLanguage(GetLanguage());
331 if (!persistent_state)
332 return;
333
334 const size_t var_count_before = persistent_state->GetSize();
335
336 const char *expr_prefix = nullptr;
337 lldb::ValueObjectSP result_valobj_sp;
338 Status error;
339 lldb::ExpressionResults execution_results =
340 UserExpression::Evaluate(exe_ctx, expr_options, code.c_str(),
341 expr_prefix, result_valobj_sp, error,
342 nullptr); // fixed expression
343
344 // CommandInterpreter &ci = debugger.GetCommandInterpreter();
345
346 if (process_sp && process_sp->IsAlive()) {
347 bool add_to_code = true;
348 bool handled = false;
349 if (result_valobj_sp) {
350 lldb::Format format = m_format_options.GetFormat();
351
352 if (result_valobj_sp->GetError().Success()) {
353 handled |= PrintOneVariable(debugger, output_sp, result_valobj_sp);
354 } else if (result_valobj_sp->GetError().GetError() ==
355 UserExpression::kNoResult) {
356 if (format != lldb::eFormatVoid && debugger.GetNotifyVoid()) {
357 error_sp->PutCString("(void)\n");
358 handled = true;
359 }
360 }
361 }
362
363 if (debugger.GetPrintDecls()) {
364 for (size_t vi = var_count_before, ve = persistent_state->GetSize();
365 vi != ve; ++vi) {
366 lldb::ExpressionVariableSP persistent_var_sp =
367 persistent_state->GetVariableAtIndex(vi);
368 lldb::ValueObjectSP valobj_sp = persistent_var_sp->GetValueObject();
369
370 PrintOneVariable(debugger, output_sp, valobj_sp,
371 persistent_var_sp.get());
372 }
373 }
374
375 if (!handled) {
376 bool useColors = error_sp->GetFile().GetIsTerminalWithColors();
377 switch (execution_results) {
378 case lldb::eExpressionSetupError:
379 case lldb::eExpressionParseError:
380 add_to_code = false;
381 [[fallthrough]];
382 case lldb::eExpressionDiscarded:
383 error_sp->Printf("%s\n", error.AsCString());
384 break;
385
386 case lldb::eExpressionCompleted:
387 break;
388 case lldb::eExpressionInterrupted:
389 if (useColors) {
390 error_sp->Printf(ANSI_ESCAPE1(ANSI_FG_COLOR_RED));
391 error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_BOLD));
392 }
393 error_sp->Printf("Execution interrupted. ");
394 if (useColors)
395 error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_NORMAL));
396 error_sp->Printf("Enter code to recover and continue.\nEnter LLDB "
397 "commands to investigate (type :help for "
398 "assistance.)\n");
399 break;
400
401 case lldb::eExpressionHitBreakpoint:
402 // Breakpoint was hit, drop into LLDB command interpreter
403 if (useColors) {
404 error_sp->Printf(ANSI_ESCAPE1(ANSI_FG_COLOR_RED));
405 error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_BOLD));
406 }
407 output_sp->Printf("Execution stopped at breakpoint. ");
408 if (useColors)
409 error_sp->Printf(ANSI_ESCAPE1(ANSI_CTRL_NORMAL));
410 output_sp->Printf("Enter LLDB commands to investigate (type help "
411 "for assistance.)\n");
412 {
413 lldb::IOHandlerSP io_handler_sp(ci.GetIOHandler());
414 if (io_handler_sp) {
415 io_handler_sp->SetIsDone(false);
416 debugger.RunIOHandlerAsync(ci.GetIOHandler());
417 }
418 }
419 break;
420
421 case lldb::eExpressionTimedOut:
422 error_sp->Printf("error: timeout\n");
423 if (error.AsCString())
424 error_sp->Printf("error: %s\n", error.AsCString());
425 break;
426 case lldb::eExpressionResultUnavailable:
427 // Shoulnd't happen???
428 error_sp->Printf("error: could not fetch result -- %s\n",
429 error.AsCString());
430 break;
431 case lldb::eExpressionStoppedForDebug:
432 // Shoulnd't happen???
433 error_sp->Printf("error: stopped for debug -- %s\n",
434 error.AsCString());
435 break;
436 case lldb::eExpressionThreadVanished:
437 // Shoulnd't happen???
438 error_sp->Printf("error: expression thread vanished -- %s\n",
439 error.AsCString());
440 break;
441 }
442 }
443
444 if (add_to_code) {
445 const uint32_t new_default_line = m_code.GetSize() + 1;
446
447 m_code.SplitIntoLines(code);
448
449 // Update our code on disk
450 if (!m_repl_source_path.empty()) {
451 auto file = FileSystem::Instance().Open(
452 FileSpec(m_repl_source_path),
453 File::eOpenOptionWriteOnly | File::eOpenOptionTruncate |
454 File::eOpenOptionCanCreate,
455 lldb::eFilePermissionsFileDefault);
456 if (file) {
457 std::string code(m_code.CopyList());
458 code.append(1, '\n');
459 size_t bytes_written = code.size();
460 file.get()->Write(code.c_str(), bytes_written);
461 file.get()->Close();
462 } else {
463 std::string message = llvm::toString(file.takeError());
464 error_sp->Printf("error: couldn't open %s: %s\n",
465 m_repl_source_path.c_str(), message.c_str());
466 }
467
468 // Now set the default file and line to the REPL source file
469 m_target.GetSourceManager().SetDefaultFileAndLine(
470 FileSpec(m_repl_source_path), new_default_line);
471 }
472 static_cast<IOHandlerEditline &>(io_handler)
473 .SetBaseLineNumber(m_code.GetSize() + 1);
474 }
475 if (extra_line) {
476 output_sp->Printf("\n");
477 }
478 }
479 }
480
481 // Don't complain about the REPL process going away if we are in the
482 // process of quitting.
483 if (!did_quit && (!process_sp || !process_sp->IsAlive())) {
484 error_sp->Printf(
485 "error: REPL process is no longer alive, exiting REPL\n");
486 io_handler.SetIsDone(true);
487 }
488 }
489 }
490
IOHandlerComplete(IOHandler & io_handler,CompletionRequest & request)491 void REPL::IOHandlerComplete(IOHandler &io_handler,
492 CompletionRequest &request) {
493 // Complete an LLDB command if the first character is a colon...
494 if (request.GetRawLine().startswith(":")) {
495 Debugger &debugger = m_target.GetDebugger();
496
497 // auto complete LLDB commands
498 llvm::StringRef new_line = request.GetRawLine().drop_front();
499 CompletionResult sub_result;
500 CompletionRequest sub_request(new_line, request.GetRawCursorPos() - 1,
501 sub_result);
502 debugger.GetCommandInterpreter().HandleCompletion(sub_request);
503 StringList matches, descriptions;
504 sub_result.GetMatches(matches);
505 // Prepend command prefix that was excluded in the completion request.
506 if (request.GetCursorIndex() == 0)
507 for (auto &match : matches)
508 match.insert(0, 1, ':');
509 sub_result.GetDescriptions(descriptions);
510 request.AddCompletions(matches, descriptions);
511 return;
512 }
513
514 // Strip spaces from the line and see if we had only spaces
515 if (request.GetRawLine().trim().empty()) {
516 // Only spaces on this line, so just indent
517 request.AddCompletion(m_indent_str);
518 return;
519 }
520
521 std::string current_code;
522 current_code.append(m_code.CopyList());
523
524 IOHandlerEditline &editline = static_cast<IOHandlerEditline &>(io_handler);
525 const StringList *current_lines = editline.GetCurrentLines();
526 if (current_lines) {
527 const uint32_t current_line_idx = editline.GetCurrentLineIndex();
528
529 if (current_line_idx < current_lines->GetSize()) {
530 for (uint32_t i = 0; i < current_line_idx; ++i) {
531 const char *line_cstr = current_lines->GetStringAtIndex(i);
532 if (line_cstr) {
533 current_code.append("\n");
534 current_code.append(line_cstr);
535 }
536 }
537 }
538 }
539
540 current_code.append("\n");
541 current_code += request.GetRawLine();
542
543 CompleteCode(current_code, request);
544 }
545
QuitCommandOverrideCallback(void * baton,const char ** argv)546 bool QuitCommandOverrideCallback(void *baton, const char **argv) {
547 Target *target = (Target *)baton;
548 lldb::ProcessSP process_sp(target->GetProcessSP());
549 if (process_sp) {
550 process_sp->Destroy(false);
551 process_sp->GetTarget().GetDebugger().ClearIOHandlers();
552 }
553 return false;
554 }
555
RunLoop()556 Status REPL::RunLoop() {
557 Status error;
558
559 error = DoInitialization();
560 m_repl_source_path = GetSourcePath();
561
562 if (!error.Success())
563 return error;
564
565 Debugger &debugger = m_target.GetDebugger();
566
567 lldb::IOHandlerSP io_handler_sp(GetIOHandler());
568
569 FileSpec save_default_file;
570 uint32_t save_default_line = 0;
571
572 if (!m_repl_source_path.empty()) {
573 // Save the current default file and line
574 m_target.GetSourceManager().GetDefaultFileAndLine(save_default_file,
575 save_default_line);
576 }
577
578 debugger.RunIOHandlerAsync(io_handler_sp);
579
580 // Check if we are in dedicated REPL mode where LLDB was start with the "--
581 // repl" option from the command line. Currently we know this by checking if
582 // the debugger already has a IOHandler thread.
583 if (!debugger.HasIOHandlerThread()) {
584 // The debugger doesn't have an existing IOHandler thread, so this must be
585 // dedicated REPL mode...
586 m_dedicated_repl_mode = true;
587 debugger.StartIOHandlerThread();
588 llvm::StringRef command_name_str("quit");
589 CommandObject *cmd_obj =
590 debugger.GetCommandInterpreter().GetCommandObjectForCommand(
591 command_name_str);
592 if (cmd_obj) {
593 assert(command_name_str.empty());
594 cmd_obj->SetOverrideCallback(QuitCommandOverrideCallback, &m_target);
595 }
596 }
597
598 // Wait for the REPL command interpreter to get popped
599 io_handler_sp->WaitForPop();
600
601 if (m_dedicated_repl_mode) {
602 // If we were in dedicated REPL mode we would have started the IOHandler
603 // thread, and we should kill our process
604 lldb::ProcessSP process_sp = m_target.GetProcessSP();
605 if (process_sp && process_sp->IsAlive())
606 process_sp->Destroy(false);
607
608 // Wait for the IO handler thread to exit (TODO: don't do this if the IO
609 // handler thread already exists...)
610 debugger.JoinIOHandlerThread();
611 }
612
613 // Restore the default file and line
614 if (save_default_file && save_default_line != 0)
615 m_target.GetSourceManager().SetDefaultFileAndLine(save_default_file,
616 save_default_line);
617 return error;
618 }
619