| // Copyright 2026 The GN Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "gn/edit_command.h" |
| |
| #include <iomanip> |
| #include <sstream> |
| #include <utility> |
| #include <vector> |
| |
| #include "gn/build_file_editor.h" |
| #include "gn/commands.h" |
| #include "gn/edit_subcommands.h" |
| #include "gn/filesystem_utils.h" |
| #include "gn/setup.h" |
| #include "gn/source_file.h" |
| #include "gn/standard_out.h" |
| #include "gn/value.h" |
| |
| namespace commands { |
| |
| const char kEdit[] = "edit"; |
| const char kEdit_HelpShort[] = |
| "edit: Edit BUILD.gn files from the command line."; |
| const char kEdit_Help[] = |
| "gn edit <command> <labels/patterns...>\n" |
| "\n" |
| " Executes a command to modify a set of targets\n" |
| "\n" |
| " Note: Because GN is an imperative language, it's not always entirely\n" |
| " clear what the \"correct\" thing is to do.\n" |
| "\n" |
| " In cases of ambiguity (eg. conditionals), `gn edit` will leave notes\n" |
| " in your build files instructing you what to do.\n" |
| "\n" |
| "Commands:\n" |
| " delete\n" |
| " Deletes the matched targets entirely.\n" |
| "\n" |
| " Example:\n" |
| " gn edit \"delete\" //src/tools:old_target\n" |
| "\n" |
| " remove <attribute>\n" |
| " Removes <attribute> entirely.\n" |
| "\n" |
| " Example:\n" |
| " gn edit \"remove testonly\" //src/tools:*\n" |
| "\n" |
| " set <attribute>[:list] <value(s)>\n" |
| " Sets or overwrites the target's <attribute> to <value(s)>.\n" |
| " If multiple values are provided, or if the \":list\" suffix is\n" |
| " appended to the attribute, <value(s)> is interpreted as a list.\n" |
| "\n" |
| " Examples:\n" |
| " gn edit \"set testonly true\" //src/tools:*\n" |
| " gn edit \"set srcs:list foo.cc\" //:foo\n" |
| " gn edit \"set deps :bar :baz\" //:foo\n"; |
| |
| Result<std::pair<std::vector<SourceFile>, EditState>> RunEditImpl( |
| const std::vector<std::string>& args, |
| Setup& setup) { |
| if (args.size() < 2) { |
| return Err(Location(), "Insufficient arguments.", |
| "Usage: gn edit <command> <labels...>\n" |
| "Example: gn edit \"set testonly true\" //foo:*"); |
| } |
| |
| // We use std::quoted to tokenize the command. |
| // eg. set foo "bar baz" -> ["set", "foo", "bar baz"]. |
| std::stringstream ss(args[0]); |
| std::vector<std::string> command_tokens; |
| std::string token; |
| while (ss >> std::ws && !ss.eof()) { |
| if (!(ss >> std::quoted(token))) { |
| return Err(Location(), "Unclosed quote in command string."); |
| } |
| command_tokens.push_back(std::move(token)); |
| } |
| if (command_tokens.empty()) { |
| return Err(Location(), "Empty command string."); |
| } |
| |
| ASSIGN_OR_RETURN(EditCommand command, |
| ParseCommand(std::move(command_tokens))); |
| const SourceDir current_dir = |
| SourceDirForCurrentDirectory(setup.build_settings().root_path()); |
| const std::string source_root = setup.build_settings().root_path_utf8(); |
| |
| std::vector<LabelPattern> patterns; |
| for (size_t i = 1; i < args.size(); ++i) { |
| Value val(nullptr, args[i]); |
| Err err; |
| LabelPattern pattern = |
| LabelPattern::GetPattern(current_dir, source_root, val, &err); |
| if (err.has_error()) { |
| return err; |
| } |
| patterns.push_back(std::move(pattern)); |
| } |
| |
| ASSIGN_OR_RETURN(std::vector<BuildFile> build_files, |
| ::ResolvePatternsToBuildFiles(&setup.build_settings(), |
| setup.loader(), patterns)); |
| |
| EditState state(args[0]); |
| for (auto& build_file : build_files) { |
| RETURN_IF_ERROR(command(build_file, state)); |
| RETURN_IF_ERROR(build_file.label_matcher().done()); |
| } |
| |
| std::vector<SourceFile> modified_files; |
| for (auto& build_file : build_files) { |
| ASSIGN_OR_RETURN(bool wrote, build_file.Write()); |
| if (wrote) { |
| modified_files.push_back(build_file.source_file()); |
| } |
| } |
| |
| return std::make_pair(std::move(modified_files), std::move(state)); |
| } |
| |
| int RunEdit(const std::vector<std::string>& args) { |
| Setup setup; |
| if (!setup.DoSetupForEditing()) { |
| return 1; |
| } |
| auto result = RunEditImpl(args, setup); |
| if (result.has_error()) { |
| result.error().PrintToStdout(); |
| return 1; |
| } |
| for (const auto& file : result->first) { |
| OutputString("Wrote '" + file.value() + "'.\n"); |
| } |
| for (const auto& warning : result->second.warnings) { |
| warning.PrintNonfatalToStdout(); |
| } |
| const auto& review_needed = result->second.needs_manual_review; |
| if (!review_needed.empty()) { |
| OutputString("\nThe following targets need manual review:\n", |
| DECORATION_YELLOW); |
| for (const Label& label : review_needed) { |
| OutputString("* "); |
| OutputString(label.GetUserVisibleName(false) + "\n", DECORATION_GREEN); |
| } |
| |
| OutputString( |
| "\nWhere manual review is required, comments have been added to the " |
| "build file of the form:\n'# TODO(gn edit: <command>): ...'\n", |
| DECORATION_DIM); |
| } |
| return 0; |
| } |
| |
| } // namespace commands |