aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2009-02-07 16:12:03 +0200
committerSergey Poznyakoff <gray@gnu.org.ua>2009-02-07 16:12:03 +0200
commit0efd765588da43ec8cc08c5eabde49254eeb44b8 (patch)
treeae43654b9e17c2e4008af9a3520de0bc6afe1c48 /src
parent9b5fb2470499d0d054e6dfab55611057d669ad76 (diff)
downloadidest-0efd765588da43ec8cc08c5eabde49254eeb44b8.tar.gz
idest-0efd765588da43ec8cc08c5eabde49254eeb44b8.tar.bz2
Implement ID3v2 read-only access.
* .gitignore: Add core. * gnulib.modules: Add argmatch and linked-list. * src/.gitignore: Add .gdbinit * src/slist.c: New file * src/Makefile.am (id3ed_SOURCES): Add new files. * src/cmdline.opt: Remove all options, except -q and --latin1. Add new options: --all and --set. * src/getopt.m4 (BEGIN): Fix handling of optional arguments. * src/id3ed.h: Include xalloc.h, gl_linked_list.h and argmatch.h (v1_block,title,artist,album,year,comment,track,genre): Remove. (DEFAULT_ED_LIST): New define. (set_id3v1): Takes 2 arguments. (string_list_action_fn): new data type. (new_string_list,do_string_list,concat_string_list) (print_string_list): New prototypes. (ed_list_add_item,ed_list_print,ed_list_add_assignment): New prototypes. * src/id3v1.c (id3v1_read,id3v1_write): Bugfixes. (set_id3v1): Take a pointer to id3v1_block as 2nd argument. * src/id3v2.c: Implement show_tags. * src/main.c: Rewrite.
Diffstat (limited to 'src')
-rw-r--r--src/.gitignore1
-rw-r--r--src/Makefile.am9
-rw-r--r--src/cmdline.opt79
-rw-r--r--src/getopt.m42
-rw-r--r--src/id3ed.h31
-rw-r--r--src/id3v1.c13
-rw-r--r--src/id3v2.c92
-rw-r--r--src/main.c339
-rw-r--r--src/slist.c60
9 files changed, 528 insertions, 98 deletions
diff --git a/src/.gitignore b/src/.gitignore
index a48a3df..748504d 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -1,2 +1,3 @@
+.gdbinit
cmdline.h
id3ed
diff --git a/src/Makefile.am b/src/Makefile.am
index c4205a8..f9d0736 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,5 +1,12 @@
bin_PROGRAMS=id3ed
-id3ed_SOURCES=id3ed.h id3v1.c id3v1.h id3v2.c main.c cmdline.h
+id3ed_SOURCES=\
+ id3ed.h\
+ id3v1.c\
+ id3v1.h\
+ id3v2.c\
+ main.c\
+ cmdline.h\
+ slist.c
BUILT_SOURCES=cmdline.h
EXTRA_DIST=cmdline.opt getopt.m4
INCLUDES=-I$(top_srcdir)/gnu -I$(top_builddir)/gnu
diff --git a/src/cmdline.opt b/src/cmdline.opt
index 27a8298..bc2ef91 100644
--- a/src/cmdline.opt
+++ b/src/cmdline.opt
@@ -1,61 +1,42 @@
+static int mode_set = 0;
+#define SET_MODE(m) do { \
+ if (mode_set++ && mode != m) \
+ error(1, 0, "only one of -q, -s, -d may be used"); \
+ mode = m; \
+ } while(0)
+
OPTIONS_BEGIN(gnu, "id3ed",
[<id3ed - ID3 tag editor>],
[<FILE [FILE...]>])
-OPTION(title,t,NUMBER,
- [<set song title>])
-BEGIN
- title = optarg;
- mode = MODE_MOD;
-END
-
-OPTION(artist,a,NAME,
- [<set artist name>])
-BEGIN
- artist = optarg;
- mode = MODE_MOD;
-END
-
-OPTION(album,A,NAME,
- [<set album name>])
-BEGIN
- album = optarg;
- mode = MODE_MOD;
-END
-
-OPTION(year,y,YEAR,
- [<set year>])
-BEGIN
- year = optarg;
- mode = MODE_MOD;
-END
-
-OPTION(comment,c,TEXT,
- [<set comment>])
-BEGIN
- comment = optarg;
- mode = MODE_MOD;
-END
-
-OPTION(track,T,NUMBER,
- [<set track number>])
+OPTION(query,q,[FLIST],
+ [<query mode>])
BEGIN
- track = atoi(optarg);
- mode = MODE_MOD;
+ SET_MODE(MODE_QUERY);
+ if (optarg)
+ parse_ed_items(optarg);
END
-OPTION(genre,g,NAME,
- [<set genre>])
+OPTION(all,a,,
+ [<query all frames>])
BEGIN
- genre = optarg;
- mode = MODE_MOD;
+ SET_MODE(MODE_QUERY);
+ all_frames = 1;
END
-OPTION(query,q,,
- [<query mode>])
+OPTION(set,s,FIELD=VALUE,
+ [<set FIELD to VALUE>])
BEGIN
- mode = MODE_QUERY;
+ char *p;
+
+ SET_MODE(MODE_MOD);
+ p = strchr(optarg, '=');
+ if (!p)
+ error(1, 0, "missing `=' sign in assignment");
+ *p++ = 0;
+ ed_list_add_assignment(optarg, p);
END
+
OPTION(id-version,V,VERSION,
[<set ID3 version>])
@@ -65,6 +46,12 @@ BEGIN
error(1, 0, "unsupported version");
END
+OPTION(latin1,,,
+ [<force latin1 output>])
+BEGIN
+ latin1_output = 1;
+END
+
OPTIONS_END
void
diff --git a/src/getopt.m4 b/src/getopt.m4
index 6562667..5dd7494 100644
--- a/src/getopt.m4
+++ b/src/getopt.m4
@@ -140,7 +140,7 @@ divert(3)
ifelse(SHORT_TAG,,LONG_TAG,[<SHORT_TAG[<>]ifelse(LONG_TAG,,,; LONG_TAG)>]),
[<;>],[<,>])", ifelse(ARGNAME,,[<NULL, 0>],
[<ifelse(ARGTYPE,[<optional_argument>],
-[<patsubst([<ARGNAME>],[<\[\(.*\)\]>],[<N_("\1"), 1>])>],[<N_("ARGNAME"), 0>])>]), N_("DOCSTRING") },
+[<patsubst(ARGNAME,[<\[\(.*\)\]>],[<N_("\1"), 1>])>],[<N_("ARGNAME"), 0>])>]), N_("DOCSTRING") },
divert(-1)>])
popdef([<ARGTYPE>])
popdef([<ARGNAME>])
diff --git a/src/id3ed.h b/src/id3ed.h
index 52f724d..7723598 100644
--- a/src/id3ed.h
+++ b/src/id3ed.h
@@ -5,7 +5,9 @@
#include <getopt.h>
#include <error.h>
#include <progname.h>
-
+#include <xalloc.h>
+#include <gl_linked_list.h>
+#include <argmatch.h>
#include <id3tag.h>
#define _(s) s
@@ -13,16 +15,11 @@
#include "id3v1.h"
-extern struct id3v1_block v1_block;
-extern char *title;
-extern char *artist;
-extern char *album;
-extern char *year;
-extern char *comment;
-extern unsigned track;
-extern char *genre;
+#define DEFAULT_ED_LIST "title,album,comment,artist,year,genre"
+
+extern int latin1_output;
-void set_id3v1(const char *name);
+void set_id3v1(const char *name, const struct id3v1_block *);
void query_id3v1(const char *name);
void del_id3v1(const char *name);
@@ -30,3 +27,17 @@ void set_id3v2(const char *name);
void query_id3v2(const char *name);
void del_id3v2(const char *name);
+
+/* slist.c */
+typedef int (*string_list_action_fn) (const char *, void *);
+
+gl_list_t new_string_list(bool allow_duplicates);
+int do_string_list(gl_list_t list, string_list_action_fn action, void *data);
+void concat_string_list(gl_list_t list, gl_list_t addlist);
+void print_string_list(FILE *fp, gl_list_t list);
+
+
+void ed_list_add_item(const char *id, gl_list_t list);
+void ed_list_print(void);
+void ed_list_add_assignment(const char *name, const char *value);
+
diff --git a/src/id3v1.c b/src/id3v1.c
index 9093eb8..f587a88 100644
--- a/src/id3v1.c
+++ b/src/id3v1.c
@@ -209,7 +209,7 @@ id3v1_read(FILE *fp, struct id3v1_block *blk)
struct id3v1_block id;
if (fseek(fp, -sizeof(id), SEEK_END))
return -1;
- if (fread(blk, sizeof(id), 1, fp) != 1)
+ if (fread(&id, sizeof(id), 1, fp) != 1)
return -1;
if (memcmp(id.header, ID3V1_TAG, ID3V1_HEADER_LEN))
return 1;
@@ -236,7 +236,7 @@ id3v1_write(FILE *fp, struct id3v1_block *blk)
memcpy(blk, ID3V1_TAG, ID3V1_HEADER_LEN);
if (fseek(fp, offset, SEEK_END))
return -1;
- if (fwrite(&blk, sizeof(blk), 1, fp) != 1)
+ if (fwrite(blk, sizeof(blk[0]), 1, fp) != 1)
return -1;
return 0;
}
@@ -265,25 +265,26 @@ id3v1_init(struct id3v1_block *blk)
void
-set_id3v1(const char *name)
+set_id3v1(const char *name, const struct id3v1_block *blk)
{
int rc;
- struct id3v1_block old;
+ struct id3v1_block new, old;
FILE *fp = fopen(name, "r+");
if (!fp)
error(1, errno, "cannot open file %s", name);
verify_mp3(fp, name);
+ memcpy(&new, blk, sizeof(new));
switch (id3v1_read(fp, &old)) {
case 0:
- id3v1_merge(&v1_block, &old);
+ id3v1_merge(&new, &old);
break;
case 1:
break;
default:
error(1, errno, "%s: read error", name);
}
- if (id3v1_write(fp, &v1_block))
+ if (id3v1_write(fp, &new))
error(1, errno, "%s: write error", name);
fclose(fp);
diff --git a/src/id3v2.c b/src/id3v2.c
index f37606f..9401643 100644
--- a/src/id3v2.c
+++ b/src/id3v2.c
@@ -5,9 +5,97 @@ set_id3v2(const char *name)
{
}
+
+char *
+ucs4_cvt(id3_ucs4_t const *ucs4)
+{
+ if (latin1_output)
+ return (char*)id3_ucs4_latin1duplicate(ucs4);
+ else
+ return (char*)id3_ucs4_utf8duplicate(ucs4);
+}
+
+void
+add_stringlist(gl_list_t list, struct id3_frame *frame,
+ union id3_field *field)
+{
+ unsigned i, nstrings = id3_field_getnstrings(field);
+ for (i = 0; i < nstrings; i++) {
+ id3_ucs4_t const *ucs4;
+ char *str;
+
+ ucs4 = id3_field_getstrings(field, i);
+ if (!ucs4)
+ continue;
+ if (strcmp(frame->id, ID3_FRAME_GENRE) == 0)
+ ucs4 = id3_genre_name(ucs4);
+ str = ucs4_cvt(ucs4);
+ gl_list_add_last(list, str);
+ }
+}
+
+void
+add_field(gl_list_t list, struct id3_frame *frame, union id3_field *field)
+{
+ id3_ucs4_t const *ucs4;
+ char *str;
+
+ switch (id3_field_type(field)) {
+ case ID3_FIELD_TYPE_TEXTENCODING:
+ break;
+ case ID3_FIELD_TYPE_LATIN1:
+ case ID3_FIELD_TYPE_LATIN1FULL:
+ case ID3_FIELD_TYPE_LATIN1LIST:
+ break;
+ case ID3_FIELD_TYPE_STRING:
+ break;
+ case ID3_FIELD_TYPE_STRINGFULL:
+ ucs4 = id3_field_getfullstring(field);
+ str = ucs4_cvt(ucs4);
+ gl_list_add_last(list, str);
+ break;
+ case ID3_FIELD_TYPE_STRINGLIST:
+ add_stringlist(list, frame, field);
+ break;
+ case ID3_FIELD_TYPE_LANGUAGE:
+ case ID3_FIELD_TYPE_FRAMEID:
+ case ID3_FIELD_TYPE_DATE:
+ case ID3_FIELD_TYPE_INT8:
+ case ID3_FIELD_TYPE_INT16:
+ case ID3_FIELD_TYPE_INT24:
+ case ID3_FIELD_TYPE_INT32:
+ case ID3_FIELD_TYPE_INT32PLUS:
+ case ID3_FIELD_TYPE_BINARYDATA:;
+ }
+}
+
+gl_list_t
+frame_to_list(struct id3_frame *frame)
+{
+ gl_list_t list;
+ unsigned i, j, nstrings;
+ union id3_field *field;
+
+ list = new_string_list(true);
+ for (i = 0; field = id3_frame_field(frame, i); i++)
+ add_field(list, frame, field);
+ return list;
+}
+
static void
-show_tag(struct id3_tag *tag)
+show_tags(struct id3_tag *tag)
{
+ struct id3_frame *frame;
+ unsigned i;
+
+ for (i = 0; frame = id3_tag_findframe(tag, NULL, i); i++) {
+ gl_list_t list = frame_to_list(frame);
+ if (gl_list_size(list) > 0)
+ ed_list_add_item(frame->id, list);
+ else
+ gl_list_free(list);
+ }
+ ed_list_print();
}
void
@@ -22,7 +110,7 @@ query_id3v2(const char *name)
tag = id3_file_tag(file);
if (tag)
- show_tag(tag);
+ show_tags(tag);
id3_file_close(file);
}
diff --git a/src/main.c b/src/main.c
index a08c7db..19b51a2 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,14 +1,244 @@
#include "id3ed.h"
-unsigned version_option = 2;
-struct id3v1_block v1_block;
-char *title;
-char *artist;
-char *album;
-char *year;
-char *comment;
-unsigned track;
-char *genre;
+unsigned version_option = 2;
+int latin1_output = 0;
+
+struct ed_item {
+ char *name;
+ char id[5];
+ union {
+ gl_list_t vlist;
+ char *value;
+ } v;
+};
+
+int all_frames = 0;
+gl_list_t ed_list;
+
+static bool
+ed_item_eq(const void *elt1, const void *elt2)
+{
+ const struct ed_item *i1 = elt1;
+ const struct ed_item *i2 = elt2;
+ return strcmp(i1->id, i2->id) == 0;
+}
+
+static void
+ed_item_dispose(const void *elt)
+{
+ free((void*)elt);
+}
+
+void
+ed_list_create()
+{
+ if (ed_list)
+ return;
+ ed_list = gl_list_create_empty(&gl_linked_list_implementation,
+ ed_item_eq,
+ NULL,
+ ed_item_dispose,
+ false);
+}
+
+struct ed_item *
+ed_item_create0(char *name, const char *id)
+{
+ struct ed_item *itm = xzalloc(sizeof(itm[0]));
+ itm->name = name;
+ strncpy(itm->id, id, sizeof(itm->id));
+ return itm;
+}
+
+struct ed_item *
+ed_item_create(const char *name, const char *id)
+{
+ return ed_item_create0(xstrdup(name), id);
+}
+
+
+enum item_ids {
+ item_title,
+ item_artist,
+ item_album,
+ item_year,
+ item_comment,
+ item_track,
+ item_genre
+};
+
+int item_ids[] = {
+ item_title,
+ item_artist,
+ item_album,
+ item_year,
+ item_comment,
+ item_track,
+ item_genre
+};
+
+const char *item_names[] = {
+ "title",
+ "artist",
+ "album",
+ "year",
+ "comment",
+ "track",
+ "genre",
+ NULL
+};
+ARGMATCH_VERIFY(item_names, item_ids);
+
+const char *item_frames[] = {
+ ID3_FRAME_TITLE,
+ ID3_FRAME_ARTIST,
+ ID3_FRAME_ALBUM,
+ ID3_FRAME_YEAR,
+ ID3_FRAME_COMMENT,
+ ID3_FRAME_TRACK,
+ ID3_FRAME_GENRE,
+ NULL
+};
+
+ARGMATCH_VERIFY(item_frames, item_ids);
+
+int
+item_id(const char *arg)
+{
+ return (int) ARGMATCH(arg, item_names, item_ids);
+}
+
+int
+frame_id(const char *arg)
+{
+ return (int) ARGMATCH(arg, item_frames, item_ids);
+}
+
+static struct ed_item *
+ed_list_new_item(const char *arg, size_t len)
+{
+ int d;
+ char *name;
+ const char *id;
+ struct ed_item *itm;
+
+ name = xmalloc(len+1);
+ memcpy(name, arg, len);
+ name[len] = 0;
+
+ d = item_id(name);
+ if (d < 0) {
+ if (!id3_frametype_lookup(name, len))
+ error(1, 0, d == -1
+ ? "%s: unknown frame name" :
+ "%s: ambiguous frame name",
+ name);
+ id = name;
+ } else {
+ if (strcmp(item_names[d], name)) {
+ free(name);
+ name = xstrdup(item_names[d]);
+ }
+ id = item_frames[d];
+ }
+ itm = ed_item_create0(name, id);
+ gl_list_add_last(ed_list, itm);
+ return itm;
+}
+
+void
+parse_ed_items(const char *arg)
+{
+ ed_list_create();
+ while (*arg) {
+ size_t len = strcspn(arg, " \t,");
+
+ ed_list_new_item(arg, len);
+
+ arg += len;
+ if (!*arg)
+ break;
+ arg += strspn(arg, " \t,");
+ }
+}
+
+void
+ed_list_add_item(const char *id, gl_list_t list)
+{
+ if (all_frames) {
+ const char *name;
+ struct ed_item *itm;
+ int i;
+
+ ed_list_create();
+ i = frame_id(id);
+ if (i >= 0)
+ name = item_names[i];
+ else
+ name = id;
+ itm = ed_item_create(name, id);
+ itm->v.vlist = list;
+ gl_list_add_last(ed_list, itm);
+ } else {
+ gl_list_node_t node;
+ struct ed_item item;
+ strncpy(item.id, id, sizeof(item.id));
+ node = gl_list_search(ed_list, &item);
+ if (node) {
+ struct ed_item *itm =
+ (struct ed_item *) gl_list_node_value(ed_list,
+ node);
+ if (itm->v.vlist) {
+ concat_string_list(itm->v.vlist, list);
+ gl_list_free(list);
+ } else
+ itm->v.vlist = list;
+ } else
+ gl_list_free(list);
+ }
+}
+
+void
+ed_list_add_assignment(const char *name, const char *value)
+{
+ struct ed_item *itm;
+
+ ed_list_create();
+ itm = ed_list_new_item(name, strlen(name));
+ itm->v.value = xstrdup(value);
+}
+
+void
+ed_list_print()
+{
+ gl_list_iterator_t itr;
+ const void *p;
+ if (!ed_list)
+ return;
+ itr = gl_list_iterator(ed_list);
+ while (gl_list_iterator_next(&itr, &p, NULL)) {
+ const struct ed_item *item = p;
+ printf("%s:", item->name);
+ if (item->v.vlist)
+ print_string_list(stdout, item->v.vlist);
+ putchar('\n');
+ }
+ gl_list_iterator_free(&itr);
+}
+
+const struct ed_item *
+ed_item_find(int id)
+{
+ if (ed_list) {
+ gl_list_node_t node;
+ struct ed_item item;
+ strncpy(item.id, item_frames[id], sizeof(item.id));
+ node = gl_list_search(ed_list, &item);
+ if (node)
+ return gl_list_node_value(ed_list, node);
+ }
+ return NULL;
+}
void
@@ -38,15 +268,72 @@ verify_mp3(FILE *fp, const char *name)
}
}
-
#define MODE_QUERY 0
#define MODE_MOD 1
#define MODE_DELETE 2
-void (*id3_mode[][2])(const char *) = {
- { query_id3v1, query_id3v2 },
- { set_id3v1, set_id3v2 },
- { del_id3v1, del_id3v2 }
+void
+query_id3(const char *name)
+{
+ query_id3v2(name);
+}
+
+#define __cat2__(a,b) a ## b
+#define V1_BLOCK_SET(blk, fld) do { \
+ const struct ed_item *item = \
+ ed_item_find(__cat2__(item_,fld)); \
+ if (item) \
+ ID3V1_SET((blk), fld, item->v.value); \
+ } while(0)
+
+void
+set_id3(const char *name)
+{
+ if (version_option == 1) {
+ const struct ed_item *item;
+ struct id3v1_block v1_block;
+
+ id3v1_init(&v1_block);
+ V1_BLOCK_SET(&v1_block, title);
+ V1_BLOCK_SET(&v1_block, artist);
+ V1_BLOCK_SET(&v1_block, album);
+ V1_BLOCK_SET(&v1_block, year);
+ V1_BLOCK_SET(&v1_block, comment);
+
+ item = ed_item_find(item_genre);
+ if (item && item->v.value) {
+ int n;
+
+ n = id3v1_genre_to_n(item->v.value);
+ if (n == -1)
+ error(1, 0, "%s: unknown genre",
+ item->v.value);
+ else if (n == -2)
+ error(1, 0, "%s: ambiguous genre",
+ item->v.value);
+ v1_block.genre[0] = n;
+ }
+
+ item = ed_item_find(item_track);
+ if (item && item->v.value)
+ v1_block.track[0] = atoi(item->v.value);
+
+ set_id3v1(name, &v1_block);
+ } else
+ error(1, 0, "setting ID3 v2 frames is not yet implemented");
+}
+
+void
+del_id3(const char *name)
+{
+ error(1, 0, "Deleting ID3 data is not yet implemented");
+ abort();
+}
+
+void (*id3_mode[])(const char *) = {
+ query_id3,
+ set_id3,
+ del_id3,
};
int mode = MODE_QUERY;
@@ -67,26 +354,14 @@ main(int argc, char **argv)
if (argc == 0)
error(1, 0, "no files");
-
- if (version_option == 1) {
- int n;
-
- id3v1_init(&v1_block);
- ID3V1_SET(&v1_block, title, title);
- ID3V1_SET(&v1_block, artist, artist);
- ID3V1_SET(&v1_block, album, album);
- ID3V1_SET(&v1_block, year, year);
- ID3V1_SET(&v1_block, comment, comment);
- n = id3v1_genre_to_n(genre);
- if (n == -1)
- error(1, 0, "%s: unknown genre", genre);
- else if (n == -2)
- error(1, 0, "%s: ambiguous genre", genre);
- v1_block.track[0] = n;
+ if (mode == MODE_QUERY) {
+ if (all_frames)
+ ed_list = NULL;
+ else if (!ed_list)
+ parse_ed_items(DEFAULT_ED_LIST);
}
-
while (argc--)
- id3_mode[mode][version_option - 1](*argv++);
+ id3_mode[mode](*argv++);
exit(0);
}
diff --git a/src/slist.c b/src/slist.c
new file mode 100644
index 0000000..fdc6343
--- /dev/null
+++ b/src/slist.c
@@ -0,0 +1,60 @@
+#include "id3ed.h"
+
+static bool
+sl_eq(const void *elt1, const void *elt2)
+{
+ return strcmp(elt1, elt2) == 0;
+}
+
+static void
+sl_dispose(const void *elt)
+{
+ free((void*)elt);
+}
+
+gl_list_t
+new_string_list(bool allow_duplicates)
+{
+ return gl_list_create_empty(&gl_linked_list_implementation,
+ sl_eq,
+ NULL,
+ sl_dispose,
+ allow_duplicates);
+}
+
+int
+do_string_list(gl_list_t list, string_list_action_fn action, void *data)
+{
+ int rc = 0;
+ gl_list_iterator_t itr = gl_list_iterator(list);
+ const void *p;
+ while (gl_list_iterator_next(&itr, &p, NULL))
+ if (rc = action((const char*)p, data))
+ break;
+ gl_list_iterator_free(&itr);
+ return rc;
+}
+
+void
+concat_string_list(gl_list_t list, gl_list_t addlist)
+{
+ gl_list_iterator_t itr = gl_list_iterator(list);
+ const void *p;
+ while (gl_list_iterator_next(&itr, &p, NULL))
+ gl_list_add_last(list, xstrdup((const char*)p));
+ gl_list_iterator_free(&itr);
+}
+
+static int
+_sl_printer(const char *str, void *data)
+{
+ FILE *fp = data;
+ fprintf(fp, " %s", str);
+ return 0;
+}
+
+void
+print_string_list(FILE *fp, gl_list_t list)
+{
+ do_string_list(list, _sl_printer, fp);
+}

Return to:

Send suggestions and report system problems to the System administrator.