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 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 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
183pub 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)?; }
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 let here = std::env::var("PWD")?;
227 Ok(here.into())
228}
229
230pub fn compute_hash(msg: &str) -> i64 {
231 use std::hash::{Hash, Hasher};
232 let mut hasher = std::collections::hash_map::DefaultHasher::new(); msg.hash(&mut hasher);
234 let hash = hasher.finish();
235 i64::from_ne_bytes(hash.to_ne_bytes())
236}
237pub fn compute_file_hash(path: &Path) -> Result<i64> {
238 read_file(path).map(|c| compute_hash(&c))
239}
240
241#[derive(Debug, Clone, Copy)]
242pub enum PrepareRespond {
243 New { create_time: DateTime<Utc> },
244 Old { last_hash: i64 },
245}
246pub fn prepare_script<T: AsRef<str>>(
247 path: &Path,
248 script: &ScriptInfo,
249 template: Option<String>,
250 content: &[T],
251) -> Result<PrepareRespond> {
252 log::info!("開始準備 {} 腳本內容……", script.name);
253 let has_content = !content.is_empty();
254 let is_new = !path.exists();
255 if is_new {
256 let birthplace = get_birthplace()?;
257 let birthplace_rel = relative_to_home(&birthplace);
258
259 let mut file = handle_fs_res(&[path], File::create(&path))?;
260
261 let content = content.iter().map(|s| s.as_ref().split('\n')).flatten();
262 if let Some(template) = template {
263 let content: Vec<_> = content.collect();
264 let info = json!({
265 "birthplace_in_home": birthplace_rel.is_some(),
266 "birthplace_rel": birthplace_rel,
267 "birthplace": birthplace,
268 "name": script.name.key().to_owned(),
269 "content": content,
270 });
271 log::debug!("編輯模版資訊:{:?}", info);
272 write_prepare_script(file, &path, &template, &info)?;
273 } else {
274 let mut first = true;
275 for line in content {
276 if !first {
277 writeln!(file, "")?;
278 }
279 first = false;
280 write!(file, "{}", line)?;
281 }
282 }
283
284 Ok(PrepareRespond::New {
285 create_time: file_modify_time(path)?,
286 })
287 } else {
288 if has_content {
289 log::debug!("腳本已存在,往後接上給定的訊息");
290 let mut file = handle_fs_res(
291 &[path],
292 std::fs::OpenOptions::new()
293 .append(true)
294 .write(true)
295 .open(path),
296 )?;
297 for content in content.iter() {
298 handle_fs_res(&[path], writeln!(&mut file, "{}", content.as_ref()))?;
299 }
300 }
301 Ok(PrepareRespond::Old {
302 last_hash: script.hash,
303 })
304 }
305}
306fn write_prepare_script<W: Write>(
307 w: W,
308 path: &Path,
309 template: &str,
310 info: &serde_json::Value,
311) -> Result {
312 use handlebars::{Handlebars, TemplateRenderError};
313 let reg = Handlebars::new();
314 reg.render_template_to_write(&template, &info, w)
315 .map_err(|err| match err {
316 TemplateRenderError::TemplateError(err) => {
317 log::warn!("解析模版錯誤:{}", err);
318 TemplateCode.to_err(template.to_owned())
319 }
320 TemplateRenderError::IOError(err, ..) => handle_fs_err(&[path], err),
321 TemplateRenderError::RenderError(err) => err.into(),
322 })
323}
324
325pub struct DisplayType<'a> {
327 ty: &'a ScriptType,
328 color: Option<Color>,
329}
330impl<'a> DisplayType<'a> {
331 pub fn is_unknown(&self) -> bool {
332 self.color.is_none()
333 }
334 pub fn color(&self) -> Color {
335 self.color.unwrap_or(Color::BrightBlack)
336 }
337 pub fn display(&self) -> Cow<'a, str> {
338 if self.is_unknown() {
339 Cow::Owned(format!("{}, unknown", self.ty))
340 } else {
341 Cow::Borrowed(self.ty.as_ref())
342 }
343 }
344}
345pub fn get_display_type(ty: &ScriptType) -> DisplayType<'_> {
346 let conf = Config::get();
347 match conf.get_color(ty) {
348 Err(e) => {
349 log::warn!("取腳本顏色時出錯:{},視為未知類別", e);
350 DisplayType { ty, color: None }
351 }
352 Ok(c) => DisplayType { ty, color: Some(c) },
353 }
354}
355
356pub fn print_iter<T: std::fmt::Display>(iter: impl Iterator<Item = T>, sep: &str) -> bool {
357 let mut first = true;
358 for t in iter {
359 if !first {
360 print!("{}", sep);
361 }
362 first = false;
363 print!("{}", t);
364 }
365 !first
366}
367
368pub fn option_map_res<T, F: FnOnce(T) -> Result<T>>(opt: Option<T>, f: F) -> Result<Option<T>> {
369 Ok(match opt {
370 Some(t) => Some(f(t)?),
371 None => None,
372 })
373}
374
375pub fn hijack_ctrlc_once() {
376 use std::sync::Once;
377 static CTRLC_HANDLE: Once = Once::new();
378 log::debug!("劫持 ctrl-c 回調");
379 CTRLC_HANDLE.call_once(|| {
380 let res = ctrlc::set_handler(|| log::warn!("收到 ctrl-c"));
381 if res.is_err() {
382 log::warn!("設置 ctrl-c 回調失敗 {:?}", res);
383 }
384 });
385}
386
387pub fn prompt(msg: impl std::fmt::Display, allow_enter: bool) -> Result<bool> {
388 use console::{Key, Term};
389
390 enum Res {
391 Y,
392 N,
393 Exit,
394 }
395
396 fn inner(term: &Term, msg: &str, allow_enter: bool) -> Result<Res> {
397 term.hide_cursor()?;
398 hijack_ctrlc_once();
399
400 let res = loop {
401 term.write_str(msg)?;
402 match term.read_key() {
403 Ok(Key::Char('Y' | 'y')) => break Res::Y,
404 Ok(Key::Enter) => {
405 if allow_enter {
406 break Res::Y;
407 } else {
408 term.write_line("")?;
409 }
410 }
411 Ok(Key::Char('N' | 'n')) => break Res::N,
412 Ok(Key::Char(ch)) => term.write_line(&format!(" Unknown key '{}'", ch))?,
413 Ok(Key::Escape) => {
414 break Res::Exit;
415 }
416 Err(e) => {
417 if e.kind() == std::io::ErrorKind::Interrupted {
418 break Res::Exit;
419 } else {
420 return Err(e.into());
421 }
422 }
423 _ => term.write_line(" Unknown key")?,
424 }
425 };
426 Ok(res)
427 }
428
429 let term = Term::stderr();
430 let msg = if allow_enter {
431 format!("{} [Y/Enter/N]", msg)
432 } else {
433 format!("{} [Y/N]", msg)
434 };
435 let res = inner(&term, &msg, allow_enter);
436 term.show_cursor()?;
437 match res? {
438 Res::Exit => {
439 std::process::exit(1);
440 }
441 Res::Y => {
442 term.write_line(&" Y".stylize().color(Color::Green).to_string())?;
443 Ok(true)
444 }
445 Res::N => {
446 term.write_line(&" N".stylize().color(Color::Red).to_string())?;
447 Ok(false)
448 }
449 }
450}
451
452#[derive(Serialize)]
453pub struct TmplVal<'a> {
454 home: &'static Path,
455 cmd: String,
456 exe: PathBuf,
457
458 path: Option<&'a Path>,
459 run_id: Option<i64>,
460 tags: Vec<&'a str>,
461 env_desc: Vec<String>,
462 name: Option<&'a str>,
463 content: Option<&'a str>,
464}
465impl<'a> TmplVal<'a> {
466 pub fn new() -> Self {
467 TmplVal {
468 home: path::get_home(),
469 cmd: std::env::args().next().unwrap_or_default(),
470 exe: std::env::current_exe().unwrap_or_default(),
471
472 path: None,
473 run_id: None,
474 tags: vec![],
475 env_desc: vec![],
476 name: None,
477 content: None,
478 }
479 }
480}