1 /*- 2 * SPDX-License-Identifier: BSD-2-Clause 3 * 4 * Copyright (c) 2003-2008 Tim Kientzle 5 * All rights reserved. 6 */ 7 8 /* 9 * Command line parser for tar. 10 */ 11 12 #include "bsdtar_platform.h" 13 14 #ifdef HAVE_ERRNO_H 15 #include <errno.h> 16 #endif 17 #ifdef HAVE_STDLIB_H 18 #include <stdlib.h> 19 #endif 20 #ifdef HAVE_STRING_H 21 #include <string.h> 22 #endif 23 24 #include "bsdtar.h" 25 #include "err.h" 26 27 /* 28 * Short options for tar. Please keep this sorted. 29 */ 30 static const char *short_options 31 = "aBb:C:cf:HhI:JjkLlmnOoPpqrSs:T:tUuvW:wX:xyZz"; 32 33 /* 34 * Long options for tar. Please keep this list sorted. 35 * 36 * The symbolic names for options that lack a short equivalent are 37 * defined in bsdtar.h. Also note that so far I've found no need 38 * to support optional arguments to long options. That would be 39 * a small change to the code below. 40 */ 41 42 static const struct bsdtar_option { 43 const char *name; 44 int required; /* 1 if this option requires an argument. */ 45 int equivalent; /* Equivalent short option. */ 46 } tar_longopts[] = { 47 { "absolute-paths", 0, 'P' }, 48 { "append", 0, 'r' }, 49 { "acls", 0, OPTION_ACLS }, 50 { "auto-compress", 0, 'a' }, 51 { "b64encode", 0, OPTION_B64ENCODE }, 52 { "block-size", 1, 'b' }, 53 { "blocking-factor", 1, 'b' }, 54 { "bunzip2", 0, 'j' }, 55 { "bzip", 0, 'j' }, 56 { "bzip2", 0, 'j' }, 57 { "cd", 1, 'C' }, 58 { "check-links", 0, OPTION_CHECK_LINKS }, 59 { "chroot", 0, OPTION_CHROOT }, 60 { "clear-nochange-fflags", 0, OPTION_CLEAR_NOCHANGE_FFLAGS }, 61 { "compress", 0, 'Z' }, 62 { "confirmation", 0, 'w' }, 63 { "create", 0, 'c' }, 64 { "dereference", 0, 'L' }, 65 { "directory", 1, 'C' }, 66 { "disable-copyfile", 0, OPTION_NO_MAC_METADATA }, 67 { "exclude", 1, OPTION_EXCLUDE }, 68 { "exclude-from", 1, 'X' }, 69 { "exclude-vcs", 0, OPTION_EXCLUDE_VCS }, 70 { "extract", 0, 'x' }, 71 { "fast-read", 0, 'q' }, 72 { "fflags", 0, OPTION_FFLAGS }, 73 { "file", 1, 'f' }, 74 { "files-from", 1, 'T' }, 75 { "format", 1, OPTION_FORMAT }, 76 { "gid", 1, OPTION_GID }, 77 { "gname", 1, OPTION_GNAME }, 78 { "group", 1, OPTION_GROUP }, 79 { "grzip", 0, OPTION_GRZIP }, 80 { "gunzip", 0, 'z' }, 81 { "gzip", 0, 'z' }, 82 { "help", 0, OPTION_HELP }, 83 { "hfsCompression", 0, OPTION_HFS_COMPRESSION }, 84 { "ignore-zeros", 0, OPTION_IGNORE_ZEROS }, 85 { "include", 1, OPTION_INCLUDE }, 86 { "insecure", 0, 'P' }, 87 { "interactive", 0, 'w' }, 88 { "keep-newer-files", 0, OPTION_KEEP_NEWER_FILES }, 89 { "keep-old-files", 0, 'k' }, 90 { "list", 0, 't' }, 91 { "lrzip", 0, OPTION_LRZIP }, 92 { "lz4", 0, OPTION_LZ4 }, 93 { "lzip", 0, OPTION_LZIP }, 94 { "lzma", 0, OPTION_LZMA }, 95 { "lzop", 0, OPTION_LZOP }, 96 { "mac-metadata", 0, OPTION_MAC_METADATA }, 97 { "modification-time", 0, 'm' }, 98 { "newer", 1, OPTION_NEWER_CTIME }, 99 { "newer-ctime", 1, OPTION_NEWER_CTIME }, 100 { "newer-ctime-than", 1, OPTION_NEWER_CTIME_THAN }, 101 { "newer-mtime", 1, OPTION_NEWER_MTIME }, 102 { "newer-mtime-than", 1, OPTION_NEWER_MTIME_THAN }, 103 { "newer-than", 1, OPTION_NEWER_CTIME_THAN }, 104 { "no-acls", 0, OPTION_NO_ACLS }, 105 { "no-fflags", 0, OPTION_NO_FFLAGS }, 106 { "no-mac-metadata", 0, OPTION_NO_MAC_METADATA }, 107 { "no-read-sparse", 0, OPTION_NO_READ_SPARSE }, 108 { "no-recursion", 0, 'n' }, 109 { "no-safe-writes", 0, OPTION_NO_SAFE_WRITES }, 110 { "no-same-owner", 0, OPTION_NO_SAME_OWNER }, 111 { "no-same-permissions", 0, OPTION_NO_SAME_PERMISSIONS }, 112 { "no-xattr", 0, OPTION_NO_XATTRS }, 113 { "no-xattrs", 0, OPTION_NO_XATTRS }, 114 { "nodump", 0, OPTION_NODUMP }, 115 { "nopreserveHFSCompression",0, OPTION_NOPRESERVE_HFS_COMPRESSION }, 116 { "norecurse", 0, 'n' }, 117 { "null", 0, OPTION_NULL }, 118 { "numeric-owner", 0, OPTION_NUMERIC_OWNER }, 119 { "older", 1, OPTION_OLDER_CTIME }, 120 { "older-ctime", 1, OPTION_OLDER_CTIME }, 121 { "older-ctime-than", 1, OPTION_OLDER_CTIME_THAN }, 122 { "older-mtime", 1, OPTION_OLDER_MTIME }, 123 { "older-mtime-than", 1, OPTION_OLDER_MTIME_THAN }, 124 { "older-than", 1, OPTION_OLDER_CTIME_THAN }, 125 { "one-file-system", 0, OPTION_ONE_FILE_SYSTEM }, 126 { "options", 1, OPTION_OPTIONS }, 127 { "owner", 1, OPTION_OWNER }, 128 { "passphrase", 1, OPTION_PASSPHRASE }, 129 { "posix", 0, OPTION_POSIX }, 130 { "preserve-permissions", 0, 'p' }, 131 { "read-full-blocks", 0, 'B' }, 132 { "read-sparse", 0, OPTION_READ_SPARSE }, 133 { "safe-writes", 0, OPTION_SAFE_WRITES }, 134 { "same-owner", 0, OPTION_SAME_OWNER }, 135 { "same-permissions", 0, 'p' }, 136 { "strip-components", 1, OPTION_STRIP_COMPONENTS }, 137 { "to-stdout", 0, 'O' }, 138 { "totals", 0, OPTION_TOTALS }, 139 { "uid", 1, OPTION_UID }, 140 { "uname", 1, OPTION_UNAME }, 141 { "uncompress", 0, 'Z' }, 142 { "unlink", 0, 'U' }, 143 { "unlink-first", 0, 'U' }, 144 { "update", 0, 'u' }, 145 { "use-compress-program", 1, OPTION_USE_COMPRESS_PROGRAM }, 146 { "uuencode", 0, OPTION_UUENCODE }, 147 { "verbose", 0, 'v' }, 148 { "version", 0, OPTION_VERSION }, 149 { "xattrs", 0, OPTION_XATTRS }, 150 { "xz", 0, 'J' }, 151 { "zstd", 0, OPTION_ZSTD }, 152 { NULL, 0, 0 } 153 }; 154 155 /* 156 * This getopt implementation has two key features that common 157 * getopt_long() implementations lack. Apart from those, it's a 158 * straightforward option parser, considerably simplified by not 159 * needing to support the wealth of exotic getopt_long() features. It 160 * has, of course, been shamelessly tailored for bsdtar. (If you're 161 * looking for a generic getopt_long() implementation for your 162 * project, I recommend Gregory Pietsch's public domain getopt_long() 163 * implementation.) The two additional features are: 164 * 165 * Old-style tar arguments: The original tar implementation treated 166 * the first argument word as a list of single-character option 167 * letters. All arguments follow as separate words. For example, 168 * tar xbf 32 /dev/tape 169 * Here, the "xbf" is three option letters, "32" is the argument for 170 * "b" and "/dev/tape" is the argument for "f". We support this usage 171 * if the first command-line argument does not begin with '-'. We 172 * also allow regular short and long options to follow, e.g., 173 * tar xbf 32 /dev/tape -P --format=pax 174 * 175 * -W long options: There's an obscure GNU convention (only rarely 176 * supported even there) that allows "-W option=argument" as an 177 * alternative way to support long options. This was supported in 178 * early bsdtar as a way to access long options on platforms that did 179 * not support getopt_long() and is preserved here for backwards 180 * compatibility. (Of course, if I'd started with a custom 181 * command-line parser from the beginning, I would have had normal 182 * long option support on every platform so that hack wouldn't have 183 * been necessary. Oh, well. Some mistakes you just have to live 184 * with.) 185 * 186 * TODO: We should be able to use this to pull files and intermingled 187 * options (such as -C) from the command line in write mode. That 188 * will require a little rethinking of the argument handling in 189 * bsdtar.c. 190 * 191 * TODO: If we want to support arbitrary command-line options from -T 192 * input (as GNU tar does), we may need to extend this to handle option 193 * words from sources other than argv/argc. I'm not really sure if I 194 * like that feature of GNU tar, so it's certainly not a priority. 195 */ 196 197 int 198 bsdtar_getopt(struct bsdtar *bsdtar) 199 { 200 enum { state_start = 0, state_old_tar, state_next_word, 201 state_short, state_long }; 202 203 const struct bsdtar_option *popt, *match, *match2; 204 const char *p, *long_prefix; 205 size_t optlength; 206 int opt; 207 int required; 208 209 again: 210 match = NULL; 211 match2 = NULL; 212 long_prefix = "--"; 213 opt = '?'; 214 required = 0; 215 bsdtar->argument = NULL; 216 217 /* First time through, initialize everything. */ 218 if (bsdtar->getopt_state == state_start) { 219 /* Skip program name. */ 220 ++bsdtar->argv; 221 --bsdtar->argc; 222 if (*bsdtar->argv == NULL) 223 return (-1); 224 /* Decide between "new style" and "old style" arguments. */ 225 if (bsdtar->argv[0][0] == '-') { 226 bsdtar->getopt_state = state_next_word; 227 } else { 228 bsdtar->getopt_state = state_old_tar; 229 bsdtar->getopt_word = *bsdtar->argv++; 230 --bsdtar->argc; 231 } 232 } 233 234 /* 235 * We're parsing old-style tar arguments 236 */ 237 if (bsdtar->getopt_state == state_old_tar) { 238 /* Get the next option character. */ 239 opt = *bsdtar->getopt_word++; 240 if (opt == '\0') { 241 /* New-style args can follow old-style. */ 242 bsdtar->getopt_state = state_next_word; 243 } else { 244 /* See if it takes an argument. */ 245 p = strchr(short_options, opt); 246 if (p == NULL) 247 return ('?'); 248 if (p[1] == ':') { 249 bsdtar->argument = *bsdtar->argv; 250 if (bsdtar->argument == NULL) { 251 lafe_warnc(0, 252 "Option %c requires an argument", 253 opt); 254 return ('?'); 255 } 256 ++bsdtar->argv; 257 --bsdtar->argc; 258 } 259 } 260 } 261 262 /* 263 * We're ready to look at the next word in argv. 264 */ 265 if (bsdtar->getopt_state == state_next_word) { 266 /* No more arguments, so no more options. */ 267 if (bsdtar->argv[0] == NULL) 268 return (-1); 269 /* Doesn't start with '-', so no more options. */ 270 if (bsdtar->argv[0][0] != '-') 271 return (-1); 272 /* "--" marks end of options; consume it and return. */ 273 if (strcmp(bsdtar->argv[0], "--") == 0) { 274 ++bsdtar->argv; 275 --bsdtar->argc; 276 return (-1); 277 } 278 /* Get next word for parsing. */ 279 bsdtar->getopt_word = *bsdtar->argv++; 280 --bsdtar->argc; 281 if (bsdtar->getopt_word[1] == '-') { 282 /* Set up long option parser. */ 283 bsdtar->getopt_state = state_long; 284 bsdtar->getopt_word += 2; /* Skip leading '--' */ 285 } else { 286 /* Set up short option parser. */ 287 bsdtar->getopt_state = state_short; 288 ++bsdtar->getopt_word; /* Skip leading '-' */ 289 } 290 } 291 292 /* 293 * We're parsing a group of POSIX-style single-character options. 294 */ 295 if (bsdtar->getopt_state == state_short) { 296 /* Peel next option off of a group of short options. */ 297 opt = *bsdtar->getopt_word++; 298 if (opt == '\0') { 299 /* End of this group; recurse to get next option. */ 300 bsdtar->getopt_state = state_next_word; 301 goto again; 302 } 303 304 /* Does this option take an argument? */ 305 p = strchr(short_options, opt); 306 if (p == NULL) 307 return ('?'); 308 if (p[1] == ':') 309 required = 1; 310 311 /* If it takes an argument, parse that. */ 312 if (required) { 313 /* If arg is run-in, bsdtar->getopt_word already points to it. */ 314 if (bsdtar->getopt_word[0] == '\0') { 315 /* Otherwise, pick up the next word. */ 316 bsdtar->getopt_word = *bsdtar->argv; 317 if (bsdtar->getopt_word == NULL) { 318 lafe_warnc(0, 319 "Option -%c requires an argument", 320 opt); 321 return ('?'); 322 } 323 ++bsdtar->argv; 324 --bsdtar->argc; 325 } 326 if (opt == 'W') { 327 bsdtar->getopt_state = state_long; 328 long_prefix = "-W "; /* For clearer errors. */ 329 } else { 330 bsdtar->getopt_state = state_next_word; 331 bsdtar->argument = bsdtar->getopt_word; 332 } 333 } 334 } 335 336 /* We're reading a long option, including -W long=arg convention. */ 337 if (bsdtar->getopt_state == state_long) { 338 /* After this long option, we'll be starting a new word. */ 339 bsdtar->getopt_state = state_next_word; 340 341 /* Option name ends at '=' if there is one. */ 342 p = strchr(bsdtar->getopt_word, '='); 343 if (p != NULL) { 344 optlength = (size_t)(p - bsdtar->getopt_word); 345 bsdtar->argument = (char *)(uintptr_t)(p + 1); 346 } else { 347 optlength = strlen(bsdtar->getopt_word); 348 } 349 350 /* Search the table for an unambiguous match. */ 351 for (popt = tar_longopts; popt->name != NULL; popt++) { 352 /* Short-circuit if first chars don't match. */ 353 if (popt->name[0] != bsdtar->getopt_word[0]) 354 continue; 355 /* If option is a prefix of name in table, record it.*/ 356 if (strncmp(bsdtar->getopt_word, popt->name, optlength) == 0) { 357 match2 = match; /* Record up to two matches. */ 358 match = popt; 359 /* If it's an exact match, we're done. */ 360 if (strlen(popt->name) == optlength) { 361 match2 = NULL; /* Forget the others. */ 362 break; 363 } 364 } 365 } 366 367 /* Fail if there wasn't a unique match. */ 368 if (match == NULL) { 369 lafe_warnc(0, 370 "Option %s%s is not supported", 371 long_prefix, bsdtar->getopt_word); 372 return ('?'); 373 } 374 if (match2 != NULL) { 375 lafe_warnc(0, 376 "Ambiguous option %s%s (matches --%s and --%s)", 377 long_prefix, bsdtar->getopt_word, match->name, match2->name); 378 return ('?'); 379 } 380 381 /* We've found a unique match; does it need an argument? */ 382 if (match->required) { 383 /* Argument required: get next word if necessary. */ 384 if (bsdtar->argument == NULL) { 385 bsdtar->argument = *bsdtar->argv; 386 if (bsdtar->argument == NULL) { 387 lafe_warnc(0, 388 "Option %s%s requires an argument", 389 long_prefix, match->name); 390 return ('?'); 391 } 392 ++bsdtar->argv; 393 --bsdtar->argc; 394 } 395 } else { 396 /* Argument forbidden: fail if there is one. */ 397 if (bsdtar->argument != NULL) { 398 lafe_warnc(0, 399 "Option %s%s does not allow an argument", 400 long_prefix, match->name); 401 return ('?'); 402 } 403 } 404 return (match->equivalent); 405 } 406 407 return (opt); 408 } 409