| //===-- CommandObjectSource.cpp ---------------------------------*- C++ -*-===// | |
| // | |
| // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| // | |
| //===----------------------------------------------------------------------===// | |
| #include "CommandObjectSource.h" | |
| #include "lldb/Core/Debugger.h" | |
| #include "lldb/Core/FileLineResolver.h" | |
| #include "lldb/Core/Module.h" | |
| #include "lldb/Core/ModuleSpec.h" | |
| #include "lldb/Core/SourceManager.h" | |
| #include "lldb/Host/OptionParser.h" | |
| #include "lldb/Interpreter/CommandCompletions.h" | |
| #include "lldb/Interpreter/CommandInterpreter.h" | |
| #include "lldb/Interpreter/CommandReturnObject.h" | |
| #include "lldb/Interpreter/OptionArgParser.h" | |
| #include "lldb/Interpreter/Options.h" | |
| #include "lldb/Symbol/CompileUnit.h" | |
| #include "lldb/Symbol/Function.h" | |
| #include "lldb/Symbol/Symbol.h" | |
| #include "lldb/Target/Process.h" | |
| #include "lldb/Target/SectionLoadList.h" | |
| #include "lldb/Target/StackFrame.h" | |
| #include "lldb/Target/TargetList.h" | |
| #include "lldb/Utility/FileSpec.h" | |
| using namespace lldb; | |
| using namespace lldb_private; | |
| #pragma mark CommandObjectSourceInfo | |
| // CommandObjectSourceInfo - debug line entries dumping command | |
| #define LLDB_OPTIONS_source_info | |
| #include "CommandOptions.inc" | |
| class CommandObjectSourceInfo : public CommandObjectParsed { | |
| class CommandOptions : public Options { | |
| public: | |
| CommandOptions() : Options() {} | |
| ~CommandOptions() override = default; | |
| Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, | |
| ExecutionContext *execution_context) override { | |
| Status error; | |
| const int short_option = GetDefinitions()[option_idx].short_option; | |
| switch (short_option) { | |
| case 'l': | |
| if (option_arg.getAsInteger(0, start_line)) | |
| error.SetErrorStringWithFormat("invalid line number: '%s'", | |
| option_arg.str().c_str()); | |
| break; | |
| case 'e': | |
| if (option_arg.getAsInteger(0, end_line)) | |
| error.SetErrorStringWithFormat("invalid line number: '%s'", | |
| option_arg.str().c_str()); | |
| break; | |
| case 'c': | |
| if (option_arg.getAsInteger(0, num_lines)) | |
| error.SetErrorStringWithFormat("invalid line count: '%s'", | |
| option_arg.str().c_str()); | |
| break; | |
| case 'f': | |
| file_name = option_arg; | |
| break; | |
| case 'n': | |
| symbol_name = option_arg; | |
| break; | |
| case 'a': { | |
| address = OptionArgParser::ToAddress(execution_context, option_arg, | |
| LLDB_INVALID_ADDRESS, &error); | |
| } break; | |
| case 's': | |
| modules.push_back(std::string(option_arg)); | |
| break; | |
| default: | |
| llvm_unreachable("Unimplemented option"); | |
| } | |
| return error; | |
| } | |
| void OptionParsingStarting(ExecutionContext *execution_context) override { | |
| file_spec.Clear(); | |
| file_name.clear(); | |
| symbol_name.clear(); | |
| address = LLDB_INVALID_ADDRESS; | |
| start_line = 0; | |
| end_line = 0; | |
| num_lines = 0; | |
| modules.clear(); | |
| } | |
| llvm::ArrayRef<OptionDefinition> GetDefinitions() override { | |
| return llvm::makeArrayRef(g_source_info_options); | |
| } | |
| // Instance variables to hold the values for command options. | |
| FileSpec file_spec; | |
| std::string file_name; | |
| std::string symbol_name; | |
| lldb::addr_t address; | |
| uint32_t start_line; | |
| uint32_t end_line; | |
| uint32_t num_lines; | |
| STLStringArray modules; | |
| }; | |
| public: | |
| CommandObjectSourceInfo(CommandInterpreter &interpreter) | |
| : CommandObjectParsed( | |
| interpreter, "source info", | |
| "Display source line information for the current target " | |
| "process. Defaults to instruction pointer in current stack " | |
| "frame.", | |
| nullptr, eCommandRequiresTarget), | |
| m_options() {} | |
| ~CommandObjectSourceInfo() override = default; | |
| Options *GetOptions() override { return &m_options; } | |
| protected: | |
| // Dump the line entries in each symbol context. Return the number of entries | |
| // found. If module_list is set, only dump lines contained in one of the | |
| // modules. If file_spec is set, only dump lines in the file. If the | |
| // start_line option was specified, don't print lines less than start_line. | |
| // If the end_line option was specified, don't print lines greater than | |
| // end_line. If the num_lines option was specified, dont print more than | |
| // num_lines entries. | |
| uint32_t DumpLinesInSymbolContexts(Stream &strm, | |
| const SymbolContextList &sc_list, | |
| const ModuleList &module_list, | |
| const FileSpec &file_spec) { | |
| uint32_t start_line = m_options.start_line; | |
| uint32_t end_line = m_options.end_line; | |
| uint32_t num_lines = m_options.num_lines; | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| uint32_t num_matches = 0; | |
| bool has_path = false; | |
| if (file_spec) { | |
| assert(file_spec.GetFilename().AsCString()); | |
| has_path = (file_spec.GetDirectory().AsCString() != nullptr); | |
| } | |
| // Dump all the line entries for the file in the list. | |
| ConstString last_module_file_name; | |
| uint32_t num_scs = sc_list.GetSize(); | |
| for (uint32_t i = 0; i < num_scs; ++i) { | |
| SymbolContext sc; | |
| sc_list.GetContextAtIndex(i, sc); | |
| if (sc.comp_unit) { | |
| Module *module = sc.module_sp.get(); | |
| CompileUnit *cu = sc.comp_unit; | |
| const LineEntry &line_entry = sc.line_entry; | |
| assert(module && cu); | |
| // Are we looking for specific modules, files or lines? | |
| if (module_list.GetSize() && | |
| module_list.GetIndexForModule(module) == LLDB_INVALID_INDEX32) | |
| continue; | |
| if (file_spec && | |
| !lldb_private::FileSpec::Equal(file_spec, line_entry.file, | |
| has_path)) | |
| continue; | |
| if (start_line > 0 && line_entry.line < start_line) | |
| continue; | |
| if (end_line > 0 && line_entry.line > end_line) | |
| continue; | |
| if (num_lines > 0 && num_matches > num_lines) | |
| continue; | |
| // Print a new header if the module changed. | |
| ConstString module_file_name = | |
| module->GetFileSpec().GetFilename(); | |
| assert(module_file_name); | |
| if (module_file_name != last_module_file_name) { | |
| if (num_matches > 0) | |
| strm << "\n\n"; | |
| strm << "Lines found in module `" << module_file_name << "\n"; | |
| } | |
| // Dump the line entry. | |
| line_entry.GetDescription(&strm, lldb::eDescriptionLevelBrief, cu, | |
| target, /*show_address_only=*/false); | |
| strm << "\n"; | |
| last_module_file_name = module_file_name; | |
| num_matches++; | |
| } | |
| } | |
| return num_matches; | |
| } | |
| // Dump the requested line entries for the file in the compilation unit. | |
| // Return the number of entries found. If module_list is set, only dump lines | |
| // contained in one of the modules. If the start_line option was specified, | |
| // don't print lines less than start_line. If the end_line option was | |
| // specified, don't print lines greater than end_line. If the num_lines | |
| // option was specified, dont print more than num_lines entries. | |
| uint32_t DumpFileLinesInCompUnit(Stream &strm, Module *module, | |
| CompileUnit *cu, const FileSpec &file_spec) { | |
| uint32_t start_line = m_options.start_line; | |
| uint32_t end_line = m_options.end_line; | |
| uint32_t num_lines = m_options.num_lines; | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| uint32_t num_matches = 0; | |
| assert(module); | |
| if (cu) { | |
| assert(file_spec.GetFilename().AsCString()); | |
| bool has_path = (file_spec.GetDirectory().AsCString() != nullptr); | |
| const FileSpecList &cu_file_list = cu->GetSupportFiles(); | |
| size_t file_idx = cu_file_list.FindFileIndex(0, file_spec, has_path); | |
| if (file_idx != UINT32_MAX) { | |
| // Update the file to how it appears in the CU. | |
| const FileSpec &cu_file_spec = | |
| cu_file_list.GetFileSpecAtIndex(file_idx); | |
| // Dump all matching lines at or above start_line for the file in the | |
| // CU. | |
| ConstString file_spec_name = file_spec.GetFilename(); | |
| ConstString module_file_name = | |
| module->GetFileSpec().GetFilename(); | |
| bool cu_header_printed = false; | |
| uint32_t line = start_line; | |
| while (true) { | |
| LineEntry line_entry; | |
| // Find the lowest index of a line entry with a line equal to or | |
| // higher than 'line'. | |
| uint32_t start_idx = 0; | |
| start_idx = cu->FindLineEntry(start_idx, line, &cu_file_spec, | |
| /*exact=*/false, &line_entry); | |
| if (start_idx == UINT32_MAX) | |
| // No more line entries for our file in this CU. | |
| break; | |
| if (end_line > 0 && line_entry.line > end_line) | |
| break; | |
| // Loop through to find any other entries for this line, dumping | |
| // each. | |
| line = line_entry.line; | |
| do { | |
| num_matches++; | |
| if (num_lines > 0 && num_matches > num_lines) | |
| break; | |
| assert(lldb_private::FileSpec::Equal(cu_file_spec, line_entry.file, | |
| has_path)); | |
| if (!cu_header_printed) { | |
| if (num_matches > 0) | |
| strm << "\n\n"; | |
| strm << "Lines found for file " << file_spec_name | |
| << " in compilation unit " << cu->GetFilename() << " in `" | |
| << module_file_name << "\n"; | |
| cu_header_printed = true; | |
| } | |
| line_entry.GetDescription(&strm, lldb::eDescriptionLevelBrief, cu, | |
| target, /*show_address_only=*/false); | |
| strm << "\n"; | |
| // Anymore after this one? | |
| start_idx++; | |
| start_idx = cu->FindLineEntry(start_idx, line, &cu_file_spec, | |
| /*exact=*/true, &line_entry); | |
| } while (start_idx != UINT32_MAX); | |
| // Try the next higher line, starting over at start_idx 0. | |
| line++; | |
| } | |
| } | |
| } | |
| return num_matches; | |
| } | |
| // Dump the requested line entries for the file in the module. Return the | |
| // number of entries found. If module_list is set, only dump lines contained | |
| // in one of the modules. If the start_line option was specified, don't print | |
| // lines less than start_line. If the end_line option was specified, don't | |
| // print lines greater than end_line. If the num_lines option was specified, | |
| // dont print more than num_lines entries. | |
| uint32_t DumpFileLinesInModule(Stream &strm, Module *module, | |
| const FileSpec &file_spec) { | |
| uint32_t num_matches = 0; | |
| if (module) { | |
| // Look through all the compilation units (CUs) in this module for ones | |
| // that contain lines of code from this source file. | |
| for (size_t i = 0; i < module->GetNumCompileUnits(); i++) { | |
| // Look for a matching source file in this CU. | |
| CompUnitSP cu_sp(module->GetCompileUnitAtIndex(i)); | |
| if (cu_sp) { | |
| num_matches += | |
| DumpFileLinesInCompUnit(strm, module, cu_sp.get(), file_spec); | |
| } | |
| } | |
| } | |
| return num_matches; | |
| } | |
| // Given an address and a list of modules, append the symbol contexts of all | |
| // line entries containing the address found in the modules and return the | |
| // count of matches. If none is found, return an error in 'error_strm'. | |
| size_t GetSymbolContextsForAddress(const ModuleList &module_list, | |
| lldb::addr_t addr, | |
| SymbolContextList &sc_list, | |
| StreamString &error_strm) { | |
| Address so_addr; | |
| size_t num_matches = 0; | |
| assert(module_list.GetSize() > 0); | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| if (target->GetSectionLoadList().IsEmpty()) { | |
| // The target isn't loaded yet, we need to lookup the file address in all | |
| // modules. Note: the module list option does not apply to addresses. | |
| const size_t num_modules = module_list.GetSize(); | |
| for (size_t i = 0; i < num_modules; ++i) { | |
| ModuleSP module_sp(module_list.GetModuleAtIndex(i)); | |
| if (!module_sp) | |
| continue; | |
| if (module_sp->ResolveFileAddress(addr, so_addr)) { | |
| SymbolContext sc; | |
| sc.Clear(true); | |
| if (module_sp->ResolveSymbolContextForAddress( | |
| so_addr, eSymbolContextEverything, sc) & | |
| eSymbolContextLineEntry) { | |
| sc_list.AppendIfUnique(sc, /*merge_symbol_into_function=*/false); | |
| ++num_matches; | |
| } | |
| } | |
| } | |
| if (num_matches == 0) | |
| error_strm.Printf("Source information for file address 0x%" PRIx64 | |
| " not found in any modules.\n", | |
| addr); | |
| } else { | |
| // The target has some things loaded, resolve this address to a compile | |
| // unit + file + line and display | |
| if (target->GetSectionLoadList().ResolveLoadAddress(addr, so_addr)) { | |
| ModuleSP module_sp(so_addr.GetModule()); | |
| // Check to make sure this module is in our list. | |
| if (module_sp && | |
| module_list.GetIndexForModule(module_sp.get()) != | |
| LLDB_INVALID_INDEX32) { | |
| SymbolContext sc; | |
| sc.Clear(true); | |
| if (module_sp->ResolveSymbolContextForAddress( | |
| so_addr, eSymbolContextEverything, sc) & | |
| eSymbolContextLineEntry) { | |
| sc_list.AppendIfUnique(sc, /*merge_symbol_into_function=*/false); | |
| ++num_matches; | |
| } else { | |
| StreamString addr_strm; | |
| so_addr.Dump(&addr_strm, nullptr, | |
| Address::DumpStyleModuleWithFileAddress); | |
| error_strm.Printf( | |
| "Address 0x%" PRIx64 " resolves to %s, but there is" | |
| " no source information available for this address.\n", | |
| addr, addr_strm.GetData()); | |
| } | |
| } else { | |
| StreamString addr_strm; | |
| so_addr.Dump(&addr_strm, nullptr, | |
| Address::DumpStyleModuleWithFileAddress); | |
| error_strm.Printf("Address 0x%" PRIx64 | |
| " resolves to %s, but it cannot" | |
| " be found in any modules.\n", | |
| addr, addr_strm.GetData()); | |
| } | |
| } else | |
| error_strm.Printf("Unable to resolve address 0x%" PRIx64 ".\n", addr); | |
| } | |
| return num_matches; | |
| } | |
| // Dump the line entries found in functions matching the name specified in | |
| // the option. | |
| bool DumpLinesInFunctions(CommandReturnObject &result) { | |
| SymbolContextList sc_list_funcs; | |
| ConstString name(m_options.symbol_name.c_str()); | |
| SymbolContextList sc_list_lines; | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| uint32_t addr_byte_size = target->GetArchitecture().GetAddressByteSize(); | |
| // Note: module_list can't be const& because FindFunctionSymbols isn't | |
| // const. | |
| ModuleList module_list = | |
| (m_module_list.GetSize() > 0) ? m_module_list : target->GetImages(); | |
| module_list.FindFunctions(name, eFunctionNameTypeAuto, | |
| /*include_symbols=*/false, | |
| /*include_inlines=*/true, sc_list_funcs); | |
| size_t num_matches = sc_list_funcs.GetSize(); | |
| if (!num_matches) { | |
| // If we didn't find any functions with that name, try searching for | |
| // symbols that line up exactly with function addresses. | |
| SymbolContextList sc_list_symbols; | |
| module_list.FindFunctionSymbols( | |
| name, eFunctionNameTypeAuto, sc_list_symbols); | |
| size_t num_symbol_matches = sc_list_symbols.GetSize(); | |
| for (size_t i = 0; i < num_symbol_matches; i++) { | |
| SymbolContext sc; | |
| sc_list_symbols.GetContextAtIndex(i, sc); | |
| if (sc.symbol && sc.symbol->ValueIsAddress()) { | |
| const Address &base_address = sc.symbol->GetAddressRef(); | |
| Function *function = base_address.CalculateSymbolContextFunction(); | |
| if (function) { | |
| sc_list_funcs.Append(SymbolContext(function)); | |
| num_matches++; | |
| } | |
| } | |
| } | |
| } | |
| if (num_matches == 0) { | |
| result.AppendErrorWithFormat("Could not find function named \'%s\'.\n", | |
| m_options.symbol_name.c_str()); | |
| return false; | |
| } | |
| for (size_t i = 0; i < num_matches; i++) { | |
| SymbolContext sc; | |
| sc_list_funcs.GetContextAtIndex(i, sc); | |
| bool context_found_for_symbol = false; | |
| // Loop through all the ranges in the function. | |
| AddressRange range; | |
| for (uint32_t r = 0; | |
| sc.GetAddressRange(eSymbolContextEverything, r, | |
| /*use_inline_block_range=*/true, range); | |
| ++r) { | |
| // Append the symbol contexts for each address in the range to | |
| // sc_list_lines. | |
| const Address &base_address = range.GetBaseAddress(); | |
| const addr_t size = range.GetByteSize(); | |
| lldb::addr_t start_addr = base_address.GetLoadAddress(target); | |
| if (start_addr == LLDB_INVALID_ADDRESS) | |
| start_addr = base_address.GetFileAddress(); | |
| lldb::addr_t end_addr = start_addr + size; | |
| for (lldb::addr_t addr = start_addr; addr < end_addr; | |
| addr += addr_byte_size) { | |
| StreamString error_strm; | |
| if (!GetSymbolContextsForAddress(module_list, addr, sc_list_lines, | |
| error_strm)) | |
| result.AppendWarningWithFormat("in symbol '%s': %s", | |
| sc.GetFunctionName().AsCString(), | |
| error_strm.GetData()); | |
| else | |
| context_found_for_symbol = true; | |
| } | |
| } | |
| if (!context_found_for_symbol) | |
| result.AppendWarningWithFormat("Unable to find line information" | |
| " for matching symbol '%s'.\n", | |
| sc.GetFunctionName().AsCString()); | |
| } | |
| if (sc_list_lines.GetSize() == 0) { | |
| result.AppendErrorWithFormat("No line information could be found" | |
| " for any symbols matching '%s'.\n", | |
| name.AsCString()); | |
| return false; | |
| } | |
| FileSpec file_spec; | |
| if (!DumpLinesInSymbolContexts(result.GetOutputStream(), sc_list_lines, | |
| module_list, file_spec)) { | |
| result.AppendErrorWithFormat( | |
| "Unable to dump line information for symbol '%s'.\n", | |
| name.AsCString()); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Dump the line entries found for the address specified in the option. | |
| bool DumpLinesForAddress(CommandReturnObject &result) { | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| SymbolContextList sc_list; | |
| StreamString error_strm; | |
| if (!GetSymbolContextsForAddress(target->GetImages(), m_options.address, | |
| sc_list, error_strm)) { | |
| result.AppendErrorWithFormat("%s.\n", error_strm.GetData()); | |
| return false; | |
| } | |
| ModuleList module_list; | |
| FileSpec file_spec; | |
| if (!DumpLinesInSymbolContexts(result.GetOutputStream(), sc_list, | |
| module_list, file_spec)) { | |
| result.AppendErrorWithFormat("No modules contain load address 0x%" PRIx64 | |
| ".\n", | |
| m_options.address); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Dump the line entries found in the file specified in the option. | |
| bool DumpLinesForFile(CommandReturnObject &result) { | |
| FileSpec file_spec(m_options.file_name); | |
| const char *filename = m_options.file_name.c_str(); | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| const ModuleList &module_list = | |
| (m_module_list.GetSize() > 0) ? m_module_list : target->GetImages(); | |
| bool displayed_something = false; | |
| const size_t num_modules = module_list.GetSize(); | |
| for (uint32_t i = 0; i < num_modules; ++i) { | |
| // Dump lines for this module. | |
| Module *module = module_list.GetModulePointerAtIndex(i); | |
| assert(module); | |
| if (DumpFileLinesInModule(result.GetOutputStream(), module, file_spec)) | |
| displayed_something = true; | |
| } | |
| if (!displayed_something) { | |
| result.AppendErrorWithFormat("No source filenames matched '%s'.\n", | |
| filename); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Dump the line entries for the current frame. | |
| bool DumpLinesForFrame(CommandReturnObject &result) { | |
| StackFrame *cur_frame = m_exe_ctx.GetFramePtr(); | |
| if (cur_frame == nullptr) { | |
| result.AppendError( | |
| "No selected frame to use to find the default source."); | |
| return false; | |
| } else if (!cur_frame->HasDebugInformation()) { | |
| result.AppendError("No debug info for the selected frame."); | |
| return false; | |
| } else { | |
| const SymbolContext &sc = | |
| cur_frame->GetSymbolContext(eSymbolContextLineEntry); | |
| SymbolContextList sc_list; | |
| sc_list.Append(sc); | |
| ModuleList module_list; | |
| FileSpec file_spec; | |
| if (!DumpLinesInSymbolContexts(result.GetOutputStream(), sc_list, | |
| module_list, file_spec)) { | |
| result.AppendError( | |
| "No source line info available for the selected frame."); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| bool DoExecute(Args &command, CommandReturnObject &result) override { | |
| const size_t argc = command.GetArgumentCount(); | |
| if (argc != 0) { | |
| result.AppendErrorWithFormat("'%s' takes no arguments, only flags.\n", | |
| GetCommandName().str().c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| if (target == nullptr) { | |
| target = GetDebugger().GetSelectedTarget().get(); | |
| if (target == nullptr) { | |
| result.AppendError("invalid target, create a debug target using the " | |
| "'target create' command."); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } | |
| uint32_t addr_byte_size = target->GetArchitecture().GetAddressByteSize(); | |
| result.GetOutputStream().SetAddressByteSize(addr_byte_size); | |
| result.GetErrorStream().SetAddressByteSize(addr_byte_size); | |
| // Collect the list of modules to search. | |
| m_module_list.Clear(); | |
| if (!m_options.modules.empty()) { | |
| for (size_t i = 0, e = m_options.modules.size(); i < e; ++i) { | |
| FileSpec module_file_spec(m_options.modules[i]); | |
| if (module_file_spec) { | |
| ModuleSpec module_spec(module_file_spec); | |
| target->GetImages().FindModules(module_spec, m_module_list); | |
| if (m_module_list.IsEmpty()) | |
| result.AppendWarningWithFormat("No module found for '%s'.\n", | |
| m_options.modules[i].c_str()); | |
| } | |
| } | |
| if (!m_module_list.GetSize()) { | |
| result.AppendError("No modules match the input."); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } else if (target->GetImages().GetSize() == 0) { | |
| result.AppendError("The target has no associated executable images."); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| // Check the arguments to see what lines we should dump. | |
| if (!m_options.symbol_name.empty()) { | |
| // Print lines for symbol. | |
| if (DumpLinesInFunctions(result)) | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| else | |
| result.SetStatus(eReturnStatusFailed); | |
| } else if (m_options.address != LLDB_INVALID_ADDRESS) { | |
| // Print lines for an address. | |
| if (DumpLinesForAddress(result)) | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| else | |
| result.SetStatus(eReturnStatusFailed); | |
| } else if (!m_options.file_name.empty()) { | |
| // Dump lines for a file. | |
| if (DumpLinesForFile(result)) | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| else | |
| result.SetStatus(eReturnStatusFailed); | |
| } else { | |
| // Dump the line for the current frame. | |
| if (DumpLinesForFrame(result)) | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| else | |
| result.SetStatus(eReturnStatusFailed); | |
| } | |
| return result.Succeeded(); | |
| } | |
| CommandOptions m_options; | |
| ModuleList m_module_list; | |
| }; | |
| #pragma mark CommandObjectSourceList | |
| // CommandObjectSourceList | |
| #define LLDB_OPTIONS_source_list | |
| #include "CommandOptions.inc" | |
| class CommandObjectSourceList : public CommandObjectParsed { | |
| class CommandOptions : public Options { | |
| public: | |
| CommandOptions() : Options() {} | |
| ~CommandOptions() override = default; | |
| Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg, | |
| ExecutionContext *execution_context) override { | |
| Status error; | |
| const int short_option = GetDefinitions()[option_idx].short_option; | |
| switch (short_option) { | |
| case 'l': | |
| if (option_arg.getAsInteger(0, start_line)) | |
| error.SetErrorStringWithFormat("invalid line number: '%s'", | |
| option_arg.str().c_str()); | |
| break; | |
| case 'c': | |
| if (option_arg.getAsInteger(0, num_lines)) | |
| error.SetErrorStringWithFormat("invalid line count: '%s'", | |
| option_arg.str().c_str()); | |
| break; | |
| case 'f': | |
| file_name = option_arg; | |
| break; | |
| case 'n': | |
| symbol_name = option_arg; | |
| break; | |
| case 'a': { | |
| address = OptionArgParser::ToAddress(execution_context, option_arg, | |
| LLDB_INVALID_ADDRESS, &error); | |
| } break; | |
| case 's': | |
| modules.push_back(std::string(option_arg)); | |
| break; | |
| case 'b': | |
| show_bp_locs = true; | |
| break; | |
| case 'r': | |
| reverse = true; | |
| break; | |
| default: | |
| llvm_unreachable("Unimplemented option"); | |
| } | |
| return error; | |
| } | |
| void OptionParsingStarting(ExecutionContext *execution_context) override { | |
| file_spec.Clear(); | |
| file_name.clear(); | |
| symbol_name.clear(); | |
| address = LLDB_INVALID_ADDRESS; | |
| start_line = 0; | |
| num_lines = 0; | |
| show_bp_locs = false; | |
| reverse = false; | |
| modules.clear(); | |
| } | |
| llvm::ArrayRef<OptionDefinition> GetDefinitions() override { | |
| return llvm::makeArrayRef(g_source_list_options); | |
| } | |
| // Instance variables to hold the values for command options. | |
| FileSpec file_spec; | |
| std::string file_name; | |
| std::string symbol_name; | |
| lldb::addr_t address; | |
| uint32_t start_line; | |
| uint32_t num_lines; | |
| STLStringArray modules; | |
| bool show_bp_locs; | |
| bool reverse; | |
| }; | |
| public: | |
| CommandObjectSourceList(CommandInterpreter &interpreter) | |
| : CommandObjectParsed(interpreter, "source list", | |
| "Display source code for the current target " | |
| "process as specified by options.", | |
| nullptr, eCommandRequiresTarget), | |
| m_options() {} | |
| ~CommandObjectSourceList() override = default; | |
| Options *GetOptions() override { return &m_options; } | |
| const char *GetRepeatCommand(Args ¤t_command_args, | |
| uint32_t index) override { | |
| // This is kind of gross, but the command hasn't been parsed yet so we | |
| // can't look at the option values for this invocation... I have to scan | |
| // the arguments directly. | |
| auto iter = | |
| llvm::find_if(current_command_args, [](const Args::ArgEntry &e) { | |
| return e.ref() == "-r" || e.ref() == "--reverse"; | |
| }); | |
| if (iter == current_command_args.end()) | |
| return m_cmd_name.c_str(); | |
| if (m_reverse_name.empty()) { | |
| m_reverse_name = m_cmd_name; | |
| m_reverse_name.append(" -r"); | |
| } | |
| return m_reverse_name.c_str(); | |
| } | |
| protected: | |
| struct SourceInfo { | |
| ConstString function; | |
| LineEntry line_entry; | |
| SourceInfo(ConstString name, const LineEntry &line_entry) | |
| : function(name), line_entry(line_entry) {} | |
| SourceInfo() : function(), line_entry() {} | |
| bool IsValid() const { return (bool)function && line_entry.IsValid(); } | |
| bool operator==(const SourceInfo &rhs) const { | |
| return function == rhs.function && | |
| line_entry.original_file == rhs.line_entry.original_file && | |
| line_entry.line == rhs.line_entry.line; | |
| } | |
| bool operator!=(const SourceInfo &rhs) const { | |
| return function != rhs.function || | |
| line_entry.original_file != rhs.line_entry.original_file || | |
| line_entry.line != rhs.line_entry.line; | |
| } | |
| bool operator<(const SourceInfo &rhs) const { | |
| if (function.GetCString() < rhs.function.GetCString()) | |
| return true; | |
| if (line_entry.file.GetDirectory().GetCString() < | |
| rhs.line_entry.file.GetDirectory().GetCString()) | |
| return true; | |
| if (line_entry.file.GetFilename().GetCString() < | |
| rhs.line_entry.file.GetFilename().GetCString()) | |
| return true; | |
| if (line_entry.line < rhs.line_entry.line) | |
| return true; | |
| return false; | |
| } | |
| }; | |
| size_t DisplayFunctionSource(const SymbolContext &sc, SourceInfo &source_info, | |
| CommandReturnObject &result) { | |
| if (!source_info.IsValid()) { | |
| source_info.function = sc.GetFunctionName(); | |
| source_info.line_entry = sc.GetFunctionStartLineEntry(); | |
| } | |
| if (sc.function) { | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| FileSpec start_file; | |
| uint32_t start_line; | |
| uint32_t end_line; | |
| FileSpec end_file; | |
| if (sc.block == nullptr) { | |
| // Not an inlined function | |
| sc.function->GetStartLineSourceInfo(start_file, start_line); | |
| if (start_line == 0) { | |
| result.AppendErrorWithFormat("Could not find line information for " | |
| "start of function: \"%s\".\n", | |
| source_info.function.GetCString()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return 0; | |
| } | |
| sc.function->GetEndLineSourceInfo(end_file, end_line); | |
| } else { | |
| // We have an inlined function | |
| start_file = source_info.line_entry.file; | |
| start_line = source_info.line_entry.line; | |
| end_line = start_line + m_options.num_lines; | |
| } | |
| // This is a little hacky, but the first line table entry for a function | |
| // points to the "{" that starts the function block. It would be nice to | |
| // actually get the function declaration in there too. So back up a bit, | |
| // but not further than what you're going to display. | |
| uint32_t extra_lines; | |
| if (m_options.num_lines >= 10) | |
| extra_lines = 5; | |
| else | |
| extra_lines = m_options.num_lines / 2; | |
| uint32_t line_no; | |
| if (start_line <= extra_lines) | |
| line_no = 1; | |
| else | |
| line_no = start_line - extra_lines; | |
| // For fun, if the function is shorter than the number of lines we're | |
| // supposed to display, only display the function... | |
| if (end_line != 0) { | |
| if (m_options.num_lines > end_line - line_no) | |
| m_options.num_lines = end_line - line_no + extra_lines; | |
| } | |
| m_breakpoint_locations.Clear(); | |
| if (m_options.show_bp_locs) { | |
| const bool show_inlines = true; | |
| m_breakpoint_locations.Reset(start_file, 0, show_inlines); | |
| SearchFilterForUnconstrainedSearches target_search_filter( | |
| m_exe_ctx.GetTargetSP()); | |
| target_search_filter.Search(m_breakpoint_locations); | |
| } | |
| result.AppendMessageWithFormat("File: %s\n", | |
| start_file.GetPath().c_str()); | |
| // We don't care about the column here. | |
| const uint32_t column = 0; | |
| return target->GetSourceManager().DisplaySourceLinesWithLineNumbers( | |
| start_file, line_no, column, 0, m_options.num_lines, "", | |
| &result.GetOutputStream(), GetBreakpointLocations()); | |
| } else { | |
| result.AppendErrorWithFormat( | |
| "Could not find function info for: \"%s\".\n", | |
| m_options.symbol_name.c_str()); | |
| } | |
| return 0; | |
| } | |
| // From Jim: The FindMatchingFunctions / FindMatchingFunctionSymbols | |
| // functions "take a possibly empty vector of strings which are names of | |
| // modules, and run the two search functions on the subset of the full module | |
| // list that matches the strings in the input vector". If we wanted to put | |
| // these somewhere, there should probably be a module-filter-list that can be | |
| // passed to the various ModuleList::Find* calls, which would either be a | |
| // vector of string names or a ModuleSpecList. | |
| void FindMatchingFunctions(Target *target, ConstString name, | |
| SymbolContextList &sc_list) { | |
| // Displaying the source for a symbol: | |
| bool include_inlines = true; | |
| bool include_symbols = false; | |
| if (m_options.num_lines == 0) | |
| m_options.num_lines = 10; | |
| const size_t num_modules = m_options.modules.size(); | |
| if (num_modules > 0) { | |
| ModuleList matching_modules; | |
| for (size_t i = 0; i < num_modules; ++i) { | |
| FileSpec module_file_spec(m_options.modules[i]); | |
| if (module_file_spec) { | |
| ModuleSpec module_spec(module_file_spec); | |
| matching_modules.Clear(); | |
| target->GetImages().FindModules(module_spec, matching_modules); | |
| matching_modules.FindFunctions( | |
| name, eFunctionNameTypeAuto, include_symbols, include_inlines, | |
| sc_list); | |
| } | |
| } | |
| } else { | |
| target->GetImages().FindFunctions(name, eFunctionNameTypeAuto, | |
| include_symbols, include_inlines, | |
| sc_list); | |
| } | |
| } | |
| void FindMatchingFunctionSymbols(Target *target, ConstString name, | |
| SymbolContextList &sc_list) { | |
| const size_t num_modules = m_options.modules.size(); | |
| if (num_modules > 0) { | |
| ModuleList matching_modules; | |
| for (size_t i = 0; i < num_modules; ++i) { | |
| FileSpec module_file_spec(m_options.modules[i]); | |
| if (module_file_spec) { | |
| ModuleSpec module_spec(module_file_spec); | |
| matching_modules.Clear(); | |
| target->GetImages().FindModules(module_spec, matching_modules); | |
| matching_modules.FindFunctionSymbols(name, eFunctionNameTypeAuto, | |
| sc_list); | |
| } | |
| } | |
| } else { | |
| target->GetImages().FindFunctionSymbols(name, eFunctionNameTypeAuto, | |
| sc_list); | |
| } | |
| } | |
| bool DoExecute(Args &command, CommandReturnObject &result) override { | |
| const size_t argc = command.GetArgumentCount(); | |
| if (argc != 0) { | |
| result.AppendErrorWithFormat("'%s' takes no arguments, only flags.\n", | |
| GetCommandName().str().c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| Target *target = m_exe_ctx.GetTargetPtr(); | |
| if (!m_options.symbol_name.empty()) { | |
| SymbolContextList sc_list; | |
| ConstString name(m_options.symbol_name.c_str()); | |
| // Displaying the source for a symbol. Search for function named name. | |
| FindMatchingFunctions(target, name, sc_list); | |
| size_t num_matches = sc_list.GetSize(); | |
| if (!num_matches) { | |
| // If we didn't find any functions with that name, try searching for | |
| // symbols that line up exactly with function addresses. | |
| SymbolContextList sc_list_symbols; | |
| FindMatchingFunctionSymbols(target, name, sc_list_symbols); | |
| size_t num_symbol_matches =sc_list_symbols.GetSize(); | |
| for (size_t i = 0; i < num_symbol_matches; i++) { | |
| SymbolContext sc; | |
| sc_list_symbols.GetContextAtIndex(i, sc); | |
| if (sc.symbol && sc.symbol->ValueIsAddress()) { | |
| const Address &base_address = sc.symbol->GetAddressRef(); | |
| Function *function = base_address.CalculateSymbolContextFunction(); | |
| if (function) { | |
| sc_list.Append(SymbolContext(function)); | |
| num_matches++; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| if (num_matches == 0) { | |
| result.AppendErrorWithFormat("Could not find function named: \"%s\".\n", | |
| m_options.symbol_name.c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| if (num_matches > 1) { | |
| std::set<SourceInfo> source_match_set; | |
| bool displayed_something = false; | |
| for (size_t i = 0; i < num_matches; i++) { | |
| SymbolContext sc; | |
| sc_list.GetContextAtIndex(i, sc); | |
| SourceInfo source_info(sc.GetFunctionName(), | |
| sc.GetFunctionStartLineEntry()); | |
| if (source_info.IsValid()) { | |
| if (source_match_set.find(source_info) == source_match_set.end()) { | |
| source_match_set.insert(source_info); | |
| if (DisplayFunctionSource(sc, source_info, result)) | |
| displayed_something = true; | |
| } | |
| } | |
| } | |
| if (displayed_something) | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| else | |
| result.SetStatus(eReturnStatusFailed); | |
| } else { | |
| SymbolContext sc; | |
| sc_list.GetContextAtIndex(0, sc); | |
| SourceInfo source_info; | |
| if (DisplayFunctionSource(sc, source_info, result)) { | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| } else { | |
| result.SetStatus(eReturnStatusFailed); | |
| } | |
| } | |
| return result.Succeeded(); | |
| } else if (m_options.address != LLDB_INVALID_ADDRESS) { | |
| Address so_addr; | |
| StreamString error_strm; | |
| SymbolContextList sc_list; | |
| if (target->GetSectionLoadList().IsEmpty()) { | |
| // The target isn't loaded yet, we need to lookup the file address in | |
| // all modules | |
| const ModuleList &module_list = target->GetImages(); | |
| const size_t num_modules = module_list.GetSize(); | |
| for (size_t i = 0; i < num_modules; ++i) { | |
| ModuleSP module_sp(module_list.GetModuleAtIndex(i)); | |
| if (module_sp && | |
| module_sp->ResolveFileAddress(m_options.address, so_addr)) { | |
| SymbolContext sc; | |
| sc.Clear(true); | |
| if (module_sp->ResolveSymbolContextForAddress( | |
| so_addr, eSymbolContextEverything, sc) & | |
| eSymbolContextLineEntry) | |
| sc_list.Append(sc); | |
| } | |
| } | |
| if (sc_list.GetSize() == 0) { | |
| result.AppendErrorWithFormat( | |
| "no modules have source information for file address 0x%" PRIx64 | |
| ".\n", | |
| m_options.address); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } else { | |
| // The target has some things loaded, resolve this address to a compile | |
| // unit + file + line and display | |
| if (target->GetSectionLoadList().ResolveLoadAddress(m_options.address, | |
| so_addr)) { | |
| ModuleSP module_sp(so_addr.GetModule()); | |
| if (module_sp) { | |
| SymbolContext sc; | |
| sc.Clear(true); | |
| if (module_sp->ResolveSymbolContextForAddress( | |
| so_addr, eSymbolContextEverything, sc) & | |
| eSymbolContextLineEntry) { | |
| sc_list.Append(sc); | |
| } else { | |
| so_addr.Dump(&error_strm, nullptr, | |
| Address::DumpStyleModuleWithFileAddress); | |
| result.AppendErrorWithFormat("address resolves to %s, but there " | |
| "is no line table information " | |
| "available for this address.\n", | |
| error_strm.GetData()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } | |
| } | |
| if (sc_list.GetSize() == 0) { | |
| result.AppendErrorWithFormat( | |
| "no modules contain load address 0x%" PRIx64 ".\n", | |
| m_options.address); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } | |
| uint32_t num_matches = sc_list.GetSize(); | |
| for (uint32_t i = 0; i < num_matches; ++i) { | |
| SymbolContext sc; | |
| sc_list.GetContextAtIndex(i, sc); | |
| if (sc.comp_unit) { | |
| if (m_options.show_bp_locs) { | |
| m_breakpoint_locations.Clear(); | |
| const bool show_inlines = true; | |
| m_breakpoint_locations.Reset(*sc.comp_unit, 0, show_inlines); | |
| SearchFilterForUnconstrainedSearches target_search_filter( | |
| target->shared_from_this()); | |
| target_search_filter.Search(m_breakpoint_locations); | |
| } | |
| bool show_fullpaths = true; | |
| bool show_module = true; | |
| bool show_inlined_frames = true; | |
| const bool show_function_arguments = true; | |
| const bool show_function_name = true; | |
| sc.DumpStopContext(&result.GetOutputStream(), | |
| m_exe_ctx.GetBestExecutionContextScope(), | |
| sc.line_entry.range.GetBaseAddress(), | |
| show_fullpaths, show_module, show_inlined_frames, | |
| show_function_arguments, show_function_name); | |
| result.GetOutputStream().EOL(); | |
| if (m_options.num_lines == 0) | |
| m_options.num_lines = 10; | |
| size_t lines_to_back_up = | |
| m_options.num_lines >= 10 ? 5 : m_options.num_lines / 2; | |
| const uint32_t column = | |
| (GetDebugger().GetStopShowColumn() != eStopShowColumnNone) | |
| ? sc.line_entry.column | |
| : 0; | |
| target->GetSourceManager().DisplaySourceLinesWithLineNumbers( | |
| sc.comp_unit, sc.line_entry.line, column, lines_to_back_up, | |
| m_options.num_lines - lines_to_back_up, "->", | |
| &result.GetOutputStream(), GetBreakpointLocations()); | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| } | |
| } | |
| } else if (m_options.file_name.empty()) { | |
| // Last valid source manager context, or the current frame if no valid | |
| // last context in source manager. One little trick here, if you type the | |
| // exact same list command twice in a row, it is more likely because you | |
| // typed it once, then typed it again | |
| if (m_options.start_line == 0) { | |
| if (target->GetSourceManager().DisplayMoreWithLineNumbers( | |
| &result.GetOutputStream(), m_options.num_lines, | |
| m_options.reverse, GetBreakpointLocations())) { | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| } | |
| } else { | |
| if (m_options.num_lines == 0) | |
| m_options.num_lines = 10; | |
| if (m_options.show_bp_locs) { | |
| SourceManager::FileSP last_file_sp( | |
| target->GetSourceManager().GetLastFile()); | |
| if (last_file_sp) { | |
| const bool show_inlines = true; | |
| m_breakpoint_locations.Reset(last_file_sp->GetFileSpec(), 0, | |
| show_inlines); | |
| SearchFilterForUnconstrainedSearches target_search_filter( | |
| target->shared_from_this()); | |
| target_search_filter.Search(m_breakpoint_locations); | |
| } | |
| } else | |
| m_breakpoint_locations.Clear(); | |
| const uint32_t column = 0; | |
| if (target->GetSourceManager() | |
| .DisplaySourceLinesWithLineNumbersUsingLastFile( | |
| m_options.start_line, // Line to display | |
| m_options.num_lines, // Lines after line to | |
| UINT32_MAX, // Don't mark "line" | |
| column, | |
| "", // Don't mark "line" | |
| &result.GetOutputStream(), GetBreakpointLocations())) { | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| } | |
| } | |
| } else { | |
| const char *filename = m_options.file_name.c_str(); | |
| bool check_inlines = false; | |
| SymbolContextList sc_list; | |
| size_t num_matches = 0; | |
| if (!m_options.modules.empty()) { | |
| ModuleList matching_modules; | |
| for (size_t i = 0, e = m_options.modules.size(); i < e; ++i) { | |
| FileSpec module_file_spec(m_options.modules[i]); | |
| if (module_file_spec) { | |
| ModuleSpec module_spec(module_file_spec); | |
| matching_modules.Clear(); | |
| target->GetImages().FindModules(module_spec, matching_modules); | |
| num_matches += matching_modules.ResolveSymbolContextForFilePath( | |
| filename, 0, check_inlines, | |
| SymbolContextItem(eSymbolContextModule | | |
| eSymbolContextCompUnit), | |
| sc_list); | |
| } | |
| } | |
| } else { | |
| num_matches = target->GetImages().ResolveSymbolContextForFilePath( | |
| filename, 0, check_inlines, | |
| eSymbolContextModule | eSymbolContextCompUnit, sc_list); | |
| } | |
| if (num_matches == 0) { | |
| result.AppendErrorWithFormat("Could not find source file \"%s\".\n", | |
| m_options.file_name.c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| if (num_matches > 1) { | |
| bool got_multiple = false; | |
| FileSpec *test_cu_spec = nullptr; | |
| for (unsigned i = 0; i < num_matches; i++) { | |
| SymbolContext sc; | |
| sc_list.GetContextAtIndex(i, sc); | |
| if (sc.comp_unit) { | |
| if (test_cu_spec) { | |
| if (test_cu_spec != static_cast<FileSpec *>(sc.comp_unit)) | |
| got_multiple = true; | |
| break; | |
| } else | |
| test_cu_spec = sc.comp_unit; | |
| } | |
| } | |
| if (got_multiple) { | |
| result.AppendErrorWithFormat( | |
| "Multiple source files found matching: \"%s.\"\n", | |
| m_options.file_name.c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } | |
| SymbolContext sc; | |
| if (sc_list.GetContextAtIndex(0, sc)) { | |
| if (sc.comp_unit) { | |
| if (m_options.show_bp_locs) { | |
| const bool show_inlines = true; | |
| m_breakpoint_locations.Reset(*sc.comp_unit, 0, show_inlines); | |
| SearchFilterForUnconstrainedSearches target_search_filter( | |
| target->shared_from_this()); | |
| target_search_filter.Search(m_breakpoint_locations); | |
| } else | |
| m_breakpoint_locations.Clear(); | |
| if (m_options.num_lines == 0) | |
| m_options.num_lines = 10; | |
| const uint32_t column = 0; | |
| target->GetSourceManager().DisplaySourceLinesWithLineNumbers( | |
| sc.comp_unit, m_options.start_line, column, | |
| 0, m_options.num_lines, | |
| "", &result.GetOutputStream(), GetBreakpointLocations()); | |
| result.SetStatus(eReturnStatusSuccessFinishResult); | |
| } else { | |
| result.AppendErrorWithFormat("No comp unit found for: \"%s.\"\n", | |
| m_options.file_name.c_str()); | |
| result.SetStatus(eReturnStatusFailed); | |
| return false; | |
| } | |
| } | |
| } | |
| return result.Succeeded(); | |
| } | |
| const SymbolContextList *GetBreakpointLocations() { | |
| if (m_breakpoint_locations.GetFileLineMatches().GetSize() > 0) | |
| return &m_breakpoint_locations.GetFileLineMatches(); | |
| return nullptr; | |
| } | |
| CommandOptions m_options; | |
| FileLineResolver m_breakpoint_locations; | |
| std::string m_reverse_name; | |
| }; | |
| #pragma mark CommandObjectMultiwordSource | |
| // CommandObjectMultiwordSource | |
| CommandObjectMultiwordSource::CommandObjectMultiwordSource( | |
| CommandInterpreter &interpreter) | |
| : CommandObjectMultiword(interpreter, "source", "Commands for examining " | |
| "source code described by " | |
| "debug information for the " | |
| "current target process.", | |
| "source <subcommand> [<subcommand-options>]") { | |
| LoadSubCommand("info", | |
| CommandObjectSP(new CommandObjectSourceInfo(interpreter))); | |
| LoadSubCommand("list", | |
| CommandObjectSP(new CommandObjectSourceList(interpreter))); | |
| } | |
| CommandObjectMultiwordSource::~CommandObjectMultiwordSource() = default; |