piątek, 1 kwietnia 2011

Obsługa parametrów wywołania programu w C++

Pisząc programy konsolowe niejednokrotnie spotykamy się problemem przekazywania parametrów startowych. Najczęstszym rozwiązaniem jest ich wpisywanie zaraz po nazwie programu, dzięki czemu będą one później dostępne w tablicy argv funkcji main. Innym rozwiązaniem, rzadziej stosowanym jest ich zapisywanie do pliku tekstowego, np. XML i dalsze jego parsowanie zaraz po starcie programu. To drugie rozwiązanie zalecane jest raczej w przypadku naprawdę złożonych parametrów wywołania. W większości przypadków w zupełności wystarcza proste przekazywanie parametrów po nazwie programu w trakcie jego wywołania.

Podejście klasyczne o przykładowym zapisie:
int zmienna1 = atoi (argv[1]);
int zmienna2 = atoi (argv[3]);
może być kłopotliwe w stosowaniu, ponieważ wymaga dokładnego pamiętania kolejności poszczególnych parametrów. Znacznie lepszym rozwiązaniem są parametry nazwane, co do których ten wymóg nie jest już konieczny. Parametry tego typu znane są na pewno każdemu, kto jakiś czas pracował w konsoli tekstowej i potrzebował zmodyfikować domyślne parametry działania programu lub podać własne.

Obecnie jest kilka mechanizmów obsługi tego typu parametrów w języku C++. Niestety nie są one wbudowane do biblioteki standardowej i wymagają stosowania zewnętrznych bibliotek, które mają nie tylko mocne, ale również słabe strony. Oto niektóre z nich:
  • Boost.Program_options - jeśli ktoś nie miał do tej pory potrzeby korzystania z bibliotek Boost to nie wydaje się dobrym pomysłem włączanie ich do projektu tylko ze względu na obsługę parametrów wejściowych. Bardziej szczegółowy opis biblioteki można znaleźć tutaj.
  • GNU getopt - użycie tego narzędzia w praktyce wymaga od użytkownika napisania kodu zawierającego instrukcję pętli while i przełącznik switch, przez co implementacja programu niepotrzebnie się wydłuża i może tracić czytelność (szczególnie w przypadku krótkich programów). Przykładowy program można zobaczyć w oficjalnej dokumentacji
  • Do innych rozwiązań tego typu można zaliczyć: bibliotekę google-gflags, bibliotekę CLPPargstreamclp.
Każde z powyższych rozwiązań, nawet jeśli w praktyce działa bardzo dobrze, wymaga korzystania z dodatkowych bibliotek, co do których należy mieć pewność, że zostały poprawnie zainstalowane w systemie. Dodatkowo w przypadku, niedużych programów, nie wydaje się sensowne korzystanie z bibliotek, które swoim rozmiarem znacznie przewyższają kod implementowanego algorytmu. Ja osobiście nie przekonałem się do żadnej z powyższych bibliotek, dlatego postanowiłem sam zaprogramować obsługę parametrów wywołania programu.

Poniższe rozwiązanie przygotowałem na podstawie przykładu ze strony: http://stackoverflow.com/questions/865668/parse-command-line-arguments/868894#868894. Przykład ten niestety nie działał do końca poprawnie i ostateczne rozwiązanie udało mi sie przygotowac analizując kod biblioteki CImg, którą używam do przetwarzania obrazu, a która to miała zaimplementowany podobny mechanizm. Poniższego kodu nie będe szczegółowo opisywał. Mam nadzieje, że jest on wystarczająco czytelny :-).

Funkcja parsująca parametry wywołania programu w C++:

inline const char*  getCmdOption(const char * name, const char * defaut, const int argc, const char *const * argv)
{
  const char *res = 0;
  
  if (argc > 0) {
    int k = 0;
    while (k < argc && strcmp(argv[k],name)) ++k;
    res = (k++==argc?defaut:(k==argc?argv[--k]:argv[k]));
  } else res = defaut;
  
  return res;
}

Przykładowe użycie funkcji getCmdOption:

Funkcja getCmdOption już okazała mi się pomocna w kilku programach z zakresu przetwarzania obrazu. Poniżej fragment programu do wczytywania danych binarnych, ze szczególnym uwzględnieniem obsługi parametrów wejściowych:

if (argc == 1) {
  printf("Usage: %s -f filename.raw -dx [value] -dy [value] -dz [value]\n", argv[0]);
  exit(0);
}

const char * filename  = getCmdOption("-f",    (char*)0, argc, argv );
const char * _dx       = getCmdOption("-dx",   (char*)0, argc, argv );
const char * _dy       = getCmdOption("-dy",   (char*)0, argc, argv );
const char * _dz       = getCmdOption("-dz",   (char*)0, argc, argv );

if ((char*)0 == filename) { 
  printf("Please specify input data file name (-f)\n"); 
  exit(0);
}
if ((char*)0 == _dx || (char*)0 == _dx || (char*)0 == _dx) { 
  printf("Please specify input data dimension sizes (-dx, -dy, -dz)\n"); 
  exit(0);
}

int dx = atoi( _dx );
int dy = atoi( _dy );
int dz = atoi( _dz );

To co było dla mnie najważniejsze i co mam nadzieje udało mi się uzyskać, to duża czytelność kodu. Mam nadzieje, że proponowana funkcja getCmdOption, okaże się również przydatna dla innych.

6 komentarze:

Anonimowy pisze...

Funkcja bardzo przydatna, szukałem czegoś prostego na szybko:) Dodaję od siebie modyfikację jednej linii:

res = (k++==argc?defaut:(k==argc?argv[--k]:(argv[k][0]==(int)'-'?argv[--k]:argv[k])));

Teraz obsługuje również parametry bez argumentów zwracając ten parametr, a nie następny parametr jako argument szukanego.

Przykład zastosowania:

c:\>moj_program -s -v -i plik_wejsciowy

Rafał Petryniak pisze...

Świetnie! Dzięki! Jedna linijka, a załatwia tak wiele :-)

Anonimowy pisze...

Super artykuł i kod. Bardzo mi pomógł. Dzięki!

Rafał Petryniak pisze...

Cieszę się, że się przydało :-)

Anonimowy pisze...

Świetna funkcja, przydaje się bardzo często do różnych programów.

Anonimowy pisze...

Myślę iż ta część kodu:
if (argc > 0) {
int k = 0;
powinna być taka:
if (argc > 1) {
int k = 1;

Skoro ten kod:
if (argc == 1) {
printf("Usage: %s -f filename.raw -dx [value] -dy [value] -dz [value]\n", argv[0]);
exit(0);
}

wyeliminuje brak argumentów(nie licząc samej nazwy programu pod argv[0]), zaoszczędzi nam to jedno ++k;.
Niby nie wiele w tym optymalizacji, ale zawsze będzie szybciej :)

Prześlij komentarz