diff --git a/CMakeLists.txt b/CMakeLists.txt index f6b06899..8b0a4384 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -70,6 +70,7 @@ set(GZ_MSGS_VER ${gz-msgs9_VERSION_MAJOR}) #-------------------------------------- # Find gz-tools find_program(HAVE_GZ_TOOLS gz) +set (GZ_TOOLS_VER 2) #============================================================================ # Configure the build diff --git a/Changelog.md b/Changelog.md index b60061a9..be5fc072 100644 --- a/Changelog.md +++ b/Changelog.md @@ -71,6 +71,20 @@ ## Gazebo Fuel Tools 7.x +### Gazebo Fuel Tools 7.3.0 (2023-06-13) + +1. Forward merges + * [Pull request #355](https://github.com/gazebosim/gz-fuel-tools/pull/355) + +1. Add bash completion + * [Pull request #329](https://github.com/gazebosim/gz-fuel-tools/pull/329) + +1. Reflect pagination of RESTful requests in iterator API + * [Pull request #350](https://github.com/gazebosim/gz-fuel-tools/pull/350) + +1. Support link referral download + * [Pull request #333](https://github.com/gazebosim/gz-fuel-tools/pull/333) + ### Gazebo Fuel Tools 7.2.2 (2023-03-29) 1. Support link referral download @@ -289,6 +303,19 @@ ### Gazebo Fuel Tools 4.8.3 (2023-03-29) +1. Support link referral download + * [Pull request #333](https://github.com/gazebosim/gz-fuel-tools/pull/333) + +### Gazebo Fuel Tools 4.9.0 (2023-05-03) + +1. Add bash completion + * [Pull request #329](https://github.com/gazebosim/gz-fuel-tools/pull/329) + +1. Reflect pagination of RESTful requests in iterator API + * [Pull request #350](https://github.com/gazebosim/gz-fuel-tools/pull/350) + +### Gazebo Fuel Tools 4.8.3 (2023-03-29) + 1. Support link referral download * [Pull request #333](https://github.com/gazebosim/gz-fuel-tools/pull/333) diff --git a/include/gz/fuel_tools/FuelClient.hh b/include/gz/fuel_tools/FuelClient.hh index c8e5f6a8..d148de47 100644 --- a/include/gz/fuel_tools/FuelClient.hh +++ b/include/gz/fuel_tools/FuelClient.hh @@ -136,25 +136,46 @@ namespace gz /// \brief Returns models matching a given identifying criteria /// \param[in] _id a partially filled out identifier used to fetch models /// \remarks Fulfills Get-One requirement - /// \remarks It's not yet clear if model names are unique, so this API + /// \remarks Model names are unique to the owner, so this API /// allows the possibility of getting multiple models with the - /// same name. + /// same name if the owner is not specified. /// \return An iterator of models with names matching the criteria public: ModelIter Models(const ModelIdentifier &_id); /// \brief Returns models matching a given identifying criteria /// \param[in] _id a partially filled out identifier used to fetch models /// \remarks Fulfills Get-One requirement - /// \remarks It's not yet clear if model names are unique, so this API + /// \remarks Model names are unique to the owner, so this API /// allows the possibility of getting multiple models with the - /// same name. + /// same name if the owner is not specified. /// \return An iterator of models with names matching the criteria public: ModelIter Models(const ModelIdentifier &_id) const; - /// \brief Returns an iterator for the models found in a collection. - /// \param[in] _id a partially filled out identifier used to fetch a - /// collection. - /// \return An iterator of models in the collection. + /// \brief Returns models matching a given identifying criteria + /// \param[in] _id a partially filled out identifier used to fetch models + /// \param[in] _checkCache Whether to check the cache. + /// \remarks Fulfills Get-One requirement + /// \remarks Model names are unique to the owner, so this API + /// allows the possibility of getting multiple models with the + /// same name if the owner is not specified. + /// \return An iterator of models with names matching the criteria + public: ModelIter Models(const ModelIdentifier &_id, bool _checkCache); + + /// \brief Returns models matching a given identifying criteria + /// \param[in] _id a partially filled out identifier used to fetch models + /// \param[in] _checkCache Whether to check the cache. + /// \remarks Fulfills Get-One requirement + /// \remarks Model names are unique to the owner, so this API + /// allows the possibility of getting multiple models with the + /// same name if the owner is not specified. + /// \return An iterator of models with names matching the criteria + public: ModelIter Models(const ModelIdentifier &_id, + bool _checkCache) const; + + /// \brief Returns an iterator for the models found in a collection. + /// \param[in] _id a partially filled out identifier used to fetch a + /// collection. + /// \return An iterator of models in the collection. public: ModelIter Models(const CollectionIdentifier &_id) const; /// \brief Returns worlds matching a given identifying criteria diff --git a/src/FuelClient.cc b/src/FuelClient.cc index a30feaaa..ea2b2fb3 100644 --- a/src/FuelClient.cc +++ b/src/FuelClient.cc @@ -399,16 +399,31 @@ WorldIter FuelClient::Worlds(const ServerConfig &_server) const ////////////////////////////////////////////////// ModelIter FuelClient::Models(const ModelIdentifier &_id) { - return const_cast(this)->Models(_id); + return this->Models(_id, true); } ////////////////////////////////////////////////// ModelIter FuelClient::Models(const ModelIdentifier &_id) const { - // Check local cache first - ModelIter localIter = this->dataPtr->cache->MatchingModels(_id); - if (localIter) - return localIter; + return this->Models(_id, true); +} + +////////////////////////////////////////////////// +ModelIter FuelClient::Models(const ModelIdentifier &_id, bool _checkCache) +{ + return const_cast(this)->Models(_id, _checkCache); +} + +////////////////////////////////////////////////// +ModelIter FuelClient::Models(const ModelIdentifier &_id, bool _checkCache) const +{ + if (_checkCache) + { + // Check local cache first + ModelIter localIter = this->dataPtr->cache->MatchingModels(_id); + if (localIter) + return localIter; + } // TODO(nkoenig) try to fetch model directly from a server // Note: gz-fuel-server doesn't like URLs ending in / @@ -419,8 +434,7 @@ ModelIter FuelClient::Models(const ModelIdentifier &_id) const path = path / _id.Owner() / "models"; if (path.Str().empty()) - // cppcheck-suppress identicalConditionAfterEarlyExit - return localIter; + return ModelIterFactory::Create(); gzmsg << _id.UniqueName() << " not found in cache, attempting download\n"; diff --git a/src/FuelClient_TEST.cc b/src/FuelClient_TEST.cc index ade73331..ce4c462e 100644 --- a/src/FuelClient_TEST.cc +++ b/src/FuelClient_TEST.cc @@ -1366,6 +1366,52 @@ TEST_F(FuelClientTest, Models) } } +////////////////////////////////////////////////// +TEST_F(FuelClientTest, ModelsCheckCached) +{ + ClientConfig config; + std::string cacheDir = common::joinPaths(common::cwd(), "test_cache"); + common::removeAll(cacheDir ); + ASSERT_TRUE(common::createDirectories(cacheDir)); + config.SetCacheLocation(cacheDir); + FuelClient client(config); + ServerConfig serverConfig; + ModelIdentifier modelId; + modelId.SetOwner("openroboticstest"); + std::vector modelInfos; + { + for (ModelIter iter = client.Models(modelId, true); iter; ++iter) + { + modelInfos.push_back(*iter); + } + } + ASSERT_FALSE(modelInfos.empty()); + // Download one of the models to test the behavior of Models() with + // different values for _checkCache + EXPECT_TRUE(client.DownloadModel(modelInfos.front().Identification())); + { + std::size_t counter = 0; + for (ModelIter iter = client.Models(modelId, true); iter; ++iter, ++counter) + { + } + std::cout << "counter: " << counter << std::endl; + // Expect only one result with checkCache=true because we've downloaded only + // one model + EXPECT_EQ(counter, 1u); + EXPECT_GT(modelInfos.size(), counter); + } + + { + std::size_t counter = 0; + for (ModelIter iter = client.Models(modelId, false); iter; + ++iter, ++counter) + { + } + std::cout << "counter: " << counter << std::endl; + EXPECT_EQ(counter, modelInfos.size()); + } +} + ///////////////////////////////////////////////// // Protocol "https" not supported or disabled in libcurl for Windows // https://github.com/gazebosim/gz-fuel-tools/issues/105 diff --git a/src/ModelIter.cc b/src/ModelIter.cc index 8829899b..c9db28b3 100644 --- a/src/ModelIter.cc +++ b/src/ModelIter.cc @@ -148,65 +148,55 @@ IterRestIds::~IterRestIds() { } +std::vector IterRestIds::ParseIdsFromResponse( + const RestResponse &resp) +{ + if (resp.data == "null\n" || resp.statusCode != 200) + return {}; + + // Parse the response. + return JSONParser::ParseModels(resp.data, this->config); +} + ////////////////////////////////////////////////// IterRestIds::IterRestIds(const Rest &_rest, const ServerConfig &_config, const std::string &_api) - : config(_config), rest(_rest) + : config(_config), rest(_rest), api(_api) +{ + this->idIter = this->ids.begin(); + this->Next(); +} + +////////////////////////////////////////////////// +RestResponse IterRestIds::MakeRestRequest(std::size_t _page) { HttpMethod method = HttpMethod::GET; - this->config = _config; - int page = 1; std::vector headers = {"Accept: application/json"}; - RestResponse resp; std::vector modelIds; - this->ids.clear(); - - do - { - // Prepare the request with the next page. - std::string queryStrPage = "page=" + std::to_string(page); - std::string path = _api; - ++page; - - // Fire the request. - resp = this->rest.Request(method, this->config.Url().Str(), + // Prepare the request with the requested page. + std::string queryStrPage = "page=" + std::to_string(_page); + std::string path = this->api; + // Fire the request. + return this->rest.Request(method, this->config.Url().Str(), this->config.Version(), std::regex_replace(path, std::regex(R"(\\)"), "/"), {queryStrPage}, headers, ""); - - // TODO(nkoenig): resp.statusCode should return != 200 when the page - // requested does - // not exist. When this happens we should stop without calling ParseModels() - if (resp.data == "null\n" || resp.statusCode != 200) - break; - - // Parse the response. - modelIds = JSONParser::ParseModels(resp.data, this->config); - - // Add the vector of models to the list. - this->ids.insert(std::end(this->ids), std::begin(modelIds), - std::end(modelIds)); - } while (!modelIds.empty()); - - if (this->ids.empty()) - return; - - this->idIter = this->ids.begin(); - - // make first model - std::shared_ptr ptr(new ModelPrivate); - ptr->id = *(this->idIter); - ptr->id.SetServer(this->config); - this->model = Model(ptr); - - gzdbg << "Got response [" << resp.data << "]\n"; } ////////////////////////////////////////////////// void IterRestIds::Next() { // advance pointer - ++(this->idIter); + if (this->idIter != this->ids.end()) + ++(this->idIter); + + if (this->idIter == this->ids.end()) + { + ++this->currentPage; + RestResponse resp = this->MakeRestRequest(this->currentPage); + this->ids = this->ParseIdsFromResponse(resp); + this->idIter = this->ids.begin(); + } // Update personal model class if (this->idIter != this->ids.end()) @@ -216,7 +206,6 @@ void IterRestIds::Next() ptr->id.SetServer(this->config); this->model = Model(ptr); } - // TODO(nkoenig) request next page if api is paginated } ////////////////////////////////////////////////// @@ -257,7 +246,6 @@ ModelIter::operator bool() const ////////////////////////////////////////////////// ModelIter &ModelIter::operator++() { - // TODO(nkoenig) Request more data if there are more pages if (!this->dataPtr->HasReachedEnd()) { this->dataPtr->Next(); diff --git a/src/ModelIterPrivate.hh b/src/ModelIterPrivate.hh index 0b58d933..0f6787f0 100644 --- a/src/ModelIterPrivate.hh +++ b/src/ModelIterPrivate.hh @@ -130,7 +130,7 @@ namespace gz }; /// \brief class for iterating through model ids from a rest API - class GZ_FUEL_TOOLS_VISIBLE IterRestIds: public ModelIterPrivate + class GZ_FUEL_TOOLS_HIDDEN IterRestIds: public ModelIterPrivate { /// \brief constructor public: IterRestIds(const Rest &_rest, @@ -153,11 +153,29 @@ namespace gz /// \brief RESTful client public: Rest rest; + /// \brief The API (path) of the RESTful requests + public: const std::string api; + + /// \brief Make a RESTful request for the given page + /// \param[in] _page Page number to request + /// \return Response from the request + protected: RestResponse MakeRestRequest(std::size_t _page); + + /// \brief Parse model identifiers from a RESTful response + /// \param[in] _resp RESTful response + /// \return A vector of identifiers extracted from the response. + protected: std::vector ParseIdsFromResponse( + const RestResponse &_resp); + /// \brief Model identifiers in the current page protected: std::vector ids; /// \brief Where the current iterator is in the list of ids protected: std::vector::iterator idIter; + + /// \brief Keep track of page number for pagination of response data from + /// server. + protected: std::size_t currentPage{0}; }; } } diff --git a/src/Zip.cc b/src/Zip.cc index c4d88da6..553c626f 100644 --- a/src/Zip.cc +++ b/src/Zip.cc @@ -37,7 +37,7 @@ bool CompressFile(zip *_archive, const std::string &_file, { if (gz::common::isDirectory(_file)) { - if (zip_add_dir(_archive, _entry.c_str()) < 0) + if (zip_dir_add(_archive, _entry.c_str(), 0) < 0) { gzerr << "Error adding directory to zip: " << _file << std::endl; return false; @@ -69,7 +69,7 @@ bool CompressFile(zip *_archive, const std::string &_file, return false; } - if (zip_add(_archive, _entry.c_str(), source) + if (zip_file_add(_archive, _entry.c_str(), source, 0) < 0) { gzerr << "Error adding file to zip: " << _file << std::endl; diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index c546769a..4e34bbd5 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -1,17 +1,60 @@ -# Generate a the ruby script. +#=============================================================================== +# Generate the ruby script for internal testing. # Note that the major version of the library is included in the name. # Ex: cmdfuel0.rb -if (APPLE) - set(GZ_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.dylib) -elseif(MSVC) - set(GZ_LIBRARY_NAME ${PROJECT_NAME_LOWER}.dll) -else() - set(GZ_LIBRARY_NAME lib${PROJECT_NAME_LOWER}.so) -endif() +# Unlike other gz libraries, the ruby script for the fuel_tools library is called cmdfuel.rb instead of cmdfuel_tools.rb +set(CMD_NAME cmdfuel) +set(cmd_script_generated_test "${CMAKE_BINARY_DIR}/test/lib/$/ruby/ignition/${CMD_NAME}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured_test "${CMAKE_CURRENT_BINARY_DIR}/test_${CMD_NAME}${PROJECT_VERSION_MAJOR}.rb.configured") + +# Set the library_location variable to the full path of the library file within +# the build directory. +set(library_location "$") + +configure_file( + "${CMD_NAME}.rb.in" + "${cmd_script_configured_test}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated_test}" + INPUT "${cmd_script_configured_test}") + + +#=============================================================================== +# Used for the installed version. +# Generate the ruby script that gets installed. +# Note that the major version of the library is included in the name. +# Ex: cmdfuel0.rb +set(cmd_script_generated "${CMAKE_CURRENT_BINARY_DIR}/$/${CMD_NAME}${PROJECT_VERSION_MAJOR}.rb") +set(cmd_script_configured "${CMAKE_CURRENT_BINARY_DIR}/${CMD_NAME}${PROJECT_VERSION_MAJOR}.rb.configured") + +# Set the library_location variable to the relative path to the library file +# within the install directory structure. +set(library_location "../../../${CMAKE_INSTALL_LIBDIR}/$") configure_file( - "cmdfuel.rb.in" - "${CMAKE_CURRENT_BINARY_DIR}/cmdfuel${PROJECT_VERSION_MAJOR}.rb" @ONLY) + "${CMD_NAME}.rb.in" + "${cmd_script_configured}" + @ONLY) + +file(GENERATE + OUTPUT "${cmd_script_generated}" + INPUT "${cmd_script_configured}") # Install the ruby command line library in an unversioned location. -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/cmdfuel${PROJECT_VERSION_MAJOR}.rb DESTINATION lib/ruby/gz) +install(FILES ${cmd_script_generated} DESTINATION lib/ruby/gz) + + +#=============================================================================== +# Bash completion + +# Tack version onto and install the bash completion script +configure_file( + "fuel.bash_completion.sh" + "${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.bash_completion.sh" @ONLY) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/fuel${PROJECT_VERSION_MAJOR}.bash_completion.sh + DESTINATION + ${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/gz/gz${GZ_TOOLS_VER}.completion.d) diff --git a/src/cmd/cmdfuel.rb.in b/src/cmd/cmdfuel.rb.in index 283c7245..632d9443 100755 --- a/src/cmd/cmdfuel.rb.in +++ b/src/cmd/cmdfuel.rb.in @@ -28,7 +28,7 @@ end require 'optparse' # Constants. -LIBRARY_NAME = '@GZ_LIBRARY_NAME@' +LIBRARY_NAME = '@library_location@' LIBRARY_VERSION = '@PROJECT_VERSION_FULL@' MAX_PARALLEL_JOBS = 16 @@ -45,10 +45,10 @@ COMMON_OPTIONS = " The following information is in regards to user authentication via \n"\ " the --header command line option. \n"\ " \n"\ - " Two types of credentials are supported on Gazebo Fuel, Private \n"\ + " Two types of credentials are supported on Gazebo Fuel, Private \n"\ " Token and JSON Web Token(JWT). The Private Token method is prefered. \n"\ " Private tokens can be created through your user settings on \n"\ - " https://app.gazebosim.org. Example usage: \n"\ + " https://app.gazebosim.org. Example usage: \n"\ " 1. Private token method: \n"\ " --header 'Private-Token: ' \n"\ " 2. JWT method: \n"\ @@ -58,7 +58,7 @@ COMMON_OPTIONS = COMMANDS = { 'fuel' => "Manage simulation resources. \n"\ " \n"\ - " gz fuel [action] [options] \n"\ + " gz fuel [action] [options] \n"\ " \n"\ "Available Actions: \n"\ " delete Delete resources \n"\ @@ -75,15 +75,15 @@ COMMANDS = { 'fuel' => " arguments for level 3. \n" + COMMON_OPTIONS + "\n\n" + "Environment variables: \n"\ - " GZ_FUEL_CACHE_PATH Path to the cache where resources are \n"\ - " downloaded to. Defaults to $HOME/.gz/fuel \n" + " GZ_FUEL_CACHE_PATH Path to the cache where resources are \n"\ + " downloaded to. Defaults to $HOME/.gz/fuel \n" } SUBCOMMANDS = { 'delete' => "Delete simulation resources \n"\ " \n"\ - " gz fuel delete [options] \n"\ + " gz fuel delete [options] \n"\ " \n"\ "Available Options: \n"\ " -u [--url] arg URL of the server that should receive \n"\ @@ -130,7 +130,7 @@ SUBCOMMANDS = { 'list' => "List simulation resources \n"\ " \n"\ - " gz fuel list [options] \n"\ + " gz fuel list [options] \n"\ " \n"\ "Available Options: \n"\ " -t [--type] arg Resource type (i.e. model, world). Required. \n"\ @@ -144,7 +144,7 @@ SUBCOMMANDS = { 'meta' => "Read and write resource metadata \n"\ " \n"\ - " gz fuel meta [options] \n"\ + " gz fuel meta [options] \n"\ " \n"\ "Available Options: \n"\ " --config2pbtxt arg Convert a model.config file to a \n"\ @@ -350,7 +350,15 @@ class Cmd options = parse(args) # Read the plugin that handles the command. - plugin = LIBRARY_NAME + if LIBRARY_NAME[0] == '/' + # If the first character is a slash, we'll assume that we've been given an + # absolute path to the library. This is only used during test mode. + plugin = LIBRARY_NAME + else + # We're assuming that the library path is relative to the current + # location of this script. + plugin = File.expand_path(File.join(File.dirname(__FILE__), LIBRARY_NAME)) + end conf_version = LIBRARY_VERSION begin @@ -452,7 +460,7 @@ class Cmd end rescue puts "Library error: Problem running [#{options['subcommand']}]() "\ - "from @GZ_LIBRARY_NAME@." + "from #{plugin}." end # begin end # execute end # class diff --git a/src/cmd/fuel.bash_completion.sh b/src/cmd/fuel.bash_completion.sh new file mode 100755 index 00000000..d29f4a21 --- /dev/null +++ b/src/cmd/fuel.bash_completion.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2023 Open Source Robotics Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# bash tab-completion + +# This is a per-library function definition, used in conjunction with the +# top-level entry point in ign-tools. + +GZ_FUEL_SUBCOMMANDS=" +delete +download +edit +list +meta +upload +" + +# TODO: In Fortress+, for each of the completion lists, remove --force-version +# and --versions. Add --help-all. Update ../gz_TEST.cc accordingly. +GZ_FUEL_COMPLETION_LIST=" + -c --config + -h --help + -v --verbose + --force-version + --versions +" + +GZ_DELETE_COMPLETION_LIST=" + --header + -c --config + -h --help + -u --url + --force-version + --versions +" + +GZ_DOWNLOAD_COMPLETION_LIST=" + --header + -c --config + -h --help + -j --jobs + -t --type + -u --url + --force-version + --versions +" + +GZ_EDIT_COMPLETION_LIST=" + --header + -b --public + -c --config + -h -help + -m --model + -p --private + -u --url + --force-version + --versions +" + +GZ_LIST_COMPLETION_LIST=" + -c --config + -h --help + -o --owner + -r --raw + -t --type + -u --url + --force-version + --versions +" + +GZ_META_COMPLETION_LIST=" + --config2pbtxt + --pbtxt2config + -c --config + -h --help + --force-version + --versions +" + +GZ_UPLOAD_COMPLETION_LIST=" + --header + -c --config + -h --help + -m --model + -p --private + -u --url + --force-version + --versions +" + +function __get_comp_from_list { + if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then + # Specify options (-*) word list for this subcommand + COMPREPLY=($(compgen -W "$@" \ + -- "${COMP_WORDS[COMP_CWORD]}" )) + return + else + # Just use bash default auto-complete, because we never have two + # subcommands in the same line. If that is ever needed, change here to + # detect subsequent subcommands + COMPREPLY=($(compgen -o default -- "${COMP_WORDS[COMP_CWORD]}")) + return + fi +} + +function _gz_fuel_delete +{ + __get_comp_from_list "$GZ_DELETE_COMPLETION_LIST" +} + +function _gz_fuel_download +{ + __get_comp_from_list "$GZ_DOWNLOAD_COMPLETION_LIST" +} + +function _gz_fuel_edit +{ + __get_comp_from_list "$GZ_EDIT_COMPLETION_LIST" +} + +function _gz_fuel_list +{ + __get_comp_from_list "$GZ_LIST_COMPLETION_LIST" +} + +function _gz_fuel_meta +{ + __get_comp_from_list "$GZ_META_COMPLETION_LIST" +} + +function _gz_fuel_upload +{ + __get_comp_from_list "$GZ_UPLOAD_COMPLETION_LIST" +} + +# This searches the current list of typed words for one of the subcommands +# listed in GZ_FUEL_SUBCOMMANDS. This should work for most cases, but may fail +# if a word that looks like a subocmmand is used as an argument to a flag. Eg. +# `gz fuel --config download list`. Here `download` is an argument to +# `--config`, but this function will think that it's the subcommand. Since this +# seems like a rare scenario, we accept this failure mode. +function __get_subcommand +{ + local known_subcmd + local subcmd + for ((i=2; $i<=$COMP_CWORD; i++)); do + for subcmd in $GZ_FUEL_SUBCOMMANDS; do + if [[ "${COMP_WORDS[i]}" == "$subcmd" ]]; then + known_subcmd="$subcmd" + fi + done + done + echo "$known_subcmd" +} + +function _gz_fuel +{ + if [[ $COMP_CWORD > 2 ]]; then + local known_subcmd=$(__get_subcommand) + if [[ ! -z $known_subcmd ]]; then + local subcmd_func="_gz_fuel_$known_subcmd" + if [[ "$(type -t $subcmd_func)" == 'function' ]]; then + $subcmd_func + return + fi + fi + fi + + # If a subcommand is not found, assume we're still completing the subcommands + # or flags for `fuel`. + if [[ ${COMP_WORDS[COMP_CWORD]} == -* ]]; then + COMPREPLY=($(compgen -W "$GZ_FUEL_COMPLETION_LIST" \ + -- "${COMP_WORDS[COMP_CWORD]}" )) + else + COMPREPLY=($(compgen -W "${GZ_FUEL_SUBCOMMANDS}" -- ${cur})) + fi +}