From 5963dfba129d45c7e0f2557a0e0bd07274908477 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 in.tif out.tif --co ZLEVEL= 1 10 11 12 2 3 4 5 6 7 8 9 ``` ``` $ 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= ``` ``` $ gdal raster reproject --dst-crs EPSG: ESRI: IAU_2015: IGNF: NKG: OGC: ``` ``` $ gdal raster reproject --dst-crs EP ==> $ gdal raster reproject --dst-crs EPSG: ``` ``` $ gdal raster reproject --dst-crs EPSG: 10150 -- MSL UK & Ireland VORF08 depth 10151 -- CD UK & Ireland VORF08 depth 10156 -- ETRS89 + MSL UK & Ireland VORF08 depth 10157 -- ETRS89 + CD UK & Ireland VORF08 depth [ ... snip ... ] ``` ``` $ gdal raster reproject --dst-crs EPSG:432 4322 -- WGS 72 4324 -- WGS 72BE 4326 -- WGS 84 4327 -- WGS 84 (geographic 3D) 4328 -- WGS 84 (geocentric) 4329 -- WGS 84 (3D) ``` 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 | 88 ++- apps/gdalalg_abstract_pipeline.h | 238 ++++++++ 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 | 305 ++++++++++ gcore/gdalalgorithm.cpp | 849 +++++++++++++++++++++++++-- gcore/gdalalgorithm.h | 62 +- scripts/completionFinder.py | 37 +- scripts/gdal-bash-completion.sh | 33 ++ scripts/setdevenv.sh | 5 + 13 files changed, 1579 insertions(+), 344 deletions(-) create mode 100644 apps/gdalalg_abstract_pipeline.h diff --git a/apps/gdal.cpp b/apps/gdal.cpp index 88ea2e4dadce..9811118f18c0 100644 --- a/apps/gdal.cpp +++ b/apps/gdal.cpp @@ -17,12 +17,96 @@ #include +// #define DEBUG_COMPLETION + +/************************************************************************/ +/* EmitCompletion() */ +/************************************************************************/ + +/** Return on stdout a space-separated list of choices for bash completion */ +static void EmitCompletion(std::unique_ptr rootAlg, + const std::vector &argsIn) +{ +#ifdef DEBUG_COMPLETION + for (size_t i = 0; i < argsIn.size(); ++i) + fprintf(stderr, "arg[%d]='%s'\n", static_cast(i), + argsIn[i].c_str()); +#endif + + std::vector args = argsIn; + + std::string ret; + const auto addSpace = [&ret]() + { + if (!ret.empty()) + ret += " "; + }; + + if (!args.empty() && + (args.back() == "--config" || + STARTS_WITH(args.back().c_str(), "--config=") || + (args.size() >= 2 && args[args.size() - 2] == "--config"))) + { + if (args.back() == "--config=" || args.back().back() != '=') + { + CPLStringList aosConfigOptions(CPLGetKnownConfigOptions()); + for (const char *pszOpt : cpl::Iterate(aosConfigOptions)) + { + addSpace(); + ret += pszOpt; + ret += '='; + } + printf("%s", ret.c_str()); + } + return; + } + + // Get inner-most algorithm + bool showAllOptions = true; + auto curAlg = std::move(rootAlg); + while (!args.empty() && !args.front().empty() && args.front()[0] != '-') + { + auto subAlg = curAlg->InstantiateSubAlgorithm(args.front()); + if (!subAlg) + break; + showAllOptions = false; + args.erase(args.begin()); + curAlg = std::move(subAlg); + } + + for (const auto &choice : curAlg->GetAutoComplete(args, showAllOptions)) + { + addSpace(); + ret += CPLString(choice).replaceAll(" ", "\\ "); + } + +#ifdef DEBUG_COMPLETION + fprintf(stderr, "ret = '%s'\n", ret.c_str()); +#endif + if (!ret.empty()) + printf("%s", ret.c_str()); +} + /************************************************************************/ /* main() */ /************************************************************************/ MAIN_START(argc, argv) { + auto alg = GDALGlobalAlgorithmRegistry::GetSingleton().Instantiate( + GDALGlobalAlgorithmRegistry::ROOT_ALG_NAME); + assert(alg); + + if (argc >= 3 && strcmp(argv[1], "completion") == 0) + { + GDALAllRegister(); + + // Process lines like "gdal completion gdal raster" + EmitCompletion(std::move(alg), + std::vector(argv + 3, argv + argc)); + return 0; + } + EarlySetConfigOptions(argc, argv); /* -------------------------------------------------------------------- */ @@ -31,14 +115,12 @@ MAIN_START(argc, argv) /* -------------------------------------------------------------------- */ GDALAllRegister(); + 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..680931b0a075 --- /dev/null +++ b/apps/gdalalg_abstract_pipeline.h @@ -0,0 +1,238 @@ +/****************************************************************************** + * + * 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 GetAutoComplete(std::vector &args, + bool /* showAllOptions*/) 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::GetAutoComplete() */ +/************************************************************************/ + +template +std::vector +GDALAbstractPipelineAlgorithm::GetAutoComplete( + std::vector &args, bool /* showAllOptions*/) +{ + std::vector ret; + if (args.size() <= 1) + { + ret.push_back("read"); + } + else if (args.back() == "!" || + (args.size() >= 2 && args[args.size() - 2] == "!")) + { + for (const std::string &name : m_stepRegistry.GetNames()) + { + if (name != "read") + { + ret.push_back(name); + } + } + } + else + { + std::string lastStep = "read"; + std::vector lastArgs; + for (size_t i = 1; i < args.size(); ++i) + { + lastArgs.push_back(args[i]); + if (i + 1 < args.size() && args[i] == "!") + { + ++i; + lastArgs.clear(); + lastStep = args[i]; + } + } + + auto curAlg = GetStepAlg(lastStep); + if (curAlg) + { + ret = + curAlg->GetAutoComplete(lastArgs, /* showAllOptions = */ false); + } + } + 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..6c7cf0546bae 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,306 @@ 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 gdal -").split(" ") + assert "--version" in out + + out = gdaltest.runexternal(f"{gdal_path} completion gdal").split(" ") + assert "convert" in out + assert "info" in out + assert "raster" in out + assert "vector" in out + + out = gdaltest.runexternal(f"{gdal_path} completion gdal raster").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 gdal raster info -").split(" ") + assert "-f" in out + assert "--of" in out + assert "--output-format" in out + + out = gdaltest.runexternal(f"{gdal_path} completion gdal raster info --of").split( + " " + ) + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal(f"{gdal_path} completion gdal raster info --of=").split( + " " + ) + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal(f"{gdal_path} completion gdal raster info --of=t").split( + " " + ) + assert "json" in out + assert "text" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of" + ).split(" ") + if gdal.GetDriverByName("GTiff"): + assert "GTiff" in out + if gdal.GetDriverByName("HFA"): + assert "HFA" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --input" + ).split(" ") + assert "data/" in out or "data\\" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --input data/" + ).split(" ") + assert "data/whiteblackred.tif" in out or "data\\whiteblackred.tif" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert /vsizip/../gcore/data/byte.tif.zip/" + ).split(" ") + assert out == ["/vsizip/../gcore/data/byte.tif.zip/byte.tif"] + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --resolution" + ) + assert ( + out + == "** description:\\ Target\\ resolution\\ (in\\ destination\\ CRS\\ units)" + ) + + +def test_gdal_completion_co(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" in out + assert "TILING_SCHEME=" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co=" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" in out + assert "TILING_SCHEME=" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co COMPRESS=" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co COMPRESS=NO" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co=COMPRESS=" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co=COMPRESS=NO" + ).split(" ") + assert "NONE" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co TILED=" + ).split(" ") + assert out == ["NO", "YES"] + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co ZLEVEL=" + ).split(" ") + assert "1" in out + assert "9" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of COG --co=" + ).split(" ") + assert "COMPRESS=" in out + assert "RPCTXT=" not in out + assert "TILING_SCHEME=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of=COG --creation-option ZOOM_LEVEL=" + ) + assert ( + out + == "## type:\\ int,\\ description:\\ Target\\ zoom\\ level.\\ Only\\ used\\ for\\ TILING_SCHEME\\ !=\\ CUSTOM" + ) + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of COG --co BLOCKSIZE=" + ) + assert out == "## validity\\ range:\\ >=\\ 128" + + if "JPEG_QUALITY" in gdal.GetDriverByName("GTiff").GetMetadataItem( + "DMD_CREATIONOPTIONLIST" + ): + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert in.tif out.tif --co JPEG_QUALITY=" + ) + assert out == "## validity\\ range:\\ [1,100]" + + if gdal.GetDriverByName("GPKG"): + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of GPKG --co=" + ).split(" ") + assert "APPEND_SUBDATASET=" in out + assert "VERSION=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal vector convert --of GPKG --co=" + ).split(" ") + assert "APPEND_SUBDATASET=" not in out + assert "VERSION=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --of GPKG --co BLOCKSIZE=" + ) + assert out == "## validity\\ range:\\ <=\\ 4096" + + +def test_gdal_completion_lco(gdal_path): + + if gdal.GetDriverByName("GPKG"): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal vector convert --of GPKG --lco" + ).split(" ") + assert "FID=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal vector convert in.shp out.gpkg --layer-creation-option=" + ).split(" ") + assert "FID=" in out + + +def test_gdal_completion_oo(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster info foo.tif --oo" + ).split(" ") + assert "COLOR_TABLE_MULTIPLIER=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster info --if GTiff --open-option" + ).split(" ") + assert "COLOR_TABLE_MULTIPLIER=" in out + + +def test_gdal_completion_dst_crs(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs" + ).split(" ") + assert "EPSG:" in out + assert "ESRI:" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs EPSG:" + ) + assert "4326\\ --\\ WGS\\ 84" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs=EPSG:" + ) + assert "4326\\ --\\ WGS\\ 84" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs EPSG:43" + ) + assert "4326\\ --\\ WGS\\ 84" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs=EPSG:43" + ) + assert "4326\\ --\\ WGS\\ 84" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster reproject --dst-crs EPSG:4326" + ) + assert out == "4326" + + +def test_gdal_completion_config(gdal_path): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --config" + ).split(" ") + assert "CPL_DEBUG=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --config=" + ).split(" ") + assert "CPL_DEBUG=" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --config FOO=" + ).split(" ") + assert out == [""] + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal raster convert --config=FOO=" + ).split(" ") + assert out == [""] + + +@pytest.mark.parametrize("subcommand", ["raster", "vector"]) +def test_gdal_completion_pipeline(gdal_path, subcommand): + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline" + ).split(" ") + assert out == ["read"] + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline re" + ).split(" ") + assert out == ["read"] + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read -" + ).split(" ") + assert "--input" in out + assert "--open-option" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read !" + ).split(" ") + assert "reproject" in out + assert "write" in out + assert "read" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read ! re" + ).split(" ") + assert "reproject" in out + assert "write" in out + assert "read" not in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read foo ! write -" + ).split(" ") + assert "--output" in out + assert "--co" in out + + if subcommand == "raster": + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read foo ! foo ! reproject -" + ).split(" ") + assert "--resampling" in out + + out = gdaltest.runexternal( + f"{gdal_path} completion gdal {subcommand} pipeline read foo ! reproject --resampling" + ).split(" ") + assert "near" in out diff --git a/gcore/gdalalgorithm.cpp b/gcore/gdalalgorithm.cpp index e86d90717c44..560f1fae63b9 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" @@ -28,6 +29,12 @@ #define _(x) (x) #endif +constexpr const char *GDAL_ARG_NAME_INPUT_FORMAT = "input-format"; + +constexpr const char *GDAL_ARG_NAME_OUTPUT_FORMAT = "output-format"; + +constexpr const char *GDAL_ARG_NAME_OPEN_OPTION = "open-option"; + //! @cond Doxygen_Suppress struct GDALAlgorithmArgHS { @@ -712,7 +719,7 @@ GDALInConstructionAlgorithmArg::SetIsCRSArg(bool noneAllowed) "SetIsCRSArg() can only be called on a String argument"); return *this; } - return AddValidationAction( + AddValidationAction( [this, noneAllowed]() { const std::string &osVal = @@ -731,6 +738,57 @@ GDALInConstructionAlgorithmArg::SetIsCRSArg(bool noneAllowed) } return true; }); + + SetAutoCompleteFunction( + [](const std::string ¤tValue) + { + std::vector oRet; + if (!currentValue.empty()) + { + const CPLStringList aosTokens( + CSLTokenizeString2(currentValue.c_str(), ":", 0)); + int nCount = 0; + OSRCRSInfo **pCRSList = OSRGetCRSInfoListFromDatabase( + aosTokens[0], nullptr, &nCount); + std::string osCode; + for (int i = 0; i < nCount; ++i) + { + if (aosTokens.size() == 1 || + STARTS_WITH(pCRSList[i]->pszCode, aosTokens[1])) + { + if (oRet.empty()) + osCode = pCRSList[i]->pszCode; + oRet.push_back(std::string(pCRSList[i]->pszCode) + .append(" -- ") + .append(pCRSList[i]->pszName)); + } + } + if (oRet.size() == 1) + { + // If there is a single match, remove the name from the suggestion. + oRet.clear(); + oRet.push_back(osCode); + } + OSRDestroyCRSInfoList(pCRSList); + } + if (oRet.empty()) + { + const CPLStringList aosAuthorities( + OSRGetAuthorityListFromDatabase()); + for (const char *pszAuth : cpl::Iterate(aosAuthorities)) + { + int nCount = 0; + OSRCRSInfo **pCRSList = OSRGetCRSInfoListFromDatabase( + pszAuth, nullptr, &nCount); + OSRDestroyCRSInfoList(pCRSList); + if (nCount) + oRet.push_back(std::string(pszAuth).append(":")); + } + } + return oRet; + }); + + return *this; } /************************************************************************/ @@ -761,6 +819,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 +1160,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 +1180,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 +1196,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 +1227,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", @@ -1171,6 +1253,47 @@ bool GDALAlgorithm::ParseCommandLineArguments( return true; } + const auto ProcessInConstructionValues = [&inConstructionValues]() + { + for (auto &[arg, value] : inConstructionValues) + { + if (arg->GetType() == GAAT_STRING_LIST) + { + if (!arg->Set(std::get>( + inConstructionValues[arg]))) + { + return false; + } + } + else if (arg->GetType() == GAAT_INTEGER_LIST) + { + if (!arg->Set( + std::get>(inConstructionValues[arg]))) + { + return false; + } + } + else if (arg->GetType() == GAAT_REAL_LIST) + { + if (!arg->Set(std::get>( + inConstructionValues[arg]))) + { + return false; + } + } + else if (arg->GetType() == GAAT_DATASET_LIST) + { + if (!arg->Set( + std::move(std::get>( + inConstructionValues[arg])))) + { + return false; + } + } + } + return true; + }; + size_t i = 0; size_t iCurPosArg = 0; while (i < lArgs.size() && iCurPosArg < m_positionalArgs.size()) @@ -1209,6 +1332,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( "varying number of values", otherArg->GetName().c_str(), arg->GetName().c_str()); + ProcessInConstructionValues(); return false; } nCountAtEnd += otherArg->GetMinCount(); @@ -1226,6 +1350,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( "values", otherArg->GetName().c_str(), arg->GetName().c_str()); + ProcessInConstructionValues(); return false; } nCountAtEnd++; @@ -1235,6 +1360,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( { ReportError(CE_Failure, CPLE_AppDefined, "Not enough positional values."); + ProcessInConstructionValues(); return false; } for (; i < lArgs.size() - nCountAtEnd; ++i) @@ -1242,6 +1368,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( if (!ParseArgument(arg, arg->GetName().c_str(), lArgs[i], inConstructionValues)) { + ProcessInConstructionValues(); return false; } } @@ -1253,6 +1380,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( if (!ParseArgument(arg, arg->GetName().c_str(), lArgs[i], inConstructionValues)) { + ProcessInConstructionValues(); return false; } } @@ -1279,7 +1407,10 @@ bool GDALAlgorithm::ParseCommandLineArguments( { if (!ParseArgument(arg, arg->GetName().c_str(), lArgs[i], inConstructionValues)) + { + ProcessInConstructionValues(); return false; + } } } ++iCurPosArg; @@ -1290,6 +1421,7 @@ bool GDALAlgorithm::ParseCommandLineArguments( ReportError(CE_Failure, CPLE_AppDefined, "Positional values starting at '%s' are not expected.", lArgs[i].c_str()); + ProcessInConstructionValues(); return false; } if (iCurPosArg < m_positionalArgs.size() && @@ -1308,47 +1440,13 @@ bool GDALAlgorithm::ParseCommandLineArguments( "Positional arguments starting at '%s' have not been " "specified.", m_positionalArgs[iCurPosArg]->GetMetaVar().c_str()); + ProcessInConstructionValues(); return false; } } - for (auto &[arg, value] : inConstructionValues) - { - if (arg->GetType() == GAAT_STRING_LIST) - { - if (!arg->Set(std::get>( - inConstructionValues[arg]))) - { - return false; - } - } - else if (arg->GetType() == GAAT_INTEGER_LIST) - { - if (!arg->Set( - std::get>(inConstructionValues[arg]))) - { - return false; - } - } - else if (arg->GetType() == GAAT_REAL_LIST) - { - if (!arg->Set( - std::get>(inConstructionValues[arg]))) - { - return false; - } - } - else if (arg->GetType() == GAAT_DATASET_LIST) - { - if (!arg->Set(std::move(std::get>( - inConstructionValues[arg])))) - { - return false; - } - } - } - - return m_skipValidationInParseCommandLine || ValidateArguments(); + return ProcessInConstructionValues() && + (m_skipValidationInParseCommandLine || ValidateArguments()); } /************************************************************************/ @@ -1426,12 +1524,12 @@ bool GDALAlgorithm::ProcessDatasetArg(GDALAlgorithmArg *arg, CPLStringList aosAllowedDrivers; if (arg->GetName() == GDAL_ARG_NAME_INPUT) { - const auto ooArg = GetArg("open-option"); + const auto ooArg = GetArg(GDAL_ARG_NAME_OPEN_OPTION); if (ooArg && ooArg->GetType() == GAAT_STRING_LIST) aosOpenOptions = CPLStringList(ooArg->Get>()); - const auto ifArg = GetArg("input-format"); + const auto ifArg = GetArg(GDAL_ARG_NAME_INPUT_FORMAT); if (ifArg && ifArg->GetType() == GAAT_STRING_LIST) aosAllowedDrivers = CPLStringList(ifArg->Get>()); @@ -1599,14 +1697,14 @@ bool GDALAlgorithm::ValidateArguments() CPLStringList aosAllowedDrivers; if (arg->GetName() == GDAL_ARG_NAME_INPUT) { - const auto ooArg = GetArg("open-option"); + const auto ooArg = GetArg(GDAL_ARG_NAME_OPEN_OPTION); if (ooArg && ooArg->GetType() == GAAT_STRING_LIST) { aosOpenOptions = CPLStringList( ooArg->Get>()); } - const auto ifArg = GetArg("input-format"); + const auto ifArg = GetArg(GDAL_ARG_NAME_INPUT_FORMAT); if (ifArg && ifArg->GetType() == GAAT_STRING_LIST) { aosAllowedDrivers = CPLStringList( @@ -1639,6 +1737,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 +1968,77 @@ GDALAlgorithm::AddInputDatasetArg(GDALArgDatasetValue *pValue, pValue, type); if (positionalAndRequired) arg.SetPositional().SetRequired(); + + arg.SetAutoCompleteFunction( + [type](const std::string ¤tValue) + { + 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(currentValue.c_str()); + auto psDir = VSIOpenDir(osDir.c_str(), 0, nullptr); + const std::string osSep = VSIGetDirectorySeparator(osDir.c_str()); + if (currentValue.empty()) + osDir.clear(); + const std::string currentFilename = + CPLGetFilename(currentValue.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; } @@ -1912,6 +2104,124 @@ GDALInConstructionAlgorithmArg &GDALAlgorithm::AddUpdateArg(bool *pValue) .SetDefault(false); } +/************************************************************************/ +/* AddOptionsSuggestions() */ +/************************************************************************/ + +static bool AddOptionsSuggestions(const char *pszXML, int datasetType, + const std::string ¤tValue, + std::vector &oRet) +{ + if (!pszXML) + return false; + CPLXMLTreeCloser poTree(CPLParseXMLString(pszXML)); + if (!poTree) + return false; + const CPLXMLNode *psRoot = + CPLGetXMLNode(poTree.get(), "=CreationOptionList"); + if (!psRoot) + psRoot = CPLGetXMLNode(poTree.get(), "=LayerCreationOptionList"); + if (!psRoot) + psRoot = CPLGetXMLNode(poTree.get(), "=OpenOptionList"); + if (!psRoot) + return false; + + for (const CPLXMLNode *psChild = psRoot->psChild; psChild; + psChild = psChild->psNext) + { + const char *pszName = CPLGetXMLValue(psChild, "name", nullptr); + if (pszName && currentValue == pszName && + EQUAL(psChild->pszValue, "Option")) + { + const char *pszType = CPLGetXMLValue(psChild, "type", ""); + const char *pszMin = CPLGetXMLValue(psChild, "min", nullptr); + const char *pszMax = CPLGetXMLValue(psChild, "max", nullptr); + 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")) + { + 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)); + } + } + + if (oRet.empty()) + { + if (pszMin && pszMax) + { + oRet.push_back(std::string("##")); + oRet.push_back(std::string("validity range: [") + .append(pszMin) + .append(",") + .append(pszMax) + .append("]")); + } + else if (pszMin) + { + oRet.push_back(std::string("##")); + oRet.push_back( + std::string("validity range: >= ").append(pszMin)); + } + else if (pszMax) + { + oRet.push_back(std::string("##")); + oRet.push_back( + std::string("validity range: <= ").append(pszMax)); + } + else if (const char *pszDescription = + CPLGetXMLValue(psChild, "description", nullptr)) + { + oRet.push_back(std::string("##")); + oRet.push_back(std::string("type: ") + .append(pszType) + .append(", description: ") + .append(pszDescription)); + } + } + + 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::AddOpenOptionsArg() */ /************************************************************************/ @@ -1919,10 +2229,99 @@ GDALInConstructionAlgorithmArg &GDALAlgorithm::AddUpdateArg(bool *pValue) GDALInConstructionAlgorithmArg & GDALAlgorithm::AddOpenOptionsArg(std::vector *pValue) { - return AddArg("open-option", 0, _("Open options"), pValue) - .AddAlias("oo") - .SetMetaVar("KEY=VALUE") - .SetCategory(GAAC_ADVANCED); + auto &arg = AddArg(GDAL_ARG_NAME_OPEN_OPTION, 0, _("Open options"), pValue) + .AddAlias("oo") + .SetMetaVar("KEY=VALUE") + .SetCategory(GAAC_ADVANCED); + + arg.SetAutoCompleteFunction( + [this](const std::string ¤tValue) + { + std::vector oRet; + + int datasetType = + GDAL_OF_RASTER | GDAL_OF_VECTOR | GDAL_OF_MULTIDIM_RASTER; + auto inputArg = GetArg(GDAL_ARG_NAME_INPUT); + if (inputArg && inputArg->GetType() == GAAT_DATASET) + { + auto &datasetValue = inputArg->Get(); + datasetType = datasetValue.GetType(); + } + + auto inputFormat = GetArg(GDAL_ARG_NAME_INPUT_FORMAT); + if (inputFormat && inputFormat->GetType() == GAAT_STRING_LIST && + inputFormat->IsExplicitlySet()) + { + const auto &aosAllowedDrivers = + inputFormat->Get>(); + if (aosAllowedDrivers.size() == 1) + { + auto poDriver = GetGDALDriverManager()->GetDriverByName( + aosAllowedDrivers[0].c_str()); + if (poDriver) + { + AddOptionsSuggestions( + poDriver->GetMetadataItem(GDAL_DMD_OPENOPTIONLIST), + datasetType, currentValue, oRet); + } + return oRet; + } + } + + if (inputArg && inputArg->GetType() == GAAT_DATASET) + { + auto poDM = GetGDALDriverManager(); + auto &datasetValue = inputArg->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 (AddOptionsSuggestions( + poDriver->GetMetadataItem( + GDAL_DMD_OPENOPTIONLIST), + datasetType, currentValue, + oRet)) + { + return oRet; + } + break; + } + } + } + } + } + } + } + + return oRet; + }); + + return arg; } /************************************************************************/ @@ -1996,6 +2395,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() */ /************************************************************************/ @@ -2003,10 +2447,13 @@ bool GDALAlgorithm::ValidateFormat(const GDALAlgorithmArg &arg) const GDALInConstructionAlgorithmArg & GDALAlgorithm::AddInputFormatsArg(std::vector *pValue) { - auto &arg = AddArg("input-format", 0, _("Input formats"), pValue) - .AddAlias("if") - .SetCategory(GAAC_ADVANCED); + auto &arg = + AddArg(GDAL_ARG_NAME_INPUT_FORMAT, 0, _("Input formats"), pValue) + .AddAlias("if") + .SetCategory(GAAC_ADVANCED); arg.AddValidationAction([this, &arg]() { return ValidateFormat(arg); }); + arg.SetAutoCompleteFunction([&arg](const std::string &) + { return FormatAutoCompleteFunction(arg); }); return arg; } @@ -2017,10 +2464,13 @@ GDALAlgorithm::AddInputFormatsArg(std::vector *pValue) GDALInConstructionAlgorithmArg & GDALAlgorithm::AddOutputFormatArg(std::string *pValue) { - auto &arg = AddArg("output-format", 'f', _("Output format"), pValue) - .AddAlias("of") - .AddAlias("format"); + auto &arg = + AddArg(GDAL_ARG_NAME_OUTPUT_FORMAT, 'f', _("Output format"), pValue) + .AddAlias("of") + .AddAlias("format"); arg.AddValidationAction([this, &arg]() { return ValidateFormat(arg); }); + arg.SetAutoCompleteFunction([&arg](const std::string &) + { return FormatAutoCompleteFunction(arg); }); return arg; } @@ -2105,6 +2555,89 @@ GDALAlgorithm::AddCreationOptionsArg(std::vector *pValue) .AddAlias("co") .SetMetaVar("="); arg.AddValidationAction([this, &arg]() { return ValidateKeyValue(arg); }); + + arg.SetAutoCompleteFunction( + [this](const std::string ¤tValue) + { + 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(GDAL_ARG_NAME_OUTPUT_FORMAT); + if (outputFormat && outputFormat->GetType() == GAAT_STRING && + outputFormat->IsExplicitlySet()) + { + auto poDriver = GetGDALDriverManager()->GetDriverByName( + outputFormat->Get().c_str()); + if (poDriver) + { + AddOptionsSuggestions( + poDriver->GetMetadataItem(GDAL_DMD_CREATIONOPTIONLIST), + datasetType, currentValue, 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 (AddOptionsSuggestions( + poDriver->GetMetadataItem( + GDAL_DMD_CREATIONOPTIONLIST), + datasetType, currentValue, + oRet)) + { + return oRet; + } + break; + } + } + } + } + } + } + } + + return oRet; + }); + return arg; } @@ -2120,6 +2653,75 @@ GDALAlgorithm::AddLayerCreationOptionsArg(std::vector *pValue) .AddAlias("lco") .SetMetaVar("="); arg.AddValidationAction([this, &arg]() { return ValidateKeyValue(arg); }); + + arg.SetAutoCompleteFunction( + [this](const std::string ¤tValue) + { + std::vector oRet; + + auto outputFormat = GetArg(GDAL_ARG_NAME_OUTPUT_FORMAT); + if (outputFormat && outputFormat->GetType() == GAAT_STRING && + outputFormat->IsExplicitlySet()) + { + auto poDriver = GetGDALDriverManager()->GetDriverByName( + outputFormat->Get().c_str()); + if (poDriver) + { + AddOptionsSuggestions(poDriver->GetMetadataItem( + GDAL_DS_LAYER_CREATIONOPTIONLIST), + GDAL_OF_VECTOR, currentValue, oRet); + } + 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; + for (int i = 0; i < poDM->GetDriverCount(); ++i) + { + auto poDriver = poDM->GetDriver(i); + if (poDriver->GetMetadataItem(GDAL_DCAP_VECTOR)) + { + 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 (AddOptionsSuggestions( + poDriver->GetMetadataItem( + GDAL_DS_LAYER_CREATIONOPTIONLIST), + GDAL_OF_VECTOR, currentValue, + oRet)) + { + return oRet; + } + break; + } + } + } + } + } + } + } + + return oRet; + }); + return arg; } @@ -2796,6 +3398,149 @@ std::string GDALAlgorithm::GetUsageAsJSON() const return oDoc.SaveAsString(); } +/************************************************************************/ +/* GDALAlgorithm::GetAutoComplete() */ +/************************************************************************/ + +std::vector +GDALAlgorithm::GetAutoComplete(std::vector &args, + bool showAllOptions) +{ + std::vector ret; + + std::string option; + std::string value; + ExtractLastOptionAndValue(args, option, value); + + if (option.empty() && !args.empty() && !args.back().empty() && + args.back()[0] == '-') + { + // List available options + for (const auto &arg : GetArgs()) + { + if (arg->IsHiddenForCLI() || + (!showAllOptions && + (arg->GetName() == "help" || arg->GetName() == "drivers" || + arg->GetName() == "config" || 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())); + } + } + } + else if (!option.empty()) + { + // List possible choices for current option + auto arg = GetArg(option); + if (arg && arg->GetType() != GAAT_BOOLEAN) + { + ret = arg->GetChoices(); + if (ret.empty()) + { + { + CPLErrorStateBackuper oErrorQuieter(CPLQuietErrorHandler); + SetParseForAutoCompletion(); + CPL_IGNORE_RET_VAL(ParseCommandLineArguments(args)); + } + ret = arg->GetAutoCompleteChoices(value); + } + if (ret.empty()) + { + ret.push_back("**"); + ret.push_back( + std::string("description: ").append(arg->GetDescription())); + } + } + } + else if (!args.empty() && STARTS_WITH(args.back().c_str(), "/vsi")) + { + auto arg = GetArg("input"); + if (arg) + { + ret = arg->GetAutoCompleteChoices(args.back()); + } + } + else + { + // List possible sub-algorithms + ret = GetSubAlgorithmNames(); + } + + return ret; +} + +/************************************************************************/ +/* GDALAlgorithm::ExtractLastOptionAndValue() */ +/************************************************************************/ + +void GDALAlgorithm::ExtractLastOptionAndValue(std::vector &args, + std::string &option, + std::string &value) const +{ + if (!args.empty() && !args.back().empty() && args.back()[0] == '-') + { + const auto nPosEqual = args.back().find('='); + if (nPosEqual == std::string::npos) + { + // Deal with "gdal ... --option" + if (GetArg(args.back())) + { + option = args.back(); + args.pop_back(); + } + } + else + { + // Deal with "gdal ... --option=" + if (GetArg(args.back().substr(0, nPosEqual))) + { + option = args.back().substr(0, nPosEqual); + value = args.back().substr(nPosEqual + 1); + args.pop_back(); + } + } + } + else if (args.size() >= 2 && !args[args.size() - 2].empty() && + args[args.size() - 2][0] == '-') + { + auto arg = GetArg(args[args.size() - 2]); + if (arg && arg->GetType() != GAAT_BOOLEAN) + { + option = args[args.size() - 2]; + value = args.back(); + args.pop_back(); + } + } + + const auto IsKeyValueOption = [](const std::string &osStr) + { + return osStr == "--co" || osStr == "--creation-option" || + osStr == "--lco" || osStr == "--layer-creation-option" || + osStr == "--oo" || osStr == "--open-option"; + }; + + if (IsKeyValueOption(option)) + { + const auto nPosEqual = value.find('='); + if (nPosEqual != std::string::npos) + { + value.resize(nPosEqual); + } + } +} + /************************************************************************/ /* GDALAlgorithmRelease() */ /************************************************************************/ diff --git a/gcore/gdalalgorithm.h b/gcore/gdalalgorithm.h index cb7c50e782fb..25d0c8815f0f 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 ¤tValue) const + { + if (m_autoCompleteFunction) + return m_autoCompleteFunction(currentValue); + 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,10 @@ class CPL_DLL GDALAlgorithmRegistry return false; } + /** Return auto completion suggestions */ + virtual std::vector + GetAutoComplete(std::vector &args, bool showAllOptions); + protected: friend class GDALInConstructionAlgorithmArg; @@ -2107,12 +2140,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); @@ -2129,6 +2166,13 @@ class CPL_DLL GDALAlgorithmRegistry virtual bool RunImpl(GDALProgressFunc pfnProgress, void *pProgressData) = 0; + /** Extract the last option and its potential value from the provided + * argument list, and remove them from the list. + */ + void ExtractLastOptionAndValue(std::vector &args, + std::string &option, + std::string &value) const; + GDALAlgorithm(const GDALAlgorithm &) = delete; GDALAlgorithm &operator=(const GDALAlgorithm &) = delete; }; diff --git a/scripts/completionFinder.py b/scripts/completionFinder.py index cecf0f04f32e..bde75eb1a76c 100644 --- a/scripts/completionFinder.py +++ b/scripts/completionFinder.py @@ -183,7 +183,9 @@ def getCompletionScript(name, optList): output.append(" esac\n") else: # ogr type - formatParsingCmd = "$tool --formats | tail -n +2 | grep -o -E '\"[^\"]+\"' | sed 's/\ /__/'" # noqa: W605 + formatParsingCmd = ( + "$tool --formats | tail -n +2 | grep -o -E '\"[^\"]+\"' | sed 's/\\ /__/'" + ) if "-f" in optList: # replace ogrtindex by ogr2ogr to check --formats output.append(" tool=${COMP_WORDS[0]/ogrtindex/ogr2ogr}\n") @@ -275,6 +277,39 @@ def main(argv): # Checks that bash-completion is recent enough function_exists _get_comp_words_by_ref || return 0 +_gdal() +{ + local cur prev + COMPREPLY=() + _get_comp_words_by_ref cur prev + choices=$(gdal completion ${COMP_LINE}) + if [[ "$cur" == "=" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$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 + elif [[ $element == *: ]]; then + # Do not add a space if one of the suggestion ends with colon + 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..d34c31fe7332 100644 --- a/scripts/gdal-bash-completion.sh +++ b/scripts/gdal-bash-completion.sh @@ -9,6 +9,39 @@ function_exists() { # Checks that bash-completion is recent enough function_exists _get_comp_words_by_ref || return 0 +_gdal() +{ + local cur prev + COMPREPLY=() + _get_comp_words_by_ref cur prev + choices=$(gdal completion ${COMP_LINE}) + if [[ "$cur" == "=" ]]; then + mapfile -t COMPREPLY < <(compgen -W "$choices" --) + elif [[ "$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 + elif [[ $element == *: ]]; then + # Do not add a space if one of the suggestion ends with colon + 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