Skip to main content

nu_command/debug/
view_source.rs

1use nu_engine::command_prelude::*;
2use nu_protocol::{Config, PipelineMetadata, Span};
3
4use std::fmt::Write;
5
6#[derive(Clone)]
7pub struct ViewSource;
8
9impl Command for ViewSource {
10    fn name(&self) -> &str {
11        "view source"
12    }
13
14    fn description(&self) -> &str {
15        "View a block, module, or a definition."
16    }
17
18    fn signature(&self) -> nu_protocol::Signature {
19        Signature::build("view source")
20            .input_output_types(vec![(Type::Nothing, Type::String)])
21            .required("item", SyntaxShape::Any, "Name or block to view.")
22            .category(Category::Debug)
23    }
24
25    fn examples(&self) -> Vec<Example<'_>> {
26        vec![
27            Example {
28                description: "View the source of a code block.",
29                example: r#"let abc = {|| echo 'hi' }; view source $abc"#,
30                result: Some(Value::test_string("{|| echo 'hi' }")),
31            },
32            Example {
33                description: "View the source of a custom command.",
34                example: r#"def hi [] { echo 'Hi!' }; view source hi"#,
35                result: Some(Value::test_string("def hi [] { echo 'Hi!' }")),
36            },
37            Example {
38                description: "View the source of a custom command, which participates in the caller environment.",
39                example: r#"def --env foo [] { $env.BAR = 'BAZ' }; view source foo"#,
40                result: Some(Value::test_string("def --env foo [] { $env.BAR = 'BAZ' }")),
41            },
42            Example {
43                description: "View the source of a custom command with flags.",
44                example: r#"def --wrapped --env foo [...rest: string] { print $rest }; view source foo"#,
45                result: Some(Value::test_string(
46                    "def --env --wrapped foo [ ...rest: string] { print $rest }",
47                )),
48            },
49            Example {
50                description: "View the source of a custom command with flags and arguments.",
51                example: r#"def test [a?:any --b:int ...rest:string] { echo 'test' }; view source test"#,
52                result: Some(Value::test_string(
53                    "def test [ a?: any --b: int ...rest: string] { echo 'test' }",
54                )),
55            },
56            Example {
57                description: "View the source of a module.",
58                example: r#"module mod-foo { export-env { $env.FOO_ENV = 'BAZ' } }; view source mod-foo"#,
59                result: Some(Value::test_string(" export-env { $env.FOO_ENV = 'BAZ' }")),
60            },
61            Example {
62                description: "View the source of an alias.",
63                example: r#"alias hello = echo hi; view source hello"#,
64                result: Some(Value::test_string("echo hi")),
65            },
66            Example {
67                description: "View the file where a definition lives via metadata.",
68                example: r#"view source some_command | metadata"#,
69                result: None,
70            },
71        ]
72    }
73
74    fn run(
75        &self,
76        engine_state: &EngineState,
77        stack: &mut Stack,
78        call: &Call,
79        _input: PipelineData,
80    ) -> Result<PipelineData, ShellError> {
81        let arg: Value = call.req(engine_state, stack, 0)?;
82        let arg_span = arg.span();
83
84        match arg {
85            Value::Int { val, .. } => {
86                if let Some(block) =
87                    engine_state.try_get_block(nu_protocol::BlockId::new(val as usize))
88                {
89                    if let Some(span) = block.span {
90                        let contents = engine_state.get_span_contents(span);
91                        let src = String::from_utf8_lossy(contents).to_string();
92
93                        Ok(make_output(engine_state, src, Some(span), call.head))
94                    } else {
95                        Err(ShellError::GenericError {
96                            error: "Cannot view int value".to_string(),
97                            msg: "the block does not have a viewable span".to_string(),
98                            span: Some(arg_span),
99                            help: None,
100                            inner: vec![],
101                        })
102                    }
103                } else {
104                    Err(ShellError::GenericError {
105                        error: format!("Block Id {} does not exist", arg.coerce_into_string()?),
106                        msg: "this number does not correspond to a block".to_string(),
107                        span: Some(arg_span),
108                        help: None,
109                        inner: vec![],
110                    })
111                }
112            }
113
114            Value::String { val, .. } => {
115                if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) {
116                    // arg is a command
117                    let decl = engine_state.get_decl(decl_id);
118                    let sig = decl.signature();
119                    let vec_of_required = &sig.required_positional;
120                    let vec_of_optional = &sig.optional_positional;
121                    let rest = &sig.rest_positional;
122                    let vec_of_flags = &sig.named;
123                    let type_signatures = &sig.input_output_types;
124
125                    if decl.is_alias() {
126                        if let Some(alias) = &decl.as_alias() {
127                            let contents = String::from_utf8_lossy(
128                                engine_state.get_span_contents(alias.wrapped_call.span),
129                            );
130
131                            Ok(make_output(
132                                engine_state,
133                                contents.to_string(),
134                                Some(alias.wrapped_call.span),
135                                call.head,
136                            ))
137                        } else {
138                            Ok(make_output(
139                                engine_state,
140                                "no alias found".to_string(),
141                                None,
142                                call.head,
143                            ))
144                        }
145                    }
146                    // gets vector of positionals.
147                    else if let Some(block_id) = decl.block_id() {
148                        let block = engine_state.get_block(block_id);
149                        if let Some(block_span) = block.span {
150                            let contents = engine_state.get_span_contents(block_span);
151                            // name of function
152                            let mut final_contents = String::new();
153                            // Collect def flags based on block and signature properties
154                            let flags: Vec<&str> = [
155                                block.redirect_env.then_some("--env"),
156                                sig.allows_unknown_args.then_some("--wrapped"),
157                            ]
158                            .into_iter()
159                            .flatten()
160                            .collect();
161                            let flags_str = if flags.is_empty() {
162                                String::new()
163                            } else {
164                                format!("{} ", flags.join(" "))
165                            };
166                            if val.contains(' ') {
167                                let _ = write!(&mut final_contents, "def {flags_str}\"{val}\" [");
168                            } else {
169                                let _ = write!(&mut final_contents, "def {flags_str}{val} [");
170                            };
171                            if !vec_of_required.is_empty()
172                                || !vec_of_optional.is_empty()
173                                || vec_of_flags.len() != 1
174                                || rest.is_some()
175                            {
176                                final_contents.push(' ');
177                            }
178                            for n in vec_of_required {
179                                let _ = write!(&mut final_contents, "{}: {} ", n.name, n.shape);
180                                // positional arguments
181                            }
182                            for n in vec_of_optional {
183                                if let Some(s) = n.default_value.clone() {
184                                    let _ = write!(
185                                        &mut final_contents,
186                                        "{}: {} = {} ",
187                                        n.name,
188                                        n.shape,
189                                        s.to_expanded_string(" ", &Config::default())
190                                    );
191                                } else {
192                                    let _ =
193                                        write!(&mut final_contents, "{}?: {} ", n.name, n.shape);
194                                }
195                            }
196                            for n in vec_of_flags {
197                                // skip adding the help flag
198                                if n.long == "help" {
199                                    continue;
200                                }
201                                let _ = write!(&mut final_contents, "--{}", n.long);
202                                if let Some(short) = n.short {
203                                    let _ = write!(&mut final_contents, "(-{short})");
204                                }
205                                if let Some(arg) = &n.arg {
206                                    let _ = write!(&mut final_contents, ": {arg}");
207                                }
208                                final_contents.push(' ');
209                            }
210                            if let Some(rest_arg) = rest {
211                                let _ = write!(
212                                    &mut final_contents,
213                                    "...{}:{}",
214                                    rest_arg.name, rest_arg.shape
215                                );
216                            }
217                            let len = type_signatures.len();
218                            if len != 0 {
219                                final_contents.push_str("]: [");
220                                let mut c = 0;
221                                for (insig, outsig) in type_signatures {
222                                    c += 1;
223                                    let s = format!("{insig} -> {outsig}");
224                                    final_contents.push_str(&s);
225                                    if c != len {
226                                        final_contents.push_str(", ")
227                                    }
228                                }
229                            }
230                            final_contents.push_str("] ");
231                            final_contents.push_str(&String::from_utf8_lossy(contents));
232
233                            Ok(make_output(
234                                engine_state,
235                                final_contents,
236                                Some(block_span),
237                                call.head,
238                            ))
239                        } else {
240                            Err(ShellError::GenericError {
241                                error: "Cannot view string value".to_string(),
242                                msg: "the command does not have a viewable block span".to_string(),
243                                span: Some(arg_span),
244                                help: None,
245                                inner: vec![],
246                            })
247                        }
248                    } else {
249                        Err(ShellError::GenericError {
250                            error: "Cannot view string decl value".to_string(),
251                            msg: "the command does not have a viewable block".to_string(),
252                            span: Some(arg_span),
253                            help: None,
254                            inner: vec![],
255                        })
256                    }
257                } else if let Some(module_id) = engine_state.find_module(val.as_bytes(), &[]) {
258                    // arg is a module
259                    let module = engine_state.get_module(module_id);
260                    if let Some(module_span) = module.span {
261                        let contents = engine_state.get_span_contents(module_span);
262
263                        Ok(make_output(
264                            engine_state,
265                            String::from_utf8_lossy(contents).to_string(),
266                            Some(module_span),
267                            call.head,
268                        ))
269                    } else {
270                        Err(ShellError::GenericError {
271                            error: "Cannot view string module value".to_string(),
272                            msg: "the module does not have a viewable block".to_string(),
273                            span: Some(arg_span),
274                            help: None,
275                            inner: vec![],
276                        })
277                    }
278                } else {
279                    Err(ShellError::GenericError {
280                        error: "Cannot view string value".to_string(),
281                        msg: "this name does not correspond to a viewable value".to_string(),
282                        span: Some(arg_span),
283                        help: None,
284                        inner: vec![],
285                    })
286                }
287            }
288            value => {
289                if let Ok(closure) = value.as_closure() {
290                    let block = engine_state.get_block(closure.block_id);
291
292                    if let Some(span) = block.span {
293                        let contents = engine_state.get_span_contents(span);
294
295                        Ok(make_output(
296                            engine_state,
297                            String::from_utf8_lossy(contents).to_string(),
298                            Some(span),
299                            call.head,
300                        ))
301                    } else {
302                        Ok(make_output(
303                            engine_state,
304                            "<internal command>".to_string(),
305                            None,
306                            call.head,
307                        ))
308                    }
309                } else {
310                    Err(ShellError::GenericError {
311                        error: "Cannot view value".to_string(),
312                        msg: "this value cannot be viewed".to_string(),
313                        span: Some(arg_span),
314                        help: None,
315                        inner: vec![],
316                    })
317                }
318            }
319        }
320    }
321}
322
323// Helper function to find the file path associated with a given span, if any.
324fn file_for_span(engine_state: &EngineState, span: Span) -> Option<String> {
325    engine_state
326        .files()
327        .find(|f| f.covered_span.contains_span(span))
328        .map(|f| f.name.to_string())
329}
330
331// Helper function to wrap source text into a string value and record the file path in
332// datasource metadata when available
333fn make_output(
334    engine_state: &EngineState,
335    src: String,
336    span_opt: Option<Span>,
337    call_span: Span,
338) -> PipelineData {
339    let pd = Value::string(src, call_span).into_pipeline_data();
340
341    let mut metadata = PipelineMetadata {
342        content_type: Some("application/x-nuscript".into()),
343        ..Default::default()
344    };
345    if let Some(span) = span_opt
346        && let Some(fname) = file_for_span(engine_state, span)
347    {
348        metadata.data_source = nu_protocol::DataSource::FilePath(std::path::PathBuf::from(fname));
349    }
350
351    pd.set_metadata(Some(metadata))
352}