From b5e439075b333628eb4957d0c7ba1b744775cb65 Mon Sep 17 00:00:00 2001 From: Ian Jackson Date: Thu, 20 May 2021 12:41:27 +0100 Subject: [PATCH] otter(1): Change TABLE-NAME to a global option argument Signed-off-by: Ian Jackson --- apitest/apitest.rs | 14 ++- apitest/at-bundles.rs | 2 +- apitest/at-otter.rs | 12 +-- apitest/main.rs | 14 +-- docs/dev.md | 4 +- docs/user.rst | 18 ++-- src/bin/otter.rs | 198 ++++++++++++++---------------------------- wdriver/wdriver.rs | 4 +- 8 files changed, 104 insertions(+), 162 deletions(-) diff --git a/apitest/apitest.rs b/apitest/apitest.rs index 49aded72..766fc588 100644 --- a/apitest/apitest.rs +++ b/apitest/apitest.rs @@ -166,6 +166,12 @@ pub trait Substitutor { .map(str::to_string) .collect() } + + #[throws(AE)] + fn gss(&self, s: &str) -> Vec + where Self: Sized { + self.ss(&format!("--game @table@ {}", s))? + } } #[derive(Clone,Debug)] @@ -817,10 +823,10 @@ pub fn prepare_game(ds: &DirSubst, prctx: &PathResolveContext, table: &str) ("game_spec", prctx.resolve(&game_spec)), ]); ds.otter_prctx(prctx, &subst.ss( - "--account server: \ + "--account server: --game @table@ \ reset \ --reset-table @specs@/test.table.toml \ - @table@ @game_spec@ \ + @game_spec@ \ ")?).context("reset table")?; let instance: InstanceName = table.parse() @@ -861,10 +867,10 @@ impl DirSubst { ].iter()); su.otter(&subst - .ss("--super \ + .ss("--super --game @table@ \ --account server:@nick@ \ --fixed-token @token@ \ - join-game @table@")?)?; + join-game")?)?; let player = mgmt_conn.has_player( &subst.subst("server:@nick@")?.parse()? diff --git a/apitest/at-bundles.rs b/apitest/at-bundles.rs index cf46b901..650f07ec 100644 --- a/apitest/at-bundles.rs +++ b/apitest/at-bundles.rs @@ -20,7 +20,7 @@ impl Ctx { "big-bundle","duped-example", "chess-purple-cannon", "a purple cannon", &mut |ctx| { - let cmd = ctx.su().ds.ss("reset @table@ modded-spec")?; + let cmd = ctx.su().ds.gss("reset modded-spec")?; ctx.reset_game(&cmd)?; let alice = ctx.connect_player(&ctx.alice)?; diff --git a/apitest/at-otter.rs b/apitest/at-otter.rs index 1810d27b..2b80e81b 100644 --- a/apitest/at-otter.rs +++ b/apitest/at-otter.rs @@ -11,8 +11,8 @@ impl Ctx { fn library_load(&mut self) { self.prepare_game()?; - let command = self.su().ds.ss( - "library-list @table@ chess-yellow-?" + let command = self.su().ds.gss( + "library-list chess-yellow-?" )?; let output: String = self.otter(&command)?.into(); assert!( Regex::new("(?m)^wikimedia *chess-yellow-K *the yellow king$")? @@ -20,8 +20,8 @@ impl Ctx { .is_some(), "got: {}", &output); - let command = self.su().ds.ss( - "library-add --lib wikimedia @table@ chess-blue-?" + let command = self.su().ds.gss( + "library-add --lib wikimedia chess-blue-?" )?; let added = self.some_library_add(&command)?; assert_eq!(added.len(), 6); @@ -145,7 +145,7 @@ impl Ctx { assert_eq!(b_pieces[b_pawns[1]].pos, a_pieces[a_pawns[0]].pos); - let command = self.su().ds.ss("reset @table@ demo")?; + let command = self.su().ds.gss("reset demo")?; self.reset_game(&command)?; } @@ -177,7 +177,7 @@ impl Ctx { let (gy, game) = games.next(); if !(py || gy) { break } let command = self.su().ds.also(&[("game",&game),("perm",&perm)]) - .ss("reset --reset-table @perm@ @table@ @game@")?; + .gss("reset --reset-table @perm@ @game@")?; self.reset_game(&command).context(perm).context(game)?; } } diff --git a/apitest/main.rs b/apitest/main.rs index 2f6a6ccc..08c77a35 100644 --- a/apitest/main.rs +++ b/apitest/main.rs @@ -570,11 +570,11 @@ impl UsualCtx { ("itemlib", itemlib), ("item", item ), ]); - let command = ds.ss("library-add --lib @itemlib@ @table@ @item@")?; + let command = ds.gss("library-add --lib @itemlib@ @item@")?; let added = self.some_library_add(&command)?; assert_eq!( added.len(), 1 ); - let output: String = self.otter(&ds.ss("list-pieces @table@")?)?.into(); + let output: String = self.otter(&ds.gss("list-pieces")?)?.into(); assert_eq!( Regex::new( &format!( r#"(?m)(?:[^\w-]|^){}[^\w-].*\W{}(?:\W|$)"#, @@ -596,11 +596,11 @@ impl UsualCtx { let ds = self.su().ds.also(&[("bundle_stem", &bundle_stem)]); let bundle_file = ds.subst("@examples@/@bundle_stem@.zip")?; let ds = ds.also(&[("bundle", &bundle_file)]); - self.otter(&ds.ss("upload-bundle @table@ @bundle@")?)?; - let mut bundles = self.otter(&ds.ss("list-bundles @table@")?)?; + self.otter(&ds.gss("upload-bundle @bundle@")?)?; + let mut bundles = self.otter(&ds.gss("list-bundles")?)?; let bundles = String::from(&mut bundles); assert!(bundles.starts_with("00000.zip Loaded")); - self.otter(&ds.ss("download-bundle @table@ 0")?)?; + self.otter(&ds.gss("download-bundle 0")?)?; let st = Command::new("cmp").args(&[&bundle_file, "00000.zip"]).status()?; if ! st.success() { panic!("cmp failed {}", st) } @@ -620,8 +620,8 @@ impl UsualCtx { with(self)?; - self.otter(&ds.ss("clear-game @table@")?)?; - self.reset_game(&ds.ss("reset @table@ demo")?)?; + self.otter(&ds.gss("clear-game")?)?; + self.reset_game(&ds.gss("reset demo")?)?; } } diff --git a/docs/dev.md b/docs/dev.md index 3023b81d..f8112e77 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -25,11 +25,11 @@ quite verbose. So, in another shell: ``` target/debug/otter \ --account server: --config server-test.toml --spec-dir=specs \ - reset --reset-table test server::test demo + --game test server::test reset --reset-table demo target/debug/otter \ --account server: --config server-test.toml --spec-dir=specs \ - join-game server::test + --game server::test join-game ``` The URL printed can then be visited in a local browser. diff --git a/docs/user.rst b/docs/user.rst index 7f8e298b..ee9a3a1f 100644 --- a/docs/user.rst +++ b/docs/user.rst @@ -8,9 +8,9 @@ To join a game, you run a command like this on the server host: :: - otter [--nick ] join-game unix:ijackson::test - /^^^^^^^ ^^^^\ - game owner game name + otter [--nick ] --game unix:ijackson::test join-game + /^^^^^^^ ^^^^\ + game owner game name This will print a URL. You cut and paste that URL into your browser. @@ -155,9 +155,9 @@ The most usual game-creation command looks something like this: :: - otter reset --reset-table local-users unix:ijackson::test demo - /^^^^^^^^^^^ /^^^^^^^^^^^^^^^^^' '^^^\ - `table spec `game name game spec' + otter unix:ijackson::test reset --reset-table local-users demo + /~^^^^^^^^^^^^^^^^^' /^^^^^^^^^^^ '^^^\ + `game name table spec' game spec' Here ``local-users`` refers to the file ``local-users.table.toml`` in the Otter specs directory (``/volatile/Otter/specs`` on chiark). The table @@ -176,9 +176,9 @@ for a different game) with something like this: :: - otter reset unix:ijackson::test demo - /^^^^^^^^^^^^^^^^^^' '^^^\ - game name game spec + otter unix:ijackson::test reset demo + /^^^^^^^^^^^^^^^^^^' '^^^\ + `game name `game spec The ``otter`` command line tool has further subcommands for adding/removing players, for ad-hoc addition of pieces from the diff --git a/src/bin/otter.rs b/src/bin/otter.rs index 21d8e6e0..489bfc62 100644 --- a/src/bin/otter.rs +++ b/src/bin/otter.rs @@ -68,11 +68,20 @@ struct MainOpts { verbose: i32, superuser: bool, spec_dir: String, + game: Option, } impl MainOpts { - pub fn instance_name(&self, table_name: &str) -> InstanceName { - match table_name.strip_prefix(":") { + pub fn game(&self) -> &str { + self.game.as_ref().map(|s| s.as_str()).unwrap_or_else(||{ + eprintln!( + "game (table) name not specified; pass --game option"); + exit(EXIT_USAGE); + }) + } + + pub fn instance(&self) -> InstanceName { + match self.game().strip_prefix(":") { Some(rest) => { InstanceName { account: self.account.clone(), @@ -80,17 +89,36 @@ impl MainOpts { } } None => { - table_name.parse().unwrap_or_else(|e|{ + self.game().parse().unwrap_or_else(|e|{ eprintln!( - "instance name must start with : or be valid full name: {}", + "game (table) name must start with : or be valid full name: {}", &e); exit(EXIT_USAGE); }) } } } + + #[throws(AE)] + fn access_account(&self) -> Conn { + let mut conn = connect(self)?; + conn.prep_access_account(self)?; + conn + } + + #[throws(AE)] + fn access_game(&self) -> MgmtChannelForGame { + self.access_account()?.chan.for_game( + self.instance(), + MgmtGameUpdateMode::Online, + ) + } } +#[derive(Default,Debug)] +struct NoArgs { } +fn noargs(_sa: &mut NoArgs) -> ArgumentParser { ArgumentParser::new() } + struct Subcommand ( &'static str, // command &'static str, // desc @@ -266,6 +294,7 @@ fn main() { subcommand: String, subargs: Vec, spec_dir: Option, + game: Option, } let (subcommand, subargs, mo) = parse_args::( env::args().collect(), @@ -290,6 +319,10 @@ fn main() { StoreOption, "use NICK as nick for joining games (now and in the future) \ (default: derive from account name"); + ap.refer(&mut rma.game).metavar("TABLE-NAME") + .add_option(&["--game"], + StoreOption, + "Select the game to operate on"); ap.refer(&mut rma.timezone).metavar("TZ") .add_option(&["--timezone"], StoreOption, @@ -354,7 +387,7 @@ fn main() { }, &|RawMainArgs { account, nick, timezone, access, socket_path, verbose, config_filename, superuser, - subcommand, subargs, spec_dir, layout, + subcommand, subargs, spec_dir, layout, game, }|{ env_logger::Builder::new() .filter_level(log::LevelFilter::Info) @@ -417,6 +450,7 @@ fn main() { verbose, superuser, spec_dir, + game, })) }, Some(&|w|{ writeln!(w, "\nSubcommands:")?; @@ -645,21 +679,6 @@ fn read_spec_from_path(filename: String, _: P) -> P::T })().with_context(|| format!("read {} {:?}", P::S::WHAT, &filename))? } -#[throws(AE)] -fn access_account(ma: &MainOpts) -> Conn { - let mut conn = connect(&ma)?; - conn.prep_access_account(ma)?; - conn -} - -#[throws(AE)] -fn access_game(ma: &MainOpts, table_name: &String) -> MgmtChannelForGame { - access_account(ma)?.chan.for_game( - ma.instance_name(table_name), - MgmtGameUpdateMode::Online, - ) -} - //---------- list-games ---------- mod list_games { @@ -710,7 +729,6 @@ mod reset_game { #[derive(Default,Debug)] struct Args { - table_name: String, game_spec: String, table_file: Option, } @@ -721,8 +739,6 @@ mod reset_game { ap.refer(&mut sa.table_file).metavar("TABLE-SPEC[-TOML]") .add_option(&["--reset-table"],StoreOption, "reset the players and access too"); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut sa.game_spec).required() .add_argument("GAME-SPEC",Store, "game spec, as found in server, \ @@ -732,11 +748,8 @@ mod reset_game { fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) ->Result<(),AE> { let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance_name(&args.table_name); - let mut chan = access_account(&ma)?.chan.for_game( - instance_name.clone(), - MgmtGameUpdateMode::Bulk, - ); + let instance_name = ma.instance(); + let mut chan = ma.access_game()?; let reset_insn = if let Some(filename) = spec_arg_is_path(&args.game_spec) { @@ -790,7 +803,6 @@ mod set_link { #[derive(Debug,Default)] struct Args { - table_name: String, kind: Option, url: Option, } @@ -798,8 +810,6 @@ mod set_link { fn subargs(sa: &mut Args) -> ArgumentParser { use argparse::*; let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut sa.kind) .add_argument("LINK-KIND",StoreOption,"link kind"); ap.refer(&mut sa.url) @@ -810,7 +820,7 @@ mod set_link { #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; match args.url { None => { @@ -858,7 +868,6 @@ mod join_game { #[derive(Default,Debug)] struct Args { reset_access: bool, - table_name: String, } fn subargs(sa: &mut Args) -> ArgumentParser { @@ -867,14 +876,12 @@ mod join_game { ap.refer(&mut sa.reset_access) .add_option(&["--reset"],StoreTrue, "generate and deliver new player access token"); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap } fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) ->Result<(),AE> { let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; let mut insns = vec![]; match chan.has_player(&ma.account)? { @@ -946,22 +953,11 @@ mod join_game { mod leave_game { use super::*; - #[derive(Default,Debug)] - struct Args { - table_name: String, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); - ap - } + type Args = NoArgs; fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) ->Result<(),AE> { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; let player = match chan.has_player(&ma.account)? { None => { @@ -988,22 +984,11 @@ mod leave_game { mod delete_game { use super::*; - #[derive(Default,Debug)] - struct Args { - table_name: String, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); - ap - } + type Args = NoArgs; fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) ->Result<(),AE> { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; let game = chan.game.clone(); chan.cmd(&MC::DestroyGame { game })?; Ok(()) @@ -1020,7 +1005,6 @@ mod delete_game { #[derive(Debug,Default)] struct LibGlobArgs { - table_name: String, lib: Option, pat: Option, } @@ -1031,8 +1015,6 @@ impl LibGlobArgs { ap: &'_ mut ArgumentParser<'ap> ) { use argparse::*; - ap.refer(&mut self.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut self.lib).metavar("LIBRARY") .add_option(&["--lib"],StoreOption,"look only in LIBRARY"); ap.refer(&mut self.pat) @@ -1064,7 +1046,7 @@ mod library_list { #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; if args.lib.is_none() && args.pat.is_none() { let game = chan.game.clone(); @@ -1128,7 +1110,7 @@ mod library_add { const MAGIC: &str = "mgmt-library-load-marker"; let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.tlg.table_name)?; + let mut chan = ma.access_game()?; let (pieces, _pcaliases) = chan.list_pieces()?; let markers = pieces.iter().filter(|p| p.itemname.as_str() == MAGIC) .collect::>(); @@ -1351,23 +1333,12 @@ mod library_add { mod list_pieces { use super::*; - #[derive(Default,Debug)] - struct Args { - table_name: String, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); - ap - } + type Args = NoArgs; #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { - let args = parse_args::(args, &subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; let (pieces, pcaliases) = chan.list_pieces()?; for p in pieces { println!("{:?}", p); @@ -1487,7 +1458,6 @@ mod alter_game_adhoc { #[derive(Default,Debug)] struct Args { - table_name: String, insns: Vec, } @@ -1497,8 +1467,6 @@ mod alter_game_adhoc { ) -> ArgumentParser<'ap> { use argparse::*; let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut sa.insns).required() .add_argument(format!("{}-INSN", ahf.metavar()).leak(), Collect, @@ -1513,7 +1481,7 @@ mod alter_game_adhoc { let subargs: ApMaker<_> = &|sa| subargs(sa,ahf); let args = parse_args::(args, subargs, &ok_id, None); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; let insns: Vec = ahf.parse(args.insns, "insn")?; let resps = chan.alter_game(insns,None)?; @@ -1543,15 +1511,12 @@ mod upload_bundle { #[derive(Default,Debug)] struct Args { - table_name: String, bundle_file: String, } fn subargs(sa: &mut Args) -> ArgumentParser { use argparse::*; let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut sa.bundle_file).required() .add_argument("BUNDLE",Store,"bundle file"); ap @@ -1560,8 +1525,7 @@ mod upload_bundle { #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance_name(&args.table_name); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; let f = File::open(&args.bundle_file) .with_context(|| args.bundle_file.clone()) .context("open bundle file")?; @@ -1576,7 +1540,7 @@ mod upload_bundle { f.rewind().context("rewind bundle file")?; let cmd = MC::UploadBundle { size, - game: instance_name.clone(), + game: ma.instance(), hash: bundles::Hash(hash.into()), kind, progress: MgmtChannel::PROGRESS, }; @@ -1601,26 +1565,14 @@ mod upload_bundle { mod list_bundles { use super::*; - #[derive(Default,Debug)] - struct Args { - table_name: String, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); - ap - } + type Args = NoArgs; #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { - let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance_name(&args.table_name); - let mut chan = access_game(&ma, &args.table_name)?; + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; let resp = chan.cmd(&MC::ListBundles { - game: instance_name.clone(), + game: ma.instance(), })?; if_let!{ MR::Bundles { bundles } = resp; else throw!(anyhow!("unexpected {:?}", &resp)) }; @@ -1643,7 +1595,6 @@ mod download_bundle { #[derive(Default,Debug)] struct Args { - table_name: String, index: bundles::Index, output: Option, } @@ -1651,8 +1602,6 @@ mod download_bundle { fn subargs(sa: &mut Args) -> ArgumentParser { use argparse::*; let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); ap.refer(&mut sa.index).required() .add_argument("INDEX",Store,"bundle number"); ap.refer(&mut sa.output).metavar("OUTPUT") @@ -1664,8 +1613,7 @@ mod download_bundle { #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance_name(&args.table_name); - let mut chan = access_game(&ma, &args.table_name)?; + let mut chan = ma.access_game()?; let kind = bundles::Kind::only(); let id = bundles::Id { kind, index: args.index }; let path = args.output.unwrap_or_else(|| id.to_string().into()); @@ -1686,7 +1634,7 @@ mod download_bundle { }; let mut f = BufWriter::new(f); let cmd = MC::DownloadBundle { - game: instance_name.clone(), + game: ma.instance(), id, }; chan.cmd_withbulk(&cmd, &mut io::empty(), &mut f, @@ -1711,28 +1659,16 @@ mod download_bundle { mod clear_game { use super::*; - #[derive(Default,Debug)] - struct Args { - table_name: String, - } - - fn subargs(sa: &mut Args) -> ArgumentParser { - use argparse::*; - let mut ap = ArgumentParser::new(); - ap.refer(&mut sa.table_name).required() - .add_argument("TABLE-NAME",Store,"table name"); - ap - } + type Args = NoArgs; #[throws(AE)] fn call(_sc: &Subcommand, ma: MainOpts, args: Vec) { - let args = parse_args::(args, &subargs, &ok_id, None); - let instance_name = ma.instance_name(&args.table_name); - let mut chan = access_game(&ma, &args.table_name)?; + let _args = parse_args::(args, &noargs, &ok_id, None); + let mut chan = ma.access_game()?; chan.alter_game(vec![MGI::ClearGame{ }], None) .context("clear table")?; - chan.cmd(&MC::ClearBundles { game: instance_name.clone() }) + chan.cmd(&MC::ClearBundles { game: ma.instance() }) .context("clear bundles")?; } diff --git a/wdriver/wdriver.rs b/wdriver/wdriver.rs index 00ca2948..259e2efe 100644 --- a/wdriver/wdriver.rs +++ b/wdriver/wdriver.rs @@ -624,9 +624,9 @@ impl<'g> WindowGuard<'g> { #[throws(AE)] pub fn otter(&mut self, verb: &[&str], args: &[&str]) { let args: Vec = - ["--account", "server:"].iter().cloned().map(Into::into) + ["--account", "server:", "--game", &self.w.table()] + .iter().cloned().map(Into::into) .chain(verb.iter().cloned().map(Into::into)) - .chain(iter::once(self.w.table())) .chain(args.iter().cloned().map(Into::into)) .collect(); self.su.ds.otter(&args)?; -- 2.30.2