afh: Unify name of init functions.
[paraslash.git] / aft.c
diff --git a/aft.c b/aft.c
index 628f98f..0c4c932 100644 (file)
--- a/aft.c
+++ b/aft.c
@@ -88,18 +88,6 @@ struct ls_data {
        unsigned char *hash;
 };
 
-/** The flags accepted by the ls command. */
-enum ls_flags {
-       /** -p */
-       LS_FLAG_FULL_PATH = 1,
-       /** -a */
-       LS_FLAG_ADMISSIBLE_ONLY = 2,
-       /** -r */
-       LS_FLAG_REVERSE = 4,
-       /** -d */
-       LS_FLAG_UNIXDATE = 8,
-};
-
 /**
  * The size of the individual output fields of the ls command.
  *
@@ -130,16 +118,11 @@ struct ls_widths {
 
 /** Data passed from the ls command handler to its callback function. */
 struct ls_options {
-       /** The given command line flags. */
-       unsigned flags;
-       /** The sorting method given at the command line. */
+       struct lls_parse_result *lpr;
+       /* Derived from lpr */
        enum ls_sorting_method sorting;
-       /** The given listing mode (short, long, verbose, mbox). */
+       /* Derived from lpr */
        enum ls_listing_mode mode;
-       /** The arguments passed to the ls command. */
-       char **patterns;
-       /** Number of non-option arguments. */
-       int num_patterns;
        /** Used for long listing mode to align the output fields. */
        struct ls_widths widths;
        /** Size of the \a data array. */
@@ -337,8 +320,8 @@ enum afhi_offsets {
        CHUNKS_TOTAL_OFFSET = 20,
        /** The length of the audio file header (4 bytes). */
        HEADER_LEN_OFFSET = 24,
-       /** Was: The start of the audio file header (4 bytes). */
-       AFHI_UNUSED2_OFFSET = 28,
+       /** Size of the largest chunk in bytes. (4 bytes). */
+       AFHI_MAX_CHUNK_SIZE_OFFSET = 28,
        /** The seconds part of the chunk time (4 bytes). */
        CHUNK_TV_TV_SEC_OFFSET = 32,
        /** The microseconds part of the chunk time (4 bytes). */
@@ -378,7 +361,7 @@ static void save_afhi(struct afh_info *afhi, char *buf)
        write_u8(buf + AFHI_CHANNELS_OFFSET, afhi->channels);
        write_u32(buf + CHUNKS_TOTAL_OFFSET, afhi->chunks_total);
        write_u32(buf + HEADER_LEN_OFFSET, afhi->header_len);
-       write_u32(buf + AFHI_UNUSED2_OFFSET, 0);
+       write_u32(buf + AFHI_MAX_CHUNK_SIZE_OFFSET, afhi->max_chunk_size);
        write_u32(buf + CHUNK_TV_TV_SEC_OFFSET, afhi->chunk_tv.tv_sec);
        write_u32(buf + CHUNK_TV_TV_USEC_OFFSET, afhi->chunk_tv.tv_usec);
        p = buf + AFHI_INFO_STRING_OFFSET;
@@ -391,6 +374,7 @@ static void save_afhi(struct afh_info *afhi, char *buf)
        sprintf(p, "%s", afhi->tags.comment);
 }
 
+/* does not load the chunk table */
 static void load_afhi(const char *buf, struct afh_info *afhi)
 {
        afhi->seconds_total = read_u32(buf + AFHI_SECONDS_TOTAL_OFFSET);
@@ -400,6 +384,7 @@ static void load_afhi(const char *buf, struct afh_info *afhi)
        afhi->channels = read_u8(buf + AFHI_CHANNELS_OFFSET);
        afhi->chunks_total = read_u32(buf + CHUNKS_TOTAL_OFFSET);
        afhi->header_len = read_u32(buf + HEADER_LEN_OFFSET);
+       afhi->max_chunk_size = read_u32(buf + AFHI_MAX_CHUNK_SIZE_OFFSET);
        afhi->chunk_tv.tv_sec = read_u32(buf + CHUNK_TV_TV_SEC_OFFSET);
        afhi->chunk_tv.tv_usec = read_u32(buf + CHUNK_TV_TV_USEC_OFFSET);
        afhi->techinfo = (char *)buf + AFHI_INFO_STRING_OFFSET;
@@ -410,42 +395,37 @@ static void load_afhi(const char *buf, struct afh_info *afhi)
        afhi->tags.comment = afhi->tags.album + strlen(afhi->tags.album) + 1;
 }
 
+/* Only used for saving the chunk table, but not for loading. */
 static unsigned sizeof_chunk_table(struct afh_info *afhi)
 {
-       if (!afhi)
+       if (!afhi || !afhi->chunk_table)
                return 0;
        return 4 * (afhi->chunks_total + 1);
 }
 
-static uint32_t save_chunk_table(struct afh_info *afhi, char *buf)
+static void save_chunk_table(struct afh_info *afhi, char *buf)
 {
-       int i;
-       uint32_t max = 0, old = 0;
-
-       for (i = 0; i <= afhi->chunks_total; i++) {
-               uint32_t val = afhi->chunk_table[i];
-               write_u32(buf + 4 * i, val);
-               /*
-                * If the first chunk is the header, do not consider it for the
-                * calculation of the largest chunk size.
-                */
-               if (i == 0 || (i == 1 && afhi->header_len > 0)) {
-                       old = val;
-                       continue;
-               }
-               max = PARA_MAX(max, val - old);
-               old = val;
-       }
-       return max;
+       uint32_t n;
+
+       if (!afhi->chunk_table)
+               return;
+       for (n = 0; n <= afhi->chunks_total; n++)
+               write_u32(buf + 4 * n, afhi->chunk_table[n]);
 }
 
-static void load_chunk_table(struct afh_info *afhi, char *buf)
+static void load_chunk_table(struct afh_info *afhi, const struct osl_object *ct)
 {
        int i;
+       size_t sz;
 
-       afhi->chunk_table = para_malloc(sizeof_chunk_table(afhi));
-       for (i = 0; i <= afhi->chunks_total; i++)
-               afhi->chunk_table[i] = read_u32(buf + 4 * i);
+       if (!ct->data || ct->size < 4) {
+               afhi->chunk_table = NULL;
+               return;
+       }
+       sz  = PARA_MIN(((size_t)afhi->chunks_total + 1) * 4, ct->size) + 1;
+       afhi->chunk_table = para_malloc(sz);
+       for (i = 0; i <= afhi->chunks_total && i * 4 + 3 < ct->size; i++)
+               afhi->chunk_table[i] = read_u32(ct->data + 4 * i);
 }
 
 /**
@@ -611,6 +591,10 @@ static int get_hash_of_row(const struct osl_row *row, unsigned char **hash)
  *
  * \return The return value of the underlying call to osl_get_object().
  *
+ * After the call the members of the afhi structure point to mapped memory
+ * which is owned by the osl table, Hence the caller must not attempt to free
+ * this memory by calling \ref clear_afhi().
+ *
  * \sa get_chunk_table_of_row().
  */
 int get_afhi_of_row(const struct osl_row *row, struct afh_info *afhi)
@@ -640,7 +624,13 @@ static int save_afd(struct audio_file_data *afd)
                goto err;
        buf = shm_afd;
        buf += sizeof(*afd);
-       afd->max_chunk_size = save_chunk_table(&afd->afhi, buf);
+       save_chunk_table(&afd->afhi, buf);
+       if (afd->afhi.max_chunk_size == 0) { /* v0.5.x on-disk afhi */
+               set_max_chunk_size(&afd->afhi);
+               PARA_NOTICE_LOG("max chunk size unset, re-add required\n");
+       } else
+               PARA_INFO_LOG("using max chunk size from afhi\n");
+       afd->max_chunk_size = afd->afhi.max_chunk_size;
        *(struct audio_file_data *)shm_afd = *afd;
        shm_detach(shm_afd);
        return shmid;
@@ -665,14 +655,22 @@ int load_afd(int shmid, struct audio_file_data *afd)
 {
        void *shm_afd;
        int ret;
+       struct osl_object obj;
 
        ret = shm_attach(shmid, ATTACH_RO, &shm_afd);
        if (ret < 0)
                return ret;
+       ret = shm_size(shmid, &obj.size);
+       if (ret < 0)
+               goto detach;
        *afd = *(struct audio_file_data *)shm_afd;
-       load_chunk_table(&afd->afhi, shm_afd + sizeof(*afd));
+       obj.data = shm_afd + sizeof(*afd);
+       obj.size -= sizeof(*afd);
+       load_chunk_table(&afd->afhi, &obj);
+       ret = 1;
+detach:
        shm_detach(shm_afd);
-       return 1;
+       return ret;
 }
 
 static int get_local_time(uint64_t *seconds, char *buf, size_t size,
@@ -701,7 +699,7 @@ static int get_local_time(uint64_t *seconds, char *buf, size_t size,
        /*
         * If the given time is more than six month away from the current time,
         * we print only the year. The additional space character in the format
-        * string below makes the formated date align nicely with dates that
+        * string below makes the formatted date align nicely with dates that
         * contain the time (those written by the above strftime() statement).
         */
        if (!strftime(buf, size, "%b %e  %Y", tm))
@@ -785,11 +783,11 @@ static void write_image_items(struct para_buffer *b, struct afs_info *afsi)
 }
 
 static void write_filename_items(struct para_buffer *b, const char *path,
-               unsigned flags)
+               bool basename)
 {
        char *val;
 
-       if (!(flags & LS_FLAG_FULL_PATH)) {
+       if (basename) {
                WRITE_STATUS_ITEM(b, SI_BASENAME, "%s\n", path);
                return;
        }
@@ -822,7 +820,11 @@ static int print_chunk_table(struct ls_data *d, struct para_buffer *b)
                (long unsigned) d->afhi.chunk_tv.tv_usec
        );
        buf = chunk_table_obj.data;
-       for (i = 0; i <= d->afhi.chunks_total; i++)
+       for (
+               i = 0;
+               i <= d->afhi.chunks_total && 4 * i + 3 < chunk_table_obj.size;
+               i++
+       )
                para_printf(b, "%u ", (unsigned) read_u32(buf + 4 * i));
        para_printf(b, "\n");
        ret = 1;
@@ -830,17 +832,12 @@ static int print_chunk_table(struct ls_data *d, struct para_buffer *b)
        return ret;
 }
 
-static void write_score(struct para_buffer *b, struct ls_data *d,
-               struct ls_options *opts)
-{
-       if (!(opts->flags & LS_FLAG_ADMISSIBLE_ONLY)) /* no score*/
-               return;
-       WRITE_STATUS_ITEM(b, SI_SCORE, "%li\n", d->score);
-}
-
 static int print_list_item(struct ls_data *d, struct ls_options *opts,
        struct para_buffer *b, time_t current_time)
 {
+       const struct lls_opt_result *r_a = SERVER_CMD_OPT_RESULT(LS, ADMISSIBLE, opts->lpr);
+       const struct lls_opt_result *r_b = SERVER_CMD_OPT_RESULT(LS, BASENAME, opts->lpr);
+       const struct lls_opt_result *r_d = SERVER_CMD_OPT_RESULT(LS, UNIX_DATE, opts->lpr);
        int ret;
        char att_buf[65];
        char last_played_time[30];
@@ -859,7 +856,7 @@ static int print_list_item(struct ls_data *d, struct ls_options *opts,
                goto out;
        }
        get_attribute_bitmap(&afsi->attributes, att_buf);
-       if (opts->flags & LS_FLAG_UNIXDATE)
+       if (lls_opt_given(r_d))
                sprintf(last_played_time, "%llu",
                        (long long unsigned)afsi->last_played);
        else {
@@ -871,10 +868,9 @@ static int print_list_item(struct ls_data *d, struct ls_options *opts,
        get_duration_buf(afhi->seconds_total, duration_buf, opts);
        if (opts->mode == LS_MODE_LONG) {
                struct ls_widths *w = &opts->widths;
-               if (opts->flags & LS_FLAG_ADMISSIBLE_ONLY) {
+               if (lls_opt_given(r_a))
                        para_printf(b, "%*li ", opts->widths.score_width,
                                d->score);
-               }
                para_printf(b,
                        "%s "   /* attributes */
                        "%*u "  /* amp */
@@ -914,8 +910,9 @@ static int print_list_item(struct ls_data *d, struct ls_options *opts,
                        last_played_time,
                        bn? bn : "?");
        }
-       write_filename_items(b, d->path, opts->flags);
-       write_score(b, d, opts);
+       write_filename_items(b, d->path, lls_opt_given(r_b));
+       if (lls_opt_given(r_a))
+               WRITE_STATUS_ITEM(b, SI_SCORE, "%li\n", d->score);
        ret = write_attribute_items(b, att_buf, afsi);
        if (ret < 0)
                goto out;
@@ -937,6 +934,8 @@ static int print_list_item(struct ls_data *d, struct ls_options *opts,
        WRITE_STATUS_ITEM(b, SI_CHUNK_TIME, "%lu\n", tv2ms(&afhi->chunk_tv));
        WRITE_STATUS_ITEM(b, SI_NUM_CHUNKS, "%" PRIu32 "\n",
                afhi->chunks_total);
+       WRITE_STATUS_ITEM(b, SI_MAX_CHUNK_SIZE, "%" PRIu32 "\n",
+               afhi->max_chunk_size);
        WRITE_STATUS_ITEM(b, SI_TECHINFO, "%s\n", afhi->techinfo);
        WRITE_STATUS_ITEM(b, SI_ARTIST, "%s\n", afhi->tags.artist);
        WRITE_STATUS_ITEM(b, SI_TITLE, "%s\n", afhi->tags.title);
@@ -982,21 +981,23 @@ out:
 
 static int make_status_items(void)
 {
-       struct ls_options opts = {
-               .flags = LS_FLAG_FULL_PATH | LS_FLAG_ADMISSIBLE_ONLY,
-               .mode = LS_MODE_VERBOSE,
-       };
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS);
+       char *argv[] = {"ls", "--admissible", "--listing-mode=verbose"};
+       struct ls_options opts = {.mode = LS_MODE_VERBOSE};
        struct para_buffer pb = {.max_size = shm_get_shmmax() - 1};
        time_t current_time;
        int ret;
 
+       ret = lls_parse(ARRAY_SIZE(argv), argv, cmd, &opts.lpr, NULL);
+       assert(ret >= 0);
        time(&current_time);
        ret = print_list_item(&status_item_ls_data, &opts, &pb, current_time);
        if (ret < 0)
-               return ret;
+               goto out;
        make_inode_status_items(&pb);
        free(status_items);
        status_items = pb.buf;
+
        memset(&pb, 0, sizeof(pb));
        pb.max_size = shm_get_shmmax() - 1;
        pb.flags = PBF_SIZE_PREFIX;
@@ -1009,7 +1010,10 @@ static int make_status_items(void)
        make_inode_status_items(&pb);
        free(parser_friendly_status_items);
        parser_friendly_status_items = pb.buf;
-       return 1;
+       ret = 1;
+out:
+       lls_free_parse_result(opts.lpr, cmd);
+       return ret;
 }
 
 /**
@@ -1056,8 +1060,15 @@ again:
        d->afhi.chunk_table = afd->afhi.chunk_table = NULL;
        ret = osl(osl_open_disk_object(audio_file_table, current_aft_row,
                AFTCOL_CHUNKS, &chunk_table_obj));
-       if (ret < 0)
-               return ret;
+       if (ret < 0) {
+               if (!afh_supports_dynamic_chunks(d->afsi.audio_format_id))
+                       return ret;
+               PARA_INFO_LOG("no chunk table for %s\n", d->path);
+               chunk_table_obj.data = NULL;
+               chunk_table_obj.size = 0;
+       } else {
+               PARA_INFO_LOG("chunk table: %zu bytes\n", chunk_table_obj.size);
+       }
        ret = mmap_full_file(d->path, O_RDONLY, &map.data, &map.size, &afd->fd);
        if (ret < 0)
                goto out;
@@ -1074,7 +1085,7 @@ again:
        save_afsi(&new_afsi, &afsi_obj); /* in-place update */
 
        afd->audio_format_id = d->afsi.audio_format_id;
-       load_chunk_table(&afd->afhi, chunk_table_obj.data);
+       load_chunk_table(&afd->afhi, &chunk_table_obj);
        aced.aft_row = current_aft_row;
        aced.old_afsi = &d->afsi;
        /*
@@ -1087,7 +1098,8 @@ again:
        ret = save_afd(afd);
 out:
        free(afd->afhi.chunk_table);
-       osl_close_disk_object(&chunk_table_obj);
+       if (chunk_table_obj.data)
+               osl_close_disk_object(&chunk_table_obj);
        if (ret < 0) {
                PARA_ERROR_LOG("%s: %s\n", d->path, para_strerror(-ret));
                ret = score_delete(current_aft_row);
@@ -1163,8 +1175,16 @@ static int ls_path_compare(const void *a, const void *b)
        return strcmp(d1->path, d2->path);
 }
 
+static inline bool admissible_only(struct ls_options *opts)
+{
+       return SERVER_CMD_OPT_GIVEN(LS, ADMISSIBLE, opts->lpr)
+               || opts->sorting == LS_SORT_BY_SCORE;
+}
+
 static int sort_matching_paths(struct ls_options *options)
 {
+       const struct lls_opt_result *r_b = SERVER_CMD_OPT_RESULT(LS, BASENAME,
+               options->lpr);
        size_t nmemb = options->num_matching_paths;
        size_t size = sizeof(*options->data_ptr);
        int (*compar)(const void *, const void *);
@@ -1175,13 +1195,13 @@ static int sort_matching_paths(struct ls_options *options)
                options->data_ptr[i] = options->data + i;
 
        /* In these cases the array is already sorted */
-       if (options->sorting == LS_SORT_BY_PATH
-               && !(options->flags & LS_FLAG_ADMISSIBLE_ONLY)
-               && (options->flags & LS_FLAG_FULL_PATH))
-               return 1;
-       if (options->sorting == LS_SORT_BY_SCORE &&
-                       options->flags & LS_FLAG_ADMISSIBLE_ONLY)
-               return 1;
+       if (admissible_only(options)) {
+               if (options->sorting == LS_SORT_BY_SCORE)
+                       return 1;
+       } else {
+               if (options->sorting == LS_SORT_BY_PATH && !lls_opt_given(r_b))
+                       return 1;
+       }
 
        switch (options->sorting) {
        case LS_SORT_BY_PATH:
@@ -1219,15 +1239,16 @@ static int prepare_ls_row(struct osl_row *row, void *ls_opts)
 {
        int ret, i;
        struct ls_options *options = ls_opts;
+       bool basename_given = SERVER_CMD_OPT_GIVEN(LS, BASENAME, options->lpr);
        struct ls_data *d;
        struct ls_widths *w;
        unsigned short num_digits;
-       unsigned tmp;
+       unsigned tmp, num_inputs;
        struct osl_row *aft_row;
        long score;
        char *path;
 
-       if (options->flags & LS_FLAG_ADMISSIBLE_ONLY) {
+       if (admissible_only(options)) {
                ret = get_score_and_aft_row(row, &score, &aft_row);
                if (ret < 0)
                        return ret;
@@ -1238,21 +1259,22 @@ static int prepare_ls_row(struct osl_row *row, void *ls_opts)
        ret = get_audio_file_path_of_row(aft_row, &path);
        if (ret < 0)
                return ret;
-       if (!(options->flags & LS_FLAG_FULL_PATH)) {
+       if (basename_given) {
                char *p = strrchr(path, '/');
                if (p)
                        path = p + 1;
        }
-       if (options->num_patterns) {
-               for (i = 0; i < options->num_patterns; i++) {
-                       ret = fnmatch(options->patterns[i], path, 0);
+       num_inputs = lls_num_inputs(options->lpr);
+       if (num_inputs > 0) {
+               for (i = 0; i < num_inputs; i++) {
+                       ret = fnmatch(lls_input(i, options->lpr), path, 0);
                        if (!ret)
                                break;
                        if (ret == FNM_NOMATCH)
                                continue;
                        return -E_FNMATCH;
                }
-               if (i >= options->num_patterns) /* no match */
+               if (i >= num_inputs) /* no match */
                        return 1;
        }
        tmp = options->num_matching_paths++;
@@ -1291,7 +1313,7 @@ static int prepare_ls_row(struct osl_row *row, void *ls_opts)
        w->amp_width = PARA_MAX(w->amp_width, num_digits);
        num_digits = strlen(audio_format_name(d->afsi.audio_format_id));
        w->audio_format_width = PARA_MAX(w->audio_format_width, num_digits);
-       if (options->flags & LS_FLAG_ADMISSIBLE_ONLY) {
+       if (admissible_only(options)) {
                GET_NUM_DIGITS(score, &num_digits);
                num_digits++; /* add one for the sign (space or "-") */
                w->score_width = PARA_MAX(w->score_width, num_digits);
@@ -1304,21 +1326,19 @@ err:
 
 static int com_ls_callback(struct afs_callback_arg *aca)
 {
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS);
        struct ls_options *opts = aca->query.data;
-       char *p, *pattern_start = (char *)aca->query.data + sizeof(*opts);
        int i = 0, ret;
        time_t current_time;
+       const struct lls_opt_result *r_r;
+
+       ret = lls_deserialize_parse_result(
+               (char *)aca->query.data + sizeof(*opts), cmd, &opts->lpr);
+       assert(ret >= 0);
+       r_r = SERVER_CMD_OPT_RESULT(LS, REVERSE, opts->lpr);
 
        aca->pbout.flags = (opts->mode == LS_MODE_PARSER)? PBF_SIZE_PREFIX : 0;
-       if (opts->num_patterns) {
-               opts->patterns = para_malloc(opts->num_patterns * sizeof(char *));
-               for (i = 0, p = pattern_start; i < opts->num_patterns; i++) {
-                       opts->patterns[i] = p;
-                       p += strlen(p) + 1;
-               }
-       } else
-               opts->patterns = NULL;
-       if (opts->flags & LS_FLAG_ADMISSIBLE_ONLY)
+       if (admissible_only(opts))
                ret = admissible_file_loop(opts, prepare_ls_row);
        else
                ret = osl(osl_rbtree_loop(audio_file_table, AFTCOL_PATH, opts,
@@ -1326,14 +1346,14 @@ static int com_ls_callback(struct afs_callback_arg *aca)
        if (ret < 0)
                goto out;
        if (opts->num_matching_paths == 0) {
-               ret = opts->num_patterns > 0? -E_NO_MATCH : 0;
+               ret = lls_num_inputs(opts->lpr) > 0? -E_NO_MATCH : 0;
                goto out;
        }
        ret = sort_matching_paths(opts);
        if (ret < 0)
                goto out;
        time(&current_time);
-       if (opts->flags & LS_FLAG_REVERSE)
+       if (lls_opt_given(r_r))
                for (i = opts->num_matching_paths - 1; i >= 0; i--) {
                        ret = print_list_item(opts->data_ptr[i], opts,
                                &aca->pbout, current_time);
@@ -1348,143 +1368,90 @@ static int com_ls_callback(struct afs_callback_arg *aca)
                                goto out;
                }
 out:
+       lls_free_parse_result(opts->lpr, cmd);
        free(opts->data);
        free(opts->data_ptr);
-       free(opts->patterns);
        return ret;
 }
 
-/*
- * TODO: flags -h (sort by hash)
- */
-int com_ls(struct command_context *cc)
+/* TODO: flags -h (sort by hash) */
+static int com_ls(struct command_context *cc, struct lls_parse_result *lpr)
 {
-       int i;
-       unsigned flags = 0;
-       enum ls_sorting_method sort = LS_SORT_BY_PATH;
-       enum ls_listing_mode mode = LS_MODE_SHORT;
-       struct ls_options opts = {.patterns = NULL};
-       struct osl_object query = {.data = &opts, .size = sizeof(opts)};
-
-       for (i = 1; i < cc->argc; i++) {
-               const char *arg = cc->argv[i];
-               if (arg[0] != '-')
-                       break;
-               if (!strcmp(arg, "--")) {
-                       i++;
-                       break;
-               }
-               /*
-                * Compatibility: Prior to 0.5.5 it was necessary to specify
-                * the listing mode without the '=' character as in -lv, for
-                * example. Now the variant with '=' is preferred and
-                * documented but we still accept the old way to specify the
-                * listing mode.
-                *
-                * Support for the legacy syntax can be dropped at 0.6.0
-                * or later.
-                */
-               if (!strncmp(arg, "-l", 2)) {
-                       arg += 2;
-                       if (*arg == '=')
-                               arg++;
-                       switch (*arg) {
-                       case 's':
-                               mode = LS_MODE_SHORT;
-                               continue;
-                       case 'l':
-                       case '\0':
-                               mode = LS_MODE_LONG;
-                               continue;
-                       case 'v':
-                               mode = LS_MODE_VERBOSE;
-                               continue;
-                       case 'm':
-                               mode = LS_MODE_MBOX;
-                               continue;
-                       case 'c':
-                               mode = LS_MODE_CHUNKS;
-                               continue;
-                       case 'p':
-                               mode = LS_MODE_PARSER;
-                               continue;
-                       default:
-                               return -E_AFT_SYNTAX;
-                       }
-               }
-               if (!strcmp(arg, "-p") || !strcmp(arg, "-F")) {
-                       flags |= LS_FLAG_FULL_PATH;
-                       continue;
-               }
-               if (!strcmp(arg, "-b")) {
-                       flags &= ~LS_FLAG_FULL_PATH;
-                       continue;
-               }
-               if (!strcmp(arg, "-a")) {
-                       flags |= LS_FLAG_ADMISSIBLE_ONLY;
-                       continue;
-               }
-               if (!strcmp(arg, "-r")) {
-                       flags |= LS_FLAG_REVERSE;
-                       continue;
-               }
-               if (!strcmp(arg, "-d")) {
-                       flags |= LS_FLAG_UNIXDATE;
-                       continue;
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(LS);
+       struct ls_options *opts;
+       struct osl_object query;
+       const struct lls_opt_result *r_l = SERVER_CMD_OPT_RESULT(LS, LISTING_MODE,
+               lpr);
+       const struct lls_opt_result *r_s = SERVER_CMD_OPT_RESULT(LS, SORT, lpr);
+       int ret;
+       char *slpr;
+
+       ret = lls_serialize_parse_result(lpr, cmd, NULL, &query.size);
+       assert(ret >= 0);
+       query.size += sizeof(*opts);
+       query.data = para_malloc(query.size);
+       opts = query.data;
+       memset(opts, 0, sizeof(*opts));
+       slpr = query.data + sizeof(*opts);
+       ret = lls_serialize_parse_result(lpr, cmd, &slpr, NULL);
+       assert(ret >= 0);
+       opts->mode = LS_MODE_SHORT;
+       opts->sorting = LS_SORT_BY_PATH;
+       if (lls_opt_given(r_l)) {
+               const char *val = lls_string_val(0, r_l);
+               if (!strcmp(val, "l") || !strcmp(val, "long"))
+                       opts->mode = LS_MODE_LONG;
+               else if (!strcmp(val, "s") || !strcmp(val, "short"))
+                       opts->mode = LS_MODE_SHORT;
+               else if (!strcmp(val, "v") || !strcmp(val, "verbose"))
+                       opts->mode = LS_MODE_VERBOSE;
+               else if (!strcmp(val, "m") || !strcmp(val, "mbox"))
+                       opts->mode = LS_MODE_MBOX;
+               else if (!strcmp(val, "c") || !strcmp(val, "chunk-table"))
+                       opts->mode = LS_MODE_MBOX;
+               else if (!strcmp(val, "p") || !strcmp(val, "parser-friendly"))
+                       opts->mode = LS_MODE_PARSER;
+               else {
+                       ret = -E_AFT_SYNTAX;
+                       goto out;
                }
-               /* The compatibility remark above applies also to -s. */
-               if (!strncmp(arg, "-s", 2)) {
-                       arg += 2;
-                       if (*arg == '=')
-                               arg++;
-                       switch (*arg) {
-                       case 'p':
-                               sort = LS_SORT_BY_PATH;
-                               continue;
-                       case 's': /* -ss implies -a */
-                               sort = LS_SORT_BY_SCORE;
-                               flags |= LS_FLAG_ADMISSIBLE_ONLY;
-                               continue;
-                       case 'l':
-                               sort = LS_SORT_BY_LAST_PLAYED;
-                               continue;
-                       case 'n':
-                               sort = LS_SORT_BY_NUM_PLAYED;
-                               continue;
-                       case 'f':
-                               sort = LS_SORT_BY_FREQUENCY;
-                               continue;
-                       case 'c':
-                               sort = LS_SORT_BY_CHANNELS;
-                               continue;
-                       case 'i':
-                               sort = LS_SORT_BY_IMAGE_ID;
-                               continue;
-                       case 'y':
-                               sort = LS_SORT_BY_LYRICS_ID;
-                               continue;
-                       case 'b':
-                               sort = LS_SORT_BY_BITRATE;
-                               continue;
-                       case 'd':
-                               sort = LS_SORT_BY_DURATION;
-                               continue;
-                       case 'a':
-                               sort = LS_SORT_BY_AUDIO_FORMAT;
-                               continue;
-                       default:
-                               return -E_AFT_SYNTAX;
-                       }
+       }
+       if (lls_opt_given(r_s)) {
+               const char *val = lls_string_val(0, r_s);
+               if (!strcmp(val, "p") || !strcmp(val, "path"))
+                       opts->sorting = LS_SORT_BY_PATH;
+               else if (!strcmp(val, "s") || !strcmp(val, "score"))
+                       opts->sorting = LS_SORT_BY_SCORE;
+               else if (!strcmp(val, "l") || !strcmp(val, "lastplayed"))
+                       opts->sorting = LS_SORT_BY_LAST_PLAYED;
+               else if (!strcmp(val, "n") || !strcmp(val, "numplayed"))
+                       opts->sorting = LS_SORT_BY_NUM_PLAYED;
+               else if (!strcmp(val, "f") || !strcmp(val, "frquency"))
+                       opts->sorting = LS_SORT_BY_FREQUENCY;
+               else if (!strcmp(val, "c") || !strcmp(val, "channels"))
+                       opts->sorting = LS_SORT_BY_CHANNELS;
+               else if (!strcmp(val, "i") || !strcmp(val, "image-id"))
+                       opts->sorting = LS_SORT_BY_IMAGE_ID;
+               else if (!strcmp(val, "y") || !strcmp(val, "lyrics-id"))
+                       opts->sorting = LS_SORT_BY_LYRICS_ID;
+               else if (!strcmp(val, "b") || !strcmp(val, "bitrate"))
+                       opts->sorting = LS_SORT_BY_BITRATE;
+               else if (!strcmp(val, "d") || !strcmp(val, "duration"))
+                       opts->sorting = LS_SORT_BY_DURATION;
+               else if (!strcmp(val, "a") || !strcmp(val, "audio-format"))
+                       opts->sorting = LS_SORT_BY_AUDIO_FORMAT;
+               else {
+                       ret = -E_AFT_SYNTAX;
+                       goto out;
                }
-               return -E_AFT_SYNTAX;
        }
-       opts.flags = flags;
-       opts.sorting = sort;
-       opts.mode = mode;
-       opts.num_patterns = cc->argc - i;
-       return send_option_arg_callback_request(&query, opts.num_patterns,
-               cc->argv + i, com_ls_callback, afs_cb_result_handler, cc);
+       ret = send_callback_request(com_ls_callback, &query,
+               afs_cb_result_handler, cc);
+out:
+       free(query.data);
+       return ret;
 }
+EXPORT_SERVER_CMD_HANDLER(ls);
 
 /**
  * Call the given function for each file in the audio file table.
@@ -1734,6 +1701,7 @@ static int com_add_callback(struct afs_callback_arg *aca)
                        &objs[AFTCOL_AFHI]));
                if (ret < 0)
                        goto out;
+               /* truncate the file to size zero if there is no chunk table */
                ret = osl(osl_update_object(audio_file_table, row, AFTCOL_CHUNKS,
                        &objs[AFTCOL_CHUNKS]));
                if (ret < 0)
@@ -1956,33 +1924,26 @@ enum touch_flags {
        TOUCH_FLAG_VERBOSE = 2
 };
 
-/** Options used by com_touch(). */
-struct com_touch_options {
-       /** New num_played value. */
-       int32_t num_played;
-       /** New last played count. */
-       int64_t last_played;
-       /** New lyrics id. */
-       int32_t lyrics_id;
-       /** New image id. */
-       int32_t image_id;
-       /** New amplification value. */
-       int32_t amp;
-       /** Command line flags (see \ref touch_flags). */
-       unsigned flags;
-};
-
 static int touch_audio_file(__a_unused struct osl_table *table,
                struct osl_row *row, const char *name, void *data)
 {
        struct afs_callback_arg *aca = data;
-       struct com_touch_options *cto = aca->query.data;
+       bool v_given = SERVER_CMD_OPT_GIVEN(TOUCH, VERBOSE, aca->lpr);
+       const struct lls_opt_result *r_n, *r_l, *r_i, *r_y, *r_a;
+       int ret;
        struct osl_object obj;
        struct afs_info old_afsi, new_afsi;
-       int ret, no_options = cto->num_played < 0 && cto->last_played < 0 &&
-               cto->lyrics_id < 0 && cto->image_id < 0 && cto->amp < 0;
+       bool no_options;
        struct afsi_change_event_data aced;
 
+       r_n = SERVER_CMD_OPT_RESULT(TOUCH, NUMPLAYED, aca->lpr);
+       r_l = SERVER_CMD_OPT_RESULT(TOUCH, LASTPLAYED, aca->lpr);
+       r_i = SERVER_CMD_OPT_RESULT(TOUCH, IMAGE_ID, aca->lpr);
+       r_y = SERVER_CMD_OPT_RESULT(TOUCH, LYRICS_ID, aca->lpr);
+       r_a = SERVER_CMD_OPT_RESULT(TOUCH, AMP, aca->lpr);
+       no_options = !lls_opt_given(r_n) && !lls_opt_given(r_l) && !lls_opt_given(r_i)
+               && !lls_opt_given(r_y) && !lls_opt_given(r_a);
+
        ret = get_afsi_object_of_row(row, &obj);
        if (ret < 0) {
                para_printf(&aca->pbout, "cannot touch %s\n", name);
@@ -1997,23 +1958,23 @@ static int touch_audio_file(__a_unused struct osl_table *table,
        if (no_options) {
                new_afsi.num_played++;
                new_afsi.last_played = time(NULL);
-               if (cto->flags & TOUCH_FLAG_VERBOSE)
+               if (v_given)
                        para_printf(&aca->pbout, "%s: num_played = %u, "
                                "last_played = now()\n", name,
                                new_afsi.num_played);
        } else {
-               if (cto->flags & TOUCH_FLAG_VERBOSE)
+               if (lls_opt_given(r_l))
+                       new_afsi.last_played = lls_uint64_val(0, r_l);
+               if (lls_opt_given(r_n))
+                       new_afsi.num_played = lls_uint32_val(0, r_n);
+               if (lls_opt_given(r_i))
+                       new_afsi.image_id = lls_uint32_val(0, r_i);
+               if (lls_opt_given(r_y))
+                       new_afsi.lyrics_id = lls_uint32_val(0, r_y);
+               if (lls_opt_given(r_a))
+                       new_afsi.amp = lls_uint32_val(0, r_a);
+               if (v_given)
                        para_printf(&aca->pbout, "touching %s\n", name);
-               if (cto->lyrics_id >= 0)
-                       new_afsi.lyrics_id = cto->lyrics_id;
-               if (cto->image_id >= 0)
-                       new_afsi.image_id = cto->image_id;
-               if (cto->num_played >= 0)
-                       new_afsi.num_played = cto->num_played;
-               if (cto->last_played >= 0)
-                       new_afsi.last_played = cto->last_played;
-               if (cto->amp >= 0)
-                       new_afsi.amp = cto->amp;
        }
        save_afsi(&new_afsi, &obj); /* in-place update */
        aced.aft_row = row;
@@ -2023,134 +1984,73 @@ static int touch_audio_file(__a_unused struct osl_table *table,
 
 static int com_touch_callback(struct afs_callback_arg *aca)
 {
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(TOUCH);
+       bool p_given;
+       const struct lls_opt_result *r_i, *r_y;
        int ret;
-       struct com_touch_options *cto = aca->query.data;
        struct pattern_match_data pmd = {
                .table = audio_file_table,
                .loop_col_num = AFTCOL_HASH,
                .match_col_num = AFTCOL_PATH,
-               .patterns = {
-                       .data = (char *)aca->query.data
-                               + sizeof(struct com_touch_options),
-                       .size = aca->query.size
-                               - sizeof(struct com_touch_options)
-               },
                .data = aca,
                .action = touch_audio_file
        };
-       if (cto->image_id >= 0) {
-               ret = img_get_name_by_id(cto->image_id, NULL);
+
+       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
+       assert(ret >= 0);
+       pmd.lpr = aca->lpr;
+
+       r_i = SERVER_CMD_OPT_RESULT(TOUCH, IMAGE_ID, aca->lpr);
+       if (lls_opt_given(r_i)) {
+               uint32_t id = lls_uint32_val(0, r_i);
+               ret = img_get_name_by_id(id, NULL);
                if (ret < 0) {
-                       para_printf(&aca->pbout, "invalid image ID: %d\n",
-                               cto->image_id);
+                       para_printf(&aca->pbout, "invalid image ID: %u\n", id);
                        return ret;
                }
        }
-       if (cto->lyrics_id >= 0) {
-               ret = lyr_get_name_by_id(cto->lyrics_id, NULL);
+       r_y = SERVER_CMD_OPT_RESULT(TOUCH, LYRICS_ID, aca->lpr);
+       if (lls_opt_given(r_y)) {
+               uint32_t id = lls_uint32_val(0, r_y);
+               ret = lyr_get_name_by_id(id, NULL);
                if (ret < 0) {
-                       para_printf(&aca->pbout, "invalid lyrics ID: %d\n",
-                               cto->lyrics_id);
+                       para_printf(&aca->pbout, "invalid lyrics ID: %u\n", id);
                        return ret;
                }
        }
-       if (cto->flags & TOUCH_FLAG_FNM_PATHNAME)
+       p_given = SERVER_CMD_OPT_GIVEN(TOUCH, PATHNAME_MATCH, aca->lpr);
+       if (p_given)
                pmd.fnmatch_flags |= FNM_PATHNAME;
        ret = for_each_matching_row(&pmd);
        if (ret >= 0 && pmd.num_matches == 0)
                ret = -E_NO_MATCH;
+       lls_free_parse_result(aca->lpr, cmd);
        return ret;
 }
 
-int com_touch(struct command_context *cc)
+static int com_touch(struct command_context *cc, struct lls_parse_result *lpr)
 {
-       struct com_touch_options cto = {
-               .num_played = -1,
-               .last_played = -1,
-               .lyrics_id = -1,
-               .image_id = -1,
-               .amp = -1,
-       };
-       struct osl_object query = {.data = &cto, .size = sizeof(cto)};
-       int i, ret;
-
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(TOUCH);
+       int ret;
+       char *errctx;
 
-       for (i = 1; i < cc->argc; i++) {
-               const char *arg = cc->argv[i];
-               if (arg[0] != '-')
-                       break;
-               if (!strcmp(arg, "--")) {
-                       i++;
-                       break;
-               }
-               if (!strncmp(arg, "-n=", 3)) {
-                       ret = para_atoi32(arg + 3, &cto.num_played);
-                       if (ret < 0)
-                               return ret;
-                       continue;
-               }
-               if (!strncmp(arg, "-l=", 3)) {
-                       ret = para_atoi64(arg + 3, &cto.last_played);
-                       if (ret < 0)
-                               return ret;
-                       continue;
-               }
-               if (!strncmp(arg, "-y=", 3)) {
-                       ret = para_atoi32(arg + 3, &cto.lyrics_id);
-                       if (ret < 0)
-                               return ret;
-                       continue;
-               }
-               if (!strncmp(arg, "-i=", 3)) {
-                       ret = para_atoi32(arg + 3, &cto.image_id);
-                       if (ret < 0)
-                               return ret;
-                       continue;
-               }
-               if (!strncmp(arg, "-a=", 3)) {
-                       int32_t val;
-                       ret = para_atoi32(arg + 3, &val);
-                       if (ret < 0)
-                               return ret;
-                       if (val < 0 || val > 255)
-                               return -ERRNO_TO_PARA_ERROR(EINVAL);
-                       cto.amp = val;
-                       continue;
-               }
-               if (!strcmp(arg, "-p")) {
-                       cto.flags |= TOUCH_FLAG_FNM_PATHNAME;
-                       continue;
-               }
-               if (!strcmp(arg, "-v")) {
-                       cto.flags |= TOUCH_FLAG_VERBOSE;
-                       continue;
-               }
-               break; /* non-option starting with dash */
+       ret = lls(lls_check_arg_count(lpr, 1, INT_MAX, &errctx));
+       if (ret < 0) {
+               send_errctx(cc, errctx);
+               return ret;
        }
-       if (i >= cc->argc)
-               return -E_AFT_SYNTAX;
-       return send_option_arg_callback_request(&query, cc->argc - i,
-               cc->argv + i, com_touch_callback, afs_cb_result_handler, cc);
-}
-
-/** Flags for com_rm(). */
-enum rm_flags {
-       /** -v */
-       RM_FLAG_VERBOSE = 1,
-       /** -f */
-       RM_FLAG_FORCE = 2,
-       /** -p */
-       RM_FLAG_FNM_PATHNAME = 4
-};
+       return send_lls_callback_request(com_touch_callback, cmd, lpr, cc);
+}
+EXPORT_SERVER_CMD_HANDLER(touch);
 
 static int remove_audio_file(__a_unused struct osl_table *table,
                struct osl_row *row, const char *name, void *data)
 {
        struct afs_callback_arg *aca = data;
-       uint32_t flags =*(uint32_t *)aca->query.data;
+       bool v_given = SERVER_CMD_OPT_GIVEN(RM, VERBOSE, aca->lpr);
        int ret;
 
-       if (flags & RM_FLAG_VERBOSE)
+       if (v_given)
                para_printf(&aca->pbout, "removing %s\n", name);
        ret = afs_event(AUDIO_FILE_REMOVE, &aca->pbout, row);
        if (ret < 0)
@@ -2163,95 +2063,63 @@ static int remove_audio_file(__a_unused struct osl_table *table,
 
 static int com_rm_callback(struct afs_callback_arg *aca)
 {
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(RM);
        int ret;
-       uint32_t flags = *(uint32_t *)aca->query.data;
        struct pattern_match_data pmd = {
                .table = audio_file_table,
                .loop_col_num = AFTCOL_HASH,
                .match_col_num = AFTCOL_PATH,
-               .patterns = {.data = (char *)aca->query.data + sizeof(uint32_t),
-                       .size = aca->query.size - sizeof(uint32_t)},
                .data = aca,
                .action = remove_audio_file
        };
-       if (flags & RM_FLAG_FNM_PATHNAME)
+       bool v_given, p_given, f_given;
+
+       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
+       assert(ret >= 0);
+       pmd.lpr = aca->lpr;
+       v_given = SERVER_CMD_OPT_GIVEN(RM, VERBOSE, aca->lpr);
+       p_given = SERVER_CMD_OPT_GIVEN(RM, PATHNAME_MATCH, aca->lpr);
+       f_given = SERVER_CMD_OPT_GIVEN(RM, FORCE, aca->lpr);
+
+       if (p_given)
                pmd.fnmatch_flags |= FNM_PATHNAME;
        ret = for_each_matching_row(&pmd);
        if (ret < 0)
                goto out;
        if (pmd.num_matches == 0) {
-               if (!(flags & RM_FLAG_FORCE))
+               if (!f_given)
                        ret = -E_NO_MATCH;
-       } else if (flags & RM_FLAG_VERBOSE)
+       } else if (v_given)
                para_printf(&aca->pbout, "removed %u file(s)\n",
                        pmd.num_matches);
 out:
+       lls_free_parse_result(aca->lpr, cmd);
        return ret;
 }
 
 /* TODO options: -r (recursive) */
-int com_rm(struct command_context *cc)
+static int com_rm(struct command_context *cc, struct lls_parse_result *lpr)
 {
-       uint32_t flags = 0;
-       struct osl_object query = {.data = &flags, .size = sizeof(flags)};
-       int i;
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(RM);
+       char *errctx;
+       int ret;
 
-       for (i = 1; i < cc->argc; i++) {
-               const char *arg = cc->argv[i];
-               if (arg[0] != '-')
-                       break;
-               if (!strcmp(arg, "--")) {
-                       i++;
-                       break;
-               }
-               if (!strcmp(arg, "-f")) {
-                       flags |= RM_FLAG_FORCE;
-                       continue;
-               }
-               if (!strcmp(arg, "-p")) {
-                       flags |= RM_FLAG_FNM_PATHNAME;
-                       continue;
-               }
-               if (!strcmp(arg, "-v")) {
-                       flags |= RM_FLAG_VERBOSE;
-                       continue;
-               }
-               break;
+       ret = lls(lls_check_arg_count(lpr, 1, INT_MAX, &errctx));
+       if (ret < 0) {
+               send_errctx(cc, errctx);
+               return ret;
        }
-       if (i >= cc->argc)
-               return -E_AFT_SYNTAX;
-       return send_option_arg_callback_request(&query, cc->argc - i,
-               cc->argv + i, com_rm_callback, afs_cb_result_handler, cc);
+       return send_lls_callback_request(com_rm_callback, cmd, lpr, cc);
 }
-
-/**
- * Flags used by the cpsi command.
- *
- * \sa com_cpsi().
- */
-enum cpsi_flags {
-       /** Whether the lyrics id should be copied. */
-       CPSI_FLAG_COPY_LYRICS_ID = 1,
-       /** Whether the image id should be copied. */
-       CPSI_FLAG_COPY_IMAGE_ID = 2,
-       /** Whether the lastplayed time should be copied. */
-       CPSI_FLAG_COPY_LASTPLAYED = 4,
-       /** Whether the numplayed count should be copied. */
-       CPSI_FLAG_COPY_NUMPLAYED = 8,
-       /** Whether the attributes should be copied. */
-       CPSI_FLAG_COPY_ATTRIBUTES = 16,
-       /** Activates verbose mode. */
-       CPSI_FLAG_VERBOSE = 32,
-};
+EXPORT_SERVER_CMD_HANDLER(rm);
 
 /** Data passed to the action handler of com_cpsi(). */
 struct cpsi_action_data {
-       /** command line flags (see \ref cpsi_flags). */
-       unsigned flags;
        /** Values are copied from here. */
        struct afs_info source_afsi;
        /** What was passed to com_cpsi_callback(). */
        struct afs_callback_arg *aca;
+       bool copy_all;
 };
 
 static int copy_selector_info(__a_unused struct osl_table *table,
@@ -2262,24 +2130,34 @@ static int copy_selector_info(__a_unused struct osl_table *table,
        int ret;
        struct afs_info old_afsi, target_afsi;
        struct afsi_change_event_data aced;
+       bool a_given, y_given, i_given, l_given, n_given, v_given;
+
+       a_given = SERVER_CMD_OPT_GIVEN(CPSI, ATTRIBUTE_BITMAP, cad->aca->lpr);
+       y_given = SERVER_CMD_OPT_GIVEN(CPSI, LYRICS_ID, cad->aca->lpr);
+       i_given = SERVER_CMD_OPT_GIVEN(CPSI, IMAGE_ID, cad->aca->lpr);
+       l_given = SERVER_CMD_OPT_GIVEN(CPSI, LASTPLAYED, cad->aca->lpr);
+       n_given = SERVER_CMD_OPT_GIVEN(CPSI, NUMPLAYED, cad->aca->lpr);
+       v_given = SERVER_CMD_OPT_GIVEN(CPSI, VERBOSE, cad->aca->lpr);
 
        ret = get_afsi_object_of_row(row, &target_afsi_obj);
        if (ret < 0)
                return ret;
-       load_afsi(&target_afsi, &target_afsi_obj);
+       ret = load_afsi(&target_afsi, &target_afsi_obj);
+       if (ret < 0)
+               return ret;
        old_afsi = target_afsi;
-       if (cad->flags & CPSI_FLAG_COPY_LYRICS_ID)
+       if (cad->copy_all || y_given)
                target_afsi.lyrics_id = cad->source_afsi.lyrics_id;
-       if (cad->flags & CPSI_FLAG_COPY_IMAGE_ID)
+       if (cad->copy_all || i_given)
                target_afsi.image_id = cad->source_afsi.image_id;
-       if (cad->flags & CPSI_FLAG_COPY_LASTPLAYED)
+       if (cad->copy_all || l_given)
                target_afsi.last_played = cad->source_afsi.last_played;
-       if (cad->flags & CPSI_FLAG_COPY_NUMPLAYED)
+       if (cad->copy_all || n_given)
                target_afsi.num_played = cad->source_afsi.num_played;
-       if (cad->flags & CPSI_FLAG_COPY_ATTRIBUTES)
+       if (cad->copy_all || a_given)
                target_afsi.attributes = cad->source_afsi.attributes;
        save_afsi(&target_afsi, &target_afsi_obj); /* in-place update */
-       if (cad->flags & CPSI_FLAG_VERBOSE)
+       if (v_given)
                para_printf(&cad->aca->pbout, "copied afsi to %s\n", name);
        aced.aft_row = row;
        aced.old_afsi = &old_afsi;
@@ -2288,86 +2166,60 @@ static int copy_selector_info(__a_unused struct osl_table *table,
 
 static int com_cpsi_callback(struct afs_callback_arg *aca)
 {
-       struct cpsi_action_data cad = {
-               .flags = *(unsigned *)aca->query.data,
-               .aca = aca
-       };
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(CPSI);
+       bool a_given, y_given, i_given, l_given, n_given, v_given;
+       struct cpsi_action_data cad = {.aca = aca};
        int ret;
-       char *source_path = (char *)aca->query.data + sizeof(cad.flags);
        struct pattern_match_data pmd = {
                .table = audio_file_table,
                .loop_col_num = AFTCOL_HASH,
                .match_col_num = AFTCOL_PATH,
-               .patterns = {.data = source_path + strlen(source_path) + 1,
-                       .size = aca->query.size - sizeof(cad.flags)
-                               - strlen(source_path) - 1},
+               .input_skip = 1, /* skip first argument (source file) */
                .data = &cad,
                .action = copy_selector_info
        };
 
-       ret = get_afsi_of_path(source_path, &cad.source_afsi);
+       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
+       assert(ret >= 0);
+       pmd.lpr = aca->lpr;
+
+       a_given = SERVER_CMD_OPT_GIVEN(CPSI, ATTRIBUTE_BITMAP, aca->lpr);
+       y_given = SERVER_CMD_OPT_GIVEN(CPSI, LYRICS_ID, aca->lpr);
+       i_given = SERVER_CMD_OPT_GIVEN(CPSI, IMAGE_ID, aca->lpr);
+       l_given = SERVER_CMD_OPT_GIVEN(CPSI, LASTPLAYED, aca->lpr);
+       n_given = SERVER_CMD_OPT_GIVEN(CPSI, NUMPLAYED, aca->lpr);
+       v_given = SERVER_CMD_OPT_GIVEN(CPSI, VERBOSE, aca->lpr);
+       cad.copy_all = !a_given && !y_given && !i_given && !l_given && !n_given;
+
+       ret = get_afsi_of_path(lls_input(0, aca->lpr), &cad.source_afsi);
        if (ret < 0)
                goto out;
        ret = for_each_matching_row(&pmd);
        if (ret < 0)
                goto out;
        if (pmd.num_matches > 0) {
-               if (cad.flags & CPSI_FLAG_VERBOSE)
+               if (v_given)
                        para_printf(&aca->pbout, "updated afsi of %u file(s)\n",
                                pmd.num_matches);
        } else
                ret = -E_NO_MATCH;
 out:
+       lls_free_parse_result(aca->lpr, cmd);
        return ret;
 }
 
-int com_cpsi(struct command_context *cc)
+static int com_cpsi(struct command_context *cc, struct lls_parse_result *lpr)
 {
-       unsigned flags = 0;
-       int i;
-       struct osl_object options = {.data = &flags, .size = sizeof(flags)};
-
-       for (i = 1; i < cc->argc; i++) {
-               const char *arg = cc->argv[i];
-               if (arg[0] != '-')
-                       break;
-               if (!strcmp(arg, "--")) {
-                       i++;
-                       break;
-               }
-               if (!strcmp(arg, "-y")) {
-                       flags |= CPSI_FLAG_COPY_LYRICS_ID;
-                       continue;
-               }
-               if (!strcmp(arg, "-i")) {
-                       flags |= CPSI_FLAG_COPY_IMAGE_ID;
-                       continue;
-               }
-               if (!strcmp(arg, "-l")) {
-                       flags |= CPSI_FLAG_COPY_LASTPLAYED;
-                       continue;
-               }
-               if (!strcmp(arg, "-n")) {
-                       flags |= CPSI_FLAG_COPY_NUMPLAYED;
-                       continue;
-               }
-               if (!strcmp(arg, "-a")) {
-                       flags |= CPSI_FLAG_COPY_ATTRIBUTES;
-                       continue;
-               }
-               if (!strcmp(arg, "-v")) {
-                       flags |= CPSI_FLAG_VERBOSE;
-                       continue;
-               }
-               break;
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(CPSI);
+       char *errctx;
+       int ret = lls(lls_check_arg_count(lpr, 2, INT_MAX, &errctx));
+       if (ret < 0) {
+               send_errctx(cc, errctx);
+               return ret;
        }
-       if (i + 1 >= cc->argc) /* need at least source file and pattern */
-               return -E_AFT_SYNTAX;
-       if (!(flags & ~CPSI_FLAG_VERBOSE)) /* no copy flags given */
-               flags = ~(unsigned)CPSI_FLAG_VERBOSE | flags;
-       return send_option_arg_callback_request(&options, cc->argc - i,
-               cc->argv + i, com_cpsi_callback, afs_cb_result_handler, cc);
+       return send_lls_callback_request(com_cpsi_callback, cmd, lpr, cc);
 }
+EXPORT_SERVER_CMD_HANDLER(cpsi);
 
 struct change_atts_data {
        uint64_t add_mask, del_mask;
@@ -2401,9 +2253,8 @@ static int change_atts(__a_unused struct osl_table *table,
 
 static int com_setatt_callback(struct afs_callback_arg *aca)
 {
-       char *p;
-       int ret;
-       size_t len;
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SETATT);
+       int i, ret;
        struct change_atts_data cad = {.aca = aca};
        struct pattern_match_data pmd = {
                .table = audio_file_table,
@@ -2413,27 +2264,36 @@ static int com_setatt_callback(struct afs_callback_arg *aca)
                .data = &cad,
                .action = change_atts
        };
+       unsigned num_inputs;
 
-       for (
-               p = aca->query.data;
-               p < (char *)aca->query.data + aca->query.size;
-               p += len + 1
-       ) {
-               char c;
+       ret = lls_deserialize_parse_result(aca->query.data, cmd, &aca->lpr);
+       assert(ret >= 0);
+       pmd.lpr = aca->lpr;
+
+       num_inputs = lls_num_inputs(aca->lpr);
+       for (i = 0; i < num_inputs; i++) {
                unsigned char bitnum;
                uint64_t one = 1;
+               const char *arg = lls_input(i, aca->lpr);
+               char c, *p;
+               size_t len = strlen(arg);
 
-               len = strlen(p);
                ret = -E_ATTR_SYNTAX;
                if (len == 0)
                        goto out;
-               c = p[len - 1];
-               if (c != '+' && c != '-')
-                       break;
+               c = arg[len - 1];
+               if (c != '+' && c != '-') {
+                       if (cad.add_mask == 0 && cad.del_mask == 0)
+                               goto out; /* no attribute modifier given */
+                       goto set_atts;
+               }
+               p = para_malloc(len);
+               memcpy(p, arg, len - 1);
                p[len - 1] = '\0';
                ret = get_attribute_bitnum_by_name(p, &bitnum);
+               free(p);
                if (ret < 0) {
-                       para_printf(&aca->pbout, "attribute not found: %s\n", p);
+                       para_printf(&aca->pbout, "invalid argument: %s\n", arg);
                        goto out;
                }
                if (c == '+')
@@ -2441,29 +2301,32 @@ static int com_setatt_callback(struct afs_callback_arg *aca)
                else
                        cad.del_mask |= (one << bitnum);
        }
+       /* no pattern given */
        ret = -E_ATTR_SYNTAX;
-       if (!cad.add_mask && !cad.del_mask)
-               goto out;
-       pmd.patterns.data = p;
-       if (p >= (char *)aca->query.data + aca->query.size)
-               goto out;
-       pmd.patterns.size = (char *)aca->query.data + aca->query.size - p;
+       goto out;
+set_atts:
+       pmd.input_skip = i;
        ret = for_each_matching_row(&pmd);
-       if (ret < 0)
-               goto out;
-       if (pmd.num_matches == 0)
+       if (ret >= 0 && pmd.num_matches == 0)
                ret = -E_NO_MATCH;
 out:
+       lls_free_parse_result(aca->lpr, cmd);
        return ret;
 }
 
-int com_setatt(struct command_context *cc)
+static int com_setatt(struct command_context *cc, struct lls_parse_result *lpr)
 {
-       if (cc->argc < 3)
-               return -E_ATTR_SYNTAX;
-       return send_standard_callback_request(cc->argc - 1, cc->argv + 1,
-               com_setatt_callback, afs_cb_result_handler, cc);
+       const struct lls_command *cmd = SERVER_CMD_CMD_PTR(SETATT);
+       char *errctx;
+       int ret = lls(lls_check_arg_count(lpr, 2, INT_MAX, &errctx));
+
+       if (ret < 0) {
+               send_errctx(cc, errctx);
+               return ret;
+       }
+       return send_lls_callback_request(com_setatt_callback, cmd, lpr, cc);
 }
+EXPORT_SERVER_CMD_HANDLER(setatt);
 
 static int afs_stat_callback(struct afs_callback_arg *aca)
 {