diff --git a/apps/gdal.cpp b/apps/gdal.cpp index 88ea2e4dadce..f1fe410e7e92 100644 --- a/apps/gdal.cpp +++ b/apps/gdal.cpp @@ -17,6 +17,267 @@ #include +// #define DEBUG_COMPLETION + +/************************************************************************/ +/* EmitCompletion() */ +/************************************************************************/ + +/** Return on stdout a space-separated list of choices for bash completion */ +static void EmitCompletion(std::unique_ptr rootAlg, int argc, + const char *const *argv) +{ +#ifdef DEBUG_COMPLETION + for (int i = 0; i < argc; ++i) + fprintf(stderr, "arg[%d]='%s'\n", i, argv[i]); +#endif + + // Get inner-most algorithm + auto curAlg = std::move(rootAlg); + while (argc >= 1 && argv[0][0] != '-') + { + auto subAlg = curAlg->InstantiateSubAlgorithm(argv[0]); + if (!subAlg) + break; + ++argv; + --argc; + curAlg = std::move(subAlg); + } + + std::string currentOption; + std::string osKey; + bool currentWordIsPartialOrUnknownOption = false; + std::string currentWord; + bool hasCurrentWord = false; + constexpr const char *CURWORD = "curword="; + if (argc >= 1 && strncmp(argv[argc - 1], CURWORD, strlen(CURWORD)) == 0) + { + hasCurrentWord = true; + currentWord = argv[argc - 1] + strlen(CURWORD); + } + + const auto IsKeyValueOption = [](const char *pszStr) + { + return strcmp(pszStr, "--co") == 0 || + strcmp(pszStr, "--creation-option") == 0 || + strcmp(pszStr, "--oo") == 0 || + strcmp(pszStr, "--open-option") == 0; + }; + + // Deal with "gdal ... --option=" + if (argc >= 2 && currentWord == "=" && argv[argc - 2][0] == '-' && + argv[argc - 2][1] == '-') + { + if (curAlg->GetArg(argv[argc - 2])) + { + currentOption = argv[argc - 2]; + } + --argc; + } + // Deal with "gdal raster convert in.tif out.tif --co COMPRESS=" + else if (argc >= 3 && currentWord == "=" && + IsKeyValueOption(argv[argc - 3])) + { + if (curAlg->GetArg(argv[argc - 3])) + { + osKey = argv[argc - 2]; + currentOption = argv[argc - 3]; + } + argc -= 2; + } + // Deal with "gdal raster convert in.tif out.tif --co=COMPRESS=" + else if (argc >= 4 && currentWord == "=" && + strcmp(argv[argc - 3], "=") == 0 && + IsKeyValueOption(argv[argc - 4])) + { + if (curAlg->GetArg(argv[argc - 4])) + { + osKey = argv[argc - 2]; + currentOption = argv[argc - 4]; + } + argc -= 3; + } + // Deal with "gdal raster convert in.tif out.tif --co COMPRESS=NO" + else if (argc >= 4 && currentWord != "=" && + strcmp(argv[argc - 2], "=") == 0 && + IsKeyValueOption(argv[argc - 4])) + { + if (curAlg->GetArg(argv[argc - 4])) + { + osKey = argv[argc - 3]; + currentOption = argv[argc - 4]; + } + argc -= 3; + } + // Deal with "gdal raster convert in.tif out.tif --co=COMPRESS=NO" + else if (argc >= 5 && currentWord != "=" && + strcmp(argv[argc - 2], "=") == 0 && + strcmp(argv[argc - 4], "=") == 0 && + IsKeyValueOption(argv[argc - 5])) + { + if (curAlg->GetArg(argv[argc - 5])) + { + osKey = argv[argc - 3]; + currentOption = argv[argc - 5]; + } + argc -= 3; + } + else if (!currentWord.empty() && currentWord[0] == '-') + { + // Deal with "gdal ... -" + currentWordIsPartialOrUnknownOption = true; + --argc; + } + else if (hasCurrentWord) + { + --argc; + if (argc >= 1 && strcmp(argv[argc - 1], "=") == 0) + { + // Deal with "gdal ... --option=" + --argc; + } + } + + std::string ret; + const auto addSpace = [&ret]() + { + if (!ret.empty()) + ret += " "; + }; + + if (argc >= 1 && strcmp(argv[argc - 1], "--config") == 0) + { + CPLStringList aosConfigOptions(CPLGetKnownConfigOptions()); + for (const char *pszOpt : cpl::Iterate(aosConfigOptions)) + { + addSpace(); + ret += pszOpt; + ret += '='; + } + printf("%s", ret.c_str()); + return; + } + else if (argc >= 2 && strcmp(argv[argc - 2], "--config") == 0 && + currentWord == "=") + { + // Do not try to autocomplete "gdal --config FOO=" + return; + } + else if (argc >= 3 && strcmp(argv[argc - 3], "--config") == 0 && + strcmp(argv[argc - 2], "=") == 0 && currentWord == "=") + { + // Do not try to autocomplete "gdal --config=FOO=" + return; + } + + // If the algorithm has a auto-completion method, use it + const std::vector args(argv, argv + argc); + std::vector autoCompleteChoices = + curAlg->GetAutoCompleteChoices(args, currentWord); + if (!autoCompleteChoices.empty()) + { + for (const auto &osChoice : autoCompleteChoices) + { + addSpace(); + ret += osChoice; + } + } + else + { + if (!currentWordIsPartialOrUnknownOption && currentOption.empty() && + argc >= 1 && argv[argc - 1][0] == '-') + currentOption = argv[argc - 1]; + + if (currentWordIsPartialOrUnknownOption) + { + // List available options + for (const auto &arg : curAlg->GetArgs()) + { + if (arg->IsHiddenForCLI()) + continue; + if (!arg->GetShortName().empty()) + { + addSpace(); + ret += "-"; + ret += arg->GetShortName(); + } + for (const std::string &alias : arg->GetAliases()) + { + addSpace(); + ret += "--"; + ret += alias; + } + if (!arg->GetName().empty()) + { + addSpace(); + ret += "--"; + ret += arg->GetName(); + } + } + } + else if (!currentOption.empty()) + { + // List possible choices for current option + auto arg = curAlg->GetArg(currentOption); + if (arg) + { + std::vector choices = arg->GetChoices(); + if (choices.empty()) + { + { + CPLErrorStateBackuper oErrorQuieter( + CPLQuietErrorHandler); + curAlg->SetParseForAutoCompletion(); + CPL_IGNORE_RET_VAL( + curAlg->ParseCommandLineArguments(args)); + } + if (osKey.empty()) + { + choices = arg->GetAutoCompleteChoices(currentWord); + } + else + { + choices = arg->GetAutoCompleteChoices(osKey); + } + } + for (const auto &choice : choices) + { + addSpace(); + ret += CPLString(choice).replaceAll(" ", "\\ "); + } + } + } + else if (STARTS_WITH(currentWord.c_str(), "/vsi")) + { + auto arg = curAlg->GetArg("input"); + if (arg) + { + for (const auto &choice : + arg->GetAutoCompleteChoices(currentWord)) + { + addSpace(); + ret += CPLString(choice).replaceAll(" ", "\\ "); + } + } + } + else + { + // List possible sub-algorithms + for (const auto &osName : curAlg->GetSubAlgorithmNames()) + { + addSpace(); + ret += osName; + } + } + } + +#ifdef DEBUG_COMPLETION + fprintf(stderr, "ret = '%s'\n", ret.c_str()); +#endif + if (!ret.empty()) + printf("%s", ret.c_str()); +} + /************************************************************************/ /* main() */ /************************************************************************/ @@ -31,14 +292,23 @@ MAIN_START(argc, argv) /* -------------------------------------------------------------------- */ GDALAllRegister(); + + auto alg = GDALGlobalAlgorithmRegistry::GetSingleton().Instantiate( + GDALGlobalAlgorithmRegistry::ROOT_ALG_NAME); + assert(alg); + + if (argc >= 2 && strcmp(argv[1], "completion") == 0) + { + // Process lines like "gdal completion raster curword=conv" + EmitCompletion(std::move(alg), argc - 2, argv + 2); + return 0; + } + argc = GDALGeneralCmdLineProcessor( argc, &argv, GDAL_OF_RASTER | GDAL_OF_VECTOR | GDAL_OF_MULTIDIM_RASTER); if (argc < 1) return (-argc); - auto alg = GDALGlobalAlgorithmRegistry::GetSingleton().Instantiate( - GDALGlobalAlgorithmRegistry::ROOT_ALG_NAME); - assert(alg); alg->SetCallPath(std::vector{argv[0]}); std::vector args; for (int i = 1; i < argc; ++i) diff --git a/apps/gdalalg_abstract_pipeline.h b/apps/gdalalg_abstract_pipeline.h new file mode 100644 index 000000000000..7aae865cd71b --- /dev/null +++ b/apps/gdalalg_abstract_pipeline.h @@ -0,0 +1,250 @@ +/****************************************************************************** + * + * Project: GDAL + * Purpose: gdal "raster/vector pipeline" subcommand + * Author: Even Rouault + * + ****************************************************************************** + * Copyright (c) 2024, Even Rouault + * + * SPDX-License-Identifier: MIT + ****************************************************************************/ + +#ifndef GDALALG_ABSTRACT_PIPELINE_INCLUDED +#define GDALALG_ABSTRACT_PIPELINE_INCLUDED + +//! @cond Doxygen_Suppress + +#include "cpl_conv.h" +#include "cpl_json.h" +#include "gdalalgorithm.h" + +template +class GDALAbstractPipelineAlgorithm CPL_NON_FINAL : public StepAlgorithm +{ + public: + std::vector + GetAutoCompleteChoices(const std::vector &args, + const std::string ¤tWord) const override; + + bool Finalize() override; + + std::string GetUsageAsJSON() const override; + + /* cppcheck-suppress functionStatic */ + void SetDataset(GDALDataset *) + { + } + + protected: + GDALAbstractPipelineAlgorithm(const std::string &name, + const std::string &description, + const std::string &helpURL, + bool standaloneStep) + : StepAlgorithm(name, description, helpURL, standaloneStep) + { + } + + virtual GDALArgDatasetValue &GetOutputDataset() = 0; + + std::string m_pipeline{}; + + std::unique_ptr GetStepAlg(const std::string &name) const; + + GDALAlgorithmRegistry m_stepRegistry{}; + std::vector> m_steps{}; + + private: + bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) override; +}; + +/************************************************************************/ +/* GDALAbstractPipelineAlgorithm::GetStepAlg() */ +/************************************************************************/ + +template +std::unique_ptr +GDALAbstractPipelineAlgorithm::GetStepAlg( + const std::string &name) const +{ + auto alg = m_stepRegistry.Instantiate(name); + return std::unique_ptr( + cpl::down_cast(alg.release())); +} + +/************************************************************************/ +/* GDALAbstractPipelineAlgorithm::GetAutoCompleteChoices() */ +/************************************************************************/ + +template +std::vector +GDALAbstractPipelineAlgorithm::GetAutoCompleteChoices( + const std::vector &args, const std::string ¤tWord) const +{ + std::vector ret; + if (args.empty()) + { + if (currentWord.empty() || currentWord != "read") + ret.push_back("read"); + } + else if (args.back() == "!" || currentWord == "!") + { + for (const std::string &name : m_stepRegistry.GetNames()) + { + if (name != "read") + { + ret.push_back(name); + } + } + } + else + { + auto alg = GetStepAlg(args.back()); + if (alg) + { + if (!currentWord.empty() && currentWord[0] == '-') + { + for (const auto &arg : alg->GetArgs()) + { + if (arg->IsHiddenForCLI() || arg->GetName() == "help" || + arg->GetName() == "drivers" || + arg->GetName() == "version" || + arg->GetName() == "json-usage") + { + continue; + } + if (!arg->GetShortName().empty()) + { + ret.push_back( + std::string("-").append(arg->GetShortName())); + } + for (const std::string &alias : arg->GetAliases()) + { + ret.push_back(std::string("--").append(alias)); + } + if (!arg->GetName().empty()) + { + ret.push_back(std::string("--").append(arg->GetName())); + } + } + } + } + } + return ret; +} + +/************************************************************************/ +/* GDALAbstractPipelineAlgorithm::RunStep() */ +/************************************************************************/ + +template +bool GDALAbstractPipelineAlgorithm::RunStep( + GDALProgressFunc pfnProgress, void *pProgressData) +{ + if (m_steps.empty()) + { + // If invoked programmatically, not from the command line. + + if (m_pipeline.empty()) + { + StepAlgorithm::ReportError(CE_Failure, CPLE_AppDefined, + "'pipeline' argument not set"); + return false; + } + + const CPLStringList aosTokens(CSLTokenizeString(m_pipeline.c_str())); + if (!this->ParseCommandLineArguments(aosTokens)) + return false; + } + + GDALDataset *poCurDS = nullptr; + for (size_t i = 0; i < m_steps.size(); ++i) + { + auto &step = m_steps[i]; + if (i > 0) + { + if (step->m_inputDataset.GetDatasetRef()) + { + // Shouldn't happen + StepAlgorithm::ReportError( + CE_Failure, CPLE_AppDefined, + "Step nr %d (%s) has already an input dataset", + static_cast(i), step->GetName().c_str()); + return false; + } + step->m_inputDataset.Set(poCurDS); + } + if (i + 1 < m_steps.size() && step->m_outputDataset.GetDatasetRef()) + { + // Shouldn't happen + StepAlgorithm::ReportError( + CE_Failure, CPLE_AppDefined, + "Step nr %d (%s) has already an output dataset", + static_cast(i), step->GetName().c_str()); + return false; + } + if (!step->Run(i < m_steps.size() - 1 ? nullptr : pfnProgress, + i < m_steps.size() - 1 ? nullptr : pProgressData)) + { + return false; + } + poCurDS = step->m_outputDataset.GetDatasetRef(); + if (!poCurDS) + { + StepAlgorithm::ReportError( + CE_Failure, CPLE_AppDefined, + "Step nr %d (%s) failed to produce an output dataset", + static_cast(i), step->GetName().c_str()); + return false; + } + } + + if (!GetOutputDataset().GetDatasetRef()) + { + GetOutputDataset().Set(poCurDS); + } + + return true; +} + +/************************************************************************/ +/* GDALAbstractPipelineAlgorithm::Finalize() */ +/************************************************************************/ + +template +bool GDALAbstractPipelineAlgorithm::Finalize() +{ + bool ret = GDALAlgorithm::Finalize(); + for (auto &step : m_steps) + { + ret = step->Finalize() && ret; + } + return ret; +} + +/************************************************************************/ +/* GDALAbstractPipelineAlgorithm::GetUsageAsJSON() */ +/************************************************************************/ + +template +std::string GDALAbstractPipelineAlgorithm::GetUsageAsJSON() const +{ + CPLJSONDocument oDoc; + oDoc.LoadMemory(GDALAlgorithm::GetUsageAsJSON()); + + CPLJSONArray jPipelineSteps; + for (const std::string &name : m_stepRegistry.GetNames()) + { + auto alg = GetStepAlg(name); + CPLJSONDocument oStepDoc; + oStepDoc.LoadMemory(alg->GetUsageAsJSON()); + jPipelineSteps.Add(oStepDoc.GetRoot()); + } + oDoc.GetRoot().Add("pipeline_algorithms", jPipelineSteps); + + return oDoc.SaveAsString(); +} + +//! @endcond + +#endif diff --git a/apps/gdalalg_raster_pipeline.cpp b/apps/gdalalg_raster_pipeline.cpp index c135108959d5..0ab163fa531c 100644 --- a/apps/gdalalg_raster_pipeline.cpp +++ b/apps/gdalalg_raster_pipeline.cpp @@ -17,7 +17,6 @@ #include "gdalalg_raster_write.h" #include "cpl_conv.h" -#include "cpl_json.h" #include "cpl_string.h" #include @@ -149,8 +148,9 @@ bool GDALRasterPipelineStepAlgorithm::RunImpl(GDALProgressFunc pfnProgress, GDALRasterPipelineAlgorithm::GDALRasterPipelineAlgorithm( bool openForMixedRasterVector) - : GDALRasterPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL, - /*standaloneStep=*/false) + : GDALAbstractPipelineAlgorithm( + NAME, DESCRIPTION, HELP_URL, + /*standaloneStep=*/false) { AddInputArgs(openForMixedRasterVector, /* hiddenForCLI = */ true); AddProgressArg(); @@ -165,18 +165,6 @@ GDALRasterPipelineAlgorithm::GDALRasterPipelineAlgorithm( m_stepRegistry.Register(); } -/************************************************************************/ -/* GDALRasterPipelineAlgorithm::GetStepAlg() */ -/************************************************************************/ - -std::unique_ptr -GDALRasterPipelineAlgorithm::GetStepAlg(const std::string &name) const -{ - auto alg = m_stepRegistry.Instantiate(name); - return std::unique_ptr( - cpl::down_cast(alg.release())); -} - /************************************************************************/ /* GDALRasterPipelineAlgorithm::ParseCommandLineArguments() */ /************************************************************************/ @@ -387,90 +375,6 @@ bool GDALRasterPipelineAlgorithm::ParseCommandLineArguments( return true; } -/************************************************************************/ -/* GDALRasterPipelineAlgorithm::RunStep() */ -/************************************************************************/ - -bool GDALRasterPipelineAlgorithm::RunStep(GDALProgressFunc pfnProgress, - void *pProgressData) -{ - if (m_steps.empty()) - { - // If invoked programmatically, not from the command line. - - if (m_pipeline.empty()) - { - ReportError(CE_Failure, CPLE_AppDefined, - "'pipeline' argument not set"); - return false; - } - - const CPLStringList aosTokens(CSLTokenizeString(m_pipeline.c_str())); - if (!ParseCommandLineArguments(aosTokens)) - return false; - } - - GDALDataset *poCurDS = nullptr; - for (size_t i = 0; i < m_steps.size(); ++i) - { - auto &step = m_steps[i]; - if (i > 0) - { - if (step->m_inputDataset.GetDatasetRef()) - { - // Shouldn't happen - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) has already an input dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - step->m_inputDataset.Set(poCurDS); - } - if (i + 1 < m_steps.size() && step->m_outputDataset.GetDatasetRef()) - { - // Shouldn't happen - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) has already an output dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - if (!step->Run(i < m_steps.size() - 1 ? nullptr : pfnProgress, - i < m_steps.size() - 1 ? nullptr : pProgressData)) - { - return false; - } - poCurDS = step->m_outputDataset.GetDatasetRef(); - if (!poCurDS) - { - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) failed to produce an output dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - } - - if (!m_outputDataset.GetDatasetRef()) - { - m_outputDataset.Set(poCurDS); - } - - return true; -} - -/************************************************************************/ -/* GDALAlgorithm::Finalize() */ -/************************************************************************/ - -bool GDALRasterPipelineAlgorithm::Finalize() -{ - bool ret = GDALAlgorithm::Finalize(); - for (auto &step : m_steps) - { - ret = step->Finalize() && ret; - } - return ret; -} - /************************************************************************/ /* GDALRasterPipelineAlgorithm::GetUsageForCLI() */ /************************************************************************/ @@ -531,26 +435,4 @@ std::string GDALRasterPipelineAlgorithm::GetUsageForCLI( return ret; } -/************************************************************************/ -/* GDALRasterPipelineAlgorithm::GetUsageAsJSON() */ -/************************************************************************/ - -std::string GDALRasterPipelineAlgorithm::GetUsageAsJSON() const -{ - CPLJSONDocument oDoc; - oDoc.LoadMemory(GDALAlgorithm::GetUsageAsJSON()); - - CPLJSONArray jPipelineSteps; - for (const std::string &name : m_stepRegistry.GetNames()) - { - auto alg = GetStepAlg(name); - CPLJSONDocument oStepDoc; - oStepDoc.LoadMemory(alg->GetUsageAsJSON()); - jPipelineSteps.Add(oStepDoc.GetRoot()); - } - oDoc.GetRoot().Add("pipeline_algorithms", jPipelineSteps); - - return oDoc.SaveAsString(); -} - //! @endcond diff --git a/apps/gdalalg_raster_pipeline.h b/apps/gdalalg_raster_pipeline.h index 0e0a35d86562..7129ac081908 100644 --- a/apps/gdalalg_raster_pipeline.h +++ b/apps/gdalalg_raster_pipeline.h @@ -14,6 +14,7 @@ #define GDALALG_RASTER_PIPELINE_INCLUDED #include "gdalalgorithm.h" +#include "gdalalg_abstract_pipeline.h" //! @cond Doxygen_Suppress @@ -30,6 +31,7 @@ class GDALRasterPipelineStepAlgorithm /* non final */ : public GDALAlgorithm bool standaloneStep); friend class GDALRasterPipelineAlgorithm; + friend class GDALAbstractPipelineAlgorithm; virtual bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) = 0; @@ -67,7 +69,8 @@ class GDALRasterPipelineStepAlgorithm /* non final */ : public GDALAlgorithm #define GDAL_PIPELINE_PROJ_NOSTALGIA #endif -class GDALRasterPipelineAlgorithm final : public GDALRasterPipelineStepAlgorithm +class GDALRasterPipelineAlgorithm final + : public GDALAbstractPipelineAlgorithm { public: static constexpr const char *NAME = "pipeline"; @@ -91,33 +94,19 @@ class GDALRasterPipelineAlgorithm final : public GDALRasterPipelineStepAlgorithm bool ParseCommandLineArguments(const std::vector &args) override; - bool Finalize() override; - std::string GetUsageForCLI(bool shortUsage, const UsageOptions &usageOptions) const override; - std::string GetUsageAsJSON() const override; - GDALDataset *GetDatasetRef() { return m_inputDataset.GetDatasetRef(); } - /* cppcheck-suppress functionStatic */ - void SetDataset(GDALDataset *) + protected: + GDALArgDatasetValue &GetOutputDataset() override { + return m_outputDataset; } - - private: - std::string m_pipeline{}; - - bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) override; - - std::unique_ptr - GetStepAlg(const std::string &name) const; - - GDALAlgorithmRegistry m_stepRegistry{}; - std::vector> m_steps{}; }; //! @endcond diff --git a/apps/gdalalg_vector_pipeline.cpp b/apps/gdalalg_vector_pipeline.cpp index 6588ecaf6ad6..7bee6af9a19a 100644 --- a/apps/gdalalg_vector_pipeline.cpp +++ b/apps/gdalalg_vector_pipeline.cpp @@ -17,7 +17,6 @@ #include "gdalalg_vector_write.h" #include "cpl_conv.h" -#include "cpl_json.h" #include "cpl_string.h" #include @@ -161,8 +160,9 @@ bool GDALVectorPipelineStepAlgorithm::RunImpl(GDALProgressFunc pfnProgress, /************************************************************************/ GDALVectorPipelineAlgorithm::GDALVectorPipelineAlgorithm() - : GDALVectorPipelineStepAlgorithm(NAME, DESCRIPTION, HELP_URL, - /*standaloneStep=*/false) + : GDALAbstractPipelineAlgorithm( + NAME, DESCRIPTION, HELP_URL, + /*standaloneStep=*/false) { AddInputArgs(/* hiddenForCLI = */ true); AddProgressArg(); @@ -178,18 +178,6 @@ GDALVectorPipelineAlgorithm::GDALVectorPipelineAlgorithm() m_stepRegistry.Register(); } -/************************************************************************/ -/* GDALVectorPipelineAlgorithm::GetStepAlg() */ -/************************************************************************/ - -std::unique_ptr -GDALVectorPipelineAlgorithm::GetStepAlg(const std::string &name) const -{ - auto alg = m_stepRegistry.Instantiate(name); - return std::unique_ptr( - cpl::down_cast(alg.release())); -} - /************************************************************************/ /* GDALVectorPipelineAlgorithm::ParseCommandLineArguments() */ /************************************************************************/ @@ -444,90 +432,6 @@ bool GDALVectorPipelineAlgorithm::ParseCommandLineArguments( return true; } -/************************************************************************/ -/* GDALVectorPipelineAlgorithm::RunStep() */ -/************************************************************************/ - -bool GDALVectorPipelineAlgorithm::RunStep(GDALProgressFunc pfnProgress, - void *pProgressData) -{ - if (m_steps.empty()) - { - // If invoked programmatically, not from the command line. - - if (m_pipeline.empty()) - { - ReportError(CE_Failure, CPLE_AppDefined, - "'pipeline' argument not set"); - return false; - } - - const CPLStringList aosTokens(CSLTokenizeString(m_pipeline.c_str())); - if (!ParseCommandLineArguments(aosTokens)) - return false; - } - - GDALDataset *poCurDS = nullptr; - for (size_t i = 0; i < m_steps.size(); ++i) - { - auto &step = m_steps[i]; - if (i > 0) - { - if (step->m_inputDataset.GetDatasetRef()) - { - // Shouldn't happen - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) has already an input dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - step->m_inputDataset.Set(poCurDS); - } - if (i + 1 < m_steps.size() && step->m_outputDataset.GetDatasetRef()) - { - // Shouldn't happen - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) has already an output dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - if (!step->Run(i < m_steps.size() - 1 ? nullptr : pfnProgress, - i < m_steps.size() - 1 ? nullptr : pProgressData)) - { - return false; - } - poCurDS = step->m_outputDataset.GetDatasetRef(); - if (!poCurDS) - { - ReportError(CE_Failure, CPLE_AppDefined, - "Step nr %d (%s) failed to produce an output dataset", - static_cast(i), step->GetName().c_str()); - return false; - } - } - - if (!m_outputDataset.GetDatasetRef()) - { - m_outputDataset.Set(poCurDS); - } - - return true; -} - -/************************************************************************/ -/* GDALAlgorithm::Finalize() */ -/************************************************************************/ - -bool GDALVectorPipelineAlgorithm::Finalize() -{ - bool ret = GDALAlgorithm::Finalize(); - for (auto &step : m_steps) - { - ret = step->Finalize() && ret; - } - return ret; -} - /************************************************************************/ /* GDALVectorPipelineAlgorithm::GetUsageForCLI() */ /************************************************************************/ @@ -592,26 +496,4 @@ std::string GDALVectorPipelineAlgorithm::GetUsageForCLI( return ret; } -/************************************************************************/ -/* GDALVectorPipelineAlgorithm::GetUsageAsJSON() */ -/************************************************************************/ - -std::string GDALVectorPipelineAlgorithm::GetUsageAsJSON() const -{ - CPLJSONDocument oDoc; - oDoc.LoadMemory(GDALAlgorithm::GetUsageAsJSON()); - - CPLJSONArray jPipelineSteps; - for (const std::string &name : m_stepRegistry.GetNames()) - { - auto alg = GetStepAlg(name); - CPLJSONDocument oStepDoc; - oStepDoc.LoadMemory(alg->GetUsageAsJSON()); - jPipelineSteps.Add(oStepDoc.GetRoot()); - } - oDoc.GetRoot().Add("pipeline_algorithms", jPipelineSteps); - - return oDoc.SaveAsString(); -} - //! @endcond diff --git a/apps/gdalalg_vector_pipeline.h b/apps/gdalalg_vector_pipeline.h index 14b960dc6485..97564af4e04e 100644 --- a/apps/gdalalg_vector_pipeline.h +++ b/apps/gdalalg_vector_pipeline.h @@ -14,6 +14,7 @@ #define GDALALG_VECTOR_PIPELINE_INCLUDED #include "gdalalgorithm.h" +#include "gdalalg_abstract_pipeline.h" //! @cond Doxygen_Suppress @@ -30,6 +31,7 @@ class GDALVectorPipelineStepAlgorithm /* non final */ : public GDALAlgorithm bool standaloneStep); friend class GDALVectorPipelineAlgorithm; + friend class GDALAbstractPipelineAlgorithm; virtual bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) = 0; @@ -69,7 +71,8 @@ class GDALVectorPipelineStepAlgorithm /* non final */ : public GDALAlgorithm // "gdal vector pipeline ! read poly.gpkg ! reproject--dst-crs=EPSG:32632 ! write out.gpkg --overwrite" #define GDAL_PIPELINE_PROJ_NOSTALGIA -class GDALVectorPipelineAlgorithm final : public GDALVectorPipelineStepAlgorithm +class GDALVectorPipelineAlgorithm final + : public GDALAbstractPipelineAlgorithm { public: static constexpr const char *NAME = "pipeline"; @@ -93,28 +96,14 @@ class GDALVectorPipelineAlgorithm final : public GDALVectorPipelineStepAlgorithm bool ParseCommandLineArguments(const std::vector &args) override; - bool Finalize() override; - std::string GetUsageForCLI(bool shortUsage, const UsageOptions &usageOptions) const override; - std::string GetUsageAsJSON() const override; - - /* cppcheck-suppress functionStatic */ - void SetDataset(GDALDataset *) + protected: + GDALArgDatasetValue &GetOutputDataset() override { + return m_outputDataset; } - - private: - std::string m_pipeline{}; - - bool RunStep(GDALProgressFunc pfnProgress, void *pProgressData) override; - - std::unique_ptr - GetStepAlg(const std::string &name) const; - - GDALAlgorithmRegistry m_stepRegistry{}; - std::vector> m_steps{}; }; //! @endcond diff --git a/autotest/cpp/test_gdal_algorithm.cpp b/autotest/cpp/test_gdal_algorithm.cpp index 6174c5826c85..d8ca927ae20c 100644 --- a/autotest/cpp/test_gdal_algorithm.cpp +++ b/autotest/cpp/test_gdal_algorithm.cpp @@ -597,7 +597,13 @@ TEST_F(test_gdal_algorithm, GDALInConstructionAlgorithmArg_AddAlias) MyAlgorithm alg; alg.GetUsageForCLI(false); + EXPECT_NE(alg.GetArg("flag"), nullptr); + EXPECT_NE(alg.GetArg("--flag"), nullptr); + EXPECT_NE(alg.GetArg("-f"), nullptr); + EXPECT_NE(alg.GetArg("f"), nullptr); EXPECT_NE(alg.GetArg("alias"), nullptr); + EXPECT_EQ(alg.GetArg("invalid"), nullptr); + EXPECT_EQ(alg.GetArg("-"), nullptr); } TEST_F(test_gdal_algorithm, GDALInConstructionAlgorithmArg_AddAlias_redundant) @@ -2919,7 +2925,7 @@ TEST_F(test_gdal_algorithm, algorithm_c_api) char **argNames = GDALAlgorithmGetArgNames(hAlg.get()); ASSERT_NE(argNames, nullptr); - EXPECT_EQ(CSLCount(argNames), 12); + EXPECT_EQ(CSLCount(argNames), 13); CSLDestroy(argNames); EXPECT_EQ(GDALAlgorithmGetArg(hAlg.get(), "non_existing"), nullptr); diff --git a/autotest/utilities/test_gdal.py b/autotest/utilities/test_gdal.py index 420b6e9a0c98..18a219406cc2 100755 --- a/autotest/utilities/test_gdal.py +++ b/autotest/utilities/test_gdal.py @@ -17,6 +17,8 @@ import pytest import test_cli_utilities +from osgeo import gdal + pytestmark = pytest.mark.skipif( test_cli_utilities.get_gdal_path() is None, reason="gdal binary not available" ) @@ -84,3 +86,204 @@ def test_gdal_failure_during_finalize(gdal_path): f"{gdal_path} raster convert ../gcore/data/byte.tif /vsimem/out.tif||maxlength=100" ) assert "ret code = 1" in err + + +def test_gdal_completion(gdal_path): + + out = gdaltest.runexternal(f"{gdal_path} completion").split(" ") + assert "convert" in out + assert "info" in out + assert "raster" in out + assert "vector" in out + + out = gdaltest.runexternal(f"{gdal_path} completion curword=").split(" ") + assert "convert" in out + assert "info" in out + assert "raster" in out + assert "vector" in out + + out = gdaltest.runexternal(f"{gdal_path} completion raster curword=").split(" ") + assert "convert" in out + assert "edit" in out + assert "info" in out + assert "reproject" in out + assert "pipeline" in out + + out = gdaltest.runexternal(f"{gdal_path} completion raster info curword=-").split( + " " + ) + assert "-f" in out + assert "--of" in out + assert "--output-format" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster info --of curword=" + ).split(" ") + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster info --of curword==" + ).split(" ") + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster info --of = curword=t" + ).split(" ") + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --of curword=" + ).split(" ") + if gdal.GetDriverByName("GTiff"): + assert "GTiff" in out + if gdal.GetDriverByName("HFA"): + assert "HFA" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --input curword=" + ).split(" ") + assert "data/" in out or "data\\" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --input curword=data/" + ).split(" ") + assert "data/whiteblackred.tif" in out or "data\\whiteblackred.tif" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert curword=/vsizip/../gcore/data/byte.tif.zip/" + ).split(" ") + assert out == ["/vsizip/../gcore/data/byte.tif.zip/byte.tif"] + + +def test_gdal_completion_co(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co curword==" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" in out + assert "TILING_SCHEME=" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co curword==" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" in out + assert "TILING_SCHEME=" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co COMPRESS curword==" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co COMPRESS = curword=NO" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co = COMPRESS curword==" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co = COMPRESS = curword=NO" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co TILED curword==" + ).split(" ") + assert out == ["NO", "YES"] + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert in.tif out.tif --co ZLEVEL curword==" + ).split(" ") + assert "1" in out + assert "9" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --of COG --co curword==" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" not in out + assert "TILING_SCHEME=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --of COG --co ZOOM_LEVEL curword==" + ).split(" ") + assert out == [""] + + if gdal.GetDriverByName("GPKG"): + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --of GPKG --co curword==" + ).split(" ") + assert "APPEND_SUBDATASET=" in out + assert "VERSION=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion vector convert --of GPKG --co curword==" + ).split(" ") + assert "APPEND_SUBDATASET=" not in out + assert "VERSION=" in out + + +def test_gdal_completion_config(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --config curword=" + ).split(" ") + assert "CPL_DEBUG=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --config = curword=" + ).split(" ") + assert "CPL_DEBUG=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --config FOO curword==" + ).split(" ") + assert out == [""] + + out = gdaltest.runexternal( + f"{gdal_path} completion raster convert --config = FOO curword==" + ).split(" ") + assert out == [""] + + +@pytest.mark.parametrize("subcommand", ["raster", "vector"]) +def test_gdal_completion_pipeline(gdal_path, subcommand): + + out = gdaltest.runexternal( + f"{gdal_path} completion {subcommand} pipeline curword=" + ).split(" ") + assert out == ["read"] + + out = gdaltest.runexternal( + f"{gdal_path} completion {subcommand} pipeline read curword=-" + ).split(" ") + assert "--input" in out + assert "--open-option" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion {subcommand} pipeline read ! curword=" + ).split(" ") + assert "reproject" in out + assert "write" in out + assert "read" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion {subcommand} pipeline read curword=!" + ).split(" ") + assert "reproject" in out + assert "write" in out + assert "read" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion {subcommand} pipeline read foo ! write curword=-" + ).split(" ") + assert "--output" in out + assert "--co" in out diff --git a/gcore/gdalalgorithm.cpp b/gcore/gdalalgorithm.cpp index e86d90717c44..736d2ecb51a3 100644 --- a/gcore/gdalalgorithm.cpp +++ b/gcore/gdalalgorithm.cpp @@ -14,6 +14,7 @@ #include "cpl_conv.h" #include "cpl_error.h" #include "cpl_json.h" +#include "cpl_minixml.h" #include "gdalalgorithm.h" #include "gdal_priv.h" @@ -761,6 +762,10 @@ GDALAlgorithm::GDALAlgorithm(const std::string &name, &m_dummyBoolean) .SetOnlyForCLI() .SetCategory(GAAC_COMMON); + AddArg("config", 0, _("Configuration option"), &m_dummyConfigOptions) + .SetMetaVar("=") + .SetOnlyForCLI() + .SetCategory(GAAC_COMMON); } /************************************************************************/ @@ -1098,6 +1103,11 @@ bool GDALAlgorithm::ParseCommandLineArguments( auto iterArg = m_mapLongNameToArg.find(name.substr(2)); if (iterArg == m_mapLongNameToArg.end()) { + if (m_parseForAutoCompletion) + { + lArgs.erase(lArgs.begin() + i); + break; + } ReportError(CE_Failure, CPLE_IllegalArg, "Long name option '%s' is unknown.", name.c_str()); return false; @@ -1113,6 +1123,11 @@ bool GDALAlgorithm::ParseCommandLineArguments( { if (strArg.size() != 2) { + if (m_parseForAutoCompletion) + { + lArgs.erase(lArgs.begin() + i); + break; + } ReportError( CE_Failure, CPLE_IllegalArg, "Option '%s' not recognized. Should be either a long " @@ -1124,6 +1139,11 @@ bool GDALAlgorithm::ParseCommandLineArguments( auto iterArg = m_mapShortNameToArg.find(name.substr(1)); if (iterArg == m_mapShortNameToArg.end()) { + if (m_parseForAutoCompletion) + { + lArgs.erase(lArgs.begin() + i); + break; + } ReportError(CE_Failure, CPLE_IllegalArg, "Short name option '%s' is unknown.", name.c_str()); return false; @@ -1150,6 +1170,11 @@ bool GDALAlgorithm::ParseCommandLineArguments( { if (i + 1 == lArgs.size()) { + if (m_parseForAutoCompletion) + { + lArgs.erase(lArgs.begin() + i); + break; + } ReportError( CE_Failure, CPLE_IllegalArg, "Expected value for argument '%s', but ran short of tokens", @@ -1639,6 +1664,29 @@ bool GDALAlgorithm::ValidateArguments() return ret; } +/************************************************************************/ +/* GDALAlgorithm::GetArg() */ +/************************************************************************/ + +const GDALAlgorithmArg *GDALAlgorithm::GetArg(const std::string &osName) const +{ + const auto nPos = osName.find_first_not_of('-'); + if (nPos == std::string::npos) + return nullptr; + const std::string osKey = osName.substr(nPos); + { + const auto oIter = m_mapLongNameToArg.find(osKey); + if (oIter != m_mapLongNameToArg.end()) + return oIter->second; + } + { + const auto oIter = m_mapShortNameToArg.find(osKey); + if (oIter != m_mapShortNameToArg.end()) + return oIter->second; + } + return nullptr; +} + /************************************************************************/ /* GDALAlgorithm::AddAliasFor() */ /************************************************************************/ @@ -1847,6 +1895,77 @@ GDALAlgorithm::AddInputDatasetArg(GDALArgDatasetValue *pValue, pValue, type); if (positionalAndRequired) arg.SetPositional().SetRequired(); + + arg.SetAutoCompleteFunction( + [type](const std::string ¤tWord) + { + std::vector oRet; + + auto poDM = GetGDALDriverManager(); + std::set oExtensions; + for (int i = 0; i < poDM->GetDriverCount(); ++i) + { + auto poDriver = poDM->GetDriver(i); + if (((type & GDAL_OF_RASTER) != 0 && + poDriver->GetMetadataItem(GDAL_DCAP_RASTER)) || + ((type & GDAL_OF_VECTOR) != 0 && + poDriver->GetMetadataItem(GDAL_DCAP_VECTOR)) || + ((type & GDAL_OF_MULTIDIM_RASTER) != 0 && + poDriver->GetMetadataItem(GDAL_DCAP_MULTIDIM_RASTER))) + { + const char *pszExtensions = + poDriver->GetMetadataItem(GDAL_DMD_EXTENSIONS); + if (pszExtensions) + { + const CPLStringList aosExts( + CSLTokenizeString2(pszExtensions, " ", 0)); + for (const char *pszExt : cpl::Iterate(aosExts)) + oExtensions.insert(CPLString(pszExt).tolower()); + } + } + } + + std::string osDir = CPLGetDirname(currentWord.c_str()); + auto psDir = VSIOpenDir(osDir.c_str(), 0, nullptr); + const std::string osSep = VSIGetDirectorySeparator(osDir.c_str()); + if (currentWord.empty()) + osDir.clear(); + const std::string currentFilename = + CPLGetFilename(currentWord.c_str()); + if (psDir) + { + while (const VSIDIREntry *psEntry = VSIGetNextDirEntry(psDir)) + { + if ((currentFilename.empty() || + STARTS_WITH(psEntry->pszName, + currentFilename.c_str())) && + strcmp(psEntry->pszName, ".") != 0 && + strcmp(psEntry->pszName, "..") != 0 && + !strstr(psEntry->pszName, ".aux.xml")) + { + if (cpl::contains( + oExtensions, + CPLString(CPLGetExtension(psEntry->pszName)) + .tolower()) || + VSI_ISDIR(psEntry->nMode)) + { + std::string osVal; + if (osDir.empty()) + osVal = psEntry->pszName; + else + osVal = CPLFormFilename( + osDir.c_str(), psEntry->pszName, nullptr); + if (VSI_ISDIR(psEntry->nMode)) + osVal += osSep; + oRet.push_back(osVal); + } + } + } + VSICloseDir(psDir); + } + return oRet; + }); + return arg; } @@ -1996,6 +2115,51 @@ bool GDALAlgorithm::ValidateFormat(const GDALAlgorithmArg &arg) const return true; } +/************************************************************************/ +/* FormatAutoCompleteFunction() */ +/************************************************************************/ + +static std::vector +FormatAutoCompleteFunction(const GDALAlgorithmArg &arg) +{ + std::vector res; + auto poDM = GetGDALDriverManager(); + for (int i = 0; i < poDM->GetDriverCount(); ++i) + { + auto poDriver = poDM->GetDriver(i); + + const auto caps = arg.GetMetadataItem(GAAMDI_REQUIRED_CAPABILITIES); + if (caps) + { + bool ok = true; + for (const std::string &cap : *caps) + { + if (poDriver->GetMetadataItem(cap.c_str())) + { + } + else if (cap == GDAL_DCAP_CREATECOPY && + std::find(caps->begin(), caps->end(), + GDAL_DCAP_RASTER) != caps->end() && + poDriver->GetMetadataItem(GDAL_DCAP_RASTER) && + poDriver->GetMetadataItem(GDAL_DCAP_CREATE)) + { + // if it supports Create, it supports CreateCopy + } + else + { + ok = false; + break; + } + } + if (ok) + { + res.push_back(poDriver->GetDescription()); + } + } + } + return res; +} + /************************************************************************/ /* GDALAlgorithm::AddInputFormatsArg() */ /************************************************************************/ @@ -2007,6 +2171,8 @@ GDALAlgorithm::AddInputFormatsArg(std::vector *pValue) .AddAlias("if") .SetCategory(GAAC_ADVANCED); arg.AddValidationAction([this, &arg]() { return ValidateFormat(arg); }); + arg.SetAutoCompleteFunction([&arg](const std::string &) + { return FormatAutoCompleteFunction(arg); }); return arg; } @@ -2021,6 +2187,8 @@ GDALAlgorithm::AddOutputFormatArg(std::string *pValue) .AddAlias("of") .AddAlias("format"); arg.AddValidationAction([this, &arg]() { return ValidateFormat(arg); }); + arg.SetAutoCompleteFunction([&arg](const std::string &) + { return FormatAutoCompleteFunction(arg); }); return arg; } @@ -2094,6 +2262,87 @@ bool GDALAlgorithm::ValidateKeyValue(const GDALAlgorithmArg &arg) const return true; } +/************************************************************************/ +/* GDALAlgorithm::AddCreationOptionsSuggestions() */ +/************************************************************************/ + +static bool AddCreationOptionsSuggestions(GDALDriver *poDriver, int datasetType, + const std::string ¤tWord, + std::vector &oRet) +{ + const char *pszXML = poDriver->GetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST); + if (!pszXML) + return false; + CPLXMLTreeCloser poTree(CPLParseXMLString(pszXML)); + if (!poTree) + return false; + const CPLXMLNode *psRoot = + CPLGetXMLNode(poTree.get(), "=CreationOptionList"); + if (!psRoot) + return false; + + for (const CPLXMLNode *psChild = psRoot->psChild; psChild; + psChild = psChild->psNext) + { + const char *pszName = CPLGetXMLValue(psChild, "name", nullptr); + if (pszName && currentWord == pszName && + EQUAL(psChild->pszValue, "Option")) + { + const char *pszType = CPLGetXMLValue(psChild, "type", ""); + if (EQUAL(pszType, "string-select")) + { + for (const CPLXMLNode *psChild2 = psChild->psChild; psChild2; + psChild2 = psChild2->psNext) + { + if (EQUAL(psChild2->pszValue, "Value")) + { + oRet.push_back(CPLGetXMLValue(psChild2, "", "")); + } + } + } + else if (EQUAL(pszType, "boolean")) + { + oRet.push_back("NO"); + oRet.push_back("YES"); + } + else if (EQUAL(pszType, "int")) + { + const char *pszMin = CPLGetXMLValue(psChild, "min", nullptr); + const char *pszMax = CPLGetXMLValue(psChild, "max", nullptr); + if (pszMin && pszMax && atoi(pszMax) - atoi(pszMin) > 0 && + atoi(pszMax) - atoi(pszMin) < 25) + { + const int nMax = atoi(pszMax); + for (int i = atoi(pszMin); i <= nMax; ++i) + oRet.push_back(std::to_string(i)); + } + } + + return true; + } + } + + for (const CPLXMLNode *psChild = psRoot->psChild; psChild; + psChild = psChild->psNext) + { + const char *pszName = CPLGetXMLValue(psChild, "name", nullptr); + if (pszName && EQUAL(psChild->pszValue, "Option")) + { + const char *pszScope = CPLGetXMLValue(psChild, "scope", nullptr); + if (!pszScope || + (EQUAL(pszScope, "raster") && + (datasetType & GDAL_OF_RASTER) != 0) || + (EQUAL(pszScope, "vector") && + (datasetType & GDAL_OF_VECTOR) != 0)) + { + oRet.push_back(std::string(pszName).append("=")); + } + } + } + + return false; +} + /************************************************************************/ /* GDALAlgorithm::AddCreationOptionsArg() */ /************************************************************************/ @@ -2105,6 +2354,86 @@ GDALAlgorithm::AddCreationOptionsArg(std::vector *pValue) .AddAlias("co") .SetMetaVar("="); arg.AddValidationAction([this, &arg]() { return ValidateKeyValue(arg); }); + + arg.SetAutoCompleteFunction( + [this](const std::string ¤tWord) + { + std::vector oRet; + + int datasetType = + GDAL_OF_RASTER | GDAL_OF_VECTOR | GDAL_OF_MULTIDIM_RASTER; + auto outputArg = GetArg(GDAL_ARG_NAME_OUTPUT); + if (outputArg && outputArg->GetType() == GAAT_DATASET) + { + auto &datasetValue = outputArg->Get(); + datasetType = datasetValue.GetType(); + } + + auto outputFormat = GetArg("format"); + if (outputFormat && outputFormat->GetType() == GAAT_STRING && + outputFormat->IsExplicitlySet()) + { + auto poDriver = GetGDALDriverManager()->GetDriverByName( + outputFormat->Get().c_str()); + if (poDriver) + { + AddCreationOptionsSuggestions(poDriver, datasetType, + currentWord, oRet); + } + return oRet; + } + + if (outputArg && outputArg->GetType() == GAAT_DATASET) + { + auto poDM = GetGDALDriverManager(); + auto &datasetValue = outputArg->Get(); + const auto osDSName = datasetValue.GetName(); + const std::string osExt = CPLGetExtension(osDSName.c_str()); + if (!osExt.empty()) + { + std::set oVisitedExtensions; + for (int i = 0; i < poDM->GetDriverCount(); ++i) + { + auto poDriver = poDM->GetDriver(i); + if (((datasetType & GDAL_OF_RASTER) != 0 && + poDriver->GetMetadataItem(GDAL_DCAP_RASTER)) || + ((datasetType & GDAL_OF_VECTOR) != 0 && + poDriver->GetMetadataItem(GDAL_DCAP_VECTOR)) || + ((datasetType & GDAL_OF_MULTIDIM_RASTER) != 0 && + poDriver->GetMetadataItem( + GDAL_DCAP_MULTIDIM_RASTER))) + { + const char *pszExtensions = + poDriver->GetMetadataItem(GDAL_DMD_EXTENSIONS); + if (pszExtensions) + { + const CPLStringList aosExts( + CSLTokenizeString2(pszExtensions, " ", 0)); + for (const char *pszExt : cpl::Iterate(aosExts)) + { + if (EQUAL(pszExt, osExt.c_str()) && + !cpl::contains(oVisitedExtensions, + pszExt)) + { + oVisitedExtensions.insert(pszExt); + if (AddCreationOptionsSuggestions( + poDriver, datasetType, + currentWord, oRet)) + { + return oRet; + } + break; + } + } + } + } + } + } + } + + return oRet; + }); + return arg; } diff --git a/gcore/gdalalgorithm.h b/gcore/gdalalgorithm.h index cb7c50e782fb..8b207d203638 100644 --- a/gcore/gdalalgorithm.h +++ b/gcore/gdalalgorithm.h @@ -1109,6 +1109,17 @@ class CPL_DLL GDALAlgorithmArg /* non-final */ return m_decl.GetChoices(); } + /** Return auto completion choices, if a auto completion function has been + * registered. + */ + inline std::vector + GetAutoCompleteChoices(const std::string ¤tWord) const + { + if (m_autoCompleteFunction) + return m_autoCompleteFunction(currentWord); + return {}; + } + /** Return whether the argument value has been explicitly set with Set() */ inline bool IsExplicitlySet() const { @@ -1367,6 +1378,9 @@ class CPL_DLL GDALAlgorithmArg /* non-final */ std::vector> m_actions{}; /** Validation actions */ std::vector> m_validationActions{}; + /** Autocompletion function */ + std::function(const std::string &)> + m_autoCompleteFunction{}; private: bool m_skipIfAlreadySet = false; @@ -1583,6 +1597,16 @@ class CPL_DLL GDALInConstructionAlgorithmArg final : public GDALAlgorithmArg return *this; } + /** Register a function that will return a list of valid choices for + * the value of the argument. This is typically used for autocompletion. + */ + GDALInConstructionAlgorithmArg &SetAutoCompleteFunction( + std::function(const std::string &)> f) + { + m_autoCompleteFunction = f; + return *this; + } + /** Register an action to validate that the argument value is a valid * CRS definition. * @param noneAllowed Set to true to mean that "null" or "none" are allowed @@ -1773,19 +1797,15 @@ class CPL_DLL GDALAlgorithmRegistry return m_args; } - /** Return an argument from its (long) name */ + /** Return an argument from its long name, short name or an alias */ GDALAlgorithmArg *GetArg(const std::string &osName) { - auto oIter = m_mapLongNameToArg.find(osName); - return oIter != m_mapLongNameToArg.end() ? oIter->second : nullptr; + return const_cast( + const_cast(this)->GetArg(osName)); } - /** Return an argument from its (long) name */ - const GDALAlgorithmArg *GetArg(const std::string &osName) const - { - auto oIter = m_mapLongNameToArg.find(osName); - return oIter != m_mapLongNameToArg.end() ? oIter->second : nullptr; - } + /** Return an argument from its long name, short name or an alias */ + const GDALAlgorithmArg *GetArg(const std::string &osName) const; /** Set the calling path to this algorithm. * @@ -1797,6 +1817,15 @@ class CPL_DLL GDALAlgorithmRegistry m_callPath = path; } + /** Set hint before calling ParseCommandLineArguments() that it must + * try to be be graceful when possible, e.g. accepting + * "gdal raster convert in.tif out.tif --co" + */ + void SetParseForAutoCompletion() + { + m_parseForAutoCompletion = true; + } + /** Parse a command line argument, which does not include the algorithm * name, to set the value of corresponding arguments. */ @@ -1901,6 +1930,19 @@ class CPL_DLL GDALAlgorithmRegistry return false; } + /** Return auto completion choices. + * @param args Full words. + * @param currentWord Word currently under completion. + */ + virtual std::vector + GetAutoCompleteChoices(const std::vector &args, + const std::string ¤tWord) const + { + CPL_IGNORE_RET_VAL(args); + CPL_IGNORE_RET_VAL(currentWord); + return {}; + } + protected: friend class GDALInConstructionAlgorithmArg; @@ -2107,12 +2149,16 @@ class CPL_DLL GDALAlgorithmRegistry bool m_helpRequested = false; bool m_JSONUsageRequested = false; bool m_dummyBoolean = false; // Used for --version + bool m_parseForAutoCompletion = false; + std::vector m_dummyConfigOptions{}; std::vector> m_args{}; std::map m_mapLongNameToArg{}; std::map m_mapShortNameToArg{}; std::vector m_positionalArgs{}; GDALAlgorithmRegistry m_subAlgRegistry{}; std::unique_ptr m_shortCutAlg{}; + std::function(const std::vector &)> + m_autoCompleteFunction{}; GDALInConstructionAlgorithmArg & AddArg(std::unique_ptr arg); diff --git a/scripts/completionFinder.py b/scripts/completionFinder.py index cecf0f04f32e..7424ba47b103 100644 --- a/scripts/completionFinder.py +++ b/scripts/completionFinder.py @@ -275,6 +275,34 @@ def main(argv): # Checks that bash-completion is recent enough function_exists _get_comp_words_by_ref || return 0 +_gdal() +{ + local cur prev + COMPREPLY=() + max_idx=$((COMP_CWORD - 1)) + _get_comp_words_by_ref cur prev + choices=$(gdal completion "${COMP_WORDS[@]:1:${max_idx}}" curword=${cur}) + if [[ "$cur" == "=" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$cur" == "!" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" -P "! " --) + else + mapfile -t COMPREPLY < <(compgen -W "$choices" -- "$cur") + fi + for element in "${COMPREPLY[@]}"; do + if [[ $element == */ ]]; then + # Do not add a space if one of the suggestion ends with slash + compopt -o nospace + break + elif [[ $element == *= ]]; then + # Do not add a space if one of the suggestion ends with equal + compopt -o nospace + break + fi + done +} +complete -o default -F _gdal gdal + """ ) diff --git a/scripts/gdal-bash-completion.sh b/scripts/gdal-bash-completion.sh index 3a39501d3f3f..a02a4e711228 100644 --- a/scripts/gdal-bash-completion.sh +++ b/scripts/gdal-bash-completion.sh @@ -9,6 +9,34 @@ function_exists() { # Checks that bash-completion is recent enough function_exists _get_comp_words_by_ref || return 0 +_gdal() +{ + local cur prev + COMPREPLY=() + max_idx=$((COMP_CWORD - 1)) + _get_comp_words_by_ref cur prev + choices=$(gdal completion "${COMP_WORDS[@]:1:${max_idx}}" curword=${cur}) + if [[ "$cur" == "=" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$cur" == "!" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" -P "! " --) + else + mapfile -t COMPREPLY < <(compgen -W "$choices" -- "$cur") + fi + for element in "${COMPREPLY[@]}"; do + if [[ $element == */ ]]; then + # Do not add a space if one of the suggestion ends with slash + compopt -o nospace + break + elif [[ $element == *= ]]; then + # Do not add a space if one of the suggestion ends with equal + compopt -o nospace + break + fi + done +} +complete -o default -F _gdal gdal + _gdal2tiles.py() { local cur prev diff --git a/scripts/setdevenv.sh b/scripts/setdevenv.sh index eeecd1fdc757..d6968cc13b00 100755 --- a/scripts/setdevenv.sh +++ b/scripts/setdevenv.sh @@ -67,3 +67,8 @@ if [[ ! "${PYTHONPATH:-}" =~ $GDAL_PYTHONPATH ]]; then echo "Setting PYTHONPATH=$PYTHONPATH" fi unset GDAL_PYTHONPATH + +if [[ $BASH_VERSION ]]; then + echo "Sourcing ${GDAL_ROOT}/scripts/gdal-bash-completion.sh" + source "${GDAL_ROOT}/scripts/gdal-bash-completion.sh" +fi