From 5dfb911d344042797fdd03109ad76a6ab169cf7c Mon Sep 17 00:00:00 2001 From: Even Rouault Date: Wed, 1 Jan 2025 16:59:23 +0100 Subject: [PATCH] gdal CLI: add Bash completion Examples: ``` $ gdal convert info pipeline raster vector ``` ``` $ gdal r ==> $ gdal raster ``` ``` $ gdal raster convert edit info pipeline reproject ``` ``` $ gdal raster info - --approx-stats -f --help --if --json-usage --min-max --no-fl --no-md --oo --stats --checksum --format --hist --input --list-mdd --mm --no-gcp --no-nodata --open-option --subdataset --drivers -h -i --input-format --mdd --no-ct --no-mask --of --output-format --version ``` ``` $ gdal raster info --of json text ``` ``` $ gdal raster info --of= json text ``` ``` $ gdal raster info --of=j ==> $ gdal raster info --of=json ``` ``` $ gdal raster convert --of AAIGrid CALS ERS GSAG ILWIS KEA MFF OpenFileGDB R SQLite WMTS ADRG COG EXR GSBG ISCE KMLSUPEROVERLAY MFF2 PAux Rasterlite SRTMHGT XPM AVIF CTable2 FIT GTA ISIS2 KRO MRF PCIDSK RMF Terragen XYZ BAG DDS FITS GTiff ISIS3 KTX2 netCDF PCRaster ROI_PAC TileDB Zarr BASISU DTED GeoRaster GTX JP2ECW LAN NGW PDF RRASTER USGSDEM ZMap BLX ECW GIF HDF4Image JP2KAK LCP NITF PDS4 RST VICAR BMP EHdr GPKG HEIF JP2OpenJPEG Leveller NTv2 PNG SAGA VRT BT ELAS GRIB HF2 JPEG MBTiles NULL PNM SGI WEBP BYN ENVI GS7BG HFA JPEGXL MEM NWT_GRD PostGISRaster SIGDEM WMS ``` ``` $ gdal raster convert in.tif out.tif --co ALPHA= ENDIANNESS= JXL_EFFORT= PIXELTYPE= SOURCE_PRIMARIES_RED= TIFFTAG_TRANSFERRANGE_BLACK= BIGTIFF= GEOTIFF_KEYS_FLAVOR= JXL_LOSSLESS= PREDICTOR= SOURCE_WHITEPOINT= TIFFTAG_TRANSFERRANGE_WHITE= BLOCKXSIZE= GEOTIFF_VERSION= LZMA_PRESET= PROFILE= SPARSE_OK= TILED= [ ... snip ... ] ``` ``` $ gdal raster convert in.tif out.tif --co COMP ==> $ gdal raster convert in.tif out.tif --co COMPRESS= ``` ``` $ gdal raster convert in.tif out.tif --co COMPRESS= CCITTFAX3 CCITTRLE JPEG LERC LERC_ZSTD LZW PACKBITS ZSTD CCITTFAX4 DEFLATE JXL LERC_DEFLATE LZMA NONE WEBP ``` ``` $ gdal raster convert in.tif out.tif --co TILED= NO YES ``` ``` $ gdal raster convert --of COG --co ADD_ALPHA= EXTENT= JXL_LOSSLESS= NUM_THREADS= OVERVIEW_RESAMPLING= RESAMPLING= WARP_RESAMPLING= ALIGNED_LEVELS= GEOTIFF_VERSION= LEVEL= OVERVIEW_COMPRESS= OVERVIEWS= SPARSE_OK= ZOOM_LEVEL= BIGTIFF= JXL_ALPHA_DISTANCE= MAX_Z_ERROR= OVERVIEW_COUNT= PREDICTOR= STATISTICS= ZOOM_LEVEL_STRATEGY= BLOCKSIZE= JXL_DISTANCE= MAX_Z_ERROR_OVERVIEW= OVERVIEW_PREDICTOR= QUALITY= TARGET_SRS= COMPRESS= JXL_EFFORT= NBITS= OVERVIEW_QUALITY= RES= TILING_SCHEME= ``` ``` $ gdal --config ... long list of known configuration options ... ``` ``` $ gdal --config AWS_ AWS_ACCESS_KEY_ID= AWS_DEFAULT_REGION= AWS_REQUEST_PAYER= AWS_STS_ENDPOINT= AWS_CONFIG_FILE= AWS_HTTPS= AWS_ROLE_ARN= AWS_STS_REGION= AWS_CONTAINER_AUTHORIZATION_TOKEN= AWS_MAX_KEYS= AWS_ROLE_SESSION_NAME= AWS_STS_REGIONAL_ENDPOINTS= AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE= AWS_NO_SIGN_REQUEST= AWS_S3_ENDPOINT= AWS_TIMESTAMP= AWS_CONTAINER_CREDENTIALS_FULL_URI= AWS_PROFILE= AWS_SECRET_ACCESS_KEY= AWS_VIRTUAL_HOSTING= AWS_DEFAULT_PROFILE= AWS_REGION= AWS_SESSION_TOKEN= AWS_WEB_IDENTITY_TOKEN_FILE= ``` Last but not least: autocompletion of VSI files ``` $ gdal raster info /vsis3/my_bucket/b /vsis3/my_bucket/byte.tif /vsis3/my_bucket/byte2.tif ``` --- apps/gdal.cpp | 278 ++++++++++++++++++++++++- apps/gdalalg_abstract_pipeline.h | 250 ++++++++++++++++++++++ apps/gdalalg_raster_pipeline.cpp | 124 +---------- apps/gdalalg_raster_pipeline.h | 25 +-- apps/gdalalg_vector_pipeline.cpp | 124 +---------- apps/gdalalg_vector_pipeline.h | 25 +-- autotest/cpp/test_gdal_algorithm.cpp | 8 +- autotest/utilities/test_gdal.py | 179 ++++++++++++++++ gcore/gdalalgorithm.cpp | 299 +++++++++++++++++++++++++++ gcore/gdalalgorithm.h | 64 +++++- scripts/completionFinder.py | 28 +++ scripts/gdal-bash-completion.sh | 28 +++ scripts/setdevenv.sh | 5 + 13 files changed, 1146 insertions(+), 291 deletions(-) create mode 100644 apps/gdalalg_abstract_pipeline.h diff --git a/apps/gdal.cpp b/apps/gdal.cpp index 88ea2e4dadce..9a497997f3f7 100644 --- a/apps/gdal.cpp +++ b/apps/gdal.cpp @@ -17,6 +17,269 @@ #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 - 1], "=") == 0 && + strcmp(argv[argc - 3], "--config") == 0 && currentWord == "=") + { + // Do not try to autocomplete "gdal --config=FOO=" + return; + } + + // If the algorithm has a auto-completion method, use it + std::vector args; + for (int i = 0; i < argc; ++i) + args.push_back(argv[i]); + 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 += choice; + } + } + } + else if (STARTS_WITH(currentWord.c_str(), "/vsi")) + { + auto arg = curAlg->GetArg("input"); + if (arg) + { + for (const auto &choice : + arg->GetAutoCompleteChoices(currentWord)) + { + addSpace(); + ret += choice; + } + } + } + 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 +294,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..a660340b66c1 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,180 @@ 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 --of COG --co curword==" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" not in out + assert "TILING_SCHEME=" 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..b8878da63bcd 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,66 @@ bool GDALAlgorithm::ValidateKeyValue(const GDALAlgorithmArg &arg) const return true; } +/************************************************************************/ +/* GDALAlgorithm::AddCreationOptionsSuggestions() */ +/************************************************************************/ + +static bool AddCreationOptionsSuggestions(GDALDriver *poDriver, + std::vector &oRet, + const std::string ¤tWord) +{ + 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")) + { + if (EQUAL(CPLGetXMLValue(psChild, "type", ""), "string-select")) + { + for (const CPLXMLNode *psChild2 = psChild->psChild; psChild2; + psChild2 = psChild2->psNext) + { + if (EQUAL(psChild2->pszValue, "Value")) + { + oRet.push_back(CPLGetXMLValue(psChild2, "", "")); + } + } + return true; + } + else if (EQUAL(CPLGetXMLValue(psChild, "type", ""), "boolean")) + { + oRet.push_back("NO"); + oRet.push_back("YES"); + 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")) + { + oRet.push_back(std::string(pszName).append("=")); + } + } + + return false; +} + /************************************************************************/ /* GDALAlgorithm::AddCreationOptionsArg() */ /************************************************************************/ @@ -2105,6 +2333,77 @@ 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; + + auto outputFormat = GetArg("format"); + if (outputFormat && outputFormat->GetType() == GAAT_STRING && + outputFormat->IsExplicitlySet()) + { + auto poDriver = GetGDALDriverManager()->GetDriverByName( + outputFormat->Get().c_str()); + if (poDriver) + { + AddCreationOptionsSuggestions(poDriver, oRet, currentWord); + } + return oRet; + } + + auto outputArg = GetArg(GDAL_ARG_NAME_OUTPUT); + 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; + const auto datasetType = datasetValue.GetType(); + 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, oRet, currentWord)) + { + 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