Skip to content

Commit

Permalink
Add bash completion script for projinfo
Browse files Browse the repository at this point in the history
and install it in standard ${prefix}/share/bash-completion/completions directory

Demo:

```
source ./scripts/projinfo-bash-completion.sh
```

```
$ projinfo <TAB><TAB>
EPSG:      ESRI:      IAU_2015:  IGNF:      NKG:       NRCAN:     OGC:       PROJ:
```

```
$ projinfo -<TAB><TAB>
-3d                                        --boundcrs-to-wgs84                         --list-crs                                  --searchpaths
--accuracy                                  --c-ify                                     --main-db-path                              --show-superseded
--allow-ellipsoidal-height-as-vertical-crs  --crs-extent-use--grid-check                -o                                          --single-line
--area                                      --dump-db-structure                         --output-id                                 --spatial-test
--authority                                 --hide-ballpark                             --pivot-crs                                 --summary
--aux-db-path                               --identify                                  -q
--bbox                                      -k                                          --remote-data
```

```
$ projinfo -o <TAB><TAB>
all        PROJ       PROJJSON   SQL        WKT1:ESRI  WKT1:GDAL  WKT2:2015  WKT2:2019
```

```
$ projinfo "NAD83(2011) <TAB><TAB>
NAD83(2011)                                                                NAD83(2011) / New Mexico West
NAD83(2011) / Adjusted Jackson (ftUS)                                      NAD83(2011) / New Mexico West (ftUS)
NAD83(2011) / Alabama East                                                 NAD83(2011) / New York Central
[ ... snip ... ]
```

```
$ projinfo EPSG:43<TAB><TAB>
4322 -- WGS 72    4324 -- WGS 72BE  4326 -- WGS 84
```
  • Loading branch information
rouault committed Jan 7, 2025
1 parent 177f6f3 commit 0d4b97f
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@ if(BUILD_EXAMPLES)
add_subdirectory(examples)
endif()

add_subdirectory(scripts)

set(docfiles COPYING NEWS.md AUTHORS.md)
install(FILES ${docfiles}
DESTINATION ${CMAKE_INSTALL_DOCDIR})
Expand Down Expand Up @@ -459,7 +461,6 @@ set(CPACK_SOURCE_IGNORE_FILES
/m4/libtool*
/media/
/schemas/
/scripts/
/test/fuzzers/
/test/gigs/.*gie\\.failing
/test/postinstall/
Expand Down
28 changes: 28 additions & 0 deletions scripts/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
find_package(PkgConfig QUIET)
if (PKG_CONFIG_FOUND)
pkg_check_modules(PC_BASH_COMPLETION QUIET bash-completion)
if (PC_BASH_COMPLETION_FOUND)
pkg_get_variable(BASH_COMPLETIONS_FULL_DIR bash-completion completionsdir)
pkg_get_variable(BASH_COMPLETIONS_PREFIX bash-completion prefix)
if (BASH_COMPLETIONS_FULL_DIR
AND BASH_COMPLETIONS_PREFIX
AND BASH_COMPLETIONS_FULL_DIR MATCHES "^${BASH_COMPLETIONS_PREFIX}/")
string(REGEX REPLACE "^${BASH_COMPLETIONS_PREFIX}/" "" BASH_COMPLETIONS_DIR_DEFAULT ${BASH_COMPLETIONS_FULL_DIR})
endif ()
endif ()
endif ()

if (NOT DEFINED BASH_COMPLETIONS_DIR_DEFAULT)
include(GNUInstallDirs)
set(BASH_COMPLETIONS_DIR_DEFAULT ${CMAKE_INSTALL_DATADIR}/bash-completion/completions)
endif ()

set(BASH_COMPLETIONS_DIR
"${BASH_COMPLETIONS_DIR_DEFAULT}"
CACHE PATH "Installation sub-directory for bash completion scripts")

if (NOT BASH_COMPLETIONS_DIR STREQUAL "")
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/install_bash_completions.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/install_bash_completions.cmake @ONLY)
install(SCRIPT ${CMAKE_CURRENT_BINARY_DIR}/install_bash_completions.cmake)
endif ()
13 changes: 13 additions & 0 deletions scripts/install_bash_completions.cmake.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
set(PROGRAMS
projinfo
)

set(INSTALL_DIR "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@BASH_COMPLETIONS_DIR@")

file(MAKE_DIRECTORY "${INSTALL_DIR}")

foreach (program IN LISTS PROGRAMS)
message(STATUS "Installing ${INSTALL_DIR}/${program}")
configure_file("@CMAKE_CURRENT_SOURCE_DIR@/${program}-bash-completion.sh" "${INSTALL_DIR}/${program}" COPYONLY)
file(APPEND "@PROJECT_BINARY_DIR@/install_manifest_extra.txt" "${INSTALL_DIR}/${program}\n")
endforeach ()
51 changes: 51 additions & 0 deletions scripts/projinfo-bash-completion.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash

