hyper_scripter/util/
mod.rs

1use crate::color::{Color, Stylize};
2use crate::config::Config;
3use crate::error::{Contextable, Error, FormatCode::Template as TemplateCode, Result};
4use crate::path;
5use crate::script::ScriptInfo;
6use crate::script_type::{get_default_template, ScriptFullType, ScriptType};
7use ::serde::Serialize;
8use chrono::{DateTime, Utc};
9use shlex::Shlex;
10use std::borrow::Cow;
11use std::ffi::OsStr;
12use std::fs::{create_dir_all, remove_file, rename, File};
13use std::io::{Read, Write};
14use std::path::{Path, PathBuf};
15use std::process::Command;
16
17pub mod completion_util;
18pub mod holder;
19pub mod main_util;
20pub mod shebang_handle;
21pub mod writable;
22
23pub mod init_repo;
24pub use init_repo::*;
25
26pub mod serde;
27pub(crate) use self::serde::impl_de_by_from_str;
28pub(crate) use self::serde::impl_ser_by_to_string;
29
30pub fn illegal_name(s: &str) -> bool {
31    s.starts_with('-')
32        || s.starts_with('.')
33        || s.contains("..")
34        || s.contains(' ')
35        || s.contains('@')
36        || s.contains('*')
37        || s.contains('!')
38        || s.contains('?')
39        || s.contains('=')
40        || s.contains('/')
41        || s.is_empty()
42}
43
44pub fn run_cmd(mut cmd: Command) -> Result<Option<i32>> {
45    log::debug!("執行命令 {:?}", cmd);
46    let res = cmd.spawn();
47    let program = cmd.get_program();
48    let mut child = handle_fs_res(&[program], res)?;
49    let stat = handle_fs_res(&[program], child.wait())?;
50    if stat.success() {
51        Ok(None)
52    } else {
53        Ok(Some(stat.code().unwrap_or_default()))
54    }
55}
56#[cfg(not(target_os = "linux"))]
57pub fn create_cmd(cmd_str: &str, args: &[impl AsRef<OsStr>]) -> Command {
58    log::debug!("在非 linux 上執行,用 sh -c 包一層");
59    let args: Vec<_> = args
60        .iter()
61        .map(|s| {
62            s.as_ref()
63                .to_str()
64                .unwrap()
65                .to_string()
66                .replace(r"\", r"\\\\")
67        })
68        .collect();
69    let arg = format!("{} {}", cmd_str, args.join(" "));
70    let mut cmd = Command::new("sh");
71    cmd.args(&["-c", &arg]);
72    cmd
73}
74#[cfg(target_os = "linux")]
75pub fn create_cmd<I, S1, S2>(cmd_str: S2, args: I) -> Command
76where
77    I: IntoIterator<Item = S1>,
78    S1: AsRef<OsStr>,
79    S2: AsRef<OsStr>,
80{
81    let mut cmd = Command::new(&cmd_str);
82    cmd.args(args);
83    cmd
84}
85
86pub fn run_shell(args: &[String]) -> Result<i32> {
87    let cmd = args.join(" ");
88    log::debug!("shell args = {:?}", cmd);
89    let mut cmd = create_cmd("sh", ["-c", &cmd]);
90    let env = Config::get().gen_env(&TmplVal::new(), false)?;
91    cmd.envs(env.iter().map(|(a, b)| (a, b)));
92    let code = run_cmd(cmd)?;
93    Ok(code.unwrap_or_default())
94}
95
96pub fn open_editor<'a>(path: impl IntoIterator<Item = &'a Path>) -> Result {
97    let conf = Config::get();
98    let editor = conf.editor.iter().map(|s| Cow::Borrowed(s.as_ref()));
99    let cmd = create_concat_cmd(editor, path);
100    let code = run_cmd(cmd)?;
101    if let Some(code) = code {
102        return Err(Error::EditorError(code, conf.editor.clone()));
103    }
104    Ok(())
105}
106
107pub fn create_concat_cmd_shlex<'b, I2, S2>(arg1: &str, arg2: I2) -> Command
108where
109    I2: IntoIterator<Item = &'b S2>,
110    S2: AsRef<OsStr> + 'b + ?Sized,
111{
112    let arg1 = Shlex::new(arg1).map(|s| Cow::Owned(s.into()));
113    create_concat_cmd(arg1, arg2)
114}
115
116pub fn create_concat_cmd<'a, I1, I2, S2>(arg1: I1, arg2: I2) -> Command
117where
118    I1: IntoIterator<Item = Cow<'a, OsStr>>,
119    I2: IntoIterator<Item = &'a S2>,
120    S2: AsRef<OsStr> + 'a + ?Sized,
121{
122    let mut arg1 = arg1.into_iter();
123    let cmd = arg1.next().unwrap();
124    let remaining = arg1.chain(arg2.into_iter().map(|s| Cow::Borrowed(s.as_ref())));
125    create_cmd(cmd, remaining)
126}
127
128pub fn file_modify_time(path: &Path) -> Result<DateTime<Utc>> {
129    let meta = handle_fs_res(&[path], std::fs::metadata(path))?;
130    let modified = handle_fs_res(&[path], meta.modified())?;
131    Ok(modified.into())
132}
133
134pub fn read_file(path: &Path) -> Result<String> {
135    let mut file = handle_fs_res(&[path], File::open(path)).context("唯讀開啟檔案失敗")?;
136    let mut content = String::new();
137    handle_fs_res(&[path], file.read_to_string(&mut content)).context("讀取檔案失敗")?;
138    Ok(content)
139}
140
141pub fn write_file(path: &Path, content: &str) -> Result<()> {
142    let mut file = handle_fs_res(&[path], File::create(path))?;
143    handle_fs_res(&[path], file.write_all(content.as_bytes()))
144}
145pub fn remove(script_path: &Path) -> Result<()> {
146    handle_fs_res(&[&script_path], remove_file(&script_path))
147}
148pub fn mv(origin: &Path, new: &Path) -> Result<()> {
149    log::info!("修改 {:?} 為 {:?}", origin, new);
150    // NOTE: 創建資料夾和檔案
151    if let Some(parent) = new.parent() {
152        handle_fs_res(&[&new], create_dir_all(parent))?;
153    }
154    handle_fs_res(&[&new, &origin], rename(&origin, &new))
155}
156pub fn cp(origin: &Path, new: &Path) -> Result<()> {
157    // NOTE: 創建資料夾和檔案
158    if let Some(parent) = new.parent() {
159        handle_fs_res(&[parent], create_dir_all(parent))?;
160    }
161    let _copied = handle_fs_res(&[&origin, &new], std::fs::copy(&origin, &new))?;
162    Ok(())
163}
164
165pub fn handle_fs_err<P: AsRef<Path>>(path: &[P], err: std::io::Error) -> Error {
166    use std::sync::Arc;
167    let mut p = path.iter().map(|p| p.as_ref().to_owned()).collect();
168    log::warn!("檔案系統錯誤:{:?}, {:?}", p, err);
169    match err.kind() {
170        std::io::ErrorKind::PermissionDenied => Error::PermissionDenied(p),
171        std::io::ErrorKind::NotFound => Error::PathNotFound(p),
172        std::io::ErrorKind::AlreadyExists => Error::PathExist(p.remove(0)),
173        _ => Error::GeneralFS(p, Arc::new(err)),
174    }
175}
176pub fn handle_fs_res<T, P: AsRef<Path>>(path: &[P], res: std::io::Result<T>) -> Result<T> {
177    match res {
178        Ok(t) => Ok(t),
179        Err(e) => Err(handle_fs_err(path, e)),
180    }
181}
182
183/// check_subtype 是為避免太容易生出子模版
184pub fn get_or_create_template_path(
185    ty: &ScriptFullType,
186    force: bool,
187    check_subtype: bool,
188) -> Result<(PathBuf, Option<&'static str>)> {
189    if !force {
190        Config::get().get_script_conf(&ty.ty)?; // 確認類型存在與否
191    }
192    let tmpl_path = path::get_template_path(ty)?;
193    if !tmpl_path.exists() {
194        if check_subtype && ty.sub.is_some() {
195            return Err(Error::UnknownType(ty.to_string()));
196        }
197        let default_tmpl = get_default_template(ty);
198        return write_file(&tmpl_path, default_tmpl).map(|_| (tmpl_path, Some(default_tmpl)));
199    }
200    Ok((tmpl_path, None))
201}
202pub fn get_or_create_template(
203    ty: &ScriptFullType,
204    force: bool,
205    check_subtype: bool,
206) -> Result<String> {
207    let (tmpl_path, default_tmpl) = get_or_create_template_path(ty, force, check_subtype)?;
208    if let Some(default_tmpl) = default_tmpl {
209        return Ok(default_tmpl.to_owned());
210    }
211    read_file(&tmpl_path)
212}
213
214fn relative_to_home(p: &Path) -> Option<&Path> {
215    const CUR_DIR: &str = ".";
216    let home = dirs::home_dir()?;
217    if p == home {
218        return Some(CUR_DIR.as_ref());
219    }
220    p.strip_prefix(&home).ok()
221}
222
223fn get_birthplace() -> Result<PathBuf> {
224    // NOTE: 用 $PWD 可以取到 symlink 還沒解開前的路徑
225    // 若用 std::env::current_dir,該路徑已為真實路徑
226    let here = std::env::var("PWD")?;
227    Ok(here.into())
228}
229
230#[derive(Debug)]
231pub struct PrepareRespond {
232    pub is_new: bool,
233    pub time: DateTime<Utc>,
234}
235pub fn prepare_script<T: AsRef<str>>(
236    path: &Path,
237    script: &ScriptInfo,
238    template: Option<String>,
239    content: &[T],
240) -> Result<PrepareRespond> {
241    log::info!("開始準備 {} 腳本內容……", script.name);
242    let has_content = !content.is_empty();
243    let is_new = !path.exists();
244    if is_new {
245        let birthplace = get_birthplace()?;
246        let birthplace_rel = relative_to_home(&birthplace);
247
248        let mut file = handle_fs_res(&[path], File::create(&path))?;
249
250        let content = content.iter().map(|s| s.as_ref().split('\n')).flatten();
251        if let Some(template) = template {
252            let content: Vec<_> = content.collect();
253            let info = json!({
254                "birthplace_in_home": birthplace_rel.is_some(),
255                "birthplace_rel": birthplace_rel,
256                "birthplace": birthplace,
257                "name": script.name.key().to_owned(),
258                "content": content,
259            });
260            log::debug!("編輯模版資訊:{:?}", info);
261            write_prepare_script(file, &path, &template, &info)?;
262        } else {
263            let mut first = true;
264            for line in content {
265                if !first {
266                    writeln!(file, "")?;
267                }
268                first = false;
269                write!(file, "{}", line)?;
270            }
271        }
272    } else {
273        if has_content {
274            log::debug!("腳本已存在,往後接上給定的訊息");
275            let mut file = handle_fs_res(
276                &[path],
277                std::fs::OpenOptions::new()
278                    .append(true)
279                    .write(true)
280                    .open(path),
281            )?;
282            for content in content.iter() {
283                handle_fs_res(&[path], writeln!(&mut file, "{}", content.as_ref()))?;
284            }
285        }
286    }
287
288    Ok(PrepareRespond {
289        is_new,
290        time: file_modify_time(path)?,
291    })
292}
293fn write_prepare_script<W: Write>(
294    w: W,
295    path: &Path,
296    template: &str,
297    info: &serde_json::Value,
298) -> Result {
299    use handlebars::{Handlebars, TemplateRenderError};
300    let reg = Handlebars::new();
301    reg.render_template_to_write(&template, &info, w)
302        .map_err(|err| match err {
303            TemplateRenderError::TemplateError(err) => {
304                log::warn!("解析模版錯誤:{}", err);
305                TemplateCode.to_err(template.to_owned())
306            }
307            TemplateRenderError::IOError(err, ..) => handle_fs_err(&[path], err),
308            TemplateRenderError::RenderError(err) => err.into(),
309        })
310}
311
312/// 可用來表示「未知類別」的概念 TODO: 測試之
313pub struct DisplayType<'a> {
314    ty: &'a ScriptType,
315    color: Option<Color>,
316}
317impl<'a> DisplayType<'a> {
318    pub fn is_unknown(&self) -> bool {
319        self.color.is_none()
320    }
321    pub fn color(&self) -> Color {
322        self.color.unwrap_or(Color::BrightBlack)
323    }
324    pub fn display(&self) -> Cow<'a, str> {
325        if self.is_unknown() {
326            Cow::Owned(format!("{}, unknown", self.ty))
327        } else {
328            Cow::Borrowed(self.ty.as_ref())
329        }
330    }
331}
332pub fn get_display_type(ty: &ScriptType) -> DisplayType<'_> {
333    let conf = Config::get();
334    match conf.get_color(ty) {
335        Err(e) => {
336            log::warn!("取腳本顏色時出錯:{},視為未知類別", e);
337            DisplayType { ty, color: None }
338        }
339        Ok(c) => DisplayType { ty, color: Some(c) },
340    }
341}
342
343pub fn print_iter<T: std::fmt::Display>(iter: impl Iterator<Item = T>, sep: &str) -> bool {
344    let mut first = true;
345    for t in iter {
346        if !first {
347            print!("{}", sep);
348        }
349        first = false;
350        print!("{}", t);
351    }
352    !first
353}
354
355pub fn option_map_res<T, F: FnOnce(T) -> Result<T>>(opt: Option<T>, f: F) -> Result<Option<T>> {
356    Ok(match opt {
357        Some(t) => Some(f(t)?),
358        None => None,
359    })
360}
361
362pub fn hijack_ctrlc_once() {
363    use std::sync::Once;
364    static CTRLC_HANDLE: Once = Once::new();
365    log::debug!("劫持 ctrl-c 回調");
366    CTRLC_HANDLE.call_once(|| {
367        let res = ctrlc::set_handler(|| log::warn!("收到 ctrl-c"));
368        if res.is_err() {
369            log::warn!("設置 ctrl-c 回調失敗 {:?}", res);
370        }
371    });
372}
373
374pub fn prompt(msg: impl std::fmt::Display, allow_enter: bool) -> Result<bool> {
375    use console::{Key, Term};
376
377    enum Res {
378        Y,
379        N,
380        Exit,
381    }
382
383    fn inner(term: &Term, msg: &str, allow_enter: bool) -> Result<Res> {
384        term.hide_cursor()?;
385        hijack_ctrlc_once();
386
387        let res = loop {
388            term.write_str(msg)?;
389            match term.read_key() {
390                Ok(Key::Char('Y' | 'y')) => break Res::Y,
391                Ok(Key::Enter) => {
392                    if allow_enter {
393                        break Res::Y;
394                    } else {
395                        term.write_line("")?;
396                    }
397                }
398                Ok(Key::Char('N' | 'n')) => break Res::N,
399                Ok(Key::Char(ch)) => term.write_line(&format!(" Unknown key '{}'", ch))?,
400                Ok(Key::Escape) => {
401                    break Res::Exit;
402                }
403                Err(e) => {
404                    if e.kind() == std::io::ErrorKind::Interrupted {
405                        break Res::Exit;
406                    } else {
407                        return Err(e.into());
408                    }
409                }
410                _ => term.write_line(" Unknown key")?,
411            }
412        };
413        Ok(res)
414    }
415
416    let term = Term::stderr();
417    let msg = if allow_enter {
418        format!("{} [Y/Enter/N]", msg)
419    } else {
420        format!("{} [Y/N]", msg)
421    };
422    let res = inner(&term, &msg, allow_enter);
423    term.show_cursor()?;
424    match res? {
425        Res::Exit => {
426            std::process::exit(1);
427        }
428        Res::Y => {
429            term.write_line(&" Y".stylize().color(Color::Green).to_string())?;
430            Ok(true)
431        }
432        Res::N => {
433            term.write_line(&" N".stylize().color(Color::Red).to_string())?;
434            Ok(false)
435        }
436    }
437}
438
439#[derive(Serialize)]
440pub struct TmplVal<'a> {
441    home: &'static Path,
442    cmd: String,
443    exe: PathBuf,
444
445    path: Option<&'a Path>,
446    run_id: Option<i64>,
447    tags: Vec<&'a str>,
448    env_desc: Vec<String>,
449    name: Option<&'a str>,
450    content: Option<&'a str>,
451}
452impl<'a> TmplVal<'a> {
453    pub fn new() -> Self {
454        TmplVal {
455            home: path::get_home(),
456            cmd: std::env::args().next().unwrap_or_default(),
457            exe: std::env::current_exe().unwrap_or_default(),
458
459            path: None,
460            run_id: None,
461            tags: vec![],
462            env_desc: vec![],
463            name: None,
464            content: None,
465        }
466    }
467}