1use std::ffi::OsStr;
2use std::io::Write;
3use std::path::Path;
4use std::path::PathBuf;
5use std::process::{Child, Command, Output, Stdio};
6
7use super::{ExclusiveOption, SubCommand, Unselected};
8
9use crate::global::GlobalOpts;
10use crate::spawn::ParameterizedSpawn;
11
12#[derive(Debug, Clone, Default)]
18pub struct Standard {
19 values: Option<Vec<(String, Option<String>)>>,
20
21 hex: bool,
22}
23
24impl ExclusiveOption for Standard {
25 fn inject_args(&self, command: &mut Command) {
26 if self.hex {
27 command.arg("-e");
28 }
29
30 if let Some(pairs) = &self.values {
31 for (name, value) in pairs {
32 command.arg("-n").arg(name);
33
34 if let Some(value) = value {
35 command.arg("-v").arg(value);
36 }
37 }
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
47pub struct FromStdin {
48 hex: bool,
49
50 name: String,
51}
52
53impl ExclusiveOption for FromStdin {
54 fn inject_args(&self, command: &mut Command) {
55 if self.hex {
56 command.arg("-e");
57 }
58
59 command.arg("-i").arg("-n").arg(&self.name);
60 }
61}
62
63#[cfg(not(feature = "lt2024_2"))]
68#[derive(Debug, Clone, Default)]
69pub struct FromFile {
70 name: String,
71
72 file: PathBuf,
73}
74
75#[cfg(not(feature = "lt2024_2"))]
76impl ExclusiveOption for FromFile {
77 fn inject_args(&self, command: &mut Command) {
78 command.arg("-I").arg(&self.file).arg("-n").arg(&self.name);
79 }
80}
81
82#[cfg(not(feature = "lt2023_2"))]
85pub mod storage {
86 #[derive(Debug, Clone, Copy, Default)]
88 pub struct DatabaseTraits;
89
90 #[derive(Debug, Clone, Copy, Default)]
92 pub struct TraitDepot;
93}
94
95#[cfg(not(feature = "lt2023_2"))]
96impl ExclusiveOption for storage::DatabaseTraits {
97 fn inject_args(&self, command: &mut Command) {
98 command.arg("-T0");
99 }
100}
101
102#[cfg(not(feature = "lt2023_2"))]
103impl ExclusiveOption for storage::TraitDepot {
104 fn inject_args(&self, command: &mut Command) {
105 command.arg("-T1");
106 }
107}
108
109#[cfg_attr(
110 feature = "lt2023_2",
111 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p] -i -n name file`"
112)]
113#[cfg_attr(
114 all(feature = "lt2024_2", not(feature = "lt2023_2")),
115 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`"
116)]
117#[cfg_attr(
118 not(feature = "lt2024_2"),
119 doc = "`p4 [g-opts] attribute [-e -f -p] -n name [-v value [-T0 | -T1]] files ...`: set per-revision attributes on revisions.\n\n`p4 [g-opts] attribute [-e -f -p [-T0 | -T1]] -i -n name file`\n\n`p4 attribute [-f -p [-T0 | -T1]] -I filename -n name file`"
120)]
121#[cfg_attr(
125 not(feature = "lt2024_2"),
126 doc = "[`FromFile`] for the `-I filename` form, and"
127)]
128#[derive(Debug, Clone, Default)]
130pub struct Attribute<S = Standard, T = Unselected> {
131 bin: PathBuf,
132
133 global_opts: GlobalOpts,
134
135 source: S,
136
137 storage: T,
138
139 on_submitted_files: bool,
140
141 propagating: bool,
142}
143
144impl<S: ExclusiveOption, T: ExclusiveOption> SubCommand for Attribute<S, T> {
145 fn name(&self) -> &str {
146 "attribute"
147 }
148
149 fn inject_local_args(&self, command: &mut Command) {
150 if self.on_submitted_files {
151 command.arg("-f");
152 }
153
154 if self.propagating {
155 command.arg("-p");
156 }
157
158 self.storage.inject_args(command);
159
160 self.source.inject_args(command);
161 }
162
163 fn global_opts(&self) -> Option<&GlobalOpts> {
164 Some(&self.global_opts)
165 }
166}
167
168impl Attribute<Standard, Unselected> {
169 pub fn new(bin: impl Into<PathBuf>, global_opts: GlobalOpts) -> Self {
173 Self {
174 bin: bin.into(),
175 global_opts,
176 on_submitted_files: false,
177 propagating: false,
178 source: Standard::default(),
179 storage: Unselected,
180 }
181 }
182}
183
184impl<T: ExclusiveOption, S, I> ParameterizedSpawn<(S,)> for Attribute<Standard, T>
185where
186 S: IntoIterator<Item = I>,
187 I: AsRef<OsStr>,
188{
189 type Output = Child;
190 type Error = std::io::Error;
191
192 fn spawn_with(&mut self, (files,): (S,)) -> Result<Self::Output, Self::Error> {
196 self.setup_command(&self.bin)
197 .args(files)
198 .stdout(Stdio::piped())
199 .stderr(Stdio::piped())
200 .spawn()
201 }
202}
203
204impl<T: ExclusiveOption> Attribute<Standard, T> {
205 #[cfg_attr(not(feature = "lt2024_2"), doc = "The supplied value must be text.")]
211 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) -> &mut Self {
212 self.source
213 .values
214 .get_or_insert_with(Vec::new)
215 .push((name.into(), Some(value.into())));
216 self
217 }
218
219 pub fn clear(&mut self, name: impl Into<String>) -> &mut Self {
226 self.source
227 .values
228 .get_or_insert_with(Vec::new)
229 .push((name.into(), None));
230 self
231 }
232
233 pub fn get_hex(&self) -> bool {
239 self.source.hex
240 }
241
242 pub fn set_hex(&mut self, v: bool) -> &mut Self {
248 self.source.hex = v;
249 self
250 }
251
252 pub fn hex(mut self, v: bool) -> Self {
258 self.source.hex = v;
259 self
260 }
261
262 #[cfg_attr(
267 feature = "lt2024_2",
268 doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option."
269 )]
270 #[cfg_attr(
271 not(feature = "lt2024_2"),
272 doc = "Read an attribute value from the standard input. Only one file argument is allowed when using this option. This option supports both textual and binary content."
273 )]
274 pub fn read_from_stdin(self, name: String) -> Attribute<FromStdin, T> {
279 Attribute {
280 bin: self.bin,
281 global_opts: self.global_opts,
282 on_submitted_files: self.on_submitted_files,
283 propagating: self.propagating,
284 source: FromStdin {
285 hex: self.source.hex,
286 name,
287 },
288 storage: self.storage,
289 }
290 }
291
292 #[cfg(not(feature = "lt2024_2"))]
313 pub fn read_from_file(self, name: String, file: PathBuf) -> Attribute<FromFile, T> {
314 Attribute {
315 bin: self.bin,
316 global_opts: self.global_opts,
317 on_submitted_files: self.on_submitted_files,
318 propagating: self.propagating,
319 source: FromFile { name, file },
320 storage: self.storage,
321 }
322 }
323}
324
325impl<T: ExclusiveOption> Attribute<FromStdin, T> {
326 pub fn get_hex(&self) -> bool {
332 self.source.hex
333 }
334
335 pub fn set_hex(&mut self, v: bool) -> &mut Self {
341 self.source.hex = v;
342 self
343 }
344
345 pub fn hex(mut self, v: bool) -> Self {
351 self.source.hex = v;
352 self
353 }
354
355 pub fn get_name(&self) -> &str {
361 &self.source.name
362 }
363
364 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
370 self.source.name = v.into();
371 self
372 }
373
374 pub fn name(mut self, v: impl Into<String>) -> Self {
380 self.source.name = v.into();
381 self
382 }
383
384 pub fn output<S, V>(&self, file: S, value: V) -> Result<Output, std::io::Error>
393 where
394 S: AsRef<OsStr>,
395 V: AsRef<[u8]>,
396 {
397 let mut child = self
398 .setup_command(&self.bin)
399 .arg(file)
400 .stdin(Stdio::piped())
401 .stdout(Stdio::piped())
402 .stderr(Stdio::piped())
403 .spawn()?;
404
405 if let Some(ref mut stdin) = child.stdin {
406 stdin.write_all(value.as_ref())?;
407 }
408 drop(child.stdin.take());
409
410 child.wait_with_output()
411 }
412}
413
414impl<T: ExclusiveOption, I> ParameterizedSpawn<(I, Stdio)> for Attribute<FromStdin, T>
415where
416 I: AsRef<OsStr>,
417{
418 type Output = Child;
419 type Error = std::io::Error;
420
421 fn spawn_with(&mut self, input: (I, Stdio)) -> Result<Self::Output, Self::Error> {
431 self.setup_command(&self.bin)
432 .arg(input.0)
433 .stdin(input.1)
434 .stdout(Stdio::piped())
435 .stderr(Stdio::piped())
436 .spawn()
437 }
438}
439
440impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromStdin, T>
441where
442 I: AsRef<OsStr>,
443{
444 type Output = Child;
445 type Error = std::io::Error;
446
447 fn spawn_with(&mut self, (input,): (I,)) -> Result<Self::Output, Self::Error> {
452 self.setup_command(&self.bin)
453 .arg(input)
454 .stdin(Stdio::piped())
455 .stdout(Stdio::piped())
456 .stderr(Stdio::piped())
457 .spawn()
458 }
459}
460
461#[cfg(not(feature = "lt2024_2"))]
462impl<T: ExclusiveOption> Attribute<FromFile, T> {
463 pub fn get_name(&self) -> &str {
469 &self.source.name
470 }
471
472 pub fn set_name(&mut self, v: impl Into<String>) -> &mut Self {
478 self.source.name = v.into();
479 self
480 }
481
482 pub fn name(mut self, v: impl Into<String>) -> Self {
488 self.source.name = v.into();
489 self
490 }
491
492 pub fn get_file(&self) -> &Path {
498 &self.source.file
499 }
500}
501
502#[cfg(not(feature = "lt2024_2"))]
503impl<T: ExclusiveOption, I> ParameterizedSpawn<(I,)> for Attribute<FromFile, T>
504where
505 I: AsRef<OsStr>,
506{
507 type Output = Child;
508 type Error = std::io::Error;
509
510 fn spawn_with(&mut self, (file,): (I,)) -> Result<Self::Output, Self::Error> {
516 self.setup_command(&self.bin)
517 .arg(file)
518 .stdout(Stdio::piped())
519 .stderr(Stdio::piped())
520 .spawn()
521 }
522}
523
524impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
525 pub fn get_bin(&self) -> &Path {
527 &self.bin
528 }
529
530 pub fn set_bin(&mut self, bin: impl Into<PathBuf>) -> &mut Self {
532 self.bin = bin.into();
533 self
534 }
535
536 pub fn bin(mut self, bin: impl Into<PathBuf>) -> Self {
538 self.set_bin(bin);
539 self
540 }
541
542 #[cfg_attr(
547 feature = "lt2014_2",
548 doc = "See the [Global Options](GlobalOpts) section."
549 )]
550 #[cfg_attr(
551 all(feature = "lt2015_1", not(feature = "lt2014_2")),
552 doc = "See the [“Global Options”](GlobalOpts) section."
553 )]
554 #[cfg_attr(
555 all(feature = "lt2017_1", not(feature = "lt2015_1")),
556 doc = "See [“Global Options”](GlobalOpts)."
557 )]
558 #[cfg_attr(
559 all(feature = "lt2018_2", not(feature = "lt2017_1")),
560 doc = "See [Global Options](GlobalOpts)."
561 )]
562 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
563 pub fn get_global_opts(&self) -> &GlobalOpts {
564 &self.global_opts
565 }
566
567 #[cfg_attr(
572 feature = "lt2014_2",
573 doc = "See the [Global Options](GlobalOpts) section."
574 )]
575 #[cfg_attr(
576 all(feature = "lt2015_1", not(feature = "lt2014_2")),
577 doc = "See the [“Global Options”](GlobalOpts) section."
578 )]
579 #[cfg_attr(
580 all(feature = "lt2017_1", not(feature = "lt2015_1")),
581 doc = "See [“Global Options”](GlobalOpts)."
582 )]
583 #[cfg_attr(
584 all(feature = "lt2018_2", not(feature = "lt2017_1")),
585 doc = "See [Global Options](GlobalOpts)."
586 )]
587 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
588 pub fn set_global_opts(&mut self, v: GlobalOpts) -> &mut Self {
589 self.global_opts = v;
590 self
591 }
592
593 #[cfg_attr(
598 feature = "lt2014_2",
599 doc = "See the [Global Options](GlobalOpts) section."
600 )]
601 #[cfg_attr(
602 all(feature = "lt2015_1", not(feature = "lt2014_2")),
603 doc = "See the [“Global Options”](GlobalOpts) section."
604 )]
605 #[cfg_attr(
606 all(feature = "lt2017_1", not(feature = "lt2015_1")),
607 doc = "See [“Global Options”](GlobalOpts)."
608 )]
609 #[cfg_attr(
610 all(feature = "lt2018_2", not(feature = "lt2017_1")),
611 doc = "See [Global Options](GlobalOpts)."
612 )]
613 #[cfg_attr(not(feature = "lt2018_2"), doc = "See [Global options](GlobalOpts).")]
614 pub fn global_opts(mut self, v: GlobalOpts) -> Self {
615 self.global_opts = v;
616 self
617 }
618
619 pub fn get_on_submitted_files(&self) -> bool {
627 self.on_submitted_files
628 }
629
630 pub fn set_on_submitted_files(&mut self, v: bool) -> &mut Self {
638 self.on_submitted_files = v;
639 self
640 }
641
642 pub fn on_submitted_files(mut self, v: bool) -> Self {
650 self.on_submitted_files = v;
651 self
652 }
653
654 #[cfg_attr(
659 feature = "lt2021_2",
660 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
661 )]
662 #[cfg_attr(
663 not(feature = "lt2021_2"),
664 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
665 )]
666 pub fn get_propagating(&self) -> bool {
667 self.propagating
668 }
669
670 #[cfg_attr(
675 feature = "lt2021_2",
676 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
677 )]
678 #[cfg_attr(
679 not(feature = "lt2021_2"),
680 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
681 )]
682 pub fn set_propagating(&mut self, v: bool) -> &mut Self {
683 self.propagating = v;
684 self
685 }
686
687 #[cfg_attr(
692 feature = "lt2021_2",
693 doc = "Create a propagating attribute: an attribute whose value is propagated to subsequent revisions whenever the file is opened with `p4 add`, `p4 edit`, or `p4 delete`."
694 )]
695 #[cfg_attr(
696 not(feature = "lt2021_2"),
697 doc = "Create a *propagating attribute*: an attribute whose value is propagated to subsequent revisions whenever the file is opened. Relevant commands include `p4 copy`, `p4 delete`, `p4 edit`, `p4 integrate`, `p4 reconcile`, `p4 resolve`, `p4 shelve`, `p4 submit`, and `p4 unshelve`."
698 )]
699 pub fn propagating(mut self, v: bool) -> Self {
700 self.propagating = v;
701 self
702 }
703}
704
705#[cfg(not(feature = "lt2023_2"))]
706impl<S: ExclusiveOption, T: ExclusiveOption> Attribute<S, T> {
707 pub fn store_in_database_traits(self) -> Attribute<S, storage::DatabaseTraits> {
714 Attribute {
715 bin: self.bin,
716 global_opts: self.global_opts,
717 on_submitted_files: self.on_submitted_files,
718 propagating: self.propagating,
719 source: self.source,
720 storage: storage::DatabaseTraits,
721 }
722 }
723
724 pub fn store_in_trait_depot(self) -> Attribute<S, storage::TraitDepot> {
734 Attribute {
735 bin: self.bin,
736 global_opts: self.global_opts,
737 on_submitted_files: self.on_submitted_files,
738 propagating: self.propagating,
739 source: self.source,
740 storage: storage::TraitDepot,
741 }
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::cmd::args_of;
749
750 #[test]
751 fn bin_accessors_update_executable_path() {
752 let mut command = Attribute::new("p4", GlobalOpts::new());
753 assert_eq!(command.get_bin(), Path::new("p4"));
754
755 command.set_bin("/usr/local/bin/p4");
756 assert_eq!(command.get_bin(), Path::new("/usr/local/bin/p4"));
757
758 let command = command.bin("p4.exe");
759 assert_eq!(command.get_bin(), Path::new("p4.exe"));
760 }
761
762 #[test]
765 fn without_options() {
766 let attr = Attribute::new("p4", GlobalOpts::new());
767
768 assert_eq!(args_of(&attr.setup_command("p4")), ["attribute"]);
769 }
770
771 #[test]
772 fn set_attribute_with_value() {
773 let mut attr = Attribute::new("p4", GlobalOpts::new());
774 attr.set("status", "approved");
775
776 assert_eq!(
777 args_of(&attr.setup_command("p4")),
778 ["attribute", "-n", "status", "-v", "approved"]
779 );
780 }
781
782 #[test]
783 fn clear_attribute_omits_value() {
784 let mut attr = Attribute::new("p4", GlobalOpts::new());
785 attr.clear("status");
786
787 assert_eq!(
788 args_of(&attr.setup_command("p4")),
789 ["attribute", "-n", "status"]
790 );
791 }
792
793 #[test]
794 fn multiple_set_and_clear_pairs_keep_order() {
795 let mut attr = Attribute::new("p4", GlobalOpts::new());
796 attr.set("color", "red")
797 .clear("status")
798 .set("owner", "alice");
799
800 assert_eq!(
801 args_of(&attr.setup_command("p4")),
802 [
803 "attribute",
804 "-n",
805 "color",
806 "-v",
807 "red",
808 "-n",
809 "status",
810 "-n",
811 "owner",
812 "-v",
813 "alice"
814 ]
815 );
816 }
817
818 #[test]
819 fn hex_submitted_propagating() {
820 let mut attr = Attribute::new("p4", GlobalOpts::new());
821 attr.set_hex(true)
822 .set_on_submitted_files(true)
823 .set_propagating(true)
824 .set("thumb", "deadbeef");
825
826 assert_eq!(
827 args_of(&attr.setup_command("p4")),
828 [
829 "attribute",
830 "-f",
831 "-p",
832 "-e",
833 "-n",
834 "thumb",
835 "-v",
836 "deadbeef"
837 ]
838 );
839 }
840
841 #[test]
842 fn read_from_stdin_state() {
843 let attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("thumb".to_string());
844
845 assert_eq!(attr.get_name(), "thumb");
846 assert_eq!(
847 args_of(&attr.setup_command("p4")),
848 ["attribute", "-i", "-n", "thumb"]
849 );
850 }
851
852 #[test]
853 fn stdin_name_can_be_replaced() {
854 let mut attr = Attribute::new("p4", GlobalOpts::new()).read_from_stdin("old".to_string());
855 attr.set_name("thumb");
856
857 assert_eq!(attr.get_name(), "thumb");
858
859 let attr = attr.name("icon");
860 assert_eq!(attr.get_name(), "icon");
861 assert_eq!(
862 args_of(&attr.setup_command("p4")),
863 ["attribute", "-i", "-n", "icon"]
864 );
865 }
866
867 #[test]
868 fn stdin_preserves_flags_and_hex() {
869 let mut attr = Attribute::new("p4", GlobalOpts::new());
870 attr.set_hex(true)
871 .set_on_submitted_files(true)
872 .set_propagating(true)
873 .set("discarded", "value");
874
875 let attr = attr.read_from_stdin("thumb".to_string());
876
877 assert!(attr.get_hex());
878 assert_eq!(
879 args_of(&attr.setup_command("p4")),
880 ["attribute", "-f", "-p", "-e", "-i", "-n", "thumb"]
881 );
882 }
883
884 #[cfg(not(feature = "lt2023_2"))]
885 #[test]
886 fn store_in_database_traits() {
887 let mut attr = Attribute::new("p4", GlobalOpts::new());
888 attr.set("thumb", "data");
889
890 let attr = attr.store_in_database_traits();
891
892 assert_eq!(
893 args_of(&attr.setup_command("p4")),
894 ["attribute", "-T0", "-n", "thumb", "-v", "data"]
895 );
896 }
897
898 #[cfg(not(feature = "lt2023_2"))]
899 #[test]
900 fn store_in_trait_depot() {
901 let mut attr = Attribute::new("p4", GlobalOpts::new());
902 attr.set("thumb", "data");
903
904 let attr = attr.store_in_trait_depot();
905
906 assert_eq!(
907 args_of(&attr.setup_command("p4")),
908 ["attribute", "-T1", "-n", "thumb", "-v", "data"]
909 );
910 }
911
912 #[cfg(not(feature = "lt2023_2"))]
913 #[test]
914 fn stdin_with_trait_depot() {
915 let attr = Attribute::new("p4", GlobalOpts::new())
916 .read_from_stdin("thumb".to_string())
917 .store_in_trait_depot();
918
919 assert_eq!(
920 args_of(&attr.setup_command("p4")),
921 ["attribute", "-T1", "-i", "-n", "thumb"]
922 );
923 }
924
925 #[cfg(not(feature = "lt2024_2"))]
926 #[test]
927 fn read_from_file_state() {
928 let attr = Attribute::new("p4", GlobalOpts::new())
929 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/thumb.bin"));
930
931 assert_eq!(attr.get_name(), "thumb");
932 assert_eq!(attr.get_file(), Path::new("/tmp/thumb.bin"));
933 assert_eq!(
934 args_of(&attr.setup_command("p4")),
935 ["attribute", "-I", "/tmp/thumb.bin", "-n", "thumb"]
936 );
937 }
938
939 #[cfg(not(feature = "lt2024_2"))]
940 #[test]
941 fn all_modern_options() {
942 let mut attr = Attribute::new("p4", GlobalOpts::new());
943 attr.set_on_submitted_files(true).set_propagating(true);
944
945 let attr = attr
946 .store_in_trait_depot()
947 .read_from_file("thumb".to_string(), PathBuf::from("/tmp/data.bin"));
948
949 assert_eq!(
950 args_of(&attr.setup_command("p4")),
951 [
952 "attribute",
953 "-f",
954 "-p",
955 "-T1",
956 "-I",
957 "/tmp/data.bin",
958 "-n",
959 "thumb"
960 ]
961 );
962 }
963
964 #[test]
965 fn set_style_with_global_opts() {
966 let mut attr = Attribute::new("p4", GlobalOpts::new().port("localhost:1666"));
967 attr.set("status", "approved");
968
969 assert_eq!(
970 args_of(&attr.setup_command("p4")),
971 [
972 "-p",
973 "localhost:1666",
974 "attribute",
975 "-n",
976 "status",
977 "-v",
978 "approved"
979 ]
980 );
981 }
982}