function_exists() {
declare -f -F "$1" > /dev/null
return $?
}

# Checks that bash-completion is recent enough
function_exists _get_comp_words_by_ref || return 0

_projinfo()
{
local cur prev
COMPREPLY=()
_get_comp_words_by_ref cur prev
choices=$(projinfo completion ${COMP_LINE})
if [[ "$cur" == "=" ]]; then
mapfile -t COMPREPLY < <(compgen -W "$choices" --)
elif [[ "$cur" == ":" ]]; then
mapfile -t COMPREPLY < <(compgen -W "$choices" --)
elif [[ "${cur: -1}" == "/" ]]; then
mapfile -t COMPREPLY < <(compgen -W "$choices" --)
elif [[ "${cur: -2}" == "/ " ]]; then
mapfile -t COMPREPLY < <(compgen -W "$choices" --)
elif [[ "${cur: -1}" == "+" ]]; then
mapfile -t COMPREPLY < <(compgen -W "$choices" --)
elif [[ "${cur: -2}" == "+ " ]]; 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 _projinfo projinfo

230 changes: 230 additions & 0 deletions src/apps/projinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,231 @@ static void outputOperations(

// ---------------------------------------------------------------------------

static void suggestCompletion(const std::vector<std::string> &args) {
#ifdef DEBUG_COMPLETION
for (const auto &arg : args)
fprintf(stderr, "'%s' ", arg.c_str());
fprintf(stderr, "\n");
#endif

bool first = true;
if (args.empty()) {
try {
auto dbContext = DatabaseContext::create();
for (const auto &authName : dbContext->getAuthorities()) {
if (!first)
printf(" ");
first = false;
printf("%s:", authName.c_str());
}
printf("\n");
} catch (const std::exception &) {
}
return;
} else if (args.size() == 1 && args[0].front() != '-' &&
args[0].find(':') == std::string::npos) {
try {
auto dbContext = DatabaseContext::create();
for (const auto &authName : dbContext->getAuthorities()) {
if (starts_with(authName, args[0])) {
if (!first)
printf(" ");
first = false;
printf("%s:", authName.c_str());
}
}
} catch (const std::exception &) {
}
}

const auto isOption = [&args](const char *opt) {
return args.back() == opt ||
(args.size() >= 2 && args[args.size() - 2] == opt);
};

if (isOption("-k")) {
printf("crs operation datum ensemble ellipsoid\n");
return;
}

if (isOption("-o")) {
if (starts_with(args.back(), "WKT1:"))
printf("GDAL ESRI\n");
else if (starts_with(args.back(), "WKT2:"))
printf("2019 2015\n");
else
printf("all PROJ WKT2:2019 WKT2:2015 WKT1:GDAL WKT1:ESRI PROJJSON "
"SQL\n");
return;
}

if (isOption("--spatial-test")) {
printf("contains intersects\n");
return;
}

if (isOption("--crs-extent-use")) {
printf("none both intersection smallest\n");
return;
}

if (isOption("--grid-check")) {
printf("none discard_missing sort known_available\n");
return;
}

if (isOption("--pivot-crs")) {
if (args.back().back() == ':')
return;
printf("always if_no_direct_transformation never");
try {
auto dbContext = DatabaseContext::create();
for (const auto &authName : dbContext->getAuthorities()) {
printf(" %s:", authName.c_str());
}
printf("\n");
} catch (const std::exception &) {
}
return;
}

if (args.back()[0] == '-') {
const char *const knownOptions[] = {
"-o",
"-k",
"--summary",
"-q",
"--area",
"--bbox",
"--spatial-test",
"--crs-extent-use",
"--grid-check",
"--pivot-crs",
"--show-superseded",
"--hide-ballpark",
"--accuracy",
"--allow-ellipsoidal-height-as-vertical-crs",
"--boundcrs-to-wgs84",
"--authority",
"--main-db-path",
"--aux-db-path",
"--identify",
"--3d",
"--output-id",
"--c-ify",
"--single-line",
"--searchpaths",
"--remote-data",
"--list-crs",
"--dump-db-structure",
"-s",
"--s_epoch",
"-t",
"--t_epoch",
};

for (const char *opt : knownOptions) {
if (args.back() == opt)
return;
}
for (const char *opt : knownOptions) {
if (!first)
printf(" ");
first = false;
printf("%s", opt);
}
printf("\n");
return;
}

std::string lastArg = args.back();
for (size_t i = args.size(); i >= 1;) {
--i;
if (args[i].size() >= 2 && args[i].back() == '"') {
break;
}
if (args[i].size() >= 2 && args[i][0] == '"') {
lastArg = args[i].substr(1);
++i;
for (; i < args.size(); ++i) {
lastArg += " ";
lastArg += args[i];
}
break;
}
}
#ifdef DEBUG_COMPLETION
fprintf(stderr, "lastArg='%s'\n", lastArg.c_str());
#endif

try {
auto dbContext = DatabaseContext::create();
const auto columnPos = args.back().find(':');
if (columnPos != std::string::npos) {
const auto authName = args.back().substr(0, columnPos);
const auto codeStart = columnPos + 1 < args.back().size()
? args.back().substr(columnPos + 1)
: std::string();
auto factory = AuthorityFactory::create(dbContext, authName);
const auto list = factory->getCRSInfoList();

std::vector<std::string> res;
std::string code;
for (const auto &info : list) {
if (!info.deprecated &&
(codeStart.empty() || starts_with(info.code, codeStart))) {
if (res.empty())
code = info.code;
res.push_back(std::string(info.code).append(" -- ").append(
info.name));
}
}
if (res.size() == 1) {
// If there is a single match, remove the name from the
// suggestion.
res.clear();
res.push_back(code);
}
for (const auto &val : res) {
if (!first)
printf(" ");
first = false;
printf("%s", replaceAll(val, " ", "\\ ").c_str());
}
printf("\n");
return;
}

for (const char *authName : {"EPSG", ""}) {
auto factory =
AuthorityFactory::create(dbContext, std::string(authName));
const auto list = factory->getCRSInfoList();
for (const auto &info : list) {
if (!info.deprecated && starts_with(info.name, lastArg)) {
if (!first)
printf(" ");
first = false;
std::string val = info.name;
if (args.back() == "+" || args.back() == "/") {
const auto pos = val.find(args.back()[0]);
if (pos != std::string::npos && pos + 1 < val.size() &&
val[pos + 1] == ' ')
val = val.substr(pos + 2);
}
printf("%s", replaceAll(val, " ", "\\ ").c_str());
}
}
if (!first) {
printf("\n");
break;
}
}
} catch (const std::exception &) {
}
}

// ---------------------------------------------------------------------------

int main(int argc, char **argv) {

pj_stderr_proj_lib_deprecation_warning();
Expand All @@ -1062,6 +1287,11 @@ int main(int argc, char **argv) {
usage();
}

if (argc >= 3 && strcmp(argv[1], "completion") == 0) {
suggestCompletion(std::vector<std::string>(argv + 3, argv + argc));
return 0;
}

std::vector<std::string> positional_args;
std::string sourceCRSStr;
std::string sourceEpoch;
Expand Down
32 changes: 32 additions & 0 deletions test/cli/test_projinfo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1890,3 +1890,35 @@ tests:
out: |
Candidate operations found: 1
unknown id, Conversion from WGS 84 (G1150) (geog2D) to WGS 84 (G1150) (geocentric) + WGS 84 (G1150) to WGS 84 (G1762) (1) + WGS 84 (G1762) to WGS 84 (G2139) (1) + WGS 84 (G2139) to WGS 84 (G2296) (1) + Conversion from WGS 84 (G2296) (geocentric) to WGS 84 (G2296) (geog2D), 0.04 m, World
- args: completion projinfo -
out: |
-o -k --summary -q --area --bbox --spatial-test --crs-extent-use --grid-check --pivot-crs --show-superseded --hide-ballpark --accuracy --allow-ellipsoidal-height-as-vertical-crs --boundcrs-to-wgs84 --authority --main-db-path --aux-db-path --identify --3d --output-id --c-ify --single-line --searchpaths --remote-data --list-crs --dump-db-structure -s --s_epoch -t --t_epoch
- args: completion projinfo -o
out: all PROJ WKT2:2019 WKT2:2015 WKT1:GDAL WKT1:ESRI PROJJSON SQL
- args: completion projinfo -o "WKT2:"
out: 2019 2015
- args: completion projinfo -o "WKT1:"
out: GDAL ESRI
- args: completion projinfo --pivot-crs
out: |
always if_no_direct_transformation never EPSG: ESRI: IAU_2015: IGNF: NKG: NRCAN: OGC: PROJ:
- args: completion projinfo
out: |
EPSG: ESRI: IAU_2015: IGNF: NKG: NRCAN: OGC: PROJ:
- args: completion projinfo NKG
out: "NKG:"
- args: completion projinfo "OGC:"
out: |
CRS27\ --\ NAD27\ (CRS27) CRS83\ --\ NAD83\ (CRS83) CRS84\ --\ WGS\ 84\ (CRS84) CRS84h\ --\ WGS\ 84\ longitude-latitude-height
- args: completion projinfo EPSG:432
out: |
4322\ --\ WGS\ 72 4324\ --\ WGS\ 72BE 4326\ --\ WGS\ 84
- args: completion projinfo EPSG:4326
out: |
4326
- args: completion projinfo EGM
out: |
EGM2008\ height EGM96\ height EGM84\ height
- args: completion projinfo "\"RGF93" v1 "/"
out: |
Lambert-93 CC42 CC43 CC44 CC45 CC46 CC47 CC48 CC49 CC50 Lambert-93\ +\ NGF-IGN69\ height Lambert-93\ +\ NGF-IGN78\ height

0 comments on commit 0d4b97f

Please sign in to comment.