diff --git a/crates/iota/src/client_ptb/lexer.rs b/crates/iota/src/client_ptb/lexer.rs index 9b773c8b7c8..ccf788f67c9 100644 --- a/crates/iota/src/client_ptb/lexer.rs +++ b/crates/iota/src/client_ptb/lexer.rs @@ -54,6 +54,60 @@ impl<'l, I: Iterator> Lexer<'l, I> { } } + /// Skip the rest of the current shell token (used for comments). + fn skip_rest_of_token(&mut self) { + let len = self.buf.len(); + self.offset += len; + self.buf = ""; + } + + /// Skip tokens until we find the next real command or EOF. + /// + /// Since shell line continuations (`\`) convert newlines to spaces, we + /// cannot detect line boundaries. Instead, we use `//` and `--` as + /// delimiters: + /// + /// - `//` marks the start of a comment + /// - `--` marks the start of a command + /// + /// To comment out a `--command`, place `//` directly + /// before it (as the immediately preceding token). Any `--` that follows + /// non-`//` tokens is treated as a real command. + /// + /// # Examples + /// + /// ```text + /// // comment text --cmd → "--cmd" runs (text before it) + /// // --cmd → "--cmd" is commented (// directly before) + /// // --cmd1 --cmd2 → "--cmd1" commented, "--cmd2" runs + /// // // --cmd → "--cmd" is commented (// directly before) + /// // text // --cmd → "--cmd" is commented (// directly before) + /// ``` + fn skip_comment(&mut self) { + self.skip_rest_of_token(); + let mut after_comment_marker = true; + + for next in self.tokens.by_ref() { + self.offset += next.len() + 1; + + if next.starts_with("//") { + after_comment_marker = true; + } else if next.starts_with("--") { + if after_comment_marker { + // "--" directly after "//" → commented out, keep skipping + after_comment_marker = false; + } else { + // "--" after other tokens → real command + self.buf = next; + return; + } + } else { + after_comment_marker = false; + } + } + self.buf = ""; + } + /// Checks whether the current shell token starts with the prefix `patt`, /// and consumes it if so, returning a spanned slice of the consumed /// prefix. @@ -226,6 +280,13 @@ impl<'l, I: Iterator> Iterator for Lexer<'l, I> { } Some(match c { + // Comment: skip all tokens until we hit a command (--) or EOF + // Using // style comments as they work with shell line continuations + sp!(_, "/") if self.buf.starts_with("//") => { + self.skip_comment(); + return self.next(); + } + // Single character tokens sp!(_, ",") => token!(T::Comma), sp!(_, "[") => token!(T::LBracket), @@ -518,4 +579,116 @@ mod tests { let unexpected = vec!["4 * 5"]; insta::assert_debug_snapshot!(lex(unexpected)); } + + #[test] + fn slash_comments_basic() { + // Basic comment: // text --cmd → "--cmd" runs + let tokens = vec![ + "//", + "this", + "is", + "a", + "comment", + "--split-coins", + "gas", + "[1000]", + ]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_command_directly_after() { + // Commented command: // --cmd → "--cmd" is skipped + let tokens = vec!["//", "--split-coins", "gas", "[1]", "--assign", "result"]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_two_commands_after() { + // Two commands after //: // --cmd1 --cmd2 → "--cmd1" skipped, "--cmd2" runs + let tokens = vec![ + "//", + "--split-coins", + "gas", + "[1]", + "--assign", + "result", + "--dry-run", + ]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_chained() { + // Chained comments: // // --cmd → "--cmd" is skipped + let tokens = vec![ + "//", + "//", + "--split-coins", + "gas", + "[1]", + "--assign", + "result", + ]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_text_then_comment_then_command() { + // Text then comment: // text // --cmd → "--cmd" is skipped + let tokens = vec![ + "//", + "some", + "text", + "//", + "--split-coins", + "gas", + "[1]", + "--assign", + "result", + ]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_multiple_comment_lines() { + // Multiple comment lines with commands + // // comment1 + // // --commented-cmd + // --real-cmd + let tokens = vec![ + "//", + "comment1", + "//", + "--commented-cmd", + "arg1", + "--real-cmd", + "arg2", + ]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_inline_with_text() { + // Inline comment: //comment (no space) also works + let tokens = vec!["//comment", "text", "--cmd", "arg"]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_only_comments() { + // Only comments, no commands + let tokens = vec!["//", "just", "a", "comment"]; + insta::assert_debug_snapshot!(lex(tokens)); + } + + #[test] + fn slash_comments_empty_comment() { + // Empty comment followed by commented command + // // \ + // // --cmd \ + // --real + let tokens = vec!["//", "//", "--commented", "--real"]; + insta::assert_debug_snapshot!(lex(tokens)); + } } diff --git a/crates/iota/src/client_ptb/ptb.rs b/crates/iota/src/client_ptb/ptb.rs index 80dddd37cfa..2e92218fb19 100644 --- a/crates/iota/src/client_ptb/ptb.rs +++ b/crates/iota/src/client_ptb/ptb.rs @@ -342,6 +342,7 @@ pub fn ptb_description() -> clap::Command { .about( "Build, preview, and execute programmable transaction blocks. Depending on your \ shell, you might have to use quotes around arrays or other passed values. \ + Use // for comments (with trailing \\). To comment out a command, place // directly before it (e.g., // --cmd). \ Use --help to see examples for how to use the core functionality of this command.") .arg(arg!( --"assign" diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_basic.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_basic.snap new file mode 100644 index 00000000000..af8f3d3553c --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_basic.snap @@ -0,0 +1,67 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 591 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 34, + end: 47, + }, + value: Lexeme( + Command, + "split-coins", + ), + }, + Spanned { + span: Span { + start: 48, + end: 51, + }, + value: Lexeme( + Ident, + "gas", + ), + }, + Spanned { + span: Span { + start: 52, + end: 53, + }, + value: Lexeme( + LBracket, + "[", + ), + }, + Spanned { + span: Span { + start: 53, + end: 57, + }, + value: Lexeme( + Number, + "1000", + ), + }, + Spanned { + span: Span { + start: 57, + end: 58, + }, + value: Lexeme( + RBracket, + "]", + ), + }, + Spanned { + span: Span { + start: 58, + end: 58, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_chained.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_chained.snap new file mode 100644 index 00000000000..57fc90f8f3f --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_chained.snap @@ -0,0 +1,37 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 622 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 36, + end: 44, + }, + value: Lexeme( + Command, + "assign", + ), + }, + Spanned { + span: Span { + start: 45, + end: 51, + }, + value: Lexeme( + Ident, + "result", + ), + }, + Spanned { + span: Span { + start: 51, + end: 51, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_command_directly_after.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_command_directly_after.snap new file mode 100644 index 00000000000..865ecab184a --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_command_directly_after.snap @@ -0,0 +1,37 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 601 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 33, + end: 41, + }, + value: Lexeme( + Command, + "assign", + ), + }, + Spanned { + span: Span { + start: 42, + end: 48, + }, + value: Lexeme( + Ident, + "result", + ), + }, + Spanned { + span: Span { + start: 48, + end: 48, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_empty_comment.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_empty_comment.snap new file mode 100644 index 00000000000..b9cfeb33b4c --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_empty_comment.snap @@ -0,0 +1,27 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 679 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 24, + end: 30, + }, + value: Lexeme( + Command, + "real", + ), + }, + Spanned { + span: Span { + start: 30, + end: 30, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_inline_with_text.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_inline_with_text.snap new file mode 100644 index 00000000000..bd2c22769e1 --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_inline_with_text.snap @@ -0,0 +1,37 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 656 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 20, + end: 25, + }, + value: Lexeme( + Command, + "cmd", + ), + }, + Spanned { + span: Span { + start: 26, + end: 29, + }, + value: Lexeme( + Ident, + "arg", + ), + }, + Spanned { + span: Span { + start: 29, + end: 29, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_multiple_comment_lines.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_multiple_comment_lines.snap new file mode 100644 index 00000000000..235402ab794 --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_multiple_comment_lines.snap @@ -0,0 +1,37 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 646 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 46, + end: 56, + }, + value: Lexeme( + Command, + "real-cmd", + ), + }, + Spanned { + span: Span { + start: 57, + end: 61, + }, + value: Lexeme( + Ident, + "arg2", + ), + }, + Spanned { + span: Span { + start: 61, + end: 61, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_only_comments.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_only_comments.snap new file mode 100644 index 00000000000..94cee834038 --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_only_comments.snap @@ -0,0 +1,17 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 665 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 17, + end: 17, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_text_then_comment_then_command.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_text_then_comment_then_command.snap new file mode 100644 index 00000000000..e957040c720 --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_text_then_comment_then_command.snap @@ -0,0 +1,37 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 632 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 46, + end: 54, + }, + value: Lexeme( + Command, + "assign", + ), + }, + Spanned { + span: Span { + start: 55, + end: 61, + }, + value: Lexeme( + Ident, + "result", + ), + }, + Spanned { + span: Span { + start: 61, + end: 61, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_two_commands_after.snap b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_two_commands_after.snap new file mode 100644 index 00000000000..329818a67d6 --- /dev/null +++ b/crates/iota/src/client_ptb/snapshots/iota__client_ptb__lexer__tests__slash_comments_two_commands_after.snap @@ -0,0 +1,47 @@ +--- +source: crates/iota/src/client_ptb/lexer.rs +assertion_line: 612 +expression: lex(tokens) +--- +[ + Spanned { + span: Span { + start: 33, + end: 41, + }, + value: Lexeme( + Command, + "assign", + ), + }, + Spanned { + span: Span { + start: 42, + end: 48, + }, + value: Lexeme( + Ident, + "result", + ), + }, + Spanned { + span: Span { + start: 49, + end: 58, + }, + value: Lexeme( + Command, + "dry-run", + ), + }, + Spanned { + span: Span { + start: 58, + end: 58, + }, + value: Lexeme( + Eof, + "", + ), + }, +] diff --git a/docs/content/developer/references/cli/ptb.mdx b/docs/content/developer/references/cli/ptb.mdx index ea0f623d6c4..5dcb3fc6296 100644 --- a/docs/content/developer/references/cli/ptb.mdx +++ b/docs/content/developer/references/cli/ptb.mdx @@ -15,7 +15,7 @@ The `client ptb` command allows you to specify the transactions for execution in The following list itemizes all the available args for the `iota client ptb` command. Use the `--help` for a long help version that includes some examples on how to use this command. ``` -Build, preview, and execute programmable transaction blocks. Depending on your shell, you might have to use quotes around arrays or other passed values. Use --help to see examples for how to use the core functionality of this command. +Build, preview, and execute programmable transaction blocks. Depending on your shell, you might have to use quotes around arrays or other passed values. Use `//` for comments (with trailing `\`). To comment out a command, place // directly before it (e.g., // --cmd). Use --help to see examples for how to use the core functionality of this command. Usage: iota client ptb [OPTIONS] @@ -127,6 +127,29 @@ Here are some examples for `transfer-objects` and `gas-coin`: iota client ptb --transfer-objects [ARRAY_OF_OBJECTS] @0x02a212de6a9dfa3a69e22387acfbafbb1a9e591bd9d636e7895dcfc8de05f331 --gas-coin @0x00002819ee07a66e53800495ccf5eeade8a02054a2e0827546c70e4b226f0495 ``` +### Comments + +You can add comments to multi-line PTB commands using `//` (with trailing `\`). This is useful for documenting complex transaction blocks: + +```bash +iota client ptb \ +// Get the sender address \ +--move-call iota::tx_context::sender \ +--assign sender \ +// Split 1 NANOS from gas coin \ +--split-coins gas "[1]" \ +--assign coin \ +// Transfer the new coin to the sender \ +--transfer-objects "[coin]" sender \ +--dry-run +``` + +:::note + +Use `//` for comments instead of `#`. The `#` character is interpreted by bash/zsh as a shell comment, which breaks line continuations. Remember to add `\` at the end of comment lines to continue the command. + +::: + ### Assign Use the `--assign` argument to bind values to variables. There are two ways you can use it